diff --git a/.travis.yml b/.travis.yml index 4654487..9a776bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,9 @@ install: - travis_retry npm install script: - grunt -- grunt intern:node --combined -- grunt intern:browserstack --combined -- grunt remapIstanbul:ci +- grunt intern:browserstack --test-reporter - grunt uploadCoverage +- grunt dist - grunt doc notifications: slack: diff --git a/Gruntfile.js b/Gruntfile.js index 1379e94..11ca65e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,23 +1,9 @@ module.exports = function (grunt) { - const gruntConfig = { + require('grunt-dojo2').initConfig(grunt, { typedoc: { options: { ignoreCompilerErrors: true // Remove this once compile errors are resolved } - }, - - watch: { - dev: { - files: '**/*.ts', - tasks: ['dev'], - options: { - spawn: false - } - } } - }; - grunt.initConfig(gruntConfig); - require('grunt-dojo2').initConfig(grunt, gruntConfig); - - grunt.loadNpmTasks('grunt-contrib-watch'); + }); }; diff --git a/README.md b/README.md index b205985..d0d4f63 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,13 @@ # @dojo/stores [![Build Status](https://travis-ci.org/dojo/stores.svg?branch=master)](https://travis-ci.org/dojo/stores) -[![codecov.io](http://codecov.io/gh/dojo/stores/branch/master/graph/badge.svg)](http://codecov.io/gh/dojo/stores/branch/master) +[![codecov.io](https://codecov.io/gh/dojo/stores/branch/master/graph/badge.svg)](https://codecov.io/gh/dojo/stores/branch/master) [![npm version](https://badge.fury.io/js/%40dojo%2Fstores.svg)](https://badge.fury.io/js/%40dojo%2Fstores) -This library provides a basic data store, as well as extensions that provide the ability to query and observe the store. +This library provides an application store designed to complement @dojo/widgets and @dojo/widget-core or any other reactive application. **WARNING** This is *alpha* software. It is not yet production ready, so you should use at your own risk. -- [Usage](#usage) -- [Features](#features) - - [Storage](#storage) - - [Store](#store) - - [Basic Usage](#basic-usage) - - [Store Observable](#store-observable) - - [ObservableStore](#observablestore) - - [Observing the store](#observing-the-store) - - [QueryableStore](#queryablestore) - - [MappedQueryResult](#mappedqueryresult) - - [Fetch Results](#fetch-results) -- [How do I contribute?](#how-do-i-contribute) - - [Installation](#installation) - - [Testing](#testing) -- [Licensing information](#licensing-information) - ## Usage To use `@dojo/stores`, install the package along with its required peer dependencies: @@ -32,7 +16,6 @@ To use `@dojo/stores`, install the package along with its required peer dependen npm install @dojo/stores # peer dependencies -npm install @dojo/compose npm install @dojo/core npm install @dojo/has npm install @dojo/shim @@ -40,420 +23,347 @@ npm install @dojo/shim ## Features -### Storage + * Application state store designed to work with a reactive component architecture + * Out of the box support for asynchronous commands + * All state operations are recorded per process and undoable via a process callback + * Supports the optimistic pattern with that can be rolled back on a failure + * Fully serializable operations and state -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 allows the `StoreBase`, `ObservableStore`, and `QueryableStore` classes to be repurposed to interact with any storage medium by providing an object implementing the simpler `Storage` interface at instantiation. By default these classes all use the `InMemoryStorage` implementation. -```typescript -import StoreBase from 'store/StoreBase'; -import { Storage } from 'store/interfaces'; -const myCustomStorage: Storage = { - // Implement storage API -} -const myCustomStore = new StoreBase({ - storage: myCustomStorage -}); -``` +## Overview -### Store +Dojo 2 stores is a predictable, consistent state container for Javascript applications with inspiration from Redux and Flux architectures. However Dojo 2 stores aims to provide more built in support for common patterns such as asynchronous behaviors, undo support and **more**! -The `Store` interface provides basic CRUD operations, methods to retrieve records, and methods to create IDs. -```typescript -get(ids: string[]): Promise; -get(id: string): Promise; -get(ids: string | string[]): Promise; -``` -Retrieves store items associated with the provided ID or IDs. Will return undefined for any items that don't exist in the store. -```typescript -identify(items: T[]): string[]; -identify(items: T): string; -identify(items: T | T[]): string | 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 or composite ID is needed. -```typescript -createId(): Promise; +Managing state can become difficult to coordinate when an application becomes complicated with multiple views, widgets, components and models. With each of these attempting to update attributes of state at varying points within the application lifecycle things can get **confusing**. When state changes are hard to understand and/or non-deterministic it becomes increasingly difficult to identify and reproduce bug or add new features. + +Dojo 2 stores provides a centralized store is designed to be the **single source of truth** for an application and operates using uni-directional data flow. This means all application data follows the same lifecycle, ensuring the application logic is predictable and easy to understand. + +## Basics + +To work with the Dojo 2 store there are three core but simple concepts - Operations, Commands and Processes. + + * `Operation` + * Granular instructions to manipulate state based on JSON Patch + * `Command` + * Simple functions that ultimately return operations needed to perform the required state change + * `Process` + * A function that to execute a group of commands that usually represent a complete application behavior + +### Operations + +Operations are the raw instructions the store uses to make modifications to the state. The operations are based on the JSON Patch and JSON Pointer specifications that have been customized specifically for Dojo 2 stores, primarily to prevent access to the state's root. + +Each operation is a simple object which contains instructions with the `OperationType`, `path` and optionally the `value` (depending on operation). + +```ts +const operations = [ + { op: OperationType.ADD, path: new JsonPointer('/foo'), value: 'foo' }, + { op: OperationType.REPLACE, path: new JsonPointer('/bar'), value: 'bar' }, + { op: OperationType.REMOVE, path: new JsonPointer('/qux') }, +]; ``` -Generates a new ID. The default implementation of `createId` leverages [@dojo/core/uuid](https://github.com/dojo/core/blob/master/src/uuid.ts) -```typescript -add(items: T[] | T, options?: O): StoreObservable; + +Dojo 2 stores provides a helper package that can generate `PatchOperation` objects from `@dojo/stores/state/operations`: + +* `add` - Returns a `PatchOperation` of type `OperationType.ADD` for the `path` and `value` +* `remove` - Returns a `PatchOperation` of type `OperationType.REMOVE` for the `path` +* `replace` - Returns a `PatchOperation` of type `OperationType.REPLACE` for the `path` and `value` + +### Commands + +Commands are simply functions which are called internally by the store when executing a `Process` and return an array of `PatchOperations` that tells the `store` what state changes needs to be performed. + +Each command is passed a `CommandRequest` which provides a `get` function for access to the stores state and a `payload` object which contains the an array of arguments that the process executor was called with. + +The `get` function returns back state for a given "path" or "selector", for example `get('/my/deep/state')` or `get('/my/array/item/9')`. + +```ts +function addTodoCommand({ get, payload }: CommandRequest) { + const todos = get('/todos'); + const operations = [ + { op: OperationType.ADD, path: `/todos/${todos.length}`, value: payload[0] } + ]; + + return operations; +} + +function calculateCountsCommand({ get }: CommandRequest) { + const todos = get('/todos'); + const completedTodos = todos.filter((todo: any) => todo.completed); + const operations = [ + { op: OperationType.REPLACE, path: '/activeCount', value: todos.length - completedTodos.length }, + { op: OperationType.REPLACE, path: '/completedCount', value: completedTodos.length } + ]; + return operations; +} ``` -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. -```typescript -put(items: T[] | T, options?: O): StoreObservable; + + *Important:* Access to state root is not permitted and will throw an error, for example `get('/')`. This applies for `Operations` also, it is not possible to create an operation that will update the state root. + + ##### Asynchronous Commands + +Commands support asynchronous behavior out of the box simply by returning a `Promise`. + +```ts +async function postTodoCommand({ get, payload: [ id ] }: CommandRequest): Promise { + const response = await fetch('/todos'); + if (!response.ok) { + throw new Error('Unable to post todo'); + } + const json = await response.json(); + const todos = get('/todos'); + const index = findIndex(todos, byId(id)); + // success + return [ + replace(`/todos/${index}`, { ...todos[index], loading: false, id: data.uuid + ]; +} ``` -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. -```typescript -patch(updates: PatchArgument, options?: O): StoreObservable; + +### Processes + +A `Process` is the construct used to execute commands against a `store` instance in order to make changes to the application state. `Processes` are created using the `createProcess` factory function that accepts an array of commands and an optional callback that can be used command to manage errors thrown from a command. The optional callback receives an `error` object and a `result` object. The `error` object contains the `error` stack and the command that caused the error. The `result` object contains the `payload` passed to the process, a function to undo the operations of the `process` and a function to execute an additional `process`. + +The array of `Commands` that are executed in sequence by the store until the last Command is completed or a `Command` throws an error, these processes often represent an application behavior. For example adding a todo in a simple todo application which will be made up with multiple discreet commands. + +A simple `process` to add a todo and recalculate the todo count: + +```ts +const addTodoProcess = createProcess([ addTodoCommand, calculateCountCommand ]); ``` -Updates the item(s) indicated by PatchArgument in place in the store. The `Patch` interface is based on the [JSON Patch spec](https://tools.ietf.org/html/rfc6902), and can be serialized(not fully tested) to a format compliant with an HTTP patch request -```typescript -delete(ids: string[] | string): StoreObservable; + +A `callback` can be provided which will be called when an error occurs or the process is successfully completed: + + +```ts +function addTodoProcessCallback(error, result) { + if (error) { + // do something with the error + // possibly run the `undo` function from result to rollback the changes up to the + // error + result.undo(); + } + // possible additional state changes by running another process using result.executor(otherProcess) +} + +const addTodoProcess = createProcess([ addTodoCommand, calculateCountCommand ], addTodoProcessCallback); ``` -Delete the item(s) with the provided IDs from the store -```typescript -fetch(query?: Query): FetchResult; + +The `Process` creates a deferred executor by passing the `store` instance `addTodoProcess(store)` which can be executed immediately by passing the `payload` `addTodoProcess(store)(arg1, arg2)` or more often passed to your widgets and used to initiate state changes on user interactions. The `payload` arguments passed to the `executor` are passed to each of the `Process`'s commands in a `payload` argument + +```ts +const addTodoExecutor = addTodoProcess(store); + +addTodoExecutor('arguments', 'get', 'passed', 'here'); ``` -Returns a promise that will resolve to the data in the store that matches the query if one is provided, and all data otherwise -#### Basic Usage -```typescript -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 StoreBase initialization options, but it can also be -// specified explicitly(i.e. new StoreBase();) -// 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 } - // ]); +### Initial State + +Initial state can be defined on store creation by executing a `Process` after the store has been instantiated. + +```ts +// Command that creates the basic initial state +function initialStateCommand() { + return [ + add('/todos', []), + add('/currentTodo', ''), + add('/activeCount', 0), + add('/completedCount', 0) + ]); +} + +const initialStateProcess = createProcess([ initialStateCommand ]); + +// creates the store, initializes the state and runs the `getTodosProcess`. +const store = createStore(); +initialStateProcess(store)(); +// if a process contains an async command, like fetching initial data from a remote service the return promise can be used +// to control the flow. +getTodosProcess(store)().then(() => { + // do things once the todos have been fetched. }); ``` -#### Store Observable +## How does this differ from Redux -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. +Although Dojo 2 stores is a big atom state store, you never get access to the entire state object. To access the sections of state that are needed we use pointers to return the slice of state that is needed i.e. `path/to/state`. State is never directly updated by the user, with state changes only being processed by the operations returned by commands. -But it is also observable. By default any subscribers will get exactly one `UpdateResults` object or an error before being completed. +There is no concept of `reducers`, meaning that there is no confusion about where logic needs to reside between `reducers` and `actions`. `Commands` are the only place that state logic resides and return `operations` that dictate what `state` changes are required and processed internally by the `store`. -```typescript -interface UpdateResults { - currentItems?: T[]; - failedData?: CrudArgument[]; - successfulData: T[] | string[]; - type: StoreOperation; -} +Additionally means that there is no need to coordinate `actions` and `reducers` using a string action key, commands are simple function references that can be reused in multiple `processes`. + +## Advanced + +### Subscribing to store changes + +In order to be notified when changes occur within the store's state, simply register to the stores `.on()` for a type of `invalidate` passing the function to be called. + +```ts +store.on('invalidate', () => { + // do something when the store's state has been updated. +}); ``` -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). - -### ObservableStore - -This store provides an API for observing the store itself, or specific items within the store. - -```typescript -export interface ObservableStoreInterface> extends Store { - /** - * 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>; - /** - * 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; - /** - * 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>; -} +### Undo Processes -interface StoreDelta { - updates: T[]; - deletes: string[]; - adds: T[]; - beforeAll: T[]; - afterAll: T[]; -} +The store records undo operations for every `Command`, grouped by it's `Process`. The `undo` function is passed as part of the `result` argument in the `Process` callback. -interface ItemUpdate { - item?: T; - id: string; +```ts +function processCallback(error, result) { + result.undo(); } ``` -#### Observing the store +The `undo` function will rollback all the operations that were performed by the `process`. -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. +**Note:** Each undo operation has an associated `test` operation to ensure that the store is in the expected state to successfully run the undo operation, if the test fails then an error is thrown and no changes are performed. -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. +### Transforming Executor Arguments -Example usage -```typescript -import ObservableStore from '@dojo/stores/store/ObservableStore'; +An optional `transformer` can be passed to the `createExecutor` function that will be used to parse the arguments passed to the executor. -const observableStore = new ObservableStore({ - data: [{ id: '1', value: 1 }] -}); +```ts +function transformer(...payload: any[]): any { + return { id: uuid(), value: payload[0] }; +} -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 ] - // } -}); +const executor = process(state, transformer); -observableStore.observe('itemId').subscribe(function(update) { - // update will be the item itself -}, undefined, function() { - // completion callback will be called if the item is deleted -}); +executor('id'); +``` -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 -}); +Each `Command` will be passed the result of the transformer as the `payload` for example: `{ id: 'UUID-VALUE', value }` -``` +### Typing with `store.get` -### QueryableStore - -This store provides the ability to filter, sort, select a range of, transform, or query for items using a custom query function. -```typescript -export interface QueryableStoreInterface> extends ObservableStoreInterface { - /** - * Creates a query transform result with the provided query - * @param query - */ - query(query: Query): MappedQueryResultInterface; - /** - * Creates a query transform result with the provided filter - * @param filter - */ - filter(filter: Filter): MappedQueryResultInterface; - /** - * Creates a query transform result with a filter built from the provided test - * @param test - */ - filter(test: (item: T) => boolean): MappedQueryResultInterface; - /** - * Creates a query transform result with the provided range - * @param range - */ - range(range: StoreRange): MappedQueryResultInterface; - /** - * 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): MappedQueryResultInterface; - /** - * 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 | ((a: T, b: T) => number) | keyof T, descending?: boolean): MappedQueryResultInterface; - /** - * 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(transformation: Patch | ((item: T) => V)): QueryResultInterface; - /** - * Create a trackable query transform result with the specified transformation - * @param transformation - * @param idTransform - */ - transform(transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string)): MappedQueryResultInterface; +All access to the internal store state is restricted through `store.get`, the function that is passed to each `Command` when they are executed. It is possible to specify the expected type of the data by passing a generic to `get`. + +```ts +interface Todo { + id: string; + label: string; + completed: boolean; } + +// Will return an array typed as Todo items +const todos = store.get('/todos'); ``` +### Optimistic Update Pattern + +Optimistic updating can be used to build a responsive UI despite interactions that might take some time to respond, for example saving to a remote resource. + +In the case of adding a todo item for instance, with optimistic updating we can immediately add the todo before we even make a request to the server and avoid having an unnatural waiting period or loading indicator. When the server responds we can then reconcile the outcome based on whether it is successful or not. -The return value of a call to query or transform will be an instance of the `QueryResult` interface. - -```typescript -export interface QueryResultInterface> { - query(query: Query): this; - filter(filter: Filter): this; - filter(test: (item: T) => boolean): this; - range(range: StoreRange): this; - range(start: number, count: number): this; - sort(sort: Sort | ((a: T, b: T) => number) | string, descending?: boolean): this; - observe(): Observable>; - observe(id: string): Observable; - observe(ids: string[]): Observable>; - get(ids: string | string[]): Promise; - transform(transformation: Patch | ((item: T) => V)): QueryResultInterface; - transform(transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string)): MappedQueryResult; - fetch(query?: Query): FetchResult; - source: S; +In the success scenario, we might need to update the added Todo item with an id that was provided in the response from the server, and change the color of the Todo item to green to indicate it was successfully saved. + +In the error scenario, it might be that we want to show a notification to say the request failed, and turn the Todo item red, with a "retry" button. It's even possible to revert/undo the adding of the Todo item or anything else that happened in the process. + +```ts +const handleAddTodoErrorProcess = createProcess([ () => [ add('/failed', true) ]; ]); + +function addTodoCallback(error, result) { + if (error) { + result.undo(); + result.executor(handleAddTodoErrorProcess); + } } + +const addTodoProcess = createProcess([ + addTodoCommand, + calculateCountsCommand, + postTodoCommand, + calculateCountsCommand + ], + addTodoCallback); ``` -The `QueryResult` 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 `QueryResult` 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 `QueryableStore` will be a `MappedQueryResult`, which includes additional data in its `StoreDelta` updates, and provides a `track()` method. The `StoreDelta` interface is extended by the `TrackedStoreDelta` interface which the `MappedQueryResult` 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 `MappedQueryResult`. 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. - -```typescript -export interface TrackableStoreDelta extends StoreDelta { - /** - * 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 }[]; +* `addTodoCommand`: Adds the new todo into the application state +* `calculateCountsCommand`: Recalculates the count of completed and active todo items +* `postTodoCommand`: posts the todo item to a remote service and using the process callback we can make changes if there is a failure + * on failure: the previous two commands are reverted and the `failed` state field is set to `true` + * on success: Returns operations that update the todo item `id` field with the value received from the remote service +* `calculateCountsCommand`: Runs again after the success of `postTodoCommand` + +To support "pessimistic" updates to the application state, i.e. wait until a remote service call has been completed before changing the application state simply put the async command before the application store update. This can be useful when performing a deletion of resource, when it can be surprising if item is removed from the UI "optimistically" only for it to reappear back if the remote service call fails. + +```ts +function byId(id: string) { + return (item: any) => id === item.id; } +async function deleteTodoCommand({ get, payload: [ id ] }: CommandRequest) { + const { todo, index } = find(get('/todos'), byId(id)) + await fetch(`/todo/${todo.id}`, { method: 'DELETE' } ); + return [ remove(`/todos/${index}`) ]; +} + +const deleteTodoProcess = createProcess([ deleteTodoCommand, calculateCountsCommand ]); ``` -As with the `ObservableStore`, the locally tracked data in a `MappedQueryResult` 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 `MappedQueryResult` interface can be used to create a copy of a `QueryResult` that will fetch after any updates from its source to make sure it has the latest data. `track()` produces a `TrackedQueryResult`, which has a `release()` method that provides a new, non-tracked query transform result. +*Note:* The process requires the counts to be recalculated after successfully deleting a todo, the process above shows how easily commands can be shared and reused. -When `transform()` is called without an `idTransform`, the resulting `QueryResult` 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 `QueryResult` created this way will not contain positional information in its updates to observers, and cannot be tracked. +### Executing concurrent commands -Example Usage -```typescript -import QueryableStore from '@dojo/stores/store/QueryableStore'; +A `Process` supports multiple commands to be executed concurrently by specifying the commands in an array when creating the process: -const data = [ - { id: '1', value: 1 }, - { id: '2', value: 2 }, - { id: '3', value: 3 } - ]; -const queryStore = new QueryableStore({ - data: data -}); +```ts +const myProcess = createProcess([ commandOne, [ concurrentCommandOne, concurrentCommandTwo ], commandTwo ]); +``` -const filteredView = queryStore.filter((item) => item.value > 1); -filteredView.fetch().then((data) => { - // data = [ { id: '2', value: 2 }, { id: '3', value: 3 } ] -}); +In this example, `commandOne` is executed, then both `concurrentCommandOne` and `concurrentCommandTwo` are executed concurrently. Once all of the concurrent commands are completed the results are applied in order before continuing with the process and executing `commandTwo`. -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: [] - }; - - */ -}); +**Note:** Concurrent commands are always assumed to be asynchronous and resolved using `Promise.all`. +### Decorating Processes + +The `Process` callback provides a hook to apply generic/global functionality across multiple or all processes used within an application. This is done using higher order functions that wrap the process' local `callback` using the error and result payload to decorate or perform an action for all processes it used for. + +Dojo 2 stores provides a simple `UndoManager` that collects the undo function for each process onto a single stack and exposes an `undoer` function that can be used to undo the last `process` executed. If the local `undo` function is called then it will be automatically removed from the managers stack. + +```ts +import { createProcess } from '@dojo/stores/process'; +import { createUndoManager } from '@dojo/stores/extras'; + +const { undoCollector, undoer } = createUndoManager(); +// if the process doesn't need a local callback, the collector can be used without. +const myProcess = createProcess([ commandOne, commandTwo ], undoCollector()); +const myOtherProcess = createProcess([ commandThree, commandFour ], undoCollector()); + +// running `undeor` will undo the last process executed, that had registered the `collector` as a callback. +undoer(); ``` -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 from a `fetch` to the source. +Decorating `callbacks` can composed together to combine multiple units of functionality, such that in the example below `myProcess` would run the `error` and `result` through the `collector`, `logger` and then `snapshot` callbacks. +```ts +const myProcess = createProcess([ commandOne, commandTwo ], collector(logger(snapshot()))); +``` -### Fetch Results +#### Decorating Multiple Process -Both the `Store` and `QueryResult` interfaces return a type called a `FetchResult` from `fetch`. -This is a `Promise` that resolves to the fetched data, but it also has two other properties: `totalLength` and `dataLength`. -For both the `Store` and `QueryResult`, `totalLength` is a `Promise` that resolves to the total number of items -in the underlying `Storage`. +Specifying a `callback` decorator on an individual process explicitly works for targeted behavior but can become cumbersome when the decorator needs to be applied across multiple processes throughout the application. -For a `Store`, `dataLength` resolves to the same value as `totalLength`, and is only provided for consistency between the interfaces. +The `createProcessWith` higher order function can be used to specify `callback` decorators that need to be applied across multiple `processes`. The function accepts an array of `callback` decorators and returns a new `createProcess` factory function that will automatically apply the decorators to any process that it creates. -For a `QueryResult`, `dataLength` resolves to the number of items that match the `QueryResult`'s -queries. Note that in all cases, these values do not change if a query is passed to fetch. +```ts +const customCreateProcess = createProcessWith([ undoCollector, logger ]); -Example Usage -```typescript -import QueryableStore from '@dojo/stores/store/QueryableStore'; -const queryStore = new QueryableStore({ - data: [ - { id: 'item-1', value: 1 }, - { id: 'item-2', value: 2 }, - { id: 'item-3', value: 3 } - ] -}); +// `myProcess` will automatically be decorated with the `undoCollector` and `logger` callback decorators. +const myProcess = customCreateProcess([ commandOne, commandTwo ]); +``` -const withoutQuery = queryStore.fetch(); -// This filter will not change the value of dataLength or totalLength -const withQuery = queryStore.fetch(createFilter().lessThan('value', 2)); +An additional helper function `createCallbackDecorator` can be used to turn a simple `ProcessCallback` into a decorator that ensures passed callback is executed after the decorating `callback` has been run. -Promise.all( - [ withoutQuery.totalLength, withoutQuery.dataLength, withQuery.totalLength, withQuery.dataLength ] -).then((values) => { - // values[0] === values[1] === values[2] === values[3] === 3 -}); +```ts +const myCallback = (error: ProcessError, result: ProcessResult) => { + // do things with the outcome from the process +}; -const queryResult = queryStore.filter((item) => item.value < 3); -const queryResultWithoutQuery = queryResult.fetch(); -// This filter will not change the value of dataLength or totalLength -const queryResultsWithQuery = queryResult.fetch(createFilter().lessThan('value', 2)); -Promise.all([ - queryResultWithoutQuery.totalLength, - queryResultsWithQuery.totalLength, - queryResultWithoutQuery.dataLength, - queryResultsWithQuery.dataLength -]).then((values) => { - // values[0] === values[1] === 3 The totalLength in both cases is still the total number of items - // values[2] === values[3] === 2 The dataLength in both cases is the number of items matching the - // result's queries -}); +// turns the callback into a callback decorator +const myCallbackDecorator = createCallbackDecorator(myCallback); + +// use the callback decorator as normal +const myProcess = createProcess([ commandOne ], myCallbackDecorator()); ``` ## How do I contribute? diff --git a/index.js b/index.js deleted file mode 100644 index bf84549..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/umd/main.js'); diff --git a/intern.json b/intern.json new file mode 100644 index 0000000..8334a65 --- /dev/null +++ b/intern.json @@ -0,0 +1,64 @@ +{ + "capabilities": { + "project": "Dojo 2", + "name": "@dojo/stores" + }, + "environments": [ + { "browserName": "node" } + ], + "suites": [ + "./_build/tests/unit/all.js" + ], + "functionalSuites": [ + "./_build/tests/functional/all.js" + ], + "loader": { + "script": "dojo2", + "options": { + "packages": [ + { "name": "src", "location": "_build/src" }, + { "name": "tests", "location": "_build/tests" }, + { "name": "@dojo", "location": "node_modules/@dojo" }, + { "name": "sinon", "location": "node_modules/sinon/pkg", "main": "sinon" } + ] + } + }, + "coverage": [ + "./_build/src/**/*.js" + ], + "configs": { + "local": { + "tunnel": "selenium", + "environments+": [ + { "browserName": "chrome" } + ] + }, + "browserstack": { + "tunnel": "browserstack", + "capabilities+": { + "browserstack.debug": false + }, + "environments+": [ + { "browserName": "edge" }, + { "browserName": "chrome", "platform": "WINDOWS" }, + { "browserName": "firefox", "os": "WINDOWS", "os_version": "10" }, + { "browserName": "safari", "version": "9.1", "platform": "MAC" }, + { "browserName": "iPhone", "version": "9.1" } + ] + }, + "saucelabs": { + "tunnel": "saucelabs", + "capabilities+": { + "fixSessionCapabilities": false + }, + + "defaultTimeout": 10000, + "environments+": [ + { "browserName": "internet explorer", "version": [ "11.0" ], "platform": "Windows 7" }, + { "browserName": "firefox", "version": "43", "platform": "Windows 10" }, + { "browserName": "chrome", "platform": "Windows 10" } + ], + "maxConcurrency": 4 + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d36989c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7242 @@ +{ + "name": "@dojo/stores", + "version": "2.0.0-pre", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@dojo/interfaces": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@dojo/interfaces/-/interfaces-0.1.0.tgz", + "integrity": "sha512-rpBALDc5Ya/+JrlyFvrt7wKGdGA1xq2gSFGce6j3L9meB8tAFYQvs/bx9DDp+CSdpEzzeVZWr8C4FpoUId2New==", + "dev": true + }, + "@dojo/loader": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@dojo/loader/-/loader-0.1.0.tgz", + "integrity": "sha512-3qoMKkewhIUqV99JzGiAkXdJuT3HUQkSviYut9828Cv4uKOqKqnb6znE8ofZhaAuv25ZU9anHTPiC8fepONfVQ==", + "dev": true + }, + "@theintern/digdug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@theintern/digdug/-/digdug-2.0.0.tgz", + "integrity": "sha512-PDcflahjSPdnJnChtWhej3l5jknGDdDl15p1+7rNxjOpb2gpgnhoWzXCfkUFJvUL/q1DuAowRQNNh2hdUk5MGw==", + "dev": true, + "requires": { + "@dojo/core": "2.0.0-beta2.4", + "@dojo/has": "2.0.0-beta2.3", + "@dojo/interfaces": "2.0.0-beta2.4", + "@dojo/shim": "2.0.0-beta2.4", + "decompress": "4.2.0", + "semver": "5.4.1" + }, + "dependencies": { + "@dojo/core": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/core/-/core-2.0.0-beta2.4.tgz", + "integrity": "sha1-AjZimDJ2QxZrtUITk2hJW1CLzBQ=", + "dev": true + }, + "@dojo/has": { + "version": "2.0.0-beta2.3", + "resolved": "https://registry.npmjs.org/@dojo/has/-/has-2.0.0-beta2.3.tgz", + "integrity": "sha1-faojdQXj7KN2EG1zmMTH3tg2oOE=", + "dev": true + }, + "@dojo/interfaces": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/interfaces/-/interfaces-2.0.0-beta2.4.tgz", + "integrity": "sha1-OGXj9EQJMuCNJNJPgj5UFwaiKNM=", + "dev": true + }, + "@dojo/shim": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/shim/-/shim-2.0.0-beta2.4.tgz", + "integrity": "sha1-TUjz2MMmxF5k3TRq+tAK61gmPZo=", + "dev": true + } + } + }, + "@theintern/leadfoot": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@theintern/leadfoot/-/leadfoot-2.0.0.tgz", + "integrity": "sha512-kcC/tpbdaO8DplWdZ1BeOXwII5HCwiVk3Dck0dnmm/vdqtzRE4yApY5um062PAJh8TqzT1cdR/zy4k4HWo8dBQ==", + "dev": true, + "requires": { + "@dojo/core": "2.0.0-beta2.4", + "@dojo/has": "2.0.0-beta2.3", + "@dojo/interfaces": "2.0.0-beta2.4", + "@dojo/shim": "2.0.0-beta2.4", + "@types/jszip": "0.0.33", + "jszip": "3.1.4", + "tslib": "1.7.1" + }, + "dependencies": { + "@dojo/core": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/core/-/core-2.0.0-beta2.4.tgz", + "integrity": "sha1-AjZimDJ2QxZrtUITk2hJW1CLzBQ=", + "dev": true + }, + "@dojo/has": { + "version": "2.0.0-beta2.3", + "resolved": "https://registry.npmjs.org/@dojo/has/-/has-2.0.0-beta2.3.tgz", + "integrity": "sha1-faojdQXj7KN2EG1zmMTH3tg2oOE=", + "dev": true + }, + "@dojo/interfaces": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/interfaces/-/interfaces-2.0.0-beta2.4.tgz", + "integrity": "sha1-OGXj9EQJMuCNJNJPgj5UFwaiKNM=", + "dev": true + }, + "@dojo/shim": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/shim/-/shim-2.0.0-beta2.4.tgz", + "integrity": "sha1-TUjz2MMmxF5k3TRq+tAK61gmPZo=", + "dev": true + } + } + }, + "@types/babel-types": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-6.25.1.tgz", + "integrity": "sha512-7Z6r20+HE0viAFhsW0d/UrC1K2tTlpXzGpNlYm8MmCv8z5PbAacFIshrM/MjlGRa5SBqxu2socpy8FHntwZKng==", + "dev": true + }, + "@types/benchmark": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.30.tgz", + "integrity": "sha1-9r6gus6xeBVZaLmYT5RUb2NndZI=", + "dev": true + }, + "@types/body-parser": { + "version": "1.16.7", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.7.tgz", + "integrity": "sha512-Obn1/GG0sYsnlAlhhSR1hvYRGBpQT+fzSi2IlGN8emCE4iu6f6xIjaq499B1sa7N9iBLzxyOUBo5bzgJd16BvA==", + "dev": true, + "requires": { + "@types/express": "4.0.39", + "@types/node": "8.0.47" + } + }, + "@types/chai": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.4.35.tgz", + "integrity": "sha1-6NZfg0ktKUT4FvxiB0GCHCioyQA=", + "dev": true + }, + "@types/charm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/charm/-/charm-1.0.1.tgz", + "integrity": "sha512-F9OalGhk60p/DnACfa1SWtmVTMni0+w9t/qfb5Bu7CsurkEjZFN7Z+ii/VGmYpaViPz7o3tBahRQae9O7skFlQ==", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/diff": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-3.2.2.tgz", + "integrity": "sha512-q3zfJvaTroV5BjAAR+peTHEGAAhGrPX0z2EzCzpt2mwFA+qzUn2nigJLqSekXRtdULKmT8am7zjvTMZSapIgHw==", + "dev": true + }, + "@types/express": { + "version": "4.0.39", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.39.tgz", + "integrity": "sha512-dBUam7jEjyuEofigUXCtublUHknRZvcRgITlGsTbFgPvnTwtQUt2NgLakbsf+PsGo/Nupqr3IXCYsOpBpofyrA==", + "dev": true, + "requires": { + "@types/body-parser": "1.16.7", + "@types/express-serve-static-core": "4.0.56", + "@types/serve-static": "1.13.0" + } + }, + "@types/express-serve-static-core": { + "version": "4.0.56", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.56.tgz", + "integrity": "sha512-/0nwIzF1Bd4KGwW4lhDZYi5StmCZG1DIXXMfQ/zjORzlm4+F1eRA4c6yJQrt4hqX//TDtPULpSlYwmSNyCMeMg==", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/fs-extra": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-0.0.33.tgz", + "integrity": "sha1-qHGcQXsIDAEtNJeyjiKKwJdF/fI=", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/grunt": { + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/@types/grunt/-/grunt-0.4.22.tgz", + "integrity": "sha512-fKrWJ+uFq9j3tP2RLm9cY7Z50LhhPnSHQCliCZP5lPAWC7TydnU+BcLR0KQIHe9Gbn1oGfkRIq3u56MNCC1qyw==", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/handlebars": { + "version": "4.0.36", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.36.tgz", + "integrity": "sha512-LjNiTX7TY7wtuC6y3QwC93hKMuqYhgV9A1uXBKNvZtVC8ZvyWAjZkJ5BvT0K7RKqORRYRLMrqCxpw5RgS+MdrQ==", + "dev": true + }, + "@types/highlight.js": { + "version": "9.1.10", + "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.1.10.tgz", + "integrity": "sha512-3uQgLVw3ukDjrgi1h2qxSgsg2W7Sp/BN/P+IBgi8D019FdCcetJzJIxk0Wp1Qfcxzy3EreUnPI7/1HXhFNCRTg==", + "dev": true + }, + "@types/http-errors": { + "version": "1.5.34", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.5.34.tgz", + "integrity": "sha1-1qVvJde5XdBwR2gL+CVjLil5aBU=", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz", + "integrity": "sha512-ohkhb9LehJy+PA40rDtGAji61NCgdtKLAlFoYp4cnuuQEswwdK3vz9SOIkkyc3wrk8dzjphQApNs56yyXLStaQ==", + "dev": true + }, + "@types/istanbul-lib-hook": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-hook/-/istanbul-lib-hook-1.0.0.tgz", + "integrity": "sha512-+kdaJ8hO/auIGcqPuSbd3cnTSzgM8ZW0zfYFZLP67vCmWkZV4LdC1XOXpMWnlONup+PChJMK8Q/+Qrh7WoxnUQ==", + "dev": true + }, + "@types/istanbul-lib-instrument": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.0.tgz", + "integrity": "sha512-BWj7zRtwVU5KzuYL/zNuVoSvYWIpT02smNMpQwQbJjwEyITEGSKsxVIT4b2bzXCWFMq76ss0sdY/5yw3NwTlIA==", + "dev": true, + "requires": { + "@types/babel-types": "6.25.1", + "@types/istanbul-lib-coverage": "1.1.0", + "@types/source-map": "0.1.29" + } + }, + "@types/istanbul-lib-report": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.0.tgz", + "integrity": "sha512-nW5QuzmMhr7fHPijtaGOemFFI8Ctrxb/dIXgouSlKmWT16RxWlGLEX/nGghIBOReKe9hPFZXoNh338nFQk2xcA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "1.1.0" + } + }, + "@types/istanbul-lib-source-maps": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.0.tgz", + "integrity": "sha512-GxjyOSIZC/HETEo61MOjM72qdZH0F9UwfuXCKhmgqhVC7PSjE61BU2I/2eZwQ43+kSdkxKvnOBqqNZi1HHHNEA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "1.1.0", + "@types/source-map": "0.1.29" + } + }, + "@types/istanbul-reports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.0.tgz", + "integrity": "sha512-wrJUtE1+HuaRz0Le7fc5l1nMTermRh6wlEvOdQPilseNScyYgQK8MdgDP2cf/X8+6e1dtsX/zP4W4kH/jyHvFw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "1.1.0", + "@types/istanbul-lib-report": "1.1.0" + } + }, + "@types/jszip": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-0.0.33.tgz", + "integrity": "sha512-zAbqAUQmXP9/ryVysJO6XkogdIdtVIYYGmV7BzhKuagaS+75QZ6muJjeSaG5M8rdE5jQ8gyhkZ23r6l4ICmxyQ==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.80", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.80.tgz", + "integrity": "sha512-FumgRtCaxilKUcgMnZCzH6K3gntIwLiLLIaR+UBGNZpT/N3ne2dKrDSGoGIxSHYpAjnq6kIVV0r51U+kLXX59A==", + "dev": true + }, + "@types/marked": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.0.28.tgz", + "integrity": "sha1-RLp1Tp+lFDJYPo6zCnxN0km1L6o=", + "dev": true + }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", + "dev": true + }, + "@types/mime-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", + "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=", + "dev": true + }, + "@types/minimatch": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-2.0.29.tgz", + "integrity": "sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o=", + "dev": true + }, + "@types/node": { + "version": "8.0.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.47.tgz", + "integrity": "sha512-kOwL746WVvt/9Phf6/JgX/bsGQvbrK5iUgzyfwZNcKVFcjAUVSpF9HxevLTld2SG9aywYHOILj38arDdY1r/iQ==", + "dev": true + }, + "@types/platform": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.1.tgz", + "integrity": "sha512-XI6JKLFNBmkADRd2FtUYtEuq5LDKTNXwUIodV3ZfTNkA+g4yo+rXXXdZL3fTE24S92BjpiEVaL3f64Fxm2JOgg==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.4.tgz", + "integrity": "sha1-m1htZalH3qiMS8JNoLkF/pUgoNU=", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/serve-static": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.0.tgz", + "integrity": "sha512-wvQkePwCDZoyQPGb64DTl2TEeLw54CQFXjY+tznxYYxNcBb4LG40ezoVbMDa0epwE4yogB0f42jCaH0356x5Mg==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "4.0.56", + "@types/mime": "2.0.0" + } + }, + "@types/shell-quote": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.6.0.tgz", + "integrity": "sha512-BFonQx849sYB2YOJZBUEfbWdaJcqRb6+ASvgUBtcmg2JRTjBaV2Wgn0SD0gWNIZ+rd7KPysPCjLUOUXnBDUlBg==", + "dev": true + }, + "@types/shelljs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.3.33.tgz", + "integrity": "sha1-32E73biCJe0JzlyDX2INyq8VXms=", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "@types/sinon": { + "version": "1.16.36", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-1.16.36.tgz", + "integrity": "sha1-dLtu15KFl8Gz+xsAkAXpTcbq41c=", + "dev": true + }, + "@types/source-map": { + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@types/source-map/-/source-map-0.1.29.tgz", + "integrity": "sha1-1wSKYBgLCfiqbVO9oxHGtRy9cBg=", + "dev": true + }, + "@types/statuses": { + "version": "1.2.28", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-1.2.28.tgz", + "integrity": "sha1-zF8Z0haUFtVWzcoFtZsp5F+kl+I=", + "dev": true + }, + "@types/ws": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-0.0.42.tgz", + "integrity": "sha512-+30f9gcx24GZRD9EqqiQM+I5pRf/MJiJoEqp2X62QRwfEjdqyn9mPmjxZAEXBUVunWotE5qkadIPqf2MMcDYNw==", + "dev": true, + "requires": { + "@types/node": "8.0.47" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "dev": true, + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + }, + "dependencies": { + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + } + } + }, + "agent-base": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true, + "requires": { + "extend": "3.0.1", + "semver": "5.0.3" + }, + "dependencies": { + "semver": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + } + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", + "dev": true, + "optional": true + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true, + "optional": true + }, + "async-each": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-0.1.6.tgz", + "integrity": "sha1-tn6Z7c3fllQeRK9WKQzX1cbnBDk=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "autoprefixer": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000758", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=", + "dev": true, + "optional": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.1", + "regenerator-runtime": "0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", + "dev": true, + "requires": { + "lodash": "4.17.4", + "platform": "1.3.4" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", + "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=", + "dev": true + }, + "bl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "dev": true, + "requires": { + "readable-stream": "1.0.34" + } + }, + "bluebird": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.3.3.tgz", + "integrity": "sha1-z5akXXe5qXpDxGo2XEYZ9iv5dtA=", + "dev": true + }, + "body-parser": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", + "dev": true, + "requires": { + "bytes": "2.2.0", + "content-type": "1.0.4", + "debug": "2.2.0", + "depd": "1.1.1", + "http-errors": "1.3.1", + "iconv-lite": "0.4.13", + "on-finished": "2.3.0", + "qs": "5.2.0", + "raw-body": "2.1.7", + "type-is": "1.6.15" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + }, + "qs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", + "dev": true + } + } + }, + "boom": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", + "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", + "dev": true, + "requires": { + "hoek": "0.9.1" + } + }, + "boxen": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.2.tgz", + "integrity": "sha1-Px1AMsMP/qnUsCwyLq8up0HcvOU=", + "dev": true, + "requires": { + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.3.0", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browserslist": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "dev": true, + "requires": { + "caniuse-db": "1.0.30000758", + "electron-to-chromium": "1.3.27" + } + }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "ieee754": "1.1.8", + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "bytes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", + "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caniuse-api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", + "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "caniuse-db": "1.0.30000758", + "lodash.memoize": "4.1.2", + "lodash.uniq": "4.5.0" + } + }, + "caniuse-db": { + "version": "1.0.30000758", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000758.tgz", + "integrity": "sha1-ojViexki6Hi2MWSULJkbhN6SyBA=", + "dev": true + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, + "caseless": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz", + "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chai": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.0.2.tgz", + "integrity": "sha1-L3MnxN5vOF3XeHmZ4qsCaXoyuDs=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "check-error": "1.0.2", + "deep-eql": "2.0.2", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "charm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-1.0.2.tgz", + "integrity": "sha1-it02cVOm2aWBMxBSxAkJkdqZXjU=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.0.6.tgz", + "integrity": "sha1-ChwLzh4kmTr8EFpbgeom3aAeI68=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "arrify": "1.0.1", + "async-each": "0.1.6", + "fsevents": "0.3.8", + "glob-parent": "1.3.0", + "is-binary-path": "1.0.1", + "is-glob": "1.1.3", + "path-is-absolute": "1.0.1", + "readdirp": "1.4.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codecov.io": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/codecov.io/-/codecov.io-0.1.6.tgz", + "integrity": "sha1-Wd/QLaH/McL7K5Uq2K0W/TeBtyg=", + "dev": true, + "requires": { + "request": "2.42.0", + "urlgrey": "0.4.0" + } + }, + "coffee-script": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz", + "integrity": "sha1-EpOLz5vhlI+gBvkuDEyegXBRCMA=", + "dev": true + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "1.0.2", + "color-convert": "1.9.0", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "dev": true, + "optional": true, + "requires": { + "delayed-stream": "0.0.5" + } + }, + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "configstore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", + "integrity": "sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=", + "dev": true, + "requires": { + "dot-prop": "3.0.0", + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "object-assign": "4.1.1", + "os-tmpdir": "1.0.2", + "osenv": "0.1.4", + "uuid": "2.0.3", + "write-file-atomic": "1.3.4", + "xdg-basedir": "2.0.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "core-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz", + "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.2.14" + } + }, + "cross-spawn-async": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", + "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.2.14" + } + }, + "cryptiles": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", + "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=", + "dev": true, + "optional": true, + "requires": { + "boom": "0.4.2" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "csproj2ts": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-0.0.7.tgz", + "integrity": "sha1-drEJRoMlbponCf1cY+7ya/R6FEI=", + "dev": true, + "requires": { + "es6-promise": "2.3.0", + "lodash": "3.10.1", + "semver": "5.4.1", + "xml2js": "0.4.19" + }, + "dependencies": { + "es6-promise": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", + "integrity": "sha1-lu258v2wGZWCKyY92KratnSBgbw=", + "dev": true + } + } + }, + "css-color-function": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/css-color-function/-/css-color-function-1.3.3.tgz", + "integrity": "sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4=", + "dev": true, + "requires": { + "balanced-match": "0.1.0", + "color": "0.11.4", + "debug": "3.1.0", + "rgb": "0.1.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz", + "integrity": "sha1-tQS9BYabOSWd0MXvw12EMXbczEo=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "source-map": "0.5.7", + "supports-color": "3.2.3" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", + "dev": true, + "optional": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "decompress-tarbz2": "4.1.1", + "decompress-targz": "4.1.1", + "decompress-unzip": "4.0.1", + "graceful-fs": "4.1.11", + "make-dir": "1.1.0", + "pify": "2.3.0", + "strip-dirs": "2.1.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "5.2.0", + "is-stream": "1.1.0", + "tar-stream": "1.5.4" + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "file-type": "6.2.0", + "is-stream": "1.1.0", + "seek-bzip": "1.0.5", + "unbzip2-stream": "1.2.5" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "4.1.1", + "file-type": "5.2.0", + "is-stream": "1.1.0" + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "3.9.0", + "get-stream": "2.3.1", + "pify": "2.3.0", + "yauzl": "2.9.1" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + } + } + }, + "deep-eql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", + "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", + "dev": true, + "requires": { + "type-detect": "3.0.0" + }, + "dependencies": { + "type-detect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", + "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", + "dev": true + } + } + }, + "deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha1-skbCuApXCkfBG+HZvRBw7IeLh84=", + "dev": true + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=", + "dev": true + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", + "dev": true, + "optional": true + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "diff": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", + "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", + "dev": true + }, + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "dts-generator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dts-generator/-/dts-generator-2.1.0.tgz", + "integrity": "sha1-A5uHpPX4R7O47wDd7j6wlUXezv4=", + "dev": true, + "requires": { + "bluebird": "3.3.3", + "glob": "7.0.0", + "mkdirp": "0.5.1" + }, + "dependencies": { + "glob": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.0.tgz", + "integrity": "sha1-OyCjV//89GuzhK7W+K6aZH/basQ=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "1.1.14" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + } + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz", + "integrity": "sha1-eOy4o5kGYYe7N07t412ccFZagD0=", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", + "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es6-promise": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", + "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "execa": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.4.0.tgz", + "integrity": "sha1-TrZGejaglfq7KXD/nV4/t7zm68M=", + "dev": true, + "requires": { + "cross-spawn-async": "2.2.5", + "is-stream": "1.1.0", + "npm-run-path": "1.0.0", + "object-assign": "4.1.1", + "path-key": "1.0.0", + "strip-eof": "1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "express": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.5.tgz", + "integrity": "sha1-ZwI1ypWYiQpa6BcLg9tyK4Qu2Sc=", + "dev": true, + "requires": { + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.1", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.0.6", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "1.1.5", + "qs": "6.5.0", + "range-parser": "1.2.0", + "send": "0.15.6", + "serve-static": "1.12.6", + "setprototypeof": "1.0.3", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.0", + "vary": "1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz", + "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "fancy-log": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", + "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "time-stamp": "1.1.0" + } + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "finalhandler": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", + "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "requires": { + "glob": "5.0.15" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "flatten": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", + "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=", + "dev": true + }, + "form-data": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=", + "dev": true, + "optional": true, + "requires": { + "async": "0.9.2", + "combined-stream": "0.0.7", + "mime": "1.2.11" + } + }, + "formatio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true, + "requires": { + "samsam": "1.1.2" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs-extra": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", + "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-0.3.8.tgz", + "integrity": "sha1-mZLxAyySXIKVVNDVmAHcoDE6U1Y=", + "dev": true, + "optional": true, + "requires": { + "nan": "2.7.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gaze": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", + "dev": true, + "requires": { + "globule": "1.2.0" + } + }, + "generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha1-LXhqEhruUIh2eWk56OO/+DbCCRc=", + "dev": true, + "requires": { + "loader-utils": "0.2.17" + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1" + } + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "git-config-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-1.0.1.tgz", + "integrity": "sha1-bTP37WPbDQ4RgTFQO6s6ykfVRmQ=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "fs-exists-sync": "0.1.0", + "homedir-polyfill": "1.0.1" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-parent": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-1.3.0.tgz", + "integrity": "sha1-lx7dgW7V21hwW1gHlkemTQrveWg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + }, + "dependencies": { + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "global-dirs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.0.tgz", + "integrity": "sha1-ENNAOeDfBCcuJizyQiT3IJQ0308=", + "dev": true, + "requires": { + "ini": "1.3.4" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "dev": true, + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "minimatch": "3.0.4" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "glogg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", + "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=", + "dev": true, + "requires": { + "sparkles": "1.0.0" + } + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "grunt": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.1.tgz", + "integrity": "sha1-6HeHZOlEsY8yuw8QuQeEdcnftWs=", + "dev": true, + "requires": { + "coffee-script": "1.10.0", + "dateformat": "1.0.12", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.3.0", + "glob": "7.0.6", + "grunt-cli": "1.2.0", + "grunt-known-options": "1.1.0", + "grunt-legacy-log": "1.0.0", + "grunt-legacy-util": "1.0.0", + "iconv-lite": "0.4.19", + "js-yaml": "3.5.5", + "minimatch": "3.0.4", + "nopt": "3.0.6", + "path-is-absolute": "1.0.1", + "rimraf": "2.2.8" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "dev": true, + "requires": { + "findup-sync": "0.3.0", + "grunt-known-options": "1.1.0", + "nopt": "3.0.6", + "resolve": "1.1.7" + } + } + } + }, + "grunt-contrib-clean": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz", + "integrity": "sha1-Vkq/LQN4qYOhW54/MO51tzjEBjg=", + "dev": true, + "requires": { + "async": "1.5.2", + "rimraf": "2.6.2" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + } + } + }, + "grunt-contrib-copy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", + "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "file-sync-cmp": "0.1.1" + } + }, + "grunt-contrib-watch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz", + "integrity": "sha1-hKGnodar0m7VaEE0lscxM+mQAY8=", + "dev": true, + "requires": { + "async": "1.5.2", + "gaze": "1.1.2", + "lodash": "3.10.1", + "tiny-lr": "0.2.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "grunt-dojo2": { + "version": "2.0.0-beta2.10", + "resolved": "https://registry.npmjs.org/grunt-dojo2/-/grunt-dojo2-2.0.0-beta2.10.tgz", + "integrity": "sha512-kV6qK4Kll+WxRQgIi330VAl9ImEmSpatlzsRf5kR+yX47pD1bjKImfrGEE9nIPeDiHt42nKi/Ec8GhOoT9fX5Q==", + "dev": true, + "requires": { + "codecov.io": "0.1.6", + "dts-generator": "2.1.0", + "execa": "0.4.0", + "glob": "7.1.2", + "grunt-contrib-clean": "1.1.0", + "grunt-contrib-copy": "1.0.0", + "grunt-contrib-watch": "1.0.0", + "grunt-postcss": "0.8.0", + "grunt-text-replace": "0.4.0", + "grunt-ts": "5.5.1", + "grunt-tslint": "4.0.1", + "grunt-typings": "0.1.5", + "intern": "4.0.2", + "lodash": "4.17.4", + "parse-git-config": "0.4.3", + "pkg-dir": "1.0.0", + "postcss-cssnext": "2.11.0", + "postcss-import": "9.1.0", + "postcss-modules": "0.6.4", + "remap-istanbul": "0.9.5", + "resolve-from": "2.0.0", + "shelljs": "0.7.8", + "tslint": "4.5.1", + "typedoc": "0.5.9", + "umd-wrapper": "0.1.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "grunt-known-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.0.tgz", + "integrity": "sha1-pCdO6zL6dl2lp6OxcSYXzjsUQUk=", + "dev": true + }, + "grunt-legacy-log": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-1.0.0.tgz", + "integrity": "sha1-+4bxgJhHvAfcR4Q/ns1srLYt8tU=", + "dev": true, + "requires": { + "colors": "1.1.2", + "grunt-legacy-log-utils": "1.0.0", + "hooker": "0.2.3", + "lodash": "3.10.1", + "underscore.string": "3.2.3" + } + }, + "grunt-legacy-log-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-1.0.0.tgz", + "integrity": "sha1-p7ji0Ps1taUPSvmG/BEnSevJbz0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "lodash": "4.3.0" + }, + "dependencies": { + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz", + "integrity": "sha1-OGqnjcbtUJhsKxiVcmWxtIq7m4Y=", + "dev": true, + "requires": { + "async": "1.5.2", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "4.3.0", + "underscore.string": "3.2.3", + "which": "1.2.14" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + } + } + }, + "grunt-postcss": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.8.0.tgz", + "integrity": "sha1-jzCor2B5A84MRfAfC+QsYOMc6w4=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "diff": "2.2.3", + "postcss": "5.2.18" + } + }, + "grunt-text-replace": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/grunt-text-replace/-/grunt-text-replace-0.4.0.tgz", + "integrity": "sha1-252c5Z4v5J2id+nbwZXD4Rz7FsI=", + "dev": true + }, + "grunt-ts": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-5.5.1.tgz", + "integrity": "sha1-lXIBxrQhx3cilATwcILY5pnRIZk=", + "dev": true, + "requires": { + "chokidar": "1.0.6", + "csproj2ts": "0.0.7", + "es6-promise": "0.1.2", + "lodash": "2.4.1", + "ncp": "0.5.1", + "rimraf": "2.2.6", + "semver": "5.4.1", + "strip-bom": "2.0.0", + "typescript": "1.8.9", + "underscore": "1.5.1", + "underscore.string": "2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", + "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA=", + "dev": true + }, + "rimraf": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", + "dev": true + }, + "typescript": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-1.8.9.tgz", + "integrity": "sha1-s7OnQFn9McvT7K2V1iRlk55+1fo=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-tslint": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/grunt-tslint/-/grunt-tslint-4.0.1.tgz", + "integrity": "sha1-dcRuAluereAUYrvrSfb9TBl4O1o=", + "dev": true + }, + "grunt-typings": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/grunt-typings/-/grunt-typings-0.1.5.tgz", + "integrity": "sha1-GluJR6DWBCIxs6oTjACnOjlhW9w=", + "dev": true, + "requires": { + "typings-core": "1.6.1" + } + }, + "gulp-util": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.7.tgz", + "integrity": "sha1-eJJcS4+LSQBawBoBHFV+YhiUHLs=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-uniq": "1.0.3", + "beeper": "1.1.1", + "chalk": "1.1.3", + "dateformat": "1.0.12", + "fancy-log": "1.3.0", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "lodash._reescape": "3.0.0", + "lodash._reevaluate": "3.0.0", + "lodash._reinterpolate": "3.0.0", + "lodash.template": "3.6.2", + "minimist": "1.2.0", + "multipipe": "0.1.2", + "object-assign": "3.0.0", + "replace-ext": "0.0.1", + "through2": "2.0.1", + "vinyl": "0.5.3" + }, + "dependencies": { + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash._basetostring": "3.0.1", + "lodash._basevalues": "3.0.0", + "lodash._isiterateecall": "3.0.9", + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0", + "lodash.keys": "3.1.2", + "lodash.restparam": "3.6.1", + "lodash.templatesettings": "3.1.1" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0" + } + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "1.0.0" + } + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "1.0.0" + } + }, + "hawk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", + "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=", + "dev": true, + "optional": true, + "requires": { + "boom": "0.4.2", + "cryptiles": "0.2.2", + "hoek": "0.9.1", + "sntp": "0.2.4" + } + }, + "highlight.js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", + "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=", + "dev": true + }, + "hoek": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", + "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "http-errors": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "statuses": "1.4.0" + } + }, + "http-parser-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", + "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=", + "dev": true + }, + "http-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz", + "integrity": "sha1-zBzjjkU7+YSg93AtLdWcc9CBKEo=", + "dev": true, + "requires": { + "agent-base": "2.1.1", + "debug": "2.2.0", + "extend": "3.0.1" + } + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.1.11", + "assert-plus": "0.1.5", + "ctype": "0.5.3" + } + }, + "https-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "requires": { + "agent-base": "2.1.1", + "debug": "2.2.0", + "extend": "3.0.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "intern": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/intern/-/intern-4.0.2.tgz", + "integrity": "sha512-tq18nBU51DT36E2+jVD6T2LdfpJ2uydT/hLtbf92qUgSgj5pTOgvQWOUwiWmJ1Wc/Mz8bYTOk9YCDPFC8+6J2g==", + "dev": true, + "requires": { + "@dojo/core": "2.0.0-beta2.4", + "@dojo/has": "2.0.0-beta2.3", + "@dojo/interfaces": "2.0.0-beta2.4", + "@dojo/shim": "2.0.0-beta2.4", + "@theintern/digdug": "2.0.0", + "@theintern/leadfoot": "2.0.0", + "@types/benchmark": "1.0.30", + "@types/chai": "4.0.4", + "@types/charm": "1.0.1", + "@types/diff": "3.2.2", + "@types/express": "4.0.39", + "@types/http-errors": "1.5.34", + "@types/istanbul-lib-coverage": "1.1.0", + "@types/istanbul-lib-hook": "1.0.0", + "@types/istanbul-lib-instrument": "1.7.0", + "@types/istanbul-lib-report": "1.1.0", + "@types/istanbul-lib-source-maps": "1.2.0", + "@types/istanbul-reports": "1.1.0", + "@types/lodash": "4.14.80", + "@types/mime-types": "2.1.0", + "@types/platform": "1.3.1", + "@types/resolve": "0.0.4", + "@types/shell-quote": "1.6.0", + "@types/source-map": "0.1.29", + "@types/statuses": "1.2.28", + "@types/ws": "0.0.42", + "benchmark": "2.1.4", + "body-parser": "1.17.2", + "chai": "4.0.2", + "charm": "1.0.2", + "diff": "3.2.0", + "express": "4.15.5", + "glob": "7.1.2", + "http-errors": "1.6.2", + "istanbul-lib-coverage": "1.1.1", + "istanbul-lib-hook": "1.0.7", + "istanbul-lib-instrument": "1.7.5", + "istanbul-lib-report": "1.1.2", + "istanbul-lib-source-maps": "1.2.2", + "istanbul-reports": "1.1.3", + "lodash": "4.17.4", + "mime-types": "2.1.17", + "minimatch": "3.0.4", + "platform": "1.3.4", + "resolve": "1.4.0", + "shell-quote": "1.6.1", + "source-map": "0.5.7", + "statuses": "1.3.1", + "ws": "2.3.1" + }, + "dependencies": { + "@dojo/core": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/core/-/core-2.0.0-beta2.4.tgz", + "integrity": "sha1-AjZimDJ2QxZrtUITk2hJW1CLzBQ=", + "dev": true + }, + "@dojo/has": { + "version": "2.0.0-beta2.3", + "resolved": "https://registry.npmjs.org/@dojo/has/-/has-2.0.0-beta2.3.tgz", + "integrity": "sha1-faojdQXj7KN2EG1zmMTH3tg2oOE=", + "dev": true + }, + "@dojo/interfaces": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/interfaces/-/interfaces-2.0.0-beta2.4.tgz", + "integrity": "sha1-OGXj9EQJMuCNJNJPgj5UFwaiKNM=", + "dev": true + }, + "@dojo/shim": { + "version": "2.0.0-beta2.4", + "resolved": "https://registry.npmjs.org/@dojo/shim/-/shim-2.0.0-beta2.4.tgz", + "integrity": "sha1-TUjz2MMmxF5k3TRq+tAK61gmPZo=", + "dev": true + }, + "@types/chai": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.4.tgz", + "integrity": "sha512-cvU0HomQ7/aGDQJZsbtJXqBQ7w4J4TqLB0Z/h8mKrpRjfeZEvTbygkfJEb7fWdmwpIeDeFmIVwAEqS0OYuUv3Q==", + "dev": true + }, + "body-parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", + "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", + "dev": true, + "requires": { + "bytes": "2.4.0", + "content-type": "1.0.4", + "debug": "2.6.7", + "depd": "1.1.1", + "http-errors": "1.6.2", + "iconv-lite": "0.4.15", + "on-finished": "2.3.0", + "qs": "6.4.0", + "raw-body": "2.2.0", + "type-is": "1.6.15" + } + }, + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", + "dev": true + }, + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "iconv-lite": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=", + "dev": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "raw-body": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", + "dev": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.15", + "unpipe": "1.0.0" + } + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "interpret": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", + "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "ipaddr.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz", + "integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA=", + "dev": true + }, + "is-absolute": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", + "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", + "dev": true, + "requires": { + "is-relative": "0.2.1", + "is-windows": "0.2.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.10.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-1.1.3.tgz", + "integrity": "sha1-tMZLgwPTkRRJKkYNNkzPsNPAoEU=", + "dev": true + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "0.1.0", + "is-path-inside": "1.0.0" + } + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-relative": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", + "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", + "dev": true, + "requires": { + "is-unc-path": "0.1.2" + } + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-unc-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", + "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", + "dev": true, + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isnumeric": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/isnumeric/-/isnumeric-0.2.0.tgz", + "integrity": "sha1-ojR7o2DeGeM9D/1ZD933dVy/LmQ=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.5.5", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.2.14", + "wordwrap": "1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "istanbul-lib-coverage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", + "integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz", + "integrity": "sha512-3U2HB9y1ZV9UmFlE12Fx+nPtFqIymzrqCksrXujm3NVbAZIJg/RfYgO1XiIa0mbmxTjWpVEVlkIZJ25xVIAfkQ==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.5.tgz", + "integrity": "sha1-rbWW+PDLi5XnOSBjUaOKWGryGx4=", + "dev": true, + "requires": { + "babel-generator": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.1.1", + "semver": "5.4.1" + } + }, + "istanbul-lib-report": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz", + "integrity": "sha512-UTv4VGx+HZivJQwAo1wnRwe1KTvFpfi/NYwN7DcsrdzMXwpRT/Yb6r4SBPoHWj4VuQPakR32g4PUUeyKkdDkBA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz", + "integrity": "sha512-8BfdqSfEdtip7/wo1RnrvLpHVEd8zMZEDmOFEnpC6dg0vXflHt9nvoAyQUzig2uMSXfF2OBEYBV3CVjIL9JvaQ==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + } + } + }, + "istanbul-reports": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.1.3.tgz", + "integrity": "sha512-ZEelkHh8hrZNI5xDaKwPMFwDsUf5wIEI2bXAFGp1e6deR2mnEKBPhLJEgr4ZBt8Gi6Mj38E/C8kcy9XLggVO2Q==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "js-base64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.3.2.tgz", + "integrity": "sha512-Y2/+DnfJJXT1/FCwUebUhLWb3QihxiSC42+ctHLGogmW2jPY6LCapMdFZXRvVP2z6qyKW7s6qncE/9gSqZiArw==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", + "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jszip": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.4.tgz", + "integrity": "sha512-z6w8iYIxZ/fcgul0j/OerkYnkomH8BZigvzbxVmr2h5HkZUrPtk2kjYtLkqR9wwQxEP6ecKNoKLsbhd18jfnGA==", + "dev": true, + "requires": { + "core-js": "2.3.0", + "es6-promise": "3.0.2", + "lie": "3.1.1", + "pako": "1.0.6", + "readable-stream": "2.0.6" + }, + "dependencies": { + "es6-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", + "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "4.0.1" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "3.0.6" + } + }, + "listify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/listify/-/listify-1.0.0.tgz", + "integrity": "sha1-A8p7otFQ1CZ3c/dOV1WNEFPSvuM=", + "dev": true + }, + "livereload-js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "lockfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", + "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true, + "requires": { + "lodash._root": "3.0.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.templatesettings": "4.1.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "lolex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", + "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, + "make-error-cause": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-1.2.2.tgz", + "integrity": "sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0=", + "dev": true, + "requires": { + "make-error": "1.3.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=", + "dev": true + }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + }, + "dependencies": { + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", + "integrity": "sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true, + "requires": { + "duplexer2": "0.0.2" + } + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", + "dev": true, + "optional": true + }, + "ncp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-run-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", + "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", + "dev": true, + "requires": { + "path-key": "1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz", + "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onecolor": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-2.4.2.tgz", + "integrity": "sha1-pT7D/xccNEYBbdUhDRobVEv32HQ=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "6.7.1", + "registry-auth-token": "3.3.1", + "registry-url": "3.1.0", + "semver": "5.4.1" + } + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parse-git-config": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-0.4.3.tgz", + "integrity": "sha1-Z9YiSN1aJOYFP4R1EF8fuelLuwA=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "fs-exists-sync": "0.1.0", + "git-config-path": "1.0.1", + "ini": "1.3.4" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", + "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pixrem": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pixrem/-/pixrem-3.0.2.tgz", + "integrity": "sha1-MNG6+0w73Ojpu0vVahOYVhkyDDQ=", + "dev": true, + "requires": { + "browserslist": "1.7.7", + "postcss": "5.2.18", + "reduce-css-calc": "1.3.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "platform": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.4.tgz", + "integrity": "sha1-bw+xftqqSPIUQrOpdcBjEw8cPr0=", + "dev": true + }, + "pleeease-filters": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pleeease-filters/-/pleeease-filters-3.0.1.tgz", + "integrity": "sha1-Tf4OjxBGYTUXxktyi8gGCKfr8i8=", + "dev": true, + "requires": { + "onecolor": "2.4.2", + "postcss": "5.2.18" + } + }, + "popsicle": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/popsicle/-/popsicle-8.2.0.tgz", + "integrity": "sha1-/0QBAFyrQ6lBipFBBhHAAZdxLSE=", + "dev": true, + "requires": { + "any-promise": "1.3.0", + "arrify": "1.0.1", + "concat-stream": "1.6.0", + "form-data": "2.3.1", + "make-error-cause": "1.2.2", + "throwback": "1.1.1", + "tough-cookie": "2.3.3", + "xtend": "4.0.1" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + } + } + }, + "popsicle-proxy-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/popsicle-proxy-agent/-/popsicle-proxy-agent-3.0.0.tgz", + "integrity": "sha1-uRM8VdlFdZq37mG3cRNkYg066tw=", + "dev": true, + "requires": { + "http-proxy-agent": "1.0.0", + "https-proxy-agent": "1.0.0" + } + }, + "popsicle-retry": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/popsicle-retry/-/popsicle-retry-3.2.1.tgz", + "integrity": "sha1-4G6GZTO0KnoSPrMwy+Y6fOvLoQw=", + "dev": true, + "requires": { + "any-promise": "1.3.0", + "xtend": "4.0.1" + } + }, + "popsicle-rewrite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/popsicle-rewrite/-/popsicle-rewrite-1.0.0.tgz", + "integrity": "sha1-HdTo6pwxgjUfuCD4eTTZkvf7kAc=", + "dev": true + }, + "popsicle-status": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/popsicle-status/-/popsicle-status-2.0.1.tgz", + "integrity": "sha1-jdcMT+fGlBCa3XhP/oDqysHnso0=", + "dev": true + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "js-base64": "2.3.2", + "source-map": "0.5.7", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "postcss-apply": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.3.0.tgz", + "integrity": "sha1-ovN8W9+ogeTBX08kXsDNlt0ucNU=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "postcss": "5.2.18" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "postcss-attribute-case-insensitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-1.0.1.tgz", + "integrity": "sha1-zrc3d+EGFn6yM/GTjJvZ8uaXMI0=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-selector-parser": "2.2.3" + } + }, + "postcss-calc": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", + "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-css-calc": "1.3.0" + } + }, + "postcss-color-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-function/-/postcss-color-function-2.0.1.tgz", + "integrity": "sha1-mtIm9VDop8f4uKd4YFRbbdf1UkE=", + "dev": true, + "requires": { + "css-color-function": "1.3.3", + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-color-gray": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-3.0.1.tgz", + "integrity": "sha1-dEMu3mbdg7HRNjVlxos3bhj/Z3A=", + "dev": true, + "requires": { + "color": "0.11.4", + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-function-call": "1.0.2" + } + }, + "postcss-color-hex-alpha": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-2.0.0.tgz", + "integrity": "sha1-RP1uyt5mAoZIyIHLZQTNy/3GzQk=", + "dev": true, + "requires": { + "color": "0.10.1", + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0" + }, + "dependencies": { + "color": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/color/-/color-0.10.1.tgz", + "integrity": "sha1-wEGI34KiCd3rzOzazT7DIPGTc58=", + "dev": true, + "requires": { + "color-convert": "0.5.3", + "color-string": "0.3.0" + } + }, + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", + "dev": true + } + } + }, + "postcss-color-hsl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/postcss-color-hsl/-/postcss-color-hsl-1.0.5.tgz", + "integrity": "sha1-9Tuxw0gxDOMHrYnjGBqGRzi15oc=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "units-css": "0.4.0" + } + }, + "postcss-color-hwb": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-hwb/-/postcss-color-hwb-2.0.1.tgz", + "integrity": "sha1-1jr6+bcMtZX5AKKcn+V78qMvq+w=", + "dev": true, + "requires": { + "color": "0.11.4", + "postcss": "5.2.18", + "postcss-message-helpers": "2.0.0", + "reduce-function-call": "1.0.2" + } + }, + "postcss-color-rebeccapurple": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-2.0.1.tgz", + "integrity": "sha1-dMZETny7fYVhO19yht96SRYIRRw=", + "dev": true, + "requires": { + "color": "0.11.4", + "postcss": "5.2.18" + } + }, + "postcss-color-rgb": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postcss-color-rgb/-/postcss-color-rgb-1.1.4.tgz", + "integrity": "sha1-8pJD4i6OjBNDRHQJI3LUzmBb6Lw=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + } + }, + "postcss-color-rgba-fallback": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-2.2.0.tgz", + "integrity": "sha1-bSlJG+WZCpMXPUfnx29YELCUAro=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "rgb-hex": "1.0.0" + } + }, + "postcss-cssnext": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/postcss-cssnext/-/postcss-cssnext-2.11.0.tgz", + "integrity": "sha1-MeaPAB5AlgTacDtm3hS4uMjJ8rE=", + "dev": true, + "requires": { + "autoprefixer": "6.7.7", + "caniuse-api": "1.6.1", + "chalk": "1.1.3", + "pixrem": "3.0.2", + "pleeease-filters": "3.0.1", + "postcss": "5.2.18", + "postcss-apply": "0.3.0", + "postcss-attribute-case-insensitive": "1.0.1", + "postcss-calc": "5.3.1", + "postcss-color-function": "2.0.1", + "postcss-color-gray": "3.0.1", + "postcss-color-hex-alpha": "2.0.0", + "postcss-color-hsl": "1.0.5", + "postcss-color-hwb": "2.0.1", + "postcss-color-rebeccapurple": "2.0.1", + "postcss-color-rgb": "1.1.4", + "postcss-color-rgba-fallback": "2.2.0", + "postcss-custom-media": "5.0.1", + "postcss-custom-properties": "5.0.2", + "postcss-custom-selectors": "3.0.0", + "postcss-font-family-system-ui": "1.0.2", + "postcss-font-variant": "2.0.1", + "postcss-image-set-polyfill": "0.3.5", + "postcss-initial": "1.5.3", + "postcss-media-minmax": "2.1.2", + "postcss-nesting": "2.3.1", + "postcss-pseudo-class-any-link": "1.0.0", + "postcss-pseudoelements": "3.0.0", + "postcss-replace-overflow-wrap": "1.0.0", + "postcss-selector-matches": "2.0.5", + "postcss-selector-not": "2.0.0" + } + }, + "postcss-custom-media": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-5.0.1.tgz", + "integrity": "sha1-E40loYS/LrVN4S1VpsAcMKnYvYE=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-custom-properties": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-5.0.2.tgz", + "integrity": "sha1-lxnXjy2pz59TgQrrwj1GVhMKzrE=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "postcss": "5.2.18" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "postcss-custom-selectors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-3.0.0.tgz", + "integrity": "sha1-j4Ekn17Qeo0JF89qOf5bBWt/lqw=", + "dev": true, + "requires": { + "balanced-match": "0.2.1", + "postcss": "5.2.18", + "postcss-selector-matches": "2.0.5" + }, + "dependencies": { + "balanced-match": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.1.tgz", + "integrity": "sha1-e8ZYtL7WHu5CStdPdfXD4sTfPMc=", + "dev": true + } + } + }, + "postcss-font-family-system-ui": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/postcss-font-family-system-ui/-/postcss-font-family-system-ui-1.0.2.tgz", + "integrity": "sha1-PhpeP7fjHl6ecUOcyw6AFFVpJ8c=", + "dev": true, + "requires": { + "lodash": "4.17.4", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "postcss-font-variant": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-2.0.1.tgz", + "integrity": "sha1-fKKRA/WfoCyjrOLKIrL3VoU9Tvg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-image-set-polyfill": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz", + "integrity": "sha1-Dxk0E3AM8fgr05Bm7wFtZaShgYE=", + "dev": true, + "requires": { + "postcss": "6.0.14", + "postcss-media-query-parser": "0.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-import": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-9.1.0.tgz", + "integrity": "sha1-lf6YdqHnmvSfvcNYnwH+WqfMHoA=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "postcss": "5.2.18", + "postcss-value-parser": "3.3.0", + "promise-each": "2.2.0", + "read-cache": "1.0.0", + "resolve": "1.1.7" + } + }, + "postcss-initial": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-1.5.3.tgz", + "integrity": "sha1-IMPpHJaCLdsb7UlQjbltVrrDd9A=", + "dev": true, + "requires": { + "lodash.template": "4.4.0", + "postcss": "5.2.18" + } + }, + "postcss-media-minmax": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-2.1.2.tgz", + "integrity": "sha1-RExc+JJqteT9iiUJ6Sl+dRZJzfg=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-modules": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-0.6.4.tgz", + "integrity": "sha1-d6WLt3uhtDkrJwwLWYUv116JqLQ=", + "dev": true, + "requires": { + "css-modules-loader-core": "1.1.0", + "generic-names": "1.0.3", + "postcss": "5.2.18", + "string-hash": "1.1.3" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "postcss": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz", + "integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "source-map": "0.6.1", + "supports-color": "4.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "postcss-nesting": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-2.3.1.tgz", + "integrity": "sha1-lKa2pO9wf77CCof+5clXdZtOAc8=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-pseudo-class-any-link": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-1.0.0.tgz", + "integrity": "sha1-kDI5GWQB0zX+c6x1YYb6YuaTryY=", + "dev": true, + "requires": { + "postcss": "5.2.18", + "postcss-selector-parser": "1.3.3" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-1.3.3.tgz", + "integrity": "sha1-0u4Z33pk+O8hwacchvfUg1yIwoE=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + } + } + }, + "postcss-pseudoelements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudoelements/-/postcss-pseudoelements-3.0.0.tgz", + "integrity": "sha1-bGghd8eQC6BTtt8X+MWQKEx7i7w=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-replace-overflow-wrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-1.0.0.tgz", + "integrity": "sha1-8KA7Meq5Y2ppNr/SEOKu8bQ0pkM=", + "dev": true, + "requires": { + "postcss": "5.2.18" + } + }, + "postcss-selector-matches": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-2.0.5.tgz", + "integrity": "sha1-+g9Dvle2jneqTNEYBwI0kqExAn8=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "postcss": "5.2.18" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "postcss-selector-not": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-2.0.0.tgz", + "integrity": "sha1-xzrSGj91I0vuf+4mnhVP1qhpeY0=", + "dev": true, + "requires": { + "balanced-match": "0.2.1", + "postcss": "5.2.18" + }, + "dependencies": { + "balanced-match": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.1.tgz", + "integrity": "sha1-e8ZYtL7WHu5CStdPdfXD4sTfPMc=", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "1.0.2", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "promise-each": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promise-each/-/promise-each-2.2.0.tgz", + "integrity": "sha1-M1MXTv8mlEgQN+BOAfd6oPttG2A=", + "dev": true, + "requires": { + "any-promise": "0.1.0" + }, + "dependencies": { + "any-promise": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-0.1.0.tgz", + "integrity": "sha1-gwtoCqflbzNFHUsEnzvYBESY7ic=", + "dev": true + } + } + }, + "promise-finally": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/promise-finally/-/promise-finally-2.2.1.tgz", + "integrity": "sha1-ImFsS6kCkW6Yi9RsVNfKoIkQzXc=", + "dev": true, + "requires": { + "any-promise": "1.3.0" + } + }, + "proxy-addr": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz", + "integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.4.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz", + "integrity": "sha1-GbV/8k3CqZzh+L32r82ln472H4g=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "dev": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + } + } + }, + "rc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz", + "integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", + "dev": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "readdirp": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-1.4.0.tgz", + "integrity": "sha1-xd5vyz3sgFI8HHARPxoZDYr4LIk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "0.2.14", + "readable-stream": "1.0.34" + }, + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.1.7" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "math-expression-evaluator": "1.2.17", + "reduce-function-call": "1.0.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", + "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "dev": true, + "requires": { + "balanced-match": "0.4.2" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "registry-auth-token": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", + "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", + "dev": true, + "requires": { + "rc": "1.2.2", + "safe-buffer": "5.1.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "1.2.2" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remap-istanbul": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.9.5.tgz", + "integrity": "sha1-oYYXsfMe7Fp9vud1OCmLd1YGqqg=", + "dev": true, + "requires": { + "amdefine": "1.0.1", + "gulp-util": "3.0.7", + "istanbul": "0.4.5", + "minimatch": "3.0.4", + "source-map": "0.5.7", + "through2": "2.0.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "request": { + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.42.0.tgz", + "integrity": "sha1-VyvQFIk4VkBArHqxSLlkI6BjMEo=", + "dev": true, + "requires": { + "aws-sign2": "0.5.0", + "bl": "0.9.5", + "caseless": "0.6.0", + "forever-agent": "0.5.2", + "form-data": "0.1.4", + "hawk": "1.1.1", + "http-signature": "0.10.1", + "json-stringify-safe": "5.0.1", + "mime-types": "1.0.2", + "node-uuid": "1.4.8", + "oauth-sign": "0.4.0", + "qs": "1.2.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.4.3" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "rgb": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", + "integrity": "sha1-vieykej+/+rBvZlylyG/pA/AN7U=", + "dev": true + }, + "rgb-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgb-hex/-/rgb-hex-1.0.0.tgz", + "integrity": "sha1-v6+M2c2RZLWibXHrTxWgllMks8E=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "samsam": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "requires": { + "commander": "2.8.1" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "5.4.1" + } + }, + "send": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.6.tgz", + "integrity": "sha1-IPI6nJJbdiq4JwX+L52yUqzkfjQ=", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "1.1.1", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "serve-static": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.6.tgz", + "integrity": "sha1-uXN3P2NEmTTaVOW+ul4x2fQhFXc=", + "dev": true, + "requires": { + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.15.6" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.0.4", + "rechoir": "0.6.2" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", + "dev": true, + "requires": { + "formatio": "1.1.1", + "lolex": "1.3.2", + "samsam": "1.1.2", + "util": "0.10.3" + } + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "sntp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", + "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=", + "dev": true, + "optional": true, + "requires": { + "hoek": "0.9.1" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "1.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sparkles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", + "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=", + "dev": true + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "split": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz", + "integrity": "sha1-Zwl8YB1pfOE2j0GPBs0gHPBSGlc=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, + "string-template": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz", + "integrity": "sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "4.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tape": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.0.tgz", + "integrity": "sha1-Df7scJIn+8yRcKvn8EaWKycUMds=", + "dev": true, + "requires": { + "deep-equal": "0.1.2", + "defined": "0.0.0", + "inherits": "2.0.3", + "jsonify": "0.0.0", + "resumer": "0.0.0", + "split": "0.2.10", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "tar-stream": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", + "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", + "dev": true, + "requires": { + "bl": "1.2.1", + "end-of-stream": "1.4.0", + "readable-stream": "2.3.3", + "xtend": "4.0.1" + }, + "dependencies": { + "bl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "dev": true, + "requires": { + "readable-stream": "2.3.3" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" + }, + "dependencies": { + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + } + } + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "dev": true, + "requires": { + "any-promise": "1.3.0" + } + }, + "throat": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-3.2.0.tgz", + "integrity": "sha512-/EY8VpvlqJ+sFtLPeOgc8Pl7kQVOWv0woD87KTXVHPIAE842FGT+rokxIhe8xIUP1cfgrkt0as0vDLjDiMtr8w==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", + "dev": true, + "requires": { + "readable-stream": "2.0.6", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "throwback": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/throwback/-/throwback-1.1.1.tgz", + "integrity": "sha1-8AfnwXYEptFtegfEGqDo/txhhKQ=", + "dev": true, + "requires": { + "any-promise": "1.3.0" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "tiny-lr": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.2.1.tgz", + "integrity": "sha1-s/26gC5dVqM8L28QeUsy5Hescp0=", + "dev": true, + "requires": { + "body-parser": "1.14.2", + "debug": "2.2.0", + "faye-websocket": "0.10.0", + "livereload-js": "2.2.2", + "parseurl": "1.3.2", + "qs": "5.1.0" + }, + "dependencies": { + "qs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz", + "integrity": "sha1-TZMuXH6kEcynajEtOaYGIA/VDNk=", + "dev": true + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "touch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz", + "integrity": "sha1-RJy+LbrlqMgDjjDXH6D/RklHxN4=", + "dev": true, + "requires": { + "nopt": "1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.1.1" + } + } + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-4.5.1.tgz", + "integrity": "sha1-BTVocb7yOkNJBnNABvwYgza6gks=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "colors": "1.1.2", + "diff": "3.4.0", + "findup-sync": "0.3.0", + "glob": "7.1.2", + "optimist": "0.6.1", + "resolve": "1.1.7", + "tsutils": "1.9.1", + "update-notifier": "2.3.0" + }, + "dependencies": { + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + } + } + }, + "tsutils": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-1.9.1.tgz", + "integrity": "sha1-ufmrROVa+WgYMdXyjQrur1x1DLA=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + }, + "dependencies": { + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + } + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedoc": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.5.9.tgz", + "integrity": "sha1-40mCQ4tleokM/YXogliz2HWYJBA=", + "dev": true, + "requires": { + "@types/fs-extra": "0.0.33", + "@types/handlebars": "4.0.36", + "@types/highlight.js": "9.1.10", + "@types/lodash": "4.14.80", + "@types/marked": "0.0.28", + "@types/minimatch": "2.0.29", + "@types/shelljs": "0.3.33", + "fs-extra": "2.1.2", + "handlebars": "4.0.5", + "highlight.js": "9.12.0", + "lodash": "4.17.4", + "marked": "0.3.6", + "minimatch": "3.0.4", + "progress": "1.1.8", + "shelljs": "0.7.8", + "typedoc-default-themes": "0.4.4", + "typescript": "2.2.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "handlebars": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.5.tgz", + "integrity": "sha1-ksbta7FkEQxQ1NjQ+93HCAbG+Oc=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "typescript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.2.1.tgz", + "integrity": "sha1-SGK2YrmIpMj/aRzHlpYi0k23auk=", + "dev": true + } + } + }, + "typedoc-default-themes": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.4.4.tgz", + "integrity": "sha1-q+mX3PF0YrYnQ4vGO2XFDTY8JS8=", + "dev": true + }, + "typescript": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", + "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "dev": true + }, + "typings-core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/typings-core/-/typings-core-1.6.1.tgz", + "integrity": "sha1-zkspMeovGbuPPay+xpmDrE6WSjc=", + "dev": true, + "requires": { + "any-promise": "1.3.0", + "array-uniq": "1.0.3", + "configstore": "2.1.0", + "debug": "2.2.0", + "detect-indent": "4.0.0", + "graceful-fs": "4.1.11", + "has": "1.0.1", + "invariant": "2.2.2", + "is-absolute": "0.2.6", + "listify": "1.0.0", + "lockfile": "1.0.3", + "make-error-cause": "1.2.2", + "mkdirp": "0.5.1", + "object.pick": "1.3.0", + "parse-json": "2.2.0", + "popsicle": "8.2.0", + "popsicle-proxy-agent": "3.0.0", + "popsicle-retry": "3.2.1", + "popsicle-rewrite": "1.0.0", + "popsicle-status": "2.0.1", + "promise-finally": "2.2.1", + "rc": "1.2.2", + "rimraf": "2.6.2", + "sort-keys": "1.1.2", + "string-template": "1.0.0", + "strip-bom": "2.0.0", + "thenify": "3.3.0", + "throat": "3.2.0", + "touch": "1.0.0", + "typescript": "2.5.3", + "xtend": "4.0.1", + "zip-object": "0.1.0" + }, + "dependencies": { + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + } + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=", + "dev": true + }, + "umd-wrapper": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/umd-wrapper/-/umd-wrapper-0.1.0.tgz", + "integrity": "sha1-iym4cLCCVDqas7Siooe0uNcVMt4=", + "dev": true + }, + "unbzip2-stream": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", + "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", + "dev": true, + "requires": { + "buffer": "3.6.0", + "through": "2.3.8" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.1.tgz", + "integrity": "sha1-0r3oF9F2/63olKtxRY5oKhS4bck=", + "dev": true + }, + "underscore.string": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.2.3.tgz", + "integrity": "sha1-gGmSYzZl1eX8tNsfs6hi62jp5to=", + "dev": true + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, + "units-css": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/units-css/-/units-css-0.4.0.tgz", + "integrity": "sha1-1iKGU6UZg9fBb/KPi53Dsf/tOgc=", + "dev": true, + "requires": { + "isnumeric": "0.2.0", + "viewport-dimensions": "0.2.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "update-notifier": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.3.0.tgz", + "integrity": "sha1-TognpruRUUCrCTVZ1wFOPruDdFE=", + "dev": true, + "requires": { + "boxen": "1.2.2", + "chalk": "2.3.0", + "configstore": "3.1.1", + "import-lazy": "2.1.0", + "is-installed-globally": "0.1.0", + "is-npm": "1.0.0", + "latest-version": "3.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "configstore": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", + "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", + "dev": true, + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.1.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + }, + "urlgrey": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.0.tgz", + "integrity": "sha1-8GU1cED7NcOzEdTl3DZITZbb6gY=", + "dev": true, + "requires": { + "tape": "2.3.0" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", + "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "viewport-dimensions": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz", + "integrity": "sha1-3nQHR9tTh/0XJfUXXpG6x2r982w=", + "dev": true + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "1.0.2", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.9", + "websocket-extensions": "0.1.2" + } + }, + "websocket-extensions": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.2.tgz", + "integrity": "sha1-Dhh4HeYpoYMIzhSBZQ9n/6JpOl0=", + "dev": true + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "widest-line": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", + "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", + "dev": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "ws": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", + "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=", + "dev": true, + "requires": { + "safe-buffer": "5.0.1", + "ultron": "1.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + } + } + }, + "xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "9.0.4" + } + }, + "xmlbuilder": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", + "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + } + } + }, + "yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", + "dev": true, + "requires": { + "buffer-crc32": "0.2.13", + "fd-slicer": "1.0.1" + } + }, + "zip-object": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zip-object/-/zip-object-0.1.0.tgz", + "integrity": "sha1-waDaBMiMg3dW4khoCgP/kC7D9To=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 375b65e..3a9edd9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@dojo/stores", "version": "2.0.0-pre", - "description": "A new data store in TypeScript", + "description": "Application data store for Dojo 2", "homepage": "https://dojo.io", "engines": { "npm": ">=3.0.0" @@ -16,26 +16,22 @@ "test": "grunt test" }, "peerDependencies": { - "@dojo/core": "2.0.0-alpha.25", - "@dojo/has": "2.0.0-alpha.8", - "@dojo/shim": "2.0.0-beta.10" + "@dojo/core": "~0.1.0", + "@dojo/has": "~0.1.0", + "@dojo/shim": "~0.1.0" }, "devDependencies": { - "@dojo/interfaces": "2.0.0-alpha.11", - "@dojo/loader": "2.0.0-beta.9", + "@dojo/interfaces": "~0.1.0", + "@dojo/loader": "~0.1.0", "@types/chai": "~3.4.0", - "@types/es6-shim": "~0.31.0", - "@types/glob": "~5.0.0", "@types/grunt": "~0.4.0", - "@types/node": "^6.0.46", "@types/sinon": "^1.16.35", "codecov.io": "0.1.6", "glob": "^7.0.6", "grunt": "~1.0.1", - "grunt-dojo2": ">=2.0.0-beta.36", - "intern": "~3.4.1", - "istanbul": "^0.4.5", + "grunt-dojo2": "latest", + "intern": "~4.0.2", "sinon": "^1.17.6", - "typescript": "~2.3.2" + "typescript": "~2.5.2" } } diff --git a/src/Store.ts b/src/Store.ts new file mode 100644 index 0000000..138c27f --- /dev/null +++ b/src/Store.ts @@ -0,0 +1,46 @@ +import { Evented } from '@dojo/core/Evented'; +import { Patch, PatchOperation } from './state/Patch'; +import { Pointer } from './state/Pointer'; + +/** + * Application state store + */ +export class Store extends Evented { + + /** + * The private state object + */ + private _state: object = {}; + + /** + * Returns the state at a specific pointer path location. + * + * @param pointer The StorePointer path to the state that is required. + */ + public get = (pointer: string): T => { + const statePointer = new Pointer(pointer); + return statePointer.get(this._state); + } + + /** + * Applies store operations to state and returns the undo operations + */ + public apply = (operations: PatchOperation[], invalidate: boolean = false): PatchOperation[] => { + const patch = new Patch(operations); + const patchResult = patch.apply(this._state); + this._state = patchResult.object; + if (invalidate) { + this.invalidate(); + } + return patchResult.undoOperations; + } + + /** + * Emits an invalidation event + */ + public invalidate(): any { + this.emit({ type: 'invalidate' }); + } +} + +export default Store; diff --git a/src/extras.ts b/src/extras.ts new file mode 100644 index 0000000..281dd96 --- /dev/null +++ b/src/extras.ts @@ -0,0 +1,47 @@ +import { + ProcessError, + ProcessResult, + ProcessCallback, + ProcessCallbackDecorator, + Undo +} from './process'; + +/** + * Undo manager interface + */ +export interface UndoManager { + undoCollector: ProcessCallbackDecorator; + undoer: () => void; +} + +/** + * Factory function that returns an undoer function that will undo the last executed process and a + * higher order collector function that can be used as the process callback. + */ +export function createUndoManager(): UndoManager { + const undoStack: Undo[] = []; + + return { + undoCollector: (callback?: any): ProcessCallback => { + return (error: ProcessError, result: ProcessResult): void => { + const { undo } = result; + undoStack.push(undo); + + result.undo = (): void => { + const index = undoStack.indexOf(undo); + if (index !== -1) { + undoStack.splice(index, 1); + } + undo(); + }; + callback && callback(error, result); + }; + }, + undoer: (): void => { + const undo = undoStack.pop(); + if (undo !== undefined) { + undo(); + } + } + }; +} diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts deleted file mode 100644 index 6fe3e99..0000000 --- a/src/interfaces.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Subscribable } from '@dojo/core/Observable'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import Patch from './patch/Patch'; - -export interface Query { - incremental?: boolean; - queryType?: QueryType; - apply(data: T[]): T[]; - toString(querySerializer?: (query: Query) => string): string; -} - -export type Constructor = new (...args: any[]) => T; - -export const enum QueryType { - Compound, - Filter, - Range, - Sort -} - -export interface CrudOptions { - id?: string; - rejectOverwrite?: boolean; -} - -export interface UpdateResults { - currentItems?: T[]; - failedData?: CrudArgument[]; - successfulData: T[] | string[]; - type: StoreOperation; -} - -/** - * Adds a then method to the observable for those consumers of the store API who - * only want to know about the end result of an operation, and don't want to deal with - * any recoverable failures. - */ -export type StoreObservable = Subscribable & Promise; - -export interface Storage { - add(items: T[], options?: O): Promise>; - createId(): Promise; - delete(ids: string[]): Promise>; - fetch(query?: Query): FetchResult; - get(ids: string[]): Promise; - identify(items: T[]|T): string[]; - patch(updates: { id: string; patch: Patch }[], options?: O): Promise>; - put(items: T[], options?: O): Promise>; -} - -export interface Store> { - add(items: T[] | T, options?: O): StoreObservable; - createId(): Promise; - delete(ids: string[] | string): StoreObservable; - fetch(query?: Query): FetchResult; - get(ids: string[]): Promise; - get(id: string): Promise; - get(ids: string | string[]): Promise; - identify(items: T[]): string[]; - identify(items: T): string; - identify(items: T | T[]): string | string[]; - patch(updates: PatchArgument, options?: O): StoreObservable; - put(items: T[] | T, options?: O): StoreObservable; -} - -export const enum StoreOperation { - Add, - Delete, - Patch, - Put -} - -export interface StoreOptions { - data?: T[]; - idFunction?: (item: T) => string; - idProperty?: keyof T; - storage?: Storage; -} - -export type CrudArgument = T | string | PatchMapEntry; - -export type BasicPatch = { id: string } & { - [P in keyof T]?: T[P] | BasicPatch; - }; - -export type PatchArgument = Map> | - { id: string; patch: Patch } | - { id: string; patch: Patch }[] | - BasicPatch | - BasicPatch[]; - -export interface FetchResult extends Promise { - /** - * For a store, this is identical to totalLength. For a QueryTransformResultInterface, this resolves to the number of items - * that match the QueryTransformResultInterface's queries - */ - dataLength: Promise; - - /** - * A Promise that resolves to the total number of items in the underlying storage. - */ - totalLength: Promise; -} - -export type PatchMapEntry = { id: string; patch: Patch }; diff --git a/src/patch/JsonPointer.ts b/src/patch/JsonPointer.ts deleted file mode 100644 index af625b8..0000000 --- a/src/patch/JsonPointer.ts +++ /dev/null @@ -1,39 +0,0 @@ -export function navigate(path: JsonPointer, target: any) { - return path.segments.reduce((prev: any, next: string) => prev ? prev[next] : prev, target); -} - -function decode(segment: string) { - return segment.replace(/~1/g, '/').replace(/~0/g, '~'); -} - -function encode(segment: string) { - return segment.replace(/~/g, '~0').replace(/\//g, '~1'); -} - -function toString(...segments: string[]): string { - return segments.reduce((prev, next) => prev + '/' + encode(next)); -} - -export default class JsonPointer { - private _segments: string[]; - - get segments() { - return this._segments.map((segment) => decode(segment)); - } - - constructor(...segments: string[]) { - this._segments = segments; - } - - pop() { - return new JsonPointer(...this._segments.slice(0, this._segments.length - 1)); - } - - push(segment: string) { - return new JsonPointer(...this._segments.concat(segment)); - } - - toString() { - return toString(...this._segments); - } -} diff --git a/src/patch/Patch.ts b/src/patch/Patch.ts deleted file mode 100644 index 22041af..0000000 --- a/src/patch/Patch.ts +++ /dev/null @@ -1,63 +0,0 @@ -import createOperation, { Operation, OperationType } from './createOperation'; -import JsonPointer from './JsonPointer'; -import { shouldRecurseInto, isEqual } from '../utils'; - -function _diff(to: any, from: any, startingPath?: JsonPointer): Operation[] { - if (!shouldRecurseInto(from) || !shouldRecurseInto(to)) { - return []; - } - const path = startingPath || new JsonPointer(); - const fromKeys = Object.keys(from); - const toKeys = Object.keys(to); - const operations: Operation[] = []; - - fromKeys.forEach((key) => { - if (!isEqual(from[key], to[key])) { - if ((key in from) && !(key in to)) { - operations.push(createOperation(OperationType.Remove, path.push(key))); - } - else if (shouldRecurseInto(from[key]) && shouldRecurseInto(to[key])) { - operations.push(..._diff(to[key], from[key], path.push(key))); - } - else { - operations.push(createOperation(OperationType.Replace, path.push(key), undefined, to[key], from[key])); - } - } - }); - - toKeys.forEach((key) => { - if (!(key in from) && (key in to)) { - operations.push(createOperation(OperationType.Add, path.push(key), undefined, to[key])); - } - }); - - return operations; -} - -export function diff(to: T): Patch; -export function diff(to: U, from: T): Patch; -export function diff(to: any, from: any = {}) { - return new Patch(_diff(to, from)); -} - -export default class Patch { - public readonly operations: Operation[]; - - constructor(operations: Operation[]) { - this.operations = operations; - } - - apply(target: T): U { - return this.operations.reduce((prev: any, next: Operation) => next.apply(prev), target); - } - - toString() { - return '[' + this.operations.reduce((prev: string, next: Operation) => { - if (prev) { - return prev + ',' + next.toString(); - } - - return next.toString(); - }, '') + ']'; - } -} diff --git a/src/patch/createOperation.ts b/src/patch/createOperation.ts deleted file mode 100644 index ed96168..0000000 --- a/src/patch/createOperation.ts +++ /dev/null @@ -1,208 +0,0 @@ -import JsonPointer from './JsonPointer'; -import { isEqual } from '../utils'; - -export const enum OperationType { - Add, - Remove, - Replace, - Copy, - Move, - Test -} - -export interface Operation { - op: string; - path: JsonPointer; - apply(target: any): any; - toString(): string; -} - -export interface Add extends Operation { - value: any; -} - -function navigatePath(target: any, path: JsonPointer) { - let currentPath = ''; - let lastSegment = ''; - const pathSegments = path.segments; - pathSegments.forEach( - (segment, index) => { - currentPath += `/${segment}`; - if (!target) { - throw new Error(`Invalid path: ${currentPath} doesn't exist in target`); - } - else if (index + 1 < pathSegments.length) { - target = target[segment]; - } - else { - lastSegment = segment; - } - } - ); - - return { - object: target, - property: lastSegment - }; -} - -function add(this: Add, target: any) { - const applyTo = navigatePath(target, this.path); - applyTo.object[applyTo.property] = this.value; - - return target; -} - -function remove(this: Remove, target: any) { - const applyTo = navigatePath(target, this.path); - delete applyTo.object[applyTo.property]; - - return target; -} - -function replace(this: Replace, target: any) { - const applyTo = navigatePath(target, this.path); - if (!(applyTo.property in applyTo.object)) { - throw new Error(`Cannot replace undefined path: ${this.path.toString()} on object`); - } - applyTo.object[applyTo.property] = this.value; - - return target; -} - -function copyOrMove(from: JsonPointer, to: JsonPointer, target: any, toDelete: boolean) { - const moveFrom = navigatePath(target, from); - if (!(moveFrom.property in moveFrom.object)) { - throw new Error(`Cannot move from undefined path: ${from.toString()} on object`); - } - - const applyTo = navigatePath(target, to); - - applyTo.object[applyTo.property] = moveFrom.object[moveFrom.property]; - if (toDelete) { - delete moveFrom.object[moveFrom.property]; - } - -} - -function move(this: Move, target: any) { - copyOrMove(this.from, this.path, target, true); - return target; -} - -function copy(this: Copy, target: any) { - copyOrMove(this.from, this.path, target, false); - return target; -} - -function test(this: Test, target: any) { - const applyTo = navigatePath(target, this.path); - return isEqual(applyTo.object[applyTo.property], this.value); -} - -export interface Remove extends Operation {} - -export interface Replace extends Operation { - oldValue: any; - value: any; -} - -export interface Move extends Operation { - from: JsonPointer; -} - -export interface Copy extends Operation { - from: JsonPointer; -} - -export interface Test extends Operation { - value: any; -} - -function getPath(path: JsonPointer | string[]) { - if (Array.isArray(path)) { - return new JsonPointer(...path); - } - else { - return path; - } -} - -function toString(this: Operation & { value?: any, from?: any } ) { - let jsonObj: any = {}; - jsonObj.op = this.op; - jsonObj.path = this.path.toString(); - if (this.value) { - jsonObj.value = this.value; - } - if (this.from) { - jsonObj.from = this.from.toString(); - } - - return JSON.stringify(jsonObj); -} -function createOperation(type: OperationType.Add, path: JsonPointer | string[], from?: JsonPointer | string[], value?: any, oldValue?: any): Add; -function createOperation(type: OperationType.Remove, path: JsonPointer | string[], from?: JsonPointer | string[], value?: any, oldValue?: any): Remove; -function createOperation(type: OperationType.Replace, path: JsonPointer | string[], from?: JsonPointer | string[], value?: any, oldValue?: any): Replace; -function createOperation(type: OperationType.Test, path: JsonPointer | string[], from?: JsonPointer | string[], value?: any, oldValue?: any): Test; -function createOperation(type: OperationType.Move, path: JsonPointer | string[], from: JsonPointer | string[], value?: any, oldValue?: any): Move; -function createOperation(type: OperationType.Copy, path: JsonPointer | string[], from: JsonPointer | string[], value?: any, oldValue?: any): Copy; -function createOperation(type: OperationType, path: JsonPointer | string[], from?: JsonPointer | string[], value?: any, oldValue?: any): Operation { - switch (type) { - case OperationType.Add: - return { - op: 'add', - path: getPath(path), - value: value, - apply: add, - toString: toString - }; - case OperationType.Remove: - return { - op: 'remove', - path: getPath(path), - apply: remove, - toString: toString - }; - case OperationType.Replace: - return { - op: 'replace', - path: getPath(path), - value: value, - oldValue: oldValue, - apply: replace, - toString: toString - }; - case OperationType.Move: - if (!from) { - throw new Error('From value is required for Move operations'); - } - return { - op: 'move', - path: getPath(path), - from: getPath(from), - apply: move, - toString: toString - }; - case OperationType.Copy: - if (!from) { - throw new Error('From value is required in Copy operation'); - } - return { - op: 'copy', - path: getPath(path), - from: getPath(from), - apply: copy, - toString: toString - }; - case OperationType.Test: - return { - op: 'test', - path: getPath(path), - value: value, - apply: test, - toString: toString - }; - } -} -export default createOperation; diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000..98bb97f --- /dev/null +++ b/src/process.ts @@ -0,0 +1,171 @@ +import { isThenable } from '@dojo/shim/Promise'; +import { PatchOperation } from './state/Patch'; +import { Store } from './Store'; + +/** + * The arguments passed to a `Command` + */ +export interface CommandRequest { + get(pointer: string): T; + payload: any[]; +} + +/** + * Command that returns patch operations based on the command request + */ +export interface Command { + (request?: CommandRequest): Promise | PatchOperation[]; +} + +/** + * Transformer function + */ +export interface Transformer { + (payload: any): any; +} + +/** + * A process that returns an executor using a Store and Transformer + */ +export interface Process { + (store: Store, transformer?: Transformer): ProcessExecutor; +} + +/** + * Represents an error from a ProcessExecutor + */ +export interface ProcessError { + error: Error; + command?: Command[] | Command; +} + +/** + * Represents a successful result from a ProcessExecutor + */ +export interface ProcessResult { + executor: (process: Process, payload?: any, payloadTransformer?: Transformer) => Promise; + undo: Undo; + operations: PatchOperation[]; + apply: (operations: PatchOperation[], invalidate?: boolean) => PatchOperation[]; + get: (pointer: string) => T; + payload: any; +} + +/** + * Runs a process for the given arguments. + */ +export interface ProcessExecutor { + (payload?: T): Promise; +} + +/** + * Callback for a process, returns an error as the first argument + */ +export interface ProcessCallback { + (error: ProcessError | null, result: ProcessResult): void; +} + +/** + * Function for undoing operations + */ +export interface Undo { + (): void; +} + +/** + * ProcessCallbackDecorator callback + */ +export interface ProcessCallbackDecorator { + (callback?: ProcessCallback): ProcessCallback; +} + +/** + * CreateProcess factory interface + */ +export interface CreateProcess { + (commands: (Command[] | Command)[], callback?: ProcessCallback): Process; +} + +/** + * Factories a process using the provided commands and an optional callback. Returns an executor used to run the process. + * + * @param commands The commands for the process + * @param callback Callback called after the process is completed + */ +export function createProcess(commands: (Command[] | Command)[], callback?: ProcessCallback): Process { + return (store: Store, transformer?: Transformer): ProcessExecutor => { + const { apply, get } = store; + function executor(process: Process, payload?: any, payloadTransformer?: Transformer): Promise { + return process(store, payloadTransformer)(payload); + } + + return async (...payload: any[]): Promise => { + const undoOperations: PatchOperation[] = []; + const operations: PatchOperation[] = []; + const commandsCopy = [ ...commands ]; + const undo = () => { + store.apply(undoOperations, true); + }; + + let command = commandsCopy.shift(); + let error: ProcessError | null = null; + payload = transformer ? [ transformer(payload) ] : payload; + try { + while (command) { + let results = []; + if (Array.isArray(command)) { + results = command.map((commandFunction) => commandFunction({ get: store.get, payload })); + results = await Promise.all(results); + } + else { + let result = command({ get: store.get, payload }); + if (isThenable(result)) { + result = await result; + } + results = [ result ]; + } + + for (let i = 0; i < results.length; i++) { + operations.push(...results[i]); + undoOperations.push(...apply(results[i])); + } + + store.invalidate(); + command = commandsCopy.shift(); + } + } + catch (e) { + error = { error: e, command }; + } + + callback && callback(error, { operations, undo, apply, get, executor, payload }); + return Promise.resolve({ error, operations, undo, apply, get, executor, payload }); + }; + }; +} + +/** + * Creates a process factory that will create processes with the specified callback decorators applied. + * @param callbackDecorators array of process callback decorators to be used by the return factory. + */ +export function createProcessFactoryWith(callbackDecorators: ProcessCallbackDecorator[]): CreateProcess { + return (commands: (Command[] | Command)[], callback?: ProcessCallback): Process => { + const decoratedCallback = callbackDecorators.reduce((callback, callbackDecorator) => { + return callbackDecorator(callback); + }, callback); + return createProcess(commands, decoratedCallback); + }; +} + +/** + * Creates a `ProcessCallbackDecorator` from a `ProcessCallback`. + * @param processCallback the process callback to convert to a decorator. + */ +export function createCallbackDecorator(processCallback: ProcessCallback): ProcessCallbackDecorator { + return (previousCallback?: ProcessCallback): ProcessCallback => { + return (error: ProcessError, result: ProcessResult): void => { + processCallback(error, result); + previousCallback && previousCallback(error, result); + }; + }; +} diff --git a/src/query/CompoundQuery.ts b/src/query/CompoundQuery.ts deleted file mode 100644 index f4f6556..0000000 --- a/src/query/CompoundQuery.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Query, QueryType } from '../interfaces'; - -export interface QueryOptions { - query?: Query; - queryStringBuilder?: (query: CompoundQuery) => string; -} - -const UnitQuery = { - incremental: true, - queryType: QueryType.Filter, - apply(data: any[]) { - return data; - }, - toString() { - return ''; - } -}; - -export default class CompoundQuery implements Query { - /** - * Private collection of queries that comprise this compound query - */ - private _queries: Array> = []; - - /** - * Private function that serializes a compound query - */ - private _queryStringBuilder: (query: CompoundQuery) => string; - - /** - * Identifies this as a compound query - */ - public readonly queryType = QueryType.Compound; - - get incremental() { - return this.queries.every((query: Query) => Boolean(query.incremental)); - } - - get queries() { - return this._queries.slice(); - } - - constructor(options: QueryOptions = {}) { - let query = options.query || UnitQuery; - this._queries = (query instanceof CompoundQuery ? query.queries : [ query ]); - this._queryStringBuilder = options.queryStringBuilder || ((query) => query._queries.join('&')); - } - - apply(data: T[]): T[] { - return this._queries.reduce((prev, next) => { - return next.apply(prev); - }, data); - } - - toString(querySerializer?: (query: Query) => string): string { - const finalQuery = this._queries[this._queries.length - 1]; - if (finalQuery === UnitQuery) { - return finalQuery.toString(); - } - return (querySerializer || this._queryStringBuilder)(this); - } - - withQuery(query: Query): CompoundQuery { - const finalQuery = this._queries[this._queries.length - 1]; - if (finalQuery === UnitQuery) { - return new CompoundQuery({ - query: query, - queryStringBuilder: this._queryStringBuilder - }); - } - else { - const newQuery = new CompoundQuery({ - query: query, - queryStringBuilder: this._queryStringBuilder - }); - - newQuery._queries = this._queries.concat(newQuery._queries); - - return newQuery; - } - } -} diff --git a/src/query/createFilter.ts b/src/query/createFilter.ts deleted file mode 100644 index d75ad8a..0000000 --- a/src/query/createFilter.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { Query, QueryType } from '../interfaces'; -import JsonPointer, { navigate } from '../patch/JsonPointer'; -import { isEqual } from '../utils'; - -export type FilterFunction = (data: T[]) => T[]; -export type ObjectPointer = JsonPointer | keyof T | ''; - -export const enum FilterType { - LessThan, - GreaterThan, - EqualTo, - DeepEqualTo, - In, - Contains, - NotEqualTo, - NotDeepEqualTo, - LessThanOrEqualTo, - GreaterThanOrEqualTo, - Matches, - Custom, - Compound -} - -export const enum BooleanOp { - And, - Or -} - -function isBooleanOp(op: any): op is BooleanOp { - return op === BooleanOp.And || op === BooleanOp.Or; -} -export type FilterChainMember = SimpleFilter | BooleanOp; - -export interface FilterDescriptor { - readonly filterType: FilterType; - readonly path: ObjectPointer; - readonly value: any; -} - -export type FilterArrayEntry = FilterDescriptor | BooleanOp | FilterArray; - -export interface FilterArray extends Array> {} - -export interface SimpleFilter extends Query { - readonly filterChain?: FilterChainMember[]; - readonly filterType: FilterType; - readonly path?: ObjectPointer; - readonly test: (item: T) => boolean; - readonly value?: any; -} - -export interface BooleanFilter extends SimpleFilter { - contains(path: ObjectPointer, value: U): Filter; - custom(test: (item: T) => boolean): Filter; - deepEqualTo(path: ObjectPointer, value: U): Filter; - deepEqualTo(path: ObjectPointer, value: U[]): Filter; - equalTo(path: ObjectPointer, value: U): Filter; - in(path: ObjectPointer, value: U[]): Filter; - greaterThan(path: ObjectPointer, value: number): Filter; - greaterThanOrEqualTo(path: ObjectPointer, value: number): Filter; - lessThan(path: ObjectPointer, value: number): Filter; - lessThanOrEqualTo(path: ObjectPointer, value: number): Filter; - matches(path: ObjectPointer, test: RegExp): Filter; - notEqualTo(path: ObjectPointer, value: U): Filter; - notDeepEqualTo(path: ObjectPointer, value: U): Filter; - notDeepEqualTo(path: ObjectPointer, value: U[]): Filter; -} - -export interface Filter extends BooleanFilter { - and(filter: Filter): Filter; - and(): BooleanFilter; - or(filter: Filter): Filter; - or(): BooleanFilter; -} - -function isFilter(filterOrFunction: FilterChainMember): filterOrFunction is Filter { - return typeof filterOrFunction !== 'function' && ( filterOrFunction).apply; -} - -function createFilterOrReturnOp(descriptorOrOp: FilterDescriptor | BooleanOp) { - if (isBooleanOp(descriptorOrOp)) { - return descriptorOrOp; - } - else { - return createComparator( - descriptorOrOp.filterType, - descriptorOrOp.value, - descriptorOrOp.path - ); - } -} - -function createFilter(filterDescriptors?: FilterDescriptor | FilterArray, serializer?: (filter: Filter) => string): Filter { - let filters: FilterChainMember[] = []; - if (filterDescriptors) { - if (Array.isArray(filterDescriptors)) { - filters = filterDescriptors.map((descriptorChainMember) => { - if (Array.isArray(descriptorChainMember)) { - return createFilter(descriptorChainMember); - } - else { - return createFilterOrReturnOp(descriptorChainMember); - } - }); - } - else { - filters.push( - createComparator( - filterDescriptors.filterType, - filterDescriptors.value, - filterDescriptors.path - ) - ); - } - } - - return createFilterHelper(filters, serializer || serializeFilter); -} - -export default createFilter; - -function createFilterHelper(filters: FilterChainMember[], serializer: (filter: Filter) => string): Filter { - // Small helpers to abstract common operations for building comparator filters - // The main helper delegates to the factory, adding and AND operation before the next filter, - // because by default each filter in a chain will be ANDed with the previous. - function comparatorFilterHelper(filterType: FilterType, value: any, path?: ObjectPointer): Filter { - path = path || new JsonPointer(); - const needsOperator = filters.length > 0 && - (filters[filters.length - 1] !== BooleanOp.And && filters[filters.length - 1] !== BooleanOp.Or); - const newFilters = needsOperator ? [ ...filters, BooleanOp.And, createComparator(filterType, value, path) ] : - [ ...filters, createComparator(filterType, value, path) ]; - return createFilterHelper(newFilters, serializer); - } - - const filter: Filter = { - filterChain: filters, - filterType: FilterType.Compound, - incremental: true, - queryType: QueryType.Filter, - and(this: Filter, newFilter?: Filter) { - let newFilters: FilterChainMember[] = []; - if (newFilter) { - newFilters.push(this, BooleanOp.And, newFilter); - } - else if (filters.length) { - newFilters.push(...filters, BooleanOp.And); - } - return createFilterHelper(newFilters, serializer); - }, - apply(this: Filter, data: T[]) { - return data.filter(this.test); - }, - contains(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.Contains, value, path); - }, - custom(test: (item: T) => boolean) { - return comparatorFilterHelper(FilterType.Custom, test); - }, - deepEqualTo(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.DeepEqualTo, value, path); - }, - equalTo(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.EqualTo, value, path); - }, - greaterThan(path: ObjectPointer, value: number) { - return comparatorFilterHelper(FilterType.GreaterThan, value, path); - }, - greaterThanOrEqualTo(path: ObjectPointer, value: number) { - return comparatorFilterHelper(FilterType.GreaterThanOrEqualTo, value, path); - }, - 'in'(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.In, value, path); - }, - lessThan(path: ObjectPointer, value: number) { - return comparatorFilterHelper(FilterType.LessThan, value, path); - }, - lessThanOrEqualTo(path: ObjectPointer, value: number) { - return comparatorFilterHelper(FilterType.LessThanOrEqualTo, value, path); - }, - matches(path: ObjectPointer, value: RegExp) { - return comparatorFilterHelper(FilterType.Matches, value, path); - }, - notEqualTo(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.NotEqualTo, value, path); - }, - notDeepEqualTo(path: ObjectPointer, value: any) { - return comparatorFilterHelper(FilterType.NotDeepEqualTo, value, path); - }, - or(this: Filter, newFilter?: Filter) { - let newFilters: FilterChainMember[] = []; - if (newFilter) { - newFilters.push(this, BooleanOp.Or, newFilter); - } - else if (filters.length) { - newFilters.push(...filters, BooleanOp.Or); - } - return createFilterHelper(newFilters, serializer); - }, - test(item) { - return applyFilterChain(item, filters); - }, - toString(this: Filter, filterSerializer?: ((query: Query) => string) | ((filter: Filter) => string)) { - return (filterSerializer || serializer)(this); - } - }; - - return filter; -} - -function applyFilterChain(item: T, filterChain: FilterChainMember[]): boolean { - let ordFilterSections: FilterChainMember[][] = []; - let startOfSlice = 0; - // Ands have higher precedence, so split into chains of - // ands between ors. - filterChain.forEach((chainMember, i) => { - if (chainMember === BooleanOp.Or) { - ordFilterSections.push(filterChain.slice(startOfSlice, i)); - startOfSlice = i + 1; - } - }); - - if (startOfSlice < filterChain.length) { - ordFilterSections.push(filterChain.slice(startOfSlice, filterChain.length)); - } - - // These sections are or'd together so only - // one has to pass - return ordFilterSections.some((filterChain: FilterChainMember[]) => - // The individual filters are and'd together, so if any - // fails the whole section fails - filterChain.every((filterOrAnd: FilterChainMember) => { - if (isFilter(filterOrAnd)) { - return filterOrAnd.test(item); - } - else { - return true; - } - }) - ); -} - -function createComparator(operator: FilterType, value: any, path: ObjectPointer): SimpleFilter { - const jsonPointer = typeof path === 'string' ? new JsonPointer(path) : path as JsonPointer; - let test: (property: any) => boolean; - const filterType: FilterType = operator; - let operatorString: string; - switch (operator) { - case FilterType.LessThan: - test = (property) => property < value; - operatorString = 'lt'; - break; - case FilterType.LessThanOrEqualTo: - test = (property) => property <= value; - operatorString = 'lte'; - break; - case FilterType.GreaterThan: - test = (property) => property > value; - operatorString = 'gt'; - break; - case FilterType.GreaterThanOrEqualTo: - test = (property) => property >= value; - operatorString = 'gte'; - break; - case FilterType.EqualTo: - test = (property) => property === value; - operatorString = 'eq'; - break; - case FilterType.NotEqualTo: - test = (property) => property !== value; - operatorString = 'ne'; - break; - case FilterType.DeepEqualTo: - test = (property) => isEqual(property, value); - operatorString = 'eq'; - break; - case FilterType.NotDeepEqualTo: - test = (property) => !isEqual(property, value); - operatorString = 'ne'; - break; - case FilterType.Contains: - test = (propertyOrItem) => { - if (Array.isArray(propertyOrItem)) { - return propertyOrItem.indexOf(value) > -1; - } - else { - return propertyOrItem && Boolean(propertyOrItem[value]); - } - }; - operatorString = 'contains'; - break; - case FilterType.In: - test = (propertyOrItem) => Array.isArray(value) && value.indexOf(propertyOrItem) > -1; - operatorString = 'in'; - break; - case FilterType.Matches: - test = (property) => value.test(property); - break; - case FilterType.Custom: - test = value; - break; - // unreachable lines - // default: - // return null; - } - return { - filterType: filterType, - path: path, - queryType: QueryType.Filter, - value: value, - apply(this: Filter, data: T[]) { - return data.filter(this.test); - }, - test(item: T) { - let propertyValue: any = navigate(jsonPointer, item); - return test(propertyValue); - }, - toString() { - if (!operatorString) { - throw Error('Cannot parse this filter type to an RQL query string'); - } - return `${operatorString}(${jsonPointer.toString()}, ${JSON.stringify(value)})`; - } - }; -} - -//// Default serialization function -function serializeFilter(filter: Filter): string { - let operator = '&'; - if (filter.filterChain && filter.filterChain.length > 0) { - return filter.filterChain.reduce((prev: string, next: FilterChainMember) => { - if (isFilter(next)) { - const start = next.filterChain ? '(' : ''; - const end = next.filterChain ? ')' : ''; - return prev + (prev ? operator : '') + (prev ? start : '') + next.toString() + (prev ? end : ''); - } - else if (next === BooleanOp.And) { - operator = '&'; - return prev; - } - else { - operator = '|'; - return prev; - } - }, ''); - } - else { - return ''; - } -} diff --git a/src/query/createSort.ts b/src/query/createSort.ts deleted file mode 100644 index 6b075c4..0000000 --- a/src/query/createSort.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Query, QueryType } from '../interfaces'; -import JsonPointer, { navigate } from '../patch/JsonPointer'; - -export type SortParameter = ((a: T, b: T) => number) | keyof T | JsonPointer; -export interface Sort extends Query { - readonly descending?: boolean[]; - readonly sortParameters: SortParameter[]; -} - -function isDescending(descending: boolean[] | undefined, index: number) { - if (!descending) { - return false; - } - else if (typeof descending[index] !== 'undefined') { - return descending[index]; - } - else { - return descending[0]; - } -} - -function createSort( - comparatorOrProperty: SortParameter | SortParameter[], - descending?: boolean | boolean[], - serializer?: (sort: Sort) => string): Sort { - - let anyAreFunction = false; - const sortParameterArray: SortParameter[] = Array.isArray(comparatorOrProperty) ? - comparatorOrProperty : [ comparatorOrProperty ]; - const descendingArray: boolean[] | undefined = descending ? - (Array.isArray(descending) ? descending : [ descending ]) : undefined; - const comparators: ((a: T, b: T) => number)[] = sortParameterArray.map((comparatorOrProperty, index) => { - const isFunction = typeof comparatorOrProperty === 'function'; - const descending = isDescending(descendingArray, index); - let comparator: (a: T, b: T) => number; - if (isFunction) { - anyAreFunction = true; - comparator = comparatorOrProperty; - } - else { - let pointer: JsonPointer; - if (typeof comparatorOrProperty === 'string') { - comparator = (a: T, b: T) => { - return sortValue(( a)[comparatorOrProperty], ( b)[comparatorOrProperty], Boolean(descending)); - }; - } - else { - pointer = comparatorOrProperty; - comparator = (a: T, b: T) => sortValue(navigate(pointer, a), navigate(pointer, b), Boolean(descending)); - } - } - - if (descending && isFunction) { - comparator = flip(comparator); - } - - return comparator; - }); - const comparator = - comparators.length > 1 ? (a: T, b: T) => comparators.reduce((prev, next) => prev || next(a, b), 0) : comparators[0]; - return { - apply(data: T[]) { - return data.sort(comparator); - }, - sortParameters: sortParameterArray, - descending: descendingArray, - queryType: QueryType.Sort, - toString(this: Sort, sortSerializer: ((sort: Sort) => string)) { - if (anyAreFunction) { - throw Error('Cannot parse this sort type to an RQL query string'); - } - return (sortSerializer || serializer || serialize)(this); - }, - incremental: true - }; -} - -function flip(comparator: (a: T, b: T) => number) { - return (a: T, b: T) => -1 * comparator(a, b); -} - -function serialize(sort: Sort) { - return 'sort(' + sort.sortParameters.map((param, index) => { - const descending = isDescending(sort.descending, index); - return (descending ? '-' : '+') + param; - }).join(',') + ')'; -} -// the `a == null` check returns `true` when a is `null` or `undefined`. -function sortValue(a: any, b: any, descending: boolean) { - let comparison: number; - a != null && (a = a.valueOf()); - b != null && (b = b.valueOf()); - if (a === b) { - comparison = 0; - } - else { - // undefined < null < defined - const isALessThanB = typeof a === 'undefined' || - a === null && typeof b !== 'undefined' || - b != null && a < b; - comparison = descending === isALessThanB ? 1 : -1; - } - return comparison; -} - -export default createSort; diff --git a/src/query/createStoreRange.ts b/src/query/createStoreRange.ts deleted file mode 100644 index 7f9ddde..0000000 --- a/src/query/createStoreRange.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Query, QueryType } from '../interfaces'; - -export interface StoreRange extends Query { - readonly count: number; - readonly start: number; -} - -function serializeRange(range: StoreRange): string { - return `limit(${range.count}${range.start ? ',' + range.start : ''})`; -} - -function createRange(start: number, count: number, serializer?: (range: StoreRange) => string): StoreRange { - return { - count: count, - incremental: false, - queryType: QueryType.Range, - start: start, - apply(data: T[]) { - return data.slice(start, start + count); - }, - toString(this: StoreRange, rangeSerializer?: ((query: Query) => string) | ((range: StoreRange) => string) ) { - return (rangeSerializer || serializer || serializeRange)(this); - } - }; -} - -export default createRange; diff --git a/src/state/Patch.ts b/src/state/Patch.ts new file mode 100644 index 0000000..9ec0624 --- /dev/null +++ b/src/state/Patch.ts @@ -0,0 +1,164 @@ +import { Pointer, walk, PointerTarget } from './Pointer'; + +export enum OperationType { + ADD = 'add', + REMOVE = 'remove', + REPLACE = 'replace', + TEST = 'test' +} + +export interface BaseOperation { + op: OperationType; + path: Pointer; +} + +export interface AddPatchOperation extends BaseOperation { + op: OperationType.ADD; + value: T; +} + +export interface RemovePatchOperation extends BaseOperation { + op: OperationType.REMOVE; +} + +export interface ReplacePatchOperation extends BaseOperation { + op: OperationType.REPLACE; + value: T; +} + +export interface TestPatchOperation extends BaseOperation { + op: OperationType.TEST; + value: T; +} + +export type PatchOperation = AddPatchOperation | RemovePatchOperation | ReplacePatchOperation | TestPatchOperation; + +export interface PatchResult { + object: any; + undoOperations: PatchOperation[]; +} + +function add(pointerTarget: PointerTarget, value: any): any { + if (Array.isArray(pointerTarget.target)) { + pointerTarget.target.splice(parseInt(pointerTarget.segment, 10), 0, value); + } + else { + pointerTarget.target[pointerTarget.segment] = value; + } + return pointerTarget.object; +} + +function replace(pointerTarget: PointerTarget, value: any): any { + if (Array.isArray(pointerTarget.target)) { + pointerTarget.target.splice(parseInt(pointerTarget.segment, 10), 1, value); + } + else { + pointerTarget.target[pointerTarget.segment] = value; + } + return pointerTarget.object; +} + +function remove(pointerTarget: PointerTarget): any { + if (Array.isArray(pointerTarget.target)) { + pointerTarget.target.splice(parseInt(pointerTarget.segment, 10), 1); + } + else { + delete pointerTarget.target[pointerTarget.segment]; + } + return pointerTarget.object; +} + +function test(pointerTarget: PointerTarget, value: any) { + return isEqual(pointerTarget.target[pointerTarget.segment], value); +} + +export function isObject(value: any): value is Object { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +export function isEqual(a: any, b: any): boolean { + if (Array.isArray(a) && Array.isArray(b)) { + return a.length === b.length && a.every((element: any, i: number) => isEqual(element, b[i])); + } + else if (isObject(a) && isObject(b)) { + const keysForA = Object.keys(a).sort(); + const keysForB = Object.keys(b).sort(); + return isEqual(keysForA, keysForB) && keysForA.every(key => isEqual(a[key], b[key])); + } + else { + return a === b; + } +} + +function inverse(operation: PatchOperation, state: any): any[] { + if (operation.op === OperationType.ADD) { + const op = { + op: OperationType.REMOVE, + path: operation.path + }; + const test = { + op: OperationType.TEST, + path: operation.path, + value: operation.value + }; + return [ test, op ]; + } + else if (operation.op === OperationType.REPLACE) { + const op = { + op: OperationType.REPLACE, + path: operation.path, + value: operation.path.get(state) + }; + const test = { + op: OperationType.TEST, + path: operation.path, + value: operation.value + }; + return [ test, op ]; + } + else { + return [{ + op: OperationType.ADD, + path: operation.path, + value: operation.path.get(state) + }]; + } +} + +export class Patch { + private _operations: PatchOperation[]; + + constructor(operations: PatchOperation | PatchOperation[]) { + this._operations = Array.isArray(operations) ? operations : [ operations ]; + } + + public apply(object: any): PatchResult { + let undoOperations: PatchOperation[] = []; + const patchedObject = this._operations.reduce((patchedObject, next) => { + let object; + const pointerTarget = walk(next.path.segments, patchedObject); + switch (next.op) { + case OperationType.ADD: + object = add(pointerTarget, next.value); + break; + case OperationType.REPLACE: + object = replace(pointerTarget, next.value); + break; + case OperationType.REMOVE: + object = remove(pointerTarget); + break; + case OperationType.TEST: + const result = test(pointerTarget, next.value); + if (!result) { + throw new Error('Test operation failure. Unable to apply any operations.'); + } + return patchedObject; + default: + throw new Error('Unknown operation'); + } + undoOperations = [ ...undoOperations, ...inverse(next, patchedObject) ]; + return object; + }, object); + return { object: patchedObject, undoOperations }; + } +} diff --git a/src/state/Pointer.ts b/src/state/Pointer.ts new file mode 100644 index 0000000..59ab7c3 --- /dev/null +++ b/src/state/Pointer.ts @@ -0,0 +1,89 @@ +export function decode(segment: string) { + return segment.replace(/~1/g, '/').replace(/~0/g, '~'); +} + +function encode(segment: string) { + return segment.replace(/~/g, '~0').replace(/\//g, '~1'); +} + +export interface PointerTarget { + object: any; + target: any; + segment: string; +} + +export function walk(segments: string[], object: any, clone = true): PointerTarget { + if (clone) { + object = { ...object }; + } + const pointerTarget: PointerTarget = { + object, + target: object, + segment: '' + }; + + return segments.reduce((pointerTarget, segment, index) => { + if (Array.isArray(pointerTarget.target) && segment === '-') { + segment = String(pointerTarget.target.length - 1); + } + if (index + 1 < segments.length) { + const nextSegment = segments[index + 1]; + let target = pointerTarget.target[segment]; + + if (clone || target === undefined) { + if (Array.isArray(target)) { + target = [ ...target ]; + } + else if (typeof target === 'object') { + target = { ...target }; + } + else if (isNaN(parseInt(nextSegment, 0))) { + target = {}; + } + else { + target = []; + } + pointerTarget.target[segment] = target; + pointerTarget.target = target; + } + else { + pointerTarget.target = target; + } + } + else { + pointerTarget.segment = segment; + } + return pointerTarget; + }, pointerTarget); +} + +export class Pointer { + private readonly _segments: string[]; + + constructor(segments: string | string[]) { + if (Array.isArray(segments)) { + this._segments = segments; + } + else { + this._segments = segments.split('/'); + this._segments.shift(); + } + if (segments.length === 0 || (segments.length === 1 && (segments[0] === '/') || segments[0] === '')) { + throw new Error('Access to the root is not supported.'); + } + this._segments = this._segments.map(decode); + } + + public get segments(): string[] { + return this._segments; + } + + public get path(): string { + return `/${this._segments.map(encode).join('/')}`; + } + + get(object: any): any { + const pointerTarget: PointerTarget = walk(this.segments, object, false); + return pointerTarget.target[pointerTarget.segment]; + } +} diff --git a/src/state/operations.ts b/src/state/operations.ts new file mode 100644 index 0000000..a5fe09e --- /dev/null +++ b/src/state/operations.ts @@ -0,0 +1,33 @@ +import { RemovePatchOperation, ReplacePatchOperation, AddPatchOperation, TestPatchOperation, OperationType } from './Patch'; +import { Pointer } from './Pointer'; + +export function add(path: string, value: any): AddPatchOperation { + return { + op: OperationType.ADD, + path: new Pointer(path), + value + }; +} + +export function replace(path: string, value: any): ReplacePatchOperation { + return { + op: OperationType.REPLACE, + path: new Pointer(path), + value + }; +} + +export function remove(path: string): RemovePatchOperation { + return { + op: OperationType.REMOVE, + path: new Pointer(path) + }; +} + +export function test(path: string, value: any): TestPatchOperation { + return { + op: OperationType.TEST, + path: new Pointer(path), + value + }; +} diff --git a/src/storage/InMemoryStorage.ts b/src/storage/InMemoryStorage.ts deleted file mode 100644 index 6add540..0000000 --- a/src/storage/InMemoryStorage.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { duplicate } from '@dojo/core/lang'; -import uuid from '@dojo/core/uuid'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import { Query, FetchResult, StoreOperation, CrudOptions, StoreOptions, UpdateResults, Storage } from '../interfaces'; -import Patch from '../patch/Patch'; - -export default class InMemoryStorage implements Storage { - private _data: T[]= []; - private _idFunction?: (item: T) => string; - private _idProperty?: keyof T; - private _index= new Map(); - private _returnsPromise: Promise = Promise.resolve(); - - constructor(options: StoreOptions = {}) { - this._idFunction = options.idFunction; - this._idProperty = options.idProperty; - } - - private _putSync(items: T[], options?: CrudOptions) { - const ids = this.identify(items); - - const updatedItems: T[] = []; - const oldIndices: number[] = []; - const newIds: string[] = []; - const newItems: T[] = []; - - ids.forEach((id, index) => { - const oldIndex = this._index.get(id); - if (typeof oldIndex === 'undefined') { - newIds.push(id); - newItems.push(items[index]); - } - else { - updatedItems.push(items[index]); - oldIndices.push(oldIndex); - } - }); - if (oldIndices.length && options && options.rejectOverwrite) { - throw Error('Objects already exist in store'); - } - - const data = this._data; - updatedItems.forEach((item, index) => { - data[oldIndices[index]] = item; - }); - newItems.forEach((item, index) => { - this._index.set(newIds[index], data.push(item) - 1); - }); - return { - successfulData: items, - type: StoreOperation.Put - }; - } - - add(items: T[], options?: CrudOptions): Promise> { - options = options || {}; - if (typeof options.rejectOverwrite === 'undefined') { - options.rejectOverwrite = true; - } - - try { - const result = this._putSync(items, options); - // Don't control the order operations are executed in, but make sure that the results - // resolve in the order they were actually executed in. - const returnPromise = this._returnsPromise.then(() => { - result.type = StoreOperation.Add; - return result; - }); - this._returnsPromise = returnPromise; - return returnPromise; - } catch (error) { - return Promise.reject(error); - } - } - - createId(): Promise { - return Promise.resolve(uuid()); - } - - delete(ids: string[]): Promise> { - const data = this._data; - const idsToRemove = ids.filter((id) => this._index.has(id)); - - const indices: number[] = idsToRemove.map((id) => this._index.get(id) || 0).sort(); - - idsToRemove.forEach((id) => { - this._index.delete(id); - }); - indices.forEach((index, indexArrayIndex) => { - return data.splice(index - indexArrayIndex, 1); - }); - if (indices.length) { - const firstInvalidIndex = indices[0]; - const updateIndexForIds = this.identify(data.slice(firstInvalidIndex)); - updateIndexForIds.forEach((id, index) => { - this._index.set(id, index + firstInvalidIndex); - }); - } - - // Don't control the order operations are executed in, but make sure that the results - // resolve in the order they were actually executed in. - const returnPromise = this._returnsPromise.then(() => ({ - successfulData: idsToRemove, - type: StoreOperation.Delete - })); - this._returnsPromise = returnPromise; - return returnPromise; - } - - fetch(query?: Query): FetchResult { - const fullData = this._data; - const data = (query ? query.apply(fullData) : fullData).slice(); - const returnPromise = this._returnsPromise.then(() => data); - this._returnsPromise = returnPromise; - ( returnPromise).totalLength = ( returnPromise).dataLength = Promise.resolve(fullData.length); - return returnPromise as FetchResult; - } - - get(ids: string[]): Promise { - const data = this._data; - const objects: T[] = []; - return Promise.resolve(ids.reduce((prev, next) => - this._index.has(next) ? prev.concat( data[this._index.get(next)!] ) : prev, objects - )); - } - - identify(items: T[]| T): string[] { - const itemArray = Array.isArray(items) ? items : [ items ]; - const idProperty = this._idProperty; - if (idProperty) { - return itemArray.map((item) => item[idProperty].toString()); - } - else if (this._idFunction) { - return itemArray.map(this._idFunction); - } - - return itemArray.map((item) => ( item).id); - } - - patch(updates: { id: string; patch: Patch }[]): Promise> { - const data = this._data; - - const filteredUpdates = updates.filter((update) => { - return this._index.has(update.id); - }); - const oldIndices = filteredUpdates.map((update) => { - return this._index.get(update.id)!; - }); - - try { - const updatedItems = filteredUpdates.map((update, index) => { - const item = duplicate(data[oldIndices[index]]); - const updatedItem = update.patch.apply(item); - data[oldIndices[index]] = updatedItem; - return updatedItem; - }); - // Don't control the order operations are executed in, but make sure that the results - // resolve in the order they were actually executed in. - const returnsPromise = this._returnsPromise.then(() => ({ - successfulData: updatedItems, - type: StoreOperation.Patch - })); - this._returnsPromise = returnsPromise; - return returnsPromise; - } catch (error) { - return Promise.reject(error); - } - } - - put(items: T[], options?: CrudOptions): Promise> { - try { - const result = this._putSync(items, options); - // Don't control the order operations are executed in, but make sure that the results - // resolve in the order they were actually executed in. - const returnPromise = this._returnsPromise.then(() => result); - this._returnsPromise = returnPromise; - return returnPromise; - } catch (error) { - return Promise.reject(error); - } - } -} diff --git a/src/storage/IndexedDBStorage.ts b/src/storage/IndexedDBStorage.ts deleted file mode 100644 index b8f907a..0000000 --- a/src/storage/IndexedDBStorage.ts +++ /dev/null @@ -1,610 +0,0 @@ -import Promise from '@dojo/shim/Promise'; -import Set from '@dojo/shim/Set'; -import InMemoryStorage from './InMemoryStorage'; -import { StoreOptions, CrudOptions, StoreOperation, UpdateResults, FetchResult, Query, QueryType } from '../interfaces'; -import Patch from '../patch/Patch'; -import CompoundQuery from '../query/CompoundQuery'; -import { Filter, FilterType, FilterChainMember, BooleanOp, SimpleFilter } from '../query/createFilter'; - -export type Indices = { - [ P in keyof T ]?: any; -}; - -export interface IndexedDBOptions extends StoreOptions { - /** - * The name to use for the database. Should be provided since this could potentially conflict - * with other existing databases - */ - dbName?: string; - - /** - * Indicates which properties to create an index for - */ - indices?: Indices; - - /** - * The name of the object store in which items should be stored. If not provided this will default - * to 'items' - */ - objectStoreName?: string; - - /** - * The version of the database if it already exists but the object store needs to be created. If this - * is creating a new database, or accessing an existing database that already has an objectStore for - * items, then this is not required. - */ - version?: number; -} - -export interface IndexQueryDescriptor { - /** - * Indicates whether the first of two filters was used. - */ - firstFilterUsed?: boolean; - - /** - * The index to query against - */ - index?: IDBIndex; - - /** - * Key range to use for the query - */ - keyRange?: IDBKeyRange; - - /** - * Indicates whether the second of two filters was used - */ - secondFilterUsed?: boolean; -} - -/** - * A query that has been broken up according to when the queries can be applied. initialFilters and initialQueries can - * be applied to the items and/or the accumulating collection of items incrementally, and can possibly even be converted - * into indexedDB queries. The inMemoryQuery contains the first non-incremental query(e.g. a range query), which needs - * to be applied to the collection as a whole to function properly. This query has to be applied after the indexedDB - * cursor has completed iterating over the available items and a candidate set of data has been constructed. - */ -interface DividedQuery { - initialQueries: Query[]; - initialFilters: SimpleFilter[]; - inMemoryQuery?: Query; -} - -/** - * Describes the properties of an IDBKeyRange - */ -interface KeyRangeDescriptor { - lowerBound?: any; - lowerInclusive?: boolean; - upperBound?: any; - upperInclusive?: boolean; -} - -/** - * Creates a promise that resolves when the provided request is successful, and fails when there is an error with the - * request. If this request involves writing data, that data will not be available for querying until the transaction - * is completed. A promise that resolves when the transaction completes can be provided, and in that case the request - * promise will not resolve until after that promise, ensuring that any operations performed after resolution of this - * promise will have access to the data. - * @param request The request to wait for - * @param transactionPromise Optional promise that indicates when a transaction is completed in case resolution should - * occur after the data is actually available - * @returns {Promise} - */ -export function createRequestPromise(request: IDBRequest, transactionPromise?: Promise): Promise { - const requestPromise = new Promise((resolve, reject) => { - request.onsuccess = (event: any) => { - resolve(event.target.result); - }; - request.onerror = (event) => { - if (request.error.name === 'ConstraintError') { - event.preventDefault(); - } - reject(request.error); - }; - }); - - if (transactionPromise) { - return transactionPromise.then(() => requestPromise); - } - - return requestPromise; -} - -/** - * Determine whether a given query is a filter - */ -function isFilter(filter?: Query | BooleanOp): filter is Filter { - return Boolean( - filter !== BooleanOp.Or && filter !== BooleanOp.And && (> filter).queryType === QueryType.Filter - ); -} - -/** - * Gets the key for the passed in filter, or undefined if the filter does not exist, doesn't have a key, or has - * a nested key - * @param filter The filter to get the key for - */ -function getKey(filter: SimpleFilter): string | undefined { - const path = filter.path; - if (!path) { - return undefined; - } - if (typeof path === 'string') { - return path; - } - const segments = path.segments; - if (segments.length === 1) { - return segments[0]; - } - return undefined; -} - -/** - * Creates a key range query based on the provided parameters or undefined if they are not valid - */ -function createKeyRange({ lowerBound, lowerInclusive, upperBound, upperInclusive }: KeyRangeDescriptor) { - if (typeof lowerBound !== 'undefined' && typeof upperBound !== 'undefined') { - return IDBKeyRange.bound(lowerBound, upperBound, !lowerInclusive, !upperInclusive); - } - if (typeof lowerBound !== 'undefined') { - return IDBKeyRange.lowerBound(lowerBound, !lowerInclusive); - } - if (typeof upperBound !== 'undefined') { - return IDBKeyRange.upperBound(upperBound, !upperInclusive); - } - return undefined; -} - -/** - * Gets the first two filters from the provided filter chain if they can be combined as a single filter(they are separated - * by an AND) - */ -function getFilters(filterChain?: FilterChainMember[]) { - const firstFilterOrOp: SimpleFilter | BooleanOp | undefined = filterChain && filterChain[0]; - const secondFilterOrOp: SimpleFilter | BooleanOp | undefined = filterChain && filterChain[2]; - let firstFilter: SimpleFilter | undefined = undefined; - let secondFilter: SimpleFilter | undefined = undefined; - if (filterChain) { - if (isFilter(firstFilterOrOp)) { - firstFilter = firstFilterOrOp; - filterChain.shift(); - } - if (firstFilter && filterChain[0] === BooleanOp.And && isFilter(secondFilterOrOp)) { - secondFilter = secondFilterOrOp; - filterChain.splice(0, 2); - } - } - - return { firstFilter, secondFilter }; -} - -/** - * Checks the type of a filter and returns whether it can be used in a query against an IndexedDB index, and some - * information about the key range to use for the query. - * @param filter The filter to check - * @param value The value being filtered against - */ -function checkFilter(filter: SimpleFilter<{}>, value: any): { - keyRangeDescriptor: KeyRangeDescriptor, isFilterUsable: boolean -} { - if (filter.filterType === FilterType.LessThan || filter.filterType === FilterType.LessThanOrEqualTo) { - return { - keyRangeDescriptor: { - upperBound: value, - upperInclusive: filter.filterType === FilterType.LessThanOrEqualTo - }, - isFilterUsable: true - }; - } - if (filter.filterType === FilterType.GreaterThan || filter.filterType === FilterType.GreaterThanOrEqualTo) { - return { - keyRangeDescriptor: { - lowerBound: value, - lowerInclusive: filter.filterType === FilterType.GreaterThanOrEqualTo - }, - isFilterUsable: true - }; - } - return { keyRangeDescriptor: {}, isFilterUsable: false }; -} - -/** - * Updates a key range descriptor based on an additional filter, and adds a flag indicating whether the additional - * filter resulted in a change in the key range. - * @param filter - * @param existingPath - * @param value - * @param upperBound - * @param lowerBound - * @param upperInclusive - * @param lowerInclusive - * @returns {any} - */ -function updateKeyRangeDescriptor( - existingPath: string, - value: any, - { upperBound, lowerBound, upperInclusive, lowerInclusive }: KeyRangeDescriptor, - filter?: SimpleFilter<{}> -): { keyRangeDescriptor: KeyRangeDescriptor, isFilterUsable: boolean } { - const defaultReturnValue = { - keyRangeDescriptor: { upperBound, lowerBound, upperInclusive, lowerInclusive }, - isFilterUsable: false - }; - if (!filter || getKey(filter) !== existingPath || typeof value === 'undefined') { - return defaultReturnValue; - } - if ((filter.filterType === FilterType.LessThan || - filter.filterType === FilterType.LessThanOrEqualTo) && typeof upperBound === 'undefined') { - return { - keyRangeDescriptor: { - lowerBound, - lowerInclusive, - upperBound: value, - upperInclusive: filter.filterType === FilterType.LessThanOrEqualTo - }, - isFilterUsable: true - }; - } - if ((filter.filterType === FilterType.GreaterThan || - filter.filterType === FilterType.GreaterThanOrEqualTo) && typeof lowerBound === 'undefined') { - return { - keyRangeDescriptor: { - upperBound, - upperInclusive, - lowerBound: value, - lowerInclusive: filter.filterType === FilterType.GreaterThanOrEqualTo - }, - isFilterUsable: true - }; - } - return defaultReturnValue; -} - -function divideQuery(query?: Query): DividedQuery { - if (query) { - const queries = query instanceof CompoundQuery && query.queries; - if (query.incremental) { - if (queries) { - return { - initialFilters: []> queries.filter((query) => isFilter(query)), - initialQueries: queries.filter((query) => !isFilter(query)) - }; - } - if (isFilter(query)) { - return { initialFilters: [ > query ], initialQueries: [] }; - } - return { initialFilters: [], initialQueries: [ query ] }; - } - if (queries) { - // Incremental queries can be applied to the items as they are iterated through, but - // non-incremental queries need to full data set available before they can be applied. - // This locates the first non incremental query, so that the queries can be split up - // and applied while iterating through results or after results are collected as - // appropriate. Filters and sorts are further broken up so that filters are applied - // individually to new items rather than the entire result set. - let splitIndex = 0; - queries.some((query, index) => { - if (!query.incremental) { - splitIndex = index; - return true; - } - else { - return false; - } - }); - const initialQueries = queries.slice(0, splitIndex); - const inMemoryQueries = queries.slice(splitIndex); - - return { - inMemoryQuery: inMemoryQueries.reduce((previousValue, nextValue) => - new CompoundQuery({ - query: previousValue - }).withQuery(nextValue) - ), - initialQueries: initialQueries.filter((query) => !isFilter(query)), - initialFilters: []> initialQueries.filter((query) => isFilter(query)) - }; - } - return { inMemoryQuery: query, initialQueries: [], initialFilters: [] }; - } - return { initialQueries: [], initialFilters: [] }; -} - -export default class IndexedDBStorage extends InMemoryStorage { - /** - * The database - */ - private _db: IDBDatabase; - - /** - * A promise that resolves when the database is finished initializing. Subsequent operations will be delayed until - * this promise resolves. If initialization fails, any subsequent operations will return an appropriate error as - * well. - */ - private _dbInitializationPromise: Promise; - - /** - * The name of the database - */ - private _dbName: string; - - /** - * Indices used for better query performance - */ - private _indices = new Set(); - - /** - * The name of the object store in which items are stored - */ - private _objectStoreName: string; - - constructor(options: IndexedDBOptions = {}) { - super(options); - let resolveInitialPromise: Function; - let rejectInitialPromise: Function; - const initializationPromise = new Promise((resolve, reject) => { - resolveInitialPromise = resolve; - rejectInitialPromise = reject; - }); - this._dbName = options.dbName || 'store-database'; - this._objectStoreName = options.objectStoreName || 'items'; - this._dbInitializationPromise = initializationPromise; - - // IE doesn't respond well to passing an undefined version, so check whether it was provided before opening - // the connection - const request = options.version ? indexedDB.open(this._dbName, options.version) : indexedDB.open(this._dbName); - let upgrading = false; - - request.onupgradeneeded = (event: any) => { - upgrading = true; - const db = event.target.result; - this._db = db; - - let objectStore: IDBObjectStore; - if (!db.objectStoreNames.contains(this._objectStoreName)) { - objectStore = db.createObjectStore(this._objectStoreName); - } - else { - objectStore = request.transaction.objectStore(this._objectStoreName); - } - const indices = options && options.indices; - if (indices) { - Object.keys(indices).forEach((index) => { - this._indices.add(index); - if (!objectStore.indexNames.contains(index)) { - objectStore.createIndex(index, index); - } - }); - } - - }; - - createRequestPromise(request).then( - (db) => { - this._db = db; - resolveInitialPromise(db); - return db; - }, - (error) => { - rejectInitialPromise(error); - } - ); - } - - /** - * Creates a cursor request based on the provided filters, and object store. - * Potentially modifies initialFilters depending on whether some filters can be used to generate an IDBKeyRange to apply - * to the DB - * @param initialFilters - * @param objectStore - * @returns {IDBRequest} - */ - protected _createCursorRequest(initialFilters: SimpleFilter[], objectStore: IDBObjectStore) { - const filterChain = initialFilters[0] && initialFilters[0].filterChain; - let { firstFilter, secondFilter } = getFilters(filterChain); - if (filterChain && !filterChain.length) { - initialFilters.shift(); - } - let { keyRange, index, firstFilterUsed, secondFilterUsed } = this._getKeyRangeAndIndex( - objectStore, firstFilter, secondFilter - ); - if (!secondFilterUsed && secondFilter) { - initialFilters.unshift(secondFilter); - } - if (!firstFilterUsed && firstFilter) { - initialFilters.unshift(firstFilter); - } - if (keyRange && index) { - return index.openCursor(keyRange); - } - else { - return objectStore.openCursor(); - } - } - - /** - * Determines whether the provided filter can be applied as a query on an indexed property in the object - * store, and if so applies it and returns . Otherwise returns undefined - * @param objectStore The object store to retrieve an index from - * @param filter The first filter to use to generate a key range - * @param secondFilter An additional filter to use to generate a key range - */ - protected _getKeyRangeAndIndex( - objectStore: IDBObjectStore, filter?: SimpleFilter, secondFilter?: SimpleFilter - ): IndexQueryDescriptor { - const valueOne = filter && filter.value; - if (filter && typeof valueOne !== 'undefined') { - const pathOne = getKey(filter); - if (pathOne) { - if (this._indices.has(pathOne) && objectStore.indexNames.contains(pathOne)) { - const index = objectStore.index(pathOne); - if (filter.filterType === FilterType.EqualTo) { - return { keyRange: IDBKeyRange.only(filter.value), index, firstFilterUsed: true }; - } - - const { keyRangeDescriptor, isFilterUsable: firstFilterUsed } = checkFilter(filter, valueOne); - const valueTwo = secondFilter && secondFilter.value; - const { - keyRangeDescriptor: updatedDescriptor, isFilterUsable: secondFilterUsed - } = updateKeyRangeDescriptor(pathOne, valueTwo, keyRangeDescriptor, secondFilter); - - return { - keyRange: createKeyRange(updatedDescriptor), - index, - firstFilterUsed, - secondFilterUsed - }; - } - } - } - - return {}; - } - - /** - * Creates a new transaction, and returns a promise that will resolve when that transaction is completed, and a reference - * to the object store specified by the _objectStoreName property. Provides a read only transaction - * unless write access is required. - * @param needsWriteAccess Whether this request requires write access. - */ - protected _getTransactionAndObjectStore(needsWriteAccess?: boolean) { - const db = this._db; - if (!db) { - throw Error('Can\'t create transaction because database does not exist'); - } - const transaction = needsWriteAccess ? - db.transaction(this._objectStoreName, 'readwrite') : db.transaction(this._objectStoreName); - const transactionPromise = new Promise((resolve, reject) => { - transaction.oncomplete = () => { - resolve(); - }; - transaction.onerror = transaction.onabort = (event: any) => { - reject(event.target.error); - }; - }); - return { transactionPromise, objectStore: transaction.objectStore(this._objectStoreName) }; - } - - add(items: T[], { rejectOverwrite = true, id }: CrudOptions = { rejectOverwrite: true } ): Promise> { - return this.put(items, { rejectOverwrite, id }) - .then((results) => { - results.type = StoreOperation.Add; - return results; - }); - } - - delete(ids: string[]): Promise> { - return this._dbInitializationPromise.then(() => { - const { transactionPromise, objectStore } = this._getTransactionAndObjectStore(true); - return Promise.all(ids.map((id) => createRequestPromise( - objectStore.delete(id), transactionPromise - ) - .then(() => id))) - .then((ids) => ({ - type: StoreOperation.Delete, - successfulData: ids - })); - }); - } - - fetch(query?: Query): FetchResult { - let resolveTotalLength: (totalLength: number) => void; - let rejectTotalLength: (error: any) => void; - const totalLength = new Promise((resolve, reject) => { - resolveTotalLength = resolve; - rejectTotalLength = reject; - }); - const fetchResult: FetchResult = this._dbInitializationPromise.then( - () => { - return new Promise((resolve) => { - const { objectStore } = this._getTransactionAndObjectStore(); - const request = objectStore.count(); - const totalLengthPromise = createRequestPromise(request); - totalLengthPromise.then(() => { - resolveTotalLength(request.result); - }, rejectTotalLength); - let items: T[] = []; - const { initialQueries, initialFilters, inMemoryQuery } = divideQuery(query); - - this._createCursorRequest(initialFilters, objectStore).onsuccess = (event: any) => { - const cursor = event.target.result; - if (cursor) { - // Apply filters and if the new item is not filtered, apply other - // queries to the collection after adding it in. - const newItems = initialFilters.reduce((prev, next) => { - if (prev.length) { - return next.apply(prev); - } else { - return prev; - } - }, [ cursor.value ]); - if (newItems.length) { - items = initialQueries.reduce( - (prev, next) => next.apply(prev), [ ...items, newItems[0] ] - ); - } - cursor.continue(); - } - else { - resolve(inMemoryQuery ? inMemoryQuery.apply(items) : items); - } - }; - }); - } - ); - fetchResult.totalLength = totalLength; - return fetchResult; - } - - get(ids: string[]): Promise { - return this._dbInitializationPromise.then(() => { - const { objectStore } = this._getTransactionAndObjectStore(); - return Promise.all(ids.map((id) => { - const request = objectStore.get(id); - return createRequestPromise(request).then(() => request.result); - })) - .then((data) => data.filter((item) => Boolean(item))); - }); - } - - patch(updates: { id: string; patch: Patch }[]): Promise> { - return this._dbInitializationPromise.then(() => { - const { transactionPromise, objectStore } = this._getTransactionAndObjectStore(false); - return Promise.all(updates.map((update) => - createRequestPromise(objectStore.get(update.id), transactionPromise).then((item) => { - const { transactionPromise, objectStore } = this._getTransactionAndObjectStore(true); - const updated = update.patch.apply(item); - return createRequestPromise( - objectStore.put(updated, update.id), transactionPromise - ) - .then(() => updated); - }) - )) - .then((updatedItems) => ({ - type: StoreOperation.Patch, - successfulData: updatedItems - })); - }); - } - - put(items: T[], options?: CrudOptions): Promise> { - return this._dbInitializationPromise.then(() => { - const { transactionPromise, objectStore } = this._getTransactionAndObjectStore(true); - const rejectOverwrite = options && options.rejectOverwrite; - const ids = this.identify(items); - return Promise.all(items.map((item, index) => { - const id = ids[index]; - return createRequestPromise( - rejectOverwrite ? objectStore.add(item, id) : objectStore.put(item, id), transactionPromise - ) - .then(() => item); - })) - .then((updatedItems) => ({ - type: StoreOperation.Put, - successfulData: updatedItems - })); - }); - } -} diff --git a/src/store/ObservableStore.ts b/src/store/ObservableStore.ts deleted file mode 100644 index 783ebb8..0000000 --- a/src/store/ObservableStore.ts +++ /dev/null @@ -1,704 +0,0 @@ -import { after } from '@dojo/core/aspect'; -import { Observable, Observer } from '@dojo/core/Observable'; -import { debounce } from '@dojo/core/util'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import Set from '@dojo/shim/Set'; -import { - CrudOptions, Store, StoreOptions, UpdateResults, Query, PatchArgument, StoreObservable, - FetchResult -} from '../interfaces'; -import StoreBase from './StoreBase'; - -export interface StoreDelta { - /** - * New items added since the last delta - */ - adds: T[]; - - /** - * The state of the store after all of these updates. Doesn't necessarily - * reflect the current state of the underlying Storage, as it updates the local - * storage based on the known updates if fetchAroundUpdates is false - */ - afterAll: T[]; - - /** - * The state of the store before any of these updates. - */ - beforeAll: T[]; - - /** - * The IDs of any deleted items - */ - deletes: string[]; - - /** - * Items updated since the last delta - */ - updates: T[]; -} - -/** - * Combines several sequential deltas into a single delta. - * It performs several checks to remove redundant data. - * - Checks for repeated copies items with the same ID in - * adds and updates, or just the same ID in deletes, and keeps - * only the last. - * - Checks for deletes followed by adds or updates and replaces with a - * single update - * - Checks for adds followed by deletes and removes both - * - Checks for updates followed by deletes and removes the update - * @param instance The instance that can identify these items - * @param currentUpdate The current store delta - * @param newUpdate The new update to merge - * @returns The merged delta - */ -export function mergeDeltas( - instance: { identify(items: T | T[]): string[] }, - currentUpdate: StoreDelta, - newUpdate: StoreDelta -): StoreDelta { - /** - * Takes the last instance of an item repeated in the list - * @param items Added or updated items - * @returns The added or updated items with repeated items replaced by only the latest version of the item - */ - function takeLastItem(items: T[]): T[] { - const found: { [ index: string ]: boolean} = {}; - const ids = instance.identify(items); - return items.reverse() - .filter((_, index) => { - const id = ids[index]; - const exists = Boolean(found[id]); - found[id] = true; - return !exists; - }) - .reverse(); - } - - /** - * Takes the last instance of an id repeated in the list - * @param ids IDs of deleted items - * @returns The list with duplicates removed - */ - function takeLastId(ids: string[]): string[] { - const found: { [ index: string ]: boolean} = {}; - return ids.reverse() - .filter((id) => { - const exists = Boolean(found[id]); - found[id] = true; - return !exists; - }) - .reverse(); - } - - /** - * Removes updates for items that were later deleted - * @param newDeletes Deletes from delta(s) after the updates - * @param oldUpdates Updates from delta(s) before the deletes - * @return The updates without updates for subsequently deleted items - */ - function removeOutdatedItems(newDeletes: string[], oldUpdates: T[]) { - const deletedIds = newDeletes.reduce((prev, next) => { - prev.set(next, null); - return prev; - }, new Map()); - const ids = instance.identify(oldUpdates); - return oldUpdates.filter((_, index) => { - return !deletedIds.has(ids[index]); - }); - } - - /** - * Finds cases where an older update has an add, and a newer update has a delete, and removes - * both, since the net effect is that the operations are cancelled out - * @param newDeletes Deletes form delta(s) after the adds - * @param oldAdds Adds from delta(s) before the deletes - * @returns An object with the filtered adds and deletes - */ - function removeCancellingUpdates(newDeletes: string[], oldAdds: T[]) { - const deletedIds = newDeletes.reduce((prev, next) => { - prev.set(next, null); - return prev; - }, new Map()); - const ids = instance.identify(oldAdds); - const addIds = ids.reduce((prev, next) => { - prev.set(next, null); - return prev; - }, new Map()); - return { - oldAdds: oldAdds.filter((_, index) => { - return !deletedIds.has(ids[index]); - }), - newDeletes: newDeletes.filter((id) => !addIds.has(id)) - }; - } - - /** - * Finds places where an item was deleted and then added or updated, and removes the delete. If the item was added, - * the add is also replaced with an update since it should already exist in the collection receiving the updates, - * as it will never receive the delete - * @param oldDeletes - Deletes from delta(s) before the adds and updates - * @param newAdds - Adds from delta(s) after the deletes - * @param newUpdates - Updates from delta(s) after the deletes - * @returns An object containing the updated deletes, adds, and updates - */ - function convertReplacementToUpdate(oldDeletes: string[], newAdds: T[], newUpdates: T[]) { - const deletes = oldDeletes.reduce((prev, next) => { - prev.set(next, null); - return prev; - }, new Map()); - const addIds = instance.identify(newAdds); - const updateIds = instance.identify(newUpdates); - const adds = addIds.concat(updateIds) - .reduce((prev, next) => { - prev.set(next, null); - return prev; - }, new Map()); - const updatedUpdates = newUpdates.slice(); - return { - oldDeletes: oldDeletes.filter((id) => !adds.has(id)), - newAdds: newAdds.filter((item, index) => { - const shouldKeep = !deletes.has(addIds[index]); - if (!shouldKeep) { - // Always add it to the beginning, because it may have been updated as well, but the add - // has to have come first. - updatedUpdates.unshift(item); - } - return shouldKeep; - }), - newUpdates: updatedUpdates - }; - } - - const { oldDeletes, newAdds, newUpdates } = convertReplacementToUpdate( - currentUpdate.deletes, newUpdate.adds, newUpdate.updates - ); - const oldUpdates = removeOutdatedItems(newUpdate.deletes, currentUpdate.updates); - const { newDeletes, oldAdds } = removeCancellingUpdates(newUpdate.deletes, currentUpdate.adds); - return { - adds: takeLastItem([ ...oldAdds, ...newAdds ]), - afterAll: newUpdate.afterAll, - beforeAll: currentUpdate.beforeAll, - deletes: takeLastId([ ...oldDeletes, ...newDeletes ]), - updates: takeLastItem([ ...oldUpdates, ...newUpdates ]) - }; -} - -/** - * An update for a single item, used to identify which item an update is for when multiple items are observed - * simultaneously. Deletes are indicated by the item property being undefined. - */ -export interface ItemUpdate { - item?: T; - id: string; -} - -export interface ObservableStoreInterface> extends Store { - /** - * 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>; - /** - * 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; - /** - * 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>; -} - -export interface ObservableStoreOptions extends StoreOptions { - /** - * If true, then the local collection will automatically fetch to get the latest data from the store whenver - * an update is made. - */ - fetchAroundUpdates?: boolean; - /** - * Specifies how long the fetch around updates should be debounced to avoid rapidly fetching when many updates - * are made within close proximity. Defaults to 200 milliseconds - */ - fetchAroundUpdateDebounce?: number; -} - -export type ObserverSetEntry = { observes: Set; observer: Observer> }; -/** - * Build a map of ids to indices for the provided collection. This requires that the array of IDs is either what - * the index if for, or that the array of items the IDs represent is in the same order, which is already the case - * if the IDs were generated using the Store's identify function. - * @param ids - The IDS to build the index for - * @returns An index mapping ids to indices - */ -export function buildIndex(ids: string[]): Map { - return ids.reduce((map, id, index) => { - map.set(id, index); - return map; - }, new Map()); -} - -/** - * Determines whether this is a single observer or a set entry - * @param observer - * @returns {boolean} - */ -function isObserverEntry(observer: Observer | ObserverSetEntry): observer is ObserverSetEntry { - return ( observer).observes instanceof Set; -} - -/** - * Determines whether this is a single observer or a set entry - * @param observer - * @returns {boolean} - */ -function isObserver(observer: Observer | ObserverSetEntry): observer is Observer { - return !isObserverEntry(observer); -} - -class ObservableStore extends StoreBase implements ObservableStoreInterface> { - /** - * A debounced function called to fetch the latest data and send updates to observers after each crud operation, - * if fetchAroundUpdates is true. - */ - private _fetchAndSendUpdates: (store: ObservableStoreInterface>) => void; - - /** - * Flag indicating that data was passed in the constructor, and we should ignore the first add - * - */ - private _ignoreFirstAdd: boolean; - - /** - * When `fetchAroundUpdates` is true, this promise is used to wait for the first fetch before sending out initial - * updates, since `localData` will be out of date as soon as the fetch completes. - */ - private _initialFetch?: Promise; - - /** - * Maps item IDs to observers for that item, or sets of observers. For Single item observers this is a one-to-many - * relationship. For `ObserverSetEntries`, this is a many to many relationship, each item can be observed as a part - * of many sets, and each set is linked to all of the items within it. - */ - private _itemObservers = new Map | ObserverSetEntry)[]>(); - - /** - * The latest local data - */ - private _localData: T[] = []; - - /** - * Maps item IDs to indices in `localData` - */ - private _localIndex = new Map(); - - /** - * All the observers of the store - */ - private _observers: Observer>[] = []; - - /** - * The single _observable provided to all observers of the store - */ - private _storeObservable: Observable>; - - /** - * Updates currently waiting to be merged and sent - */ - private _queuedUpdate?: StoreDelta; - - protected _fetchAroundUpdates: boolean; - constructor(options?: ObservableStoreOptions) { - super(options); - options = options || {}; - this._fetchAroundUpdates = Boolean(options.fetchAroundUpdates); - this._fetchAndSendUpdates = debounce((store: ObservableStore) => { - store.fetch(); - }, options.fetchAroundUpdateDebounce || 20); - this._ignoreFirstAdd = Boolean(options.data); - if (options.fetchAroundUpdates) { - this._initialFetch = this.fetch(); - } - this._storeObservable = new Observable>((observer: Observer>) => { - this._observers.push(observer); - if (this._initialFetch) { - this._initialFetch.then(() => { - observer.next({ - updates: [], - deletes: [], - adds: [], - beforeAll: [], - afterAll: this._localData.slice() - }); - }); - } - else { - observer.next({ - updates: [], - deletes: [], - adds: [], - beforeAll: [], - afterAll: this._localData.slice() - }); - } - return () => { - const remove = (observer: Observer>) => { - this._observers.splice(this._observers.indexOf(observer), 1); - }; - setTimeout(() => { - remove(observer); - }); - }; - }); - } - - /** - * Takes a collection of items and creates a new copy modified according to the provided updates. This can be used to - * attempt to track updates in the local collection when fetching after each update is disabled. - * @param update - * @returns A new collection with the modifications specified by the update - */ - private _addUpdateDelete(update: StoreDelta) { - const newData = this._localData.slice(); - update.adds.forEach((item) => { - newData.push(item); - }); - - this.identify(update.updates).forEach((id, index) => { - if (this._localIndex.has(id)) { - newData[this._localIndex.get(id)!] = update.updates[index]; - } - else { - newData.push(update.updates[index]); - } - }); - - update.deletes.sort().reverse().forEach((id) => { - if (this._localIndex.has(id)) { - newData.splice(this._localIndex.get(id)!, 1); - } - }); - - return newData; - } - - /** - * - * Iterates through the provided items and/or IDs and notifies observers. If items is provided, then the - * observers for that item, and the observers for sets of items that include that are updated. If items is null, then - * these are delete notifications for observers of multiple items. In this case, no update is sent to individual - * observers, and observers of sets receive `ItemUpdate` objects with the IDs of the deleted items and an undefined - * item - * - * @param items Items to send updates for, or null if these are delete notifications for item set observers - * @param ids - IDs of the items, should be in the same order as items - */ - private _notifyItemObservers(items: T[] | null, ids: string[]) { - const notify = (id: string, after?: any) => { - if (this._itemObservers.has(id)) { - this._itemObservers.get(id)!.map((observerOrEntry): Observer> | null => { - if (isObserverEntry(observerOrEntry)) { - return observerOrEntry.observer; - } - else { - return null; - } - }).filter((observerEntry) => { - return observerEntry; - }).forEach((observer: Observer>) => { - observer.next({ - item: after, - id: id - }); - }); - if (after) { - this._itemObservers.get(id)!.map((observerOrEntry): Observer | null => { - if (isObserver(observerOrEntry)) { - return observerOrEntry; - } - else { - return null; - } - }).filter((observer) => { - return observer; - }).forEach((observer: Observer) => { - observer.next(after); - }); - } - } - }; - if (items) { - items.forEach((after: T, index: number) => { - const id = ids[index] || this.identify(after); - notify(id, after); - }); - } - else { - ids.forEach((id) => { - notify(id, undefined); - }); - } - } - - /** - * Merges the latest queued updates, updates the local data and index based on the latest data, - * sends out updates to observers, and then removes observers that unsubscribed during the update process from the list - * of observers. If after is provided, it is assumed that that is the latest data for the store, if it is not provided - * the local data is updated according to the merged delta and that is used as the new local data. - * @param after - Optional array of items containing the latest data for the store. - */ - private _sendUpdates(after?: T[]) { - const storeDelta = this._queuedUpdate || { - updates: [], - adds: [], - deletes: [], - beforeAll: [], - afterAll: [] - }; - this._queuedUpdate = undefined; - after = after || this._addUpdateDelete(storeDelta); - - storeDelta.beforeAll = this._localData; - storeDelta.afterAll = after; - this._localData = after; - this._localIndex = buildIndex(this.identify(after)); - - this._observers.forEach((observer) => { - observer.next({ - updates: storeDelta.updates.slice(), - adds: storeDelta.adds.slice(), - deletes: storeDelta.deletes.slice(), - beforeAll: storeDelta.beforeAll.slice(), - afterAll: storeDelta.afterAll.slice() - }); - }); - } - - /** - * Queues the appropriate update and then either starts up a fetch or just triggers sending the updates depending - * on the `fetchAroundUpdates` flag - * @param updates Updated items - * @param adds Added items - * @param deletes Deleted IDs - */ - private _sendUpdatesOrFetch(updates: T[], adds: T[], deletes: string[]) { - const newUpdate = { - adds: adds, - afterAll: [], - beforeAll: [], - deletes: deletes, - updates: updates - }; - this._queuedUpdate = this._queuedUpdate ? mergeDeltas(this, this._queuedUpdate, newUpdate) : newUpdate; - if (this._fetchAroundUpdates) { - this._fetchAndSendUpdates(this); - } - else { - this._sendUpdates(); - } - } - - /** - * After the add is completed notify observers. If this is the initial add AND we are fetching around - * updates, then the first update to subscribers will already contain this data, since the initial fetch - * is performed after the initial add. In this case we do not need to send an update. We can tell this - * is the first add because it'll be triggered in the StoreBase base before the state is created for - * this instance in the mixin's initializer - */ - add(items: T[] | T, options?: CrudOptions): StoreObservable> { - const result = super.add(items, options); - result.then( - (addedItems: T[]) => { - if (!this._ignoreFirstAdd || !this._fetchAroundUpdates) { - this._sendUpdatesOrFetch([], addedItems, []); - } - this._ignoreFirstAdd = false; - }, - // Ignore errors here, they should be handled by the caller not observers - () => {} - ); - return result; - } - - /** - * After the items are deleted, notify item set observers of the deletion of one of the items they are - * observing, and then complete any observables that need to be completed. - * Completing observables is dones as follows - * - For observers of a single item, just complete the observer - * - For observers of a set of items - * - Remove the deleted ID of this item from the set of observed IDs - * - If there are now no observed IDs for the set, complete the _observable - * - Remove the item observer entry for the deleted ID - */ - delete(ids: string[] | string) { - const result = super.delete(ids); - result.then( - (deleted: string[]) => { - this._notifyItemObservers(null, deleted); - deleted.forEach((id: string) => { - if (this._itemObservers.has(id)) { - this._itemObservers.get(id)!.forEach((observerOrEntry) => { - if (isObserver(observerOrEntry)) { - observerOrEntry.complete(); - } - else { - observerOrEntry.observes.delete(id); - if (!observerOrEntry.observes.size) { - observerOrEntry.observer.complete(); - } - } - }); - this._itemObservers.delete(id); - } - }); - this._sendUpdatesOrFetch([], [], deleted); - }, - // Ignore errors here, they should be handled by the caller not observers - () => {} - ); - return result; - } - - /** - * After fetching, sends updates if no query was used. If a custom query was used then the data retrieved - * is not indicative of the local data and can't be used. We shouldn't apply the query locally because we - * have no knowledge of the underlying storage implementation or the amount of data and it may be too much - * data to retrieve or update in memory. If this is the initialFetch, don't update since that update - * will be sent to each subscriber at the time of subscription. If we're not sending updates, still set - * the local data and index to the newly retrieved data. - */ - fetch(query?: Query): FetchResult { - const result = super.fetch(query); - if (!query) { - result.then( - (data) => { - if (result !== this._initialFetch) { - this._sendUpdates(data); - } - else { - this._localData = data; - this._localIndex = buildIndex(this.identify(data)); - } - }, - // Ignore errors here, they should be handled by the caller not observers - () => {} - ); - } - return result; - } - - observe(): Observable>; - observe(id: string): Observable; - observe(ids: string[]): Observable>; - observe(idOrIds?: string | string[]): Observable> | Observable | Observable> { - if (idOrIds) { - if (Array.isArray(idOrIds)) { - const ids = idOrIds; - - const idSet = new Set(ids); - return new Observable>((observer: Observer>) => { - const observerEntry: ObserverSetEntry = { - observes: idSet, - observer: observer - }; - ids.forEach((id: string) => { - if (this._itemObservers.has(id)) { - this._itemObservers.get(id)!.push(observerEntry); - } - else { - this._itemObservers.set(id, [observerEntry]); - } - }); - const foundIds = new Set(); - after(observer, 'next', (result: any, args: IArguments) => { - const itemUpdate: ItemUpdate = args[0]; - foundIds.add(itemUpdate.id); - return result; - }); - - this.get(ids) - .then((items: T[]) => { - if (foundIds.size !== ids.length) { - const retrievedIdSet = new Set(this.identify(items)); - let missingItemIds = ids.filter(id => !retrievedIdSet.has(id)); - - if (retrievedIdSet.size !== idSet.size || missingItemIds.length) { - observer.error(new Error(`ID(s) "${missingItemIds}" not found in store`)); - } - else { - items.forEach((item, index) => observer.next({ - item: item, - id: ids[index] - })); - } - } - }); - }); - } - const id = idOrIds; - return new Observable((observer: Observer) => { - this.get(id) - .then((item: any) => { - if (!item) { - observer.error(new Error(`ID "${id}" not found in store`)); - } - else { - if (this._itemObservers.has(id)) { - this._itemObservers.get(id)!.push(observer); - } - else { - this._itemObservers.set(id, [ observer ]); - } - observer.next(item); - } - }); - }); - } - return this._storeObservable; - } - - /** - * After the patch is completed, notify the item observers, and then either queue a fetch to send updates - * if fetchAroundUpdates is true, or just send updates if not. - */ - patch(updates: PatchArgument, options?: CrudOptions) { - const result = super.patch(updates, options); - result.then( - (updatedItems: T[]) => { - this._notifyItemObservers(updatedItems, []); - this._sendUpdatesOrFetch(updatedItems, [], []); - }, - // Ignore errors here, they should be handled by the caller not observers - () => {} - ); - return result; - } - - /** - * After the put is completed, notify the item observers, and then either queue a fetch to send updates - * if fetchAroundUpdates is true, or just send updates if not. - */ - put(items: T | T[]) { - const result = super.put(items); - result.then( - (updatedItems: T[]) => { - this._notifyItemObservers(updatedItems, []); - this._sendUpdatesOrFetch(updatedItems, [], []); - }, - // Ignore errors here, they should be handled by the caller not observers - () => {} - ); - return result; - } -} - -export default ObservableStore; diff --git a/src/store/QueryResult.ts b/src/store/QueryResult.ts deleted file mode 100644 index d4b3e8d..0000000 --- a/src/store/QueryResult.ts +++ /dev/null @@ -1,871 +0,0 @@ -import { Observable, Observer } from '@dojo/core/Observable'; -import { debounce } from '@dojo/core/util'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import Set from '@dojo/shim/Set'; -import { Query, QueryType, FetchResult } from '../interfaces'; -import { ItemUpdate, StoreDelta, mergeDeltas, buildIndex } from './ObservableStore'; -import Patch from '../patch/Patch'; -import CompoundQuery from '../query/CompoundQuery'; -import createFilter, { Filter } from '../query/createFilter'; -import createSort, { Sort } from '../query/createSort'; -import createRange, { StoreRange } from '../query/createStoreRange'; -import { isFilter, isSort, QueryableStoreInterface } from './QueryableStore'; - -export interface TrackableStoreDelta extends StoreDelta { - /** - * 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 }[]; - - /** - * 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; }[]; -} - -/** - * Checks if this is a tracked update or not - * @param storeDelta - * @returns {Boolean} - */ -function isTracked(storeDelta: StoreDelta): storeDelta is TrackableStoreDelta { - const tracked = > storeDelta; - return Boolean(tracked.removedFromTracked || tracked.addedToTracked || tracked.movedInTracked); -} - -/** - * Describes a transformation - */ -export type TransformationDescriptor = { - transformation: Patch | ((item: F) => T); idTransform?: string | ((item: T) => string) -}; - -/** - * If this function is 'mapped'(Items can be identified), and it contains only transformations and incremental queries, - * then we can update it in place, assuming that we are notified about all changes and are starting from the correct - * data. - * @param queriesAndTransforms - * @param result - * @returns {boolean|boolean} - */ -function canUpdateInPlace( - queriesAndTransforms: Array | TransformationDescriptor>, - result: QueryResultInterface -) { - return isMapped(result) && queriesAndTransforms.every((queryOrTransformation) => - !isQuery(queryOrTransformation) || Boolean(queryOrTransformation.incremental) - ); -} - -export interface QueryableStoreInterfaceOptions> { - fetchAroundUpdates: boolean; - isTracking?: boolean; - queriesAndTransformations: Array | TransformationDescriptor>; - source: S; - trackingFetchDelay?: number; -} - -export interface QueryResultInterface> { - source: S; - fetch(query?: Query): FetchResult; - filter(filter: Filter): this; - filter(test: (item: T) => boolean): this; - get(ids: string | string[]): Promise; - observe(): Observable>; - observe(id: string): Observable; - observe(ids: string[]): Observable>; - query(query: Query): this; - range(range: StoreRange): this; - range(start: number, count: number): this; - sort(sort: Sort | ((a: T, b: T) => number) | string, descending?: boolean): this; - transform(transformation: Patch | ((item: T) => V)): QueryResultInterface; - transform(transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string)): MappedQueryResult; -} - -export interface MappedQueryResultInterface< - T, S extends QueryableStoreInterface -> extends QueryResultInterface { - identify(items: T[]): string[]; - identify(item: T): string; - identify(items: T | T[]): string | string[]; - - /** - * These overrides aren't actually changing the signature, they are just necessary to make typescript happy about - * the override of the no arg signature for observe - */ - observe(): Observable>; - observe(id: string): Observable; - observe(ids: string[]): Observable>; - - /** - * Starts actively tracking this view, such that any time updates are made, this will fetch if necessary to make - * sure it has the latest data. - */ - track(): TrackedQueryResultInterface; -} - -export interface TrackedQueryResultInterface< - T, S extends QueryableStoreInterface -> extends MappedQueryResultInterface { - /** - * Create a new query transform result that is not tracking the source store but represents the same queries and - * transforms - */ - release(): MappedQueryResultInterface; -} - -/** - * Check if this is a 'mapped' query transform result - * @param queryTransformResult - * @returns {boolean} - */ -function isMapped( - queryTransformResult: QueryResultInterface -): queryTransformResult is MappedQueryResultInterface { - return typeof (> queryTransformResult).track === 'function'; -} - -/** - * Check if this is a patch or just a transform function - * @param transform - * @returns {boolean} - */ -function isPatch(transform: Patch | ((item: F) => T)): transform is Patch { - return typeof transform !== 'function'; -} - -/** - * Checks if this is a query or a transformations descriptor - * @param queryOrTransformation - * @returns {boolean} - */ -function isQuery(queryOrTransformation: Query | TransformationDescriptor): queryOrTransformation is Query { - const asTransformation = queryOrTransformation as TransformationDescriptor; - const asQuery = queryOrTransformation as Query; - return !asTransformation.transformation && !asTransformation.idTransform && typeof asQuery.apply === 'function'; -} - -/** - * Checks if this is a query or a transformations descriptor - * @param queryOrTransformation - * @returns {boolean} - */ -function isTransformation(queryOrTransformation: Query | TransformationDescriptor): queryOrTransformation is TransformationDescriptor { - const asTransformation = queryOrTransformation as TransformationDescriptor; - const asQuery = queryOrTransformation as Query; - return asTransformation.transformation && typeof asQuery.apply !== 'function'; -} - -/** - * Applies only the transformations in the queries and transformations array to the provided item(s). Useful for - * converting an item from its original shape to the transformed shape when querying is not needed (e.g. for observing - * individual items). - * @param queriesAndTransformations - * @param item An item or an array of items - * @returns The transformed item or items - */ -function transformData( - queriesAndTransformations: Array | TransformationDescriptor>, - item: any | any[] -) { - function transformSingleItem(item: any) { - return queriesAndTransformations - .reduce((prev, next) => { - if (isTransformation(next)) { - const transform = next.transformation; - return isPatch(transform) ? transform.apply(prev) : transform(prev); - } - else { - return prev; - } - }, item); - } - if (Array.isArray(item)) { - return item.map(transformSingleItem); - } - else { - return transformSingleItem(item); - } -} - -/** - * Pulls the item out of an `ItemUpdate` object and then delegates to `transformData` to transform it before creating - * a new `ItemUpdate` with the modified data. - * @param queriesAndTransformations - * @param update - * @returns A new `ItemUpdate` with any transformations applied - */ -function transformItemUpdate( - queriesAndTransformations: Array | TransformationDescriptor>, - update: ItemUpdate -) { - return { - id: update.id, - item: update.item ? transformData(queriesAndTransformations, update.item) : update.item - }; -} - -export class QueryResult> implements QueryResultInterface { - /** - * Tracks whether we can modify the local collection in place or need to fetch to get the correct this after an - * update - */ - protected _canUpdateInPlace: boolean; - - /** - * Keeps track of new item IDs as updates are being queued - */ - protected _currentUpdateIndex = new Set(); - - /** - * A debounced function that just delegates to the instance's fetch method - * @param instance - */ - protected _fetchAndSendUpdates: (instance: QueryResultInterface) => void; - - /** - * Is the parent store fetching around updates - * If the parent store is fetching around updates, we will always have the latest superset of this view's data in - * the updates it receives locally. In that case, even if actively tracking, no additional fetches need to be - * performed, the local queries and transformations can just be applied to the new data directly. - */ - protected _fetchAroundUpdates: boolean; - - /** - * Promise tracking the initial fetch if we are tracking and are not fetchingAroundUpdates - */ - protected _initialFetch?: Promise; - - /** - * Tracks whether we're tracking this collection - */ - protected _isTracking?: boolean; - - /** - * The local copy of the data for this view - */ - protected _localData: T[] = []; - - /** - * Maps IDs to indices in localDAta - */ - protected _localIndex = new Map(); - - /** - * The observable that observers of this query transform result will be provided - */ - protected _observable: Observable>; - - /** - * Observers of this query transform result - */ - protected _observers: Observer>[] = []; - - /** - * Queries and transformations for this query transform result - */ - protected _queriesAndTransformations: Array | TransformationDescriptor>; - - /** - * Updates ready to be send after the next fetch - */ - protected _queuedUpdate?: StoreDelta; - - /** - * Handle to the subscription to the source store - */ - protected _sourceHandle?: Promise<{ unsubscribe: Function }>; - - /** - * Optional value that indicates the amount of time to debounce the fetch called after receiving an update. - */ - protected _trackingFetchDelay?: number; - - /** - * The store this query transform result comes from - */ - public readonly source: S; - - constructor(options?: QueryableStoreInterfaceOptions) { - if (!options) { - throw Error('Query Transform result cannot be created without providing a source store'); - } - const observable = new Observable>((observer: Observer>) => { - this._observers.push(observer); - this._handleInitialNotification(observer); - return () => { - const remove = (observer: Observer>) => { - this._observers.splice(this._observers.indexOf(observer), 1); - if (!this._observers.length && this._sourceHandle) { - this._sourceHandle.then((subscription) => { - if (!this._observers.length) { - subscription.unsubscribe(); - this._sourceHandle = undefined; - } - - }); - } - }; - - // Do the actual removal on the next tick so that - // we don't remove items from the array while we're iterating through it. - setTimeout(() => { - remove(observer); - }); - }; - }); - - const updateInPlace = canUpdateInPlace(options.queriesAndTransformations, this); - - this.source = options.source; - this._canUpdateInPlace = updateInPlace; - this._observable = observable; - this._queriesAndTransformations = options.queriesAndTransformations; - this._isTracking = options.isTracking; - this._trackingFetchDelay = options.trackingFetchDelay; - this._fetchAndSendUpdates = debounce((instance: QueryResultInterface) => { - instance.fetch(); - }, options.trackingFetchDelay || 20); - this._fetchAroundUpdates = options.fetchAroundUpdates; - - if (options.isTracking && !options.fetchAroundUpdates) { - this.fetch(); - } - } - - protected _getQueryOptions(query: Query) { - return { - source: this.source, - queriesAndTransformations: [ ...this._queriesAndTransformations, query ], - trackingFetchDelay: this._trackingFetchDelay, - fetchAroundUpdates: this._fetchAroundUpdates - }; - } - - protected _handleInitialNotification(observer: Observer>) { - observer.next({ - updates: [], - adds: [], - deletes: [], - beforeAll: [], - afterAll: this._localData.slice() - }); - } - - protected _handleUpdate(update: StoreDelta) { - update = this._localizeUpdate(update); - this._sendUpdate(update); - } - - /** - * Removes items from adds and updates, and IDs from deletes, that don't belong in this query transform result. The - * observers of this view don't want to see unrelated updates. currentUpdateIndex is used when operating on batch - * updates. If updates are processed in a batch, an item might be added in one, and then removed in a later update. The - * newly added item will not yet be represented in the local data because the update needs to be localized before it - * can be used to update the local data. A single map can be passed as the currentUpdateIndex in multiple calls to - * localizeUpdate, and can then serve as a signal that even though a deleted ID isn't in the local index it is still - * a relevant update - * @param update - * @param instance - * @param currentUpdateIndex - * @returns {{deletes: string[], adds: any[], updates: any[], beforeAll: any[], afterAll: any[]}} - */ - protected _localizeUpdate( - update: StoreDelta, - instance?: MappedQueryResultInterface, - currentUpdateIndex?: Set - ) { - - // Don't apply range queries, sorts, etc. to adds and updates, because those don't make sense in that context - const adds = this._queryAndTransformData(update.adds, undefined, undefined, true, true); - const updates = this._queryAndTransformData(update.updates, undefined, instance, true, true); - if (instance && currentUpdateIndex) { - instance.identify(adds.concat(updates)).map((id) => currentUpdateIndex.add(id)); - } - const deletes = update.deletes.filter((id) => - this._localIndex.has(id) || currentUpdateIndex && currentUpdateIndex.has(id) - ); - // Applying range queries to beforeAll and afterAll may not be completely accurate, in the case that - // we are not eagerly fetching or tracking, but the data would definitely not be accurate if we don't apply them - // and we shouldn't be returning more data than the queries require. - const beforeAll = this._queryAndTransformData(update.beforeAll); - const afterAll = this._queryAndTransformData(update.afterAll); - - return { - deletes: deletes, - adds: adds, - updates: updates, - beforeAll: beforeAll, - afterAll: afterAll - }; - } - - /** - * Applies all of the provided queries and transformations to the data, with some optional changes - * - If the instance and this are provided, then the localIndex will be checked and any items in it will be kept - * even if they would be otherwise eliminated by a filter. This is used specifically for updates, since if an item - * no longer satisfies the filters but is in the local index that means it has been modified and as a result removed - * from the tracked filter. We still want to have access to the new data for inclusion in the `removedFromTracked` - * update so that the user sees how the item changed to be removed from the collection. - * - If `ignoreSorts` is true, then sorts are not applied. This is useful for just filtering out data when it's not - * actually being used to represent the final, tracked, collection - * - If `ignoreNonIncrementalQueries` is true, non-incremental queries like ranges are ignored. Similar to ignoreSorts, - * this is used when the data being transformed is not the full data set, since in that case non incremental queries - * are meaningless. - * - * @param data - * @param queriesAndTransformations - * @param instance - * @param ignoreSorts - * @param ignoreNonIncrementalQueries - * @returns T[] - */ - protected _queryAndTransformData( - data: any[], - queriesAndTransformations?: Array | TransformationDescriptor>, - instance?: MappedQueryResultInterface, - ignoreSorts = false, - ignoreNonIncrementalQueries = false - ): T[] { - return (queriesAndTransformations || this._queriesAndTransformations).reduce((prev, next) => { - if (isTransformation(next)) { - return transformData([ next ], prev); - } - else { - if ((!ignoreSorts || next.queryType !== QueryType.Sort) && (!ignoreNonIncrementalQueries || next.incremental)) { - if (instance && isFilter(next)) { - return next - .or(createFilter().custom((item: T) => this._localIndex.has(instance.identify(item)))) - .apply(prev); - } - else { - return next.apply(prev); - } - } - else { - return prev; - } - } - }, data); - } - - /** - * Sends the update if it actually represents any change in the data, and then removes observers that unsubscribed - * from the list. - * @param update - */ - protected _sendUpdate(update: StoreDelta): void { - // Don't send an update if nothing happened - if (update.deletes.length || update.updates.length || update.adds.length || ( - isTracked(update) && ( - update.movedInTracked.length || update.addedToTracked.length || update.removedFromTracked.length - ) - )) { - this._observers.forEach((observer) => { - if (isTracked(update)) { - observer.next({ - updates: update.updates.slice(), - adds: update.adds.slice(), - deletes: update.deletes.slice(), - afterAll: update.afterAll.slice(), - beforeAll: update.beforeAll.slice(), - movedInTracked: update.movedInTracked.slice(), - removedFromTracked: update.removedFromTracked.slice(), - addedToTracked: update.addedToTracked.slice() - } as TrackableStoreDelta); - } - else { - observer.next({ - updates: update.updates.slice(), - adds: update.adds.slice(), - deletes: update.deletes.slice(), - afterAll: update.afterAll.slice(), - beforeAll: update.beforeAll.slice() - }); - } - }); - } - } - // Extension point for Mapped update - protected _updateMappedState(newData: T[], resultsPromise: Promise, nextUpdate: StoreDelta) { } - - fetch(query?: Query): FetchResult { - let firstQuery = new CompoundQuery(); - const queriesAndTransformations = this._queriesAndTransformations.slice(); - let nextQuery = queriesAndTransformations.shift(); - // Get the queries that can be passed through to the store. This includes all queries up to and including the - // first non incremental query(e.g. a range query) or up to and not including the first transformation - while (nextQuery && isQuery(nextQuery) && nextQuery.incremental) { - firstQuery = firstQuery.withQuery(nextQuery); - nextQuery = queriesAndTransformations.shift(); - } - if (nextQuery && isQuery(nextQuery)) { - firstQuery = firstQuery.withQuery(nextQuery); - } - else if (nextQuery) { - queriesAndTransformations.unshift(nextQuery); - } - - const mapped: MappedQueryResultInterface | undefined = isMapped(this) ? - this as MappedQueryResultInterface : undefined; - let nextUpdate: StoreDelta = (this._queuedUpdate && mapped) ? this._queuedUpdate : { - adds: [], - updates: [], - deletes: [], - beforeAll: [], - afterAll: [] - }; - this._currentUpdateIndex.clear(); - this._queuedUpdate = undefined; - - let resolveTotalLength: Function | undefined = undefined; - let rejectTotalLength: Function | undefined = undefined; - const totalLength = new Promise((resolve, reject) => { - resolveTotalLength = resolve; - rejectTotalLength = reject; - }); - let resolveDataLength: Function; - let rejectDataLength: Function; - const dataLength = new Promise((resolve, reject) => { - resolveDataLength = resolve; - rejectDataLength = reject; - }); - const fetchResult = this.source.fetch(firstQuery); - const resultsPromise: FetchResult = fetchResult.then( - (newData: any[]) => { - // We should apply the query transform result's own queries first so that the total size of the locally - // cached data can be determined - newData = this._queryAndTransformData(newData, queriesAndTransformations); - resolveDataLength(newData.length); - - this._updateMappedState(newData, resultsPromise, nextUpdate); - if (query) { - newData = query.apply(newData); - } - return newData; - }, - (error: any) => { - rejectDataLength(error); - throw error; - } - ); - fetchResult.totalLength.then(resolveTotalLength, rejectTotalLength); - resultsPromise.dataLength = dataLength; - resultsPromise.totalLength = totalLength; - - if (!this._initialFetch) { - this._initialFetch = resultsPromise; - } - - return resultsPromise; - } - - filter(filterOrTest: Filter | ((item: T) => boolean)) { - let filter: Filter; - if (isFilter(filterOrTest)) { - filter = filterOrTest; - } - else { - filter = createFilter().custom(<(item: T) => boolean> filterOrTest); - } - - return this.query(filter); - } - - get(ids: string | string[]) { - const promise: Promise = this._initialFetch || Promise.resolve(); - const mapped = isMapped(this); - return promise.then(() => { - if (mapped) { - if (Array.isArray(ids)) { - return ids.map((id) => this._localData[this._localIndex.get(id)!]) - .filter((item) => Boolean(item)); - } - else { - return this._localData[this._localIndex.get(ids)!]; - } - } - else { - return this.source.get(ids) - .then((data) => { - if (Array.isArray(data)) { - return this._queryAndTransformData(data); - } - else if (data) { - return this._queryAndTransformData([data])[0]; - } - - return data; - }); - } - }); - } - - observe(): Observable>; - observe(id: string): Observable; - observe(ids: string[]): Observable>; - observe(idOrIds?: string | string[]) { - if (!idOrIds) { - if (!this._sourceHandle) { - const waitForFetchPromise: Promise = this._initialFetch || Promise.resolve(); - this._sourceHandle = waitForFetchPromise.then(() => - this.source.observe() - .subscribe((update: StoreDelta) => { - this._handleUpdate(update); - }) - ); - } - return this._observable; - } - else if (Array.isArray(idOrIds)) { - return this.source - .observe(idOrIds) - .map((update: ItemUpdate) => transformItemUpdate(this._queriesAndTransformations, update)); - } - return this.source - .observe(idOrIds) - .map((update: any) => transformData(this._queriesAndTransformations, update)); - } - - query(query: Query): this { - return new ( this.constructor)(this._getQueryOptions(query)); - } - - range(rangeOrStart: StoreRange | number, count?: number) { - let range: StoreRange; - if (typeof count !== 'undefined') { - range = createRange( rangeOrStart, count); - } - else { - range = > rangeOrStart; - } - - return this.query(range); - } - - sort(sortOrComparator: Sort | ((a: T, b: T) => number), descending?: boolean) { - let sort: Sort; - if (isSort(sortOrComparator)) { - sort = sortOrComparator; - } - else { - sort = createSort(sortOrComparator, descending); - } - - return this.query(sort); - } - - transform(transformation: Patch | ((item: T) => V)): QueryResultInterface; - transform(transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string)): MappedQueryResult; - transform( - transformation: Patch | ((item: any) => V), - idTransform?: string | ((item: V) => string) - ): QueryResultInterface | MappedQueryResult { - const options: QueryableStoreInterfaceOptions = { - source: this.source, - queriesAndTransformations: [ - ...this._queriesAndTransformations, - { transformation: transformation, idTransform: idTransform } - ], - trackingFetchDelay: this._trackingFetchDelay, - fetchAroundUpdates: this._fetchAroundUpdates - }; - if (idTransform) { - return new MappedQueryResult(options); - } - - return new QueryResult(options); - } -} - -export class MappedQueryResult< - T, S extends QueryableStoreInterface -> extends QueryResult implements MappedQueryResultInterface { - protected _handleInitialNotification(observer: Observer>) { - const fetchPromise: Promise = this._initialFetch || Promise.resolve(); - fetchPromise.then(() => { - const addedToTracked: { item: any; id: string; index: number; }[] = []; - this._localIndex.forEach((index, id) => { - addedToTracked.push({ - index: index, - item: this._localData[index], - id: id - }); - }); - const trackedDelta: TrackableStoreDelta = { - updates: [], - deletes: [], - adds: [], - addedToTracked: addedToTracked.slice(), - removedFromTracked: [], - movedInTracked: [], - afterAll: this._localData.slice(), - beforeAll: [] - }; - observer.next(trackedDelta); - }); - } - - protected _handleUpdate(update: StoreDelta) { - if (this._fetchAroundUpdates || !this._isTracking) { - update = this._localizeUpdate(update, this); - const newData = update.afterAll; - const ids = this.identify(newData); - const newIndex = buildIndex(Array.isArray(ids) ? ids : [ ids ]); - this._sendTrackedUpdate(newData, newIndex, update); - this._localData = newData; - this._localIndex = newIndex; - } - else { - // Combine batched updates, use `currentUpdateIndex` to make sure deletes of items added and then deleted within - // the span of the queued updates are not lost. These will be cancelled out by mergeDeltas, but both need - // to be there to properly get cancelled out, otherwise the delete gets removed and the add survives, resulting - // in an incorrect update - update = this._localizeUpdate(update, this, this._currentUpdateIndex); - this._queuedUpdate = this._queuedUpdate ? - mergeDeltas(this, this._queuedUpdate, update) : update; - // Unfortunately if we have a non-incremental query and we are tracking, we will need to fetch - // after each update. This is debounced to avoid rapidly issuing fetch requests in the case that a - // series of updates are received in a short amount of time. - this._fetchAndSendUpdates(this); - } - } - - /** - * Compares the latest data to the previous local data to build the change records for a TrackedStoreDelta. Delegates - * to `sendUpdate` to actually send the update to observers. - * @param newData - * @param newIndex - * @param update - */ - protected _sendTrackedUpdate(newData: T[], newIndex: Map, update: StoreDelta) { - const removedFromTracked: { item: T; id: string; previousIndex: number; }[] = []; - const addedToTracked: { item: T; id: string; index: number; }[] = []; - const movedInTracked: { item: T; id: string; previousIndex: number; index: number }[] = []; - - const updateMap = this.identify(update.updates).reduce((prev, next, index) => { - prev.set(next, update.updates[index]); - return prev; - }, new Map()); - // Check updates for removals first as it will have the latest data for items moved out of - // the tracked collection. - updateMap.forEach((item, id) => { - if (!newIndex.has(id) && this._localIndex.has(id)) { - removedFromTracked.push({ - item: item, - id: id, - previousIndex: this._localIndex.get(id)! - }); - } - }); - // Handle removals and moves - this._localIndex.forEach((previousIndex, id) => { - if (!newIndex.has(id) && !updateMap.has(id)) { - removedFromTracked.push({ - item: this._localData[previousIndex], - id: id, - previousIndex: previousIndex - }); - } - else if (this._localIndex.get(id) !== newIndex.get(id)) { - const index = newIndex.get(id)!; - movedInTracked.push({ - item: newData[index], - id: id, - index: index, - previousIndex: previousIndex - }); - } - }); - - // Handle additions - newIndex.forEach((index, id) => { - if (!this._localIndex.has(id)) { - addedToTracked.push({ - item: newData[index], - id: id, - index: index - }); - } - }); - - const trackedUpdate: TrackableStoreDelta = { - updates: update.updates, - adds: update.adds, - deletes: update.deletes, - removedFromTracked: removedFromTracked, - movedInTracked: movedInTracked, - addedToTracked: addedToTracked, - beforeAll: update.beforeAll, - afterAll: update.afterAll - }; - - this._sendUpdate(trackedUpdate); - } - - protected _updateMappedState(newData: T[], resultsPromise: Promise, nextUpdate: StoreDelta) { - const ids = this.identify(newData); - const newIndex = buildIndex(ids); - // Update this way if this is not an initial fetch. If this is the initial fetch, then this - // data (or subsequent data) will already be provided to observers in the initial notification, so don't - // send a redundant one. - if (resultsPromise !== this._initialFetch) { - nextUpdate.beforeAll = this._localData; - nextUpdate.afterAll = newData; - this._sendTrackedUpdate(newData, newIndex, nextUpdate); - } - this._localIndex = newIndex; - this._localData = newData; - } - - identify(item: T): string; - identify(items: T[]): string[]; - identify(items: T[] | T): string | string[] { - const lastTransformation = this._queriesAndTransformations.reduce | undefined>( - (prev, next) => isTransformation(next) ? next : prev, undefined - ); - const itemArray = Array.isArray(items) ? items : [ items ]; - if (lastTransformation) { - const idTransform = lastTransformation.idTransform!; - if (typeof idTransform === 'string') { - return itemArray.map((item) => ( item)[idTransform]); - } - else { - return itemArray.map(idTransform); - } - } - return this.source.identify(items); - } - - track(): TrackedQueryResultInterface { - return new TrackedQueryResult({ - isTracking: true, - source: this.source, - trackingFetchDelay: this._trackingFetchDelay, - queriesAndTransformations: this._queriesAndTransformations, - fetchAroundUpdates: this._fetchAroundUpdates - }); - } -} - -export class TrackedQueryResult< - T, S extends QueryableStoreInterface -> extends MappedQueryResult implements TrackedQueryResultInterface { - release() { - return new MappedQueryResult({ - isTracking: false, - source: this.source, - queriesAndTransformations: this._queriesAndTransformations, - fetchAroundUpdates: this._fetchAroundUpdates - }); - } -} diff --git a/src/store/QueryableStore.ts b/src/store/QueryableStore.ts deleted file mode 100644 index 11fe988..0000000 --- a/src/store/QueryableStore.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Query, QueryType, CrudOptions, UpdateResults } from '../interfaces'; -import ObservableStore, { ObservableStoreInterface } from './ObservableStore'; -import Patch from '../patch/Patch'; -import createFilter, { Filter } from '../query/createFilter'; -import createSort, { Sort } from '../query/createSort'; -import createRange, { StoreRange } from '../query/createStoreRange'; -import { MappedQueryResult, QueryResultInterface, MappedQueryResultInterface, QueryResult } from './QueryResult'; - -export interface QueryableStoreInterface< - T, O extends CrudOptions, U extends UpdateResults -> extends ObservableStoreInterface { - /** - * Creates a query transform result with the provided filter - * @param filter - */ - filter(filter: Filter): MappedQueryResultInterface; - - /** - * Creates a query transform result with a filter built from the provided test - * @param test - */ - filter(test: (item: T) => boolean): MappedQueryResultInterface; - - /** - * Creates a query transform result with the provided query - * @param query - */ - query(query: Query): MappedQueryResultInterface; - - /** - * Creates a query transform result with the provided range - * @param range - */ - range(range: StoreRange): MappedQueryResultInterface; - - /** - * 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): MappedQueryResultInterface; - - /** - * 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 | ((a: T, b: T) => number) | keyof T, descending?: boolean): MappedQueryResultInterface; - - /** - * 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(transformation: Patch | ((item: T) => V)): QueryResultInterface; - - /** - * Create a trackable query transform result with the specified transformation - * @param transformation - * @param idTransform - */ - transform( - transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string) - ): MappedQueryResultInterface; -} - -/** - * Check if this is a filter query or just a test function - * @param filterOrTest - * @returns {boolean} - */ -export function isFilter(filterOrTest: Query | ((item: T) => boolean)): filterOrTest is Filter { - return typeof filterOrTest !== 'function' && (> filterOrTest).queryType === QueryType.Filter; -} - -/** - * Check if this is a sort query or just a comparator - * @param sortOrComparator - * @returns {boolean} - */ -export function isSort(sortOrComparator: Sort | ((a: T, b: T) => number) | keyof T): sortOrComparator is Sort { - const paramType = typeof sortOrComparator; - return paramType !== 'function' && paramType !== 'string' && typeof (> sortOrComparator).apply === 'function'; -} - -class QueryableStore extends ObservableStore implements QueryableStoreInterface> { - filter(filterOrTest: Filter | ((item: T) => boolean)) { - let filter: Filter; - if (isFilter(filterOrTest)) { - filter = filterOrTest; - } - else { - filter = createFilter().custom(<(item: T) => boolean> filterOrTest); - } - - return this.query(filter); - } - - query(query: Query): MappedQueryResultInterface { - return new MappedQueryResult({ - source: this, - queriesAndTransformations: [ query ], - fetchAroundUpdates: this._fetchAroundUpdates - }); - } - - range(rangeOrStart: StoreRange | number, count?: number) { - let range: StoreRange; - if (typeof count !== 'undefined') { - range = createRange( rangeOrStart, count); - } - else { - range = > rangeOrStart; - } - - return this.query(range); - } - - sort(sortOrComparator: Sort | ((a: T, b: T) => number) | keyof T, descending?: boolean) { - let sort: Sort; - if (isSort(sortOrComparator)) { - sort = sortOrComparator; - } - else { - sort = createSort(sortOrComparator, descending); - } - - return this.query(sort); - } - - transform(transformation: Patch | ((item: T) => V)): QueryResultInterface; - transform( - transformation: Patch | ((item: T) => V), idTransform: string | ((item: V) => string) - ): MappedQueryResultInterface; - transform( - transformation: Patch | ((item: T) => V), idTransform?: string | ((item: V) => string) - ): QueryResultInterface | MappedQueryResultInterface { - const options = { - source: this, - queriesAndTransformations: [ { transformation: transformation, idTransform: idTransform} ], - fetchAroundUpdates: this._fetchAroundUpdates - }; - if (idTransform) { - return new MappedQueryResult(options); - } - else { - return new QueryResult(options); - } - } -} - -export default QueryableStore; diff --git a/src/store/StoreBase.ts b/src/store/StoreBase.ts deleted file mode 100644 index 83a4da4..0000000 --- a/src/store/StoreBase.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { duplicate } from '@dojo/core/lang'; -import { Observer, Observable } from '@dojo/core/Observable'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import _createStoreObservable from './createStoreObservable'; -import { - Storage, Query, CrudOptions, UpdateResults, Store, StoreOptions, PatchArgument, FetchResult, PatchMapEntry, - StoreObservable -} from '../interfaces'; -import Patch, { diff } from '../patch/Patch'; -import InMemoryStorage from '../storage/InMemoryStorage'; - -function isPatchArray(patches: any[]): patches is { id: string; patch: Patch}[] { - return isPatch(patches[0]); -} - -function isPatch(patchObj: any): patchObj is {id: string; patch: Patch } { - const patch = patchObj && patchObj.patch; - const id = patchObj && patchObj.id; - return typeof id === 'string' && patch && Array.isArray(patch.operations) && typeof patch.apply === 'function' && - typeof patch.toString === 'function'; -} - -function createStoreObservable(storeResultsPromise: Promise>) { - - return _createStoreObservable( - new Observable>((observer: Observer>) => { - storeResultsPromise - .then((results) => { - observer.next(results); - observer.complete(); - }, (error) => { - observer.error(error); - }); - }), - (results: UpdateResults<{}>) => results.successfulData - ); -} - -export default class StoreBase implements Store> { - private _initialAddPromise: Promise = Promise.resolve(); - private _storage: Storage; - - constructor(options?: StoreOptions) { - if (!options) { - options = {}; - } - const data: T[] | undefined = options.data; - this._storage = options.storage || new InMemoryStorage(options); - if (data) { - this._initialAddPromise = this.add(data).catch((error) => { - console.error(error); - }); - } - } - - add(items: T[] | T, options?: CrudOptions): StoreObservable> { - const storeResultsPromise = this._initialAddPromise.then(() => { - return this._storage.add(Array.isArray(items) ? items : [ items ], options); - }); - return createStoreObservable(storeResultsPromise); - } - - createId() { - return this._storage.createId(); - } - - delete(ids: string | string[]): StoreObservable> { - const storeResultsPromise = this._initialAddPromise.then(() => { - return this._storage.delete(Array.isArray(ids) ? ids : [ ids ]); - }); - - return createStoreObservable(storeResultsPromise); - } - - fetch(query?: Query) { - let resolveTotalLength: (totalLength: number) => void; - let rejectTotalLength: (error: any) => void; - const totalLength = new Promise((resolve, reject) => { - resolveTotalLength = resolve; - rejectTotalLength = reject; - }); - const fetchResult: FetchResult = this._initialAddPromise.then(() => { - const result = this._storage.fetch(query); - result.totalLength.then(resolveTotalLength, rejectTotalLength); - return result; - }); - fetchResult.totalLength = fetchResult.dataLength = totalLength; - - return fetchResult; - } - - get(ids: string[]): Promise; - get(id: string): Promise; - get(ids: string[] | string): Promise { - return this._initialAddPromise.then(() => { - if (Array.isArray(ids)) { - return this._storage.get(ids).then((items) => items.filter((item) => Boolean(item))); - } - else { - return this._storage.get([ids]).then(items => items[0]); - } - }); - } - - identify(items: T[]): string[]; - identify(items: T): string; - identify(items: T | T[]): string | string[] { - if (Array.isArray(items)) { - return this._storage.identify(items); - } - else { - return this._storage.identify([items])[0]; - } - } - - patch(updates: PatchArgument, options?: CrudOptions): StoreObservable> { - let patchEntries: PatchMapEntry[] = []; - if (Array.isArray(updates)) { - if (isPatchArray(updates)) { - patchEntries = updates; - } - else { - patchEntries = updates.map(({ id }, index) => { - const dupe = duplicate(updates[index]); - delete dupe.id; - return { id: id, patch: diff(dupe)}; - }); - } - } - else if (updates instanceof Map) { - updates.forEach((value, key) => - patchEntries.push({ - id: key, - patch: value - }) - ); - } - else if (isPatch(updates)) { - patchEntries = [ updates ]; - } - else { - const dupe = duplicate(updates); - const idInOptions = (options && options.id); - const id = idInOptions || dupe.id; - if (!idInOptions) { - delete dupe.id; - } - patchEntries = [ { id: id, patch: diff(dupe) }]; - } - - const storeResultsPromise = this._initialAddPromise.then(() => { - return this._storage.patch(patchEntries); - }); - - return createStoreObservable(storeResultsPromise); - } - - put(items: T[] | T, options?: CrudOptions): StoreObservable> { - const storeResultsPromise = this._initialAddPromise.then(() => { - return this._storage.put(Array.isArray(items) ? items : [ items ], options); - }); - - return createStoreObservable(storeResultsPromise); - } -} diff --git a/src/store/createStoreObservable.ts b/src/store/createStoreObservable.ts deleted file mode 100644 index f45c7e2..0000000 --- a/src/store/createStoreObservable.ts +++ /dev/null @@ -1,28 +0,0 @@ -import global from '@dojo/core/global'; -import { Observable } from '@dojo/core/Observable'; -import { Thenable } from '@dojo/interfaces/shim'; -import Promise from '@dojo/shim/Promise'; -import { StoreObservable } from '../interfaces'; - -global.Rx = { config: { Promise } }; - -export default function createStoreObservable(observable: Observable, transform: (data: U) => T[]): StoreObservable { - // Cast to any because the signatures of catch between the Observable and Promise interfaces are not - // compatible - const storeObservable: StoreObservable = observable; - storeObservable.then = function(onFulfilled?: ((value: T[]) => (V | Thenable | undefined)) | undefined, onRejected?: (reason?: Error) => void): Promise { - // Wrap in a shim promise because the interface that leaks through _observable.toPromise is missing some - // properties on the shim(e.g. promise) - return Promise.resolve(observable.toPromise()) - .then(transform) - .then(onFulfilled, onRejected); - }; - - storeObservable.catch = function(onRejected: (reason: Error) => (U | Thenable)): Promise { - return observable.toPromise() - .then(transform) - .then(undefined, onRejected); - }; - - return storeObservable; -} diff --git a/src/store/materialize.ts b/src/store/materialize.ts deleted file mode 100644 index 678b320..0000000 --- a/src/store/materialize.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Handle } from '@dojo/interfaces/core'; -import { Store } from '../interfaces'; -import { StoreDelta } from './ObservableStore'; -import { MappedQueryResultInterface, QueryResultInterface, TrackableStoreDelta } from './QueryResult'; - -export interface Materialization, T extends Store> { - source: S; - target: T; - apply?(target: T, update: StoreDelta, source: S): void; -} - -export interface MappedMaterialization< - I, S extends MappedQueryResultInterface, T extends Store -> extends Materialization { - apply?(target: T, update: TrackableStoreDelta, source: S): void; -} - -export default function materialize, T extends Store>( - { source, target, apply }: Materialization -): Handle { - let initialUpdate = true; - const subscription = source.observe() - .subscribe((update: StoreDelta) => { - if (apply) { - apply(target, update, source); - } - else if (initialUpdate) { - initialUpdate = false; - if (update.afterAll.length) { - target.add(update.afterAll); - } - } - else { - const { adds, updates, deletes } = update; - if (adds.length) { - target.add(adds); - } - if (updates.length) { - target.put(updates); - } - if (deletes.length) { - target.delete(deletes); - } - } - }); - - return { - destroy() { - subscription.unsubscribe(); - } - }; -} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index e4c8daf..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function shouldRecurseInto(value: any): value is Object { - return Object.prototype.toString.call(value) === '[object Object]'; -} - -export function isEqual(a: any, b: any): boolean { - if (Array.isArray(a) && Array.isArray(b)) { - return a.length === b.length && a.every((element: any, i: number) => isEqual(element, b[i])); - } - else if (shouldRecurseInto(a) && shouldRecurseInto(b)) { - const keysForA = Object.keys(a).sort(); - const keysforB = Object.keys(b).sort(); - return isEqual(keysForA, keysforB) && keysForA.every(key => isEqual(a[key], b[key])); - } - else { - return a === b; - } -} diff --git a/tests/intern-local.ts b/tests/intern-local.ts deleted file mode 100644 index 4327144..0000000 --- a/tests/intern-local.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './intern'; - -export const tunnel = 'SeleniumTunnel'; -export const tunnelOptions = { - hostname: 'localhost', - port: '4444' -}; - -export const environments = [ - { browserName: 'chrome' } -]; diff --git a/tests/intern-saucelabs.ts b/tests/intern-saucelabs.ts deleted file mode 100644 index 3e93efc..0000000 --- a/tests/intern-saucelabs.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from './intern'; - -export const environments = [ - { browserName: 'internet explorer', version: [ '10.0', '11.0' ], platform: 'Windows 7' }, - // { browserName: 'microsoftedge', platform: 'Windows 10' }, - { browserName: 'firefox', version: '43', platform: 'Windows 10' }, - { browserName: 'chrome', platform: 'Windows 10' } - // { browserName: 'safari', version: '9', platform: 'OS X 10.11' }, - // { browserName: 'android', platform: 'Linux', version: '4.4', deviceName: 'Google Nexus 7 HD Emulator' } - // { browserName: 'iphone', version: '9.1', deviceName: 'iPhone 6' }]; -]; - -/* SauceLabs supports more max concurrency */ -export const maxConcurrency = 4; - -export const tunnel = 'SauceLabsTunnel'; diff --git a/tests/intern.ts b/tests/intern.ts deleted file mode 100644 index 439352e..0000000 --- a/tests/intern.ts +++ /dev/null @@ -1,72 +0,0 @@ -export const proxyPort = 9000; - -// A fully qualified URL to the Intern proxy -export const proxyUrl = 'http://localhost:9000/'; - -// Default desired capabilities for all environments. Individual capabilities can be overridden by any of the -// specified browser environments in the `environments` array below as well. See -// https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and -// https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities. -// Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment -// automatically -export const capabilities = { - 'browserstack.debug': false, - project: 'Dojo 2', - name: '@dojo/stores' -}; - -// Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce -// OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other -// capabilities options specified for an environment will be copied as-is -export const environments = [ - { browserName: 'internet explorer', version: '11', platform: 'WINDOWS' }, - { browserName: 'edge' }, - { browserName: 'firefox', platform: 'WINDOWS' }, - { browserName: 'chrome', platform: 'WINDOWS' }, - { browserName: 'Safari', version: '9.1', platform: 'MAC' }, - { browserName: 'iPhone', version: '9.1' } -]; - -// Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service -export const maxConcurrency = 2; - -// Name of the tunnel class to use for WebDriver tests -export const tunnel = 'BrowserStackTunnel'; - -// Support running unit tests from a web server that isn't the intern proxy -export const initialBaseUrl: string | null = (function () { - if (typeof location !== 'undefined' && location.pathname.indexOf('__intern/') > -1) { - return '/'; - } - return null; -})(); - -// The desired AMD loader to use when running unit tests (client.html/client.js). Omit to use the default Dojo -// loader -export const loaders = { - 'host-browser': 'node_modules/@dojo/loader/loader.js', - 'host-node': '@dojo/loader' -}; - -// Configuration options for the module loader; any AMD configuration options supported by the specified AMD loader -// can be used here -export const loaderOptions = { - // Packages that should be registered with the loader in each testing environment - packages: [ - { name: 'src', location: '_build/src' }, - { name: 'tests', location: '_build/tests' }, - { name: 'dojo', location: 'node_modules/intern/browser_modules/dojo' }, - { name: '@dojo', location: 'node_modules/@dojo' }, - { name: 'grunt-dojo2', location: 'node_modules/grunt-dojo2'}, - { name: 'sinon', location: 'node_modules/sinon/pkg', main: 'sinon' } - ] -}; - -// Non-functional test suite(s) to run in each browser -export const suites = [ 'tests/unit/all' ]; - -// Functional test suite(s) to run in each browser once non-functional tests are completed -export const functionalSuites = [ 'tests/functional/all' ]; - -// A regular expression matching URLs to files that should not be included in code coverage analysis -export const excludeInstrumentation = /(?:node_modules|bower_components|tests)[\/\\]/; diff --git a/tests/run.html b/tests/run.html index 872415e..087d124 100644 --- a/tests/run.html +++ b/tests/run.html @@ -3,7 +3,7 @@ Intern suite - + Redirecting to Intern client diff --git a/tests/support/util.ts b/tests/support/util.ts deleted file mode 100644 index 957d249..0000000 --- a/tests/support/util.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Thenable represents any object with a callable `then` property. - */ -export interface Thenable { - then(onFulfilled?: (value?: T) => U | Thenable, onRejected?: (error?: any) => U | Thenable): Thenable; -} - -export function isEventuallyRejected(promise: Thenable): Thenable { - return promise.then(() => { - throw new Error('unexpected code path'); - }, () => { - return true; // expect rejection - }); -} - -export function throwImmediatly() { - throw new Error('unexpected code path'); -} diff --git a/tests/unit/Store.ts b/tests/unit/Store.ts new file mode 100644 index 0000000..31806ba --- /dev/null +++ b/tests/unit/Store.ts @@ -0,0 +1,42 @@ +const { beforeEach, describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { Store } from './../../src/Store'; +import { OperationType, PatchOperation } from './../../src/state/Patch'; +import { Pointer } from './../../src/state/Pointer'; + +let store: Store = new Store(); + +const testPatchOperations: PatchOperation[] = [ + { op: OperationType.ADD, path: new Pointer('/test'), value: 'test'} +]; + +describe('store', () => { + beforeEach(() => { + store = new Store(); + }); + + it('create store', () => { + assert.isOk(store); + }); + + it('apply/get', () => { + const undo = store.apply(testPatchOperations); + + assert.strictEqual(store.get('/test'), 'test'); + assert.deepEqual(undo, [ + { op: OperationType.TEST, path: new Pointer('/test'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/test') } + ]); + }); + + it('invalidate', () => { + let invalidateEmitted = false; + store.on('invalidate', () => { + invalidateEmitted = true; + }); + store.invalidate(); + assert.isTrue(invalidateEmitted); + }); + +}); diff --git a/tests/unit/all.ts b/tests/unit/all.ts index 8c9a75e..e370a8b 100644 --- a/tests/unit/all.ts +++ b/tests/unit/all.ts @@ -1,15 +1,4 @@ -import './patch/Patch'; -import './patch/createOperation'; -import './patch/JsonPointer'; -import './query/createFilter'; -import './query/createSort'; -import './query/createStoreRange'; -import './query/CompoundQuery'; -import './storage/InMemoryStorage'; -import './storage/IndexedDBStorage'; -import './store/StoreBase'; -import './store/ObservableStore'; -import './store/QueryableStore/querying'; -import './store/QueryableStore/tracking'; -import './store/QueryableStore/transforming'; -import './store/materialize'; +import './extras'; +import './Store'; +import './process'; +import './state/all'; diff --git a/tests/unit/extras.ts b/tests/unit/extras.ts new file mode 100644 index 0000000..133cd78 --- /dev/null +++ b/tests/unit/extras.ts @@ -0,0 +1,64 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { OperationType, PatchOperation } from './../../src/state/Patch'; +import { CommandRequest, createProcess } from './../../src/process'; +import { Pointer } from './../../src/state/Pointer'; +import { createUndoManager } from './../../src/extras'; +import { Store } from './../../src/Store'; + +function incrementCounter({ get }: CommandRequest): PatchOperation[] { + let counter = get('/counter') || 0; + return [ + { op: OperationType.REPLACE, path: new Pointer('/counter'), value: ++counter } + ]; +}; + +describe('extras', () => { + + it('collects undo functions for all processes using collector', () => { + const { undoCollector, undoer } = createUndoManager(); + const store = new Store(); + let localUndoStack: any[] = []; + const incrementCounterProcess = createProcess([ incrementCounter ], undoCollector((error, result) => { + localUndoStack.push(result.undo); + })); + const executor = incrementCounterProcess(store); + executor(); + assert.strictEqual(store.get('/counter'), 1); + executor(); + assert.strictEqual(store.get('/counter'), 2); + executor(); + assert.strictEqual(store.get('/counter'), 3); + localUndoStack[2](); + assert.strictEqual(store.get('/counter'), 2); + undoer(); + assert.strictEqual(store.get('/counter'), 1); + }); + + it('undo has no effect if there are no undo functions on the stack', () => { + const { undoer } = createUndoManager(); + const store = new Store(); + const incrementCounterProcess = createProcess([ incrementCounter ]); + const executor = incrementCounterProcess(store); + executor(); + undoer(); + assert.strictEqual(store.get('/counter'), 1); + }); + + it('local undo throws an error if global undo has already been executed', () => { + const { undoCollector, undoer } = createUndoManager(); + const store = new Store(); + let localUndo: any; + const incrementCounterProcess = createProcess([ incrementCounter ], undoCollector((error, result) => { + localUndo = result.undo; + })); + const executor = incrementCounterProcess(store); + executor(); + assert.strictEqual(store.get('/counter'), 1); + undoer(); + assert.throws(() => { + localUndo && localUndo(); + }, Error, 'Test operation failure. Unable to apply any operations.'); + }); +}); diff --git a/tests/unit/patch/JsonPointer.ts b/tests/unit/patch/JsonPointer.ts deleted file mode 100644 index ca6b7fe..0000000 --- a/tests/unit/patch/JsonPointer.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import JsonPointer, { navigate } from '../../../src/patch/JsonPointer'; - -registerSuite({ - name: 'JsonPointer', - 'Should work with simple path.'(this: any) { - const target = { prop1: 1 }; - const path = new JsonPointer('prop1'); - const result = navigate(path, target); - - assert.strictEqual(result, 1); - }, - 'Should work with multiple paths.'(this: any) { - const target = { prop1: { prop2: { prop3: 1 } } }; - const path = new JsonPointer('prop1', 'prop2', 'prop3'); - const result = navigate(path, target); - - assert.strictEqual(result, 1); - }, - 'Should create new path using push.'(this: any) { - const target = { prop1: { prop2: { prop3: 1 } } }; - const path = new JsonPointer('prop1', 'prop2').push('prop3'); - const result = navigate(path, target); - - assert.strictEqual(result, 1); - }, - 'Should create new path using pop.'(this: any) { - const target = { prop1: { prop2: { prop3: 1 } } }; - const path = new JsonPointer('prop1', 'prop2', 'prop3').pop(); - const result = navigate(path, target); - - assert.deepEqual(result, { prop3: 1 }); - }, - 'Should return null if the target is null.'(this: any) { - const path = new JsonPointer('prop1', 'prop2', 'prop3'); - const result = navigate(path, null); - - assert.isNull(result); - }, - 'Should have a toString that describes the path.'(this: any) { - const path = new JsonPointer('a', 'b', 'c').toString(); - - assert.strictEqual(path, 'a/b/c'); - } -}); diff --git a/tests/unit/patch/Patch.ts b/tests/unit/patch/Patch.ts deleted file mode 100644 index cb7ff56..0000000 --- a/tests/unit/patch/Patch.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import { diff } from '../../../src/patch/Patch'; -import { createData } from '../support/createData'; - -registerSuite({ - name: 'Patch', - - 'Should only works with objects'(this: any) { - const data = createData(); - const patch = diff(data[1].id, data[0].id); - assert.isTrue(patch.operations.length === 0, 'operations should not be created.'); - }, - - 'Should have a toString that describes the operations.'(this: any) { - const from = { prop1: 'foo', prop2: 1 }; - const to = { prop1: 'bar', prop2: 5 }; - const patch = diff(to, from); - assert.strictEqual(patch.toString(), '[{"op":"replace","path":"prop1","value":"bar"},{"op":"replace","path":"prop2","value":5}]'); - }, - - 'Replace operation should replace "from" value based on diff.'(this: any) { - const data = createData(); - const from = data[0].nestedProperty; - const to = data[1].nestedProperty; - const patch = diff(to, from); - - const result = patch.apply(from); - - assert.deepEqual(result, to); - }, - - 'Should treat "undefined" in "to" normally.'(this: any) { - const data = createData(); - const from = data[0].nestedProperty; - const to = data[1].nestedProperty; - to.value = undefined; - const patch = diff(to, from); - - const result = patch.apply(from); - - assert.isTrue('value' in result); - assert.deepEqual(result, to); - }, - - 'Should treat "undefined" in "from" normally.'(this: any) { - const data = createData(); - const from = data[0].nestedProperty; - const to = data[1].nestedProperty; - from.value = undefined; - const patch = diff(to, from); - - const result = patch.apply(from); - - assert.isTrue('value' in result); - assert.deepEqual(result, to); - }, - - 'Remove operation should remove "from" value based on diff.'(this: any) { - const data = createData(); - const from = data[0].nestedProperty; - const to = data[1].nestedProperty; - delete to.value; - const patch = diff(to, from); - - const result = patch.apply(from); - - assert.isNotTrue('value' in result); - assert.deepEqual(result, to); - }, - - 'Add operation should add "from" value based on diff.'(this: any) { - const data = createData(); - const from = data[0].nestedProperty; - const to = data[1].nestedProperty; - delete from.value; - const patch = diff(to, from); - - const result = patch.apply(from); - - assert.isTrue('value' in result); - assert.deepEqual(result, to); - }, - - 'Should support a single argument for diff'(this: any) { - const data = createData(); - const to = data[0]; - const patch = diff(to); - - const result = patch.apply({}); - - assert.deepEqual(result, to, 'Should have made the object identical to the passed object'); - } -}); diff --git a/tests/unit/patch/createOperation.ts b/tests/unit/patch/createOperation.ts deleted file mode 100644 index 6c0c905..0000000 --- a/tests/unit/patch/createOperation.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import createOperation, { OperationType } from '../../../src/patch/createOperation'; - -registerSuite({ - name: 'Operation', - - 'Basic Operations.': { - 'Add operation should add property and value to the target.'(this: any) { - const target = {}; - const operation = createOperation(OperationType.Add, ['prop1'], undefined, 1); - const result = operation.apply(target); - - assert.deepEqual(result, { prop1: 1 }); - }, - - 'Remove operation should remove property from the target.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Remove, ['prop1']); - const result = operation.apply(target); - - assert.deepEqual(result, {}); - }, - - 'Replace operation should replace property value of the target.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Replace, ['prop1'], undefined, 2); - const result = operation.apply(target); - - assert.deepEqual(result, { prop1: 2}); - }, - - 'Replace operation should treat "undefined" normally.'(this: any) { - const target = { prop1: undefined }; - const operation = createOperation(OperationType.Replace, ['prop1'], undefined, 2); - const result = operation.apply(target); - - assert.deepEqual(result, { prop1: 2}); - }, - - 'Replace operation should throw an error when target property is missing.'(this: any) { - const target = {}; - const operation = createOperation(OperationType.Replace, ['prop1'], undefined, 2); - assert.throws(() => { - operation.apply(target); - }, /Cannot replace undefined path/); - } - }, - 'Copy and Move Operation.': { - 'Copy operation should copy property over to the new property of the target.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Copy, ['prop2'], ['prop1']); - const result = operation.apply(target); - - assert.deepEqual(result, { prop1: 1, prop2: 1}); - }, - - 'Copy operation should treat "undefined" normally.'(this: any) { - const target = { prop1: undefined }; - const operation = createOperation(OperationType.Copy, ['prop2'], ['prop1']); - const result = operation.apply(target); - - assert.deepEqual(result, { prop1: undefined, prop2: undefined}); - }, - - 'Copy operation should throw an error when fromPath is missing.'(this: any) { - assert.throws(() => { - createOperation(OperationType.Copy, ['any'], null); - }, /From value is required/); - }, - - 'Copy operation should throw an error when source property is missing.'(this: any) { - const target = {}; - const operation = createOperation(OperationType.Copy, ['prop2'], ['prop1']); - assert.throws(() => { - operation.apply(target); - }, /Cannot move from undefined path/); - - }, - - 'Move operation should move property over to the new target.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Move, ['prop2'], ['prop1']); - const result = operation.apply(target); - - assert.deepEqual(result, { prop2: 1}); - }, - - 'Move operation should treat "undefined" normally.'(this: any) { - const target = { prop1: undefined }; - const operation = createOperation(OperationType.Move, ['prop2'], ['prop1']); - const result = operation.apply(target); - - assert.deepEqual(result, { prop2: undefined}); - }, - - 'Move operation should throw an error when fromPath is missing.'(this: any) { - assert.throws(() => { - createOperation(OperationType.Move, ['any'], null); - }, /From value is required/); - }, - - 'Move operation should throw an error when source property is missing.'(this: any) { - const target = {}; - const operation = createOperation(OperationType.Move, ['prop2'], ['prop1']); - assert.throws(() => { - operation.apply(target); - }, /Cannot move from undefined path/); - - } - }, - 'Test Operations.': { - 'Test operation should return true when testing property value matches given value.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Test, ['prop1'], undefined, 1); - const result = operation.apply(target); - - assert.isTrue(result); - }, - 'Test operation should return false when testing property value doesn\'t match given value.'(this: any) { - const target = { prop1: 1 }; - const operation = createOperation(OperationType.Test, ['prop1'], undefined, 2); - const result = operation.apply(target); - - assert.isNotTrue(result); - } - }, - 'Should throw an error when target is null.'(this: any) { - const operation = createOperation(OperationType.Add, ['prop1'], undefined, 1); - assert.throws(() => { - operation.apply( null); - }, /Invalid path/); - }, - 'Should throw an error when path is not found.'(this: any) { - const target = {}; - const operation = createOperation(OperationType.Add, ['prop1', 'prop2'], undefined, 1); - assert.throws(() => { - operation.apply(target); - }, /Invalid path/); - }, - 'Should have a toString that describes the operation details.'(this: any) { - - const operation = createOperation(OperationType.Copy, ['prop1'], ['prop2']); - assert.strictEqual(operation.toString(), '{"op":"copy","path":"prop1","from":"prop2"}'); - } -}); diff --git a/tests/unit/process.ts b/tests/unit/process.ts new file mode 100644 index 0000000..ee116f3 --- /dev/null +++ b/tests/unit/process.ts @@ -0,0 +1,236 @@ +const { beforeEach, describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { Pointer } from './../../src/state/Pointer'; +import { OperationType, PatchOperation } from './../../src/state/Patch'; +import { + CommandRequest, + createCallbackDecorator, + createProcess, + createProcessFactoryWith, + ProcessCallback, + ProcessError, + ProcessResult +} from './../../src/process'; +import { Store } from './../../src/Store'; + +let store: Store; +let promises: Promise[] = []; +let promiseResolvers: Function[] = []; + +function promiseResolver() { + for (let i = 0; i < promiseResolvers.length; i++) { + promiseResolvers[i](); + } +} + +const testCommandFactory = (value: string) => { + return ({ payload }: CommandRequest): PatchOperation[] => { + return [ + { op: OperationType.ADD, path: new Pointer(`/${value}`), value: payload[0] || value } + ]; + }; +}; + +const testAsyncCommandFactory = (value: string) => { + return ({ payload }: CommandRequest): Promise => { + const promise = new Promise((resolve) => { + promiseResolvers.push(() => { + resolve([ { op: OperationType.ADD, path: new Pointer(`/${value}`), value: payload[0] || value } ]); + }); + }); + promises.push(promise); + return promise; + }; +}; + +const testErrorCommand = ({ payload }: CommandRequest): any => { + new Error('Command Failed'); +}; + +describe('process', () => { + + beforeEach(() => { + store = new Store(); + promises = []; + promiseResolvers = []; + }); + + it('with synchronous commands running in order', () => { + const process = createProcess([ testCommandFactory('foo'), testCommandFactory('foo/bar') ]); + const processExecutor = process(store); + processExecutor(); + const foo = store.get('/foo'); + const foobar = store.get('/foo/bar'); + assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(foobar, 'foo/bar'); + }); + + it('processes wait for asynchronous commands to complete before continuing', () => { + const process = createProcess([ testCommandFactory('foo'), testAsyncCommandFactory('bar'), testCommandFactory('foo/bar') ]); + const processExecutor = process(store); + const promise = processExecutor(); + const foo = store.get('/foo'); + const bar = store.get('/bar'); + assert.strictEqual(foo, 'foo'); + assert.isUndefined(bar); + promiseResolver(); + return promise.then(() => { + const foo = store.get('/foo'); + const bar = store.get('/bar'); + const foobar = store.get('/foo/bar'); + assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(bar, 'bar'); + assert.strictEqual(foobar, 'foo/bar'); + }); + }); + + it('support concurrent commands executed synchronously', () => { + const process = createProcess([ + testCommandFactory('foo'), + [ + testAsyncCommandFactory('bar'), + testAsyncCommandFactory('baz') + ], + testCommandFactory('foo/bar') + ]); + const processExecutor = process(store); + const promise = processExecutor(); + promiseResolvers[0](); + return promises[0].then(() => { + const bar = store.get('/bar'); + const baz = store.get('/baz'); + assert.isUndefined(bar); + assert.isUndefined(baz); + promiseResolver(); + return promise.then(() => { + const bar = store.get('/bar'); + const baz = store.get('/baz'); + assert.strictEqual(bar, 'bar'); + assert.strictEqual(baz, 'baz'); + }); + }); + }); + + it('passes the payload to each command', () => { + const process = createProcess([ + testCommandFactory('foo'), + testCommandFactory('bar'), + testCommandFactory('baz') + ]); + const processExecutor = process(store); + processExecutor('payload'); + const foo = store.get('/foo'); + const bar = store.get('/bar'); + const baz = store.get('/baz'); + assert.strictEqual(foo, 'payload'); + assert.strictEqual(bar, 'payload'); + assert.strictEqual(baz, 'payload'); + }); + + it('can use a transformer for the arguments passed to the process executor', () => { + const process = createProcess([ + testCommandFactory('foo'), + testCommandFactory('bar'), + testCommandFactory('baz') + ]); + const processExecutor = process(store, () => 'changed'); + processExecutor('payload'); + const foo = store.get('/foo'); + const bar = store.get('/bar'); + const baz = store.get('/baz'); + assert.strictEqual(foo, 'changed'); + assert.strictEqual(bar, 'changed'); + assert.strictEqual(baz, 'changed'); + }); + + it('can provide a callback that gets called on process completion', () => { + let callbackCalled = false; + const process = createProcess([ testCommandFactory('foo') ], () => { + callbackCalled = true; + }); + const processExecutor = process(store); + processExecutor(); + assert.isTrue(callbackCalled); + }); + + it ('when a command errors, the error and command is returned in the error argument of the callback', () => { + const process = createProcess([ testCommandFactory('foo'), testErrorCommand ], (error) => { + assert.isNotNull(error); + assert.strictEqual(error && error.command, testErrorCommand); + }); + const processExecutor = process(store); + processExecutor(); + }); + + it('executor can be used to programmatically run additional processes', () => { + const extraProcess = createProcess([ testCommandFactory('bar') ]); + const process = createProcess([ testCommandFactory('foo') ], (error, result) => { + assert.isNull(error); + let bar = store.get('/bar'); + assert.isUndefined(bar); + result.executor(extraProcess); + bar = store.get('/bar'); + assert.strictEqual(bar, 'bar'); + }); + const processExecutor = process(store); + processExecutor(); + }); + + it('process can be undone using the undo function provided via the callback', () => { + const process = createProcess([ testCommandFactory('foo') ], (error, result) => { + let foo = store.get('/foo'); + assert.strictEqual(foo, 'foo'); + result.undo(); + foo = store.get('/foo'); + assert.isUndefined(foo); + }); + const processExecutor = process(store); + processExecutor(); + }); + + it('Creating a process returned automatically decorates all process callbacks', () => { + let results: string[] = []; + + const callbackDecorator = (callback?: ProcessCallback) => { + return (error: ProcessError, result: ProcessResult): void => { + results.push('callback one'); + callback && callback(error, result); + }; + }; + + const callbackTwo = (error: ProcessError, result: ProcessResult): void => { + results.push('callback two'); + }; + + const logPointerCallback = (error: ProcessError, result: ProcessResult): void => { + const paths = result.operations.map(operation => operation.path.path); + const logs = result.get('/logs') || []; + + result.apply([ + { op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths } + ]); + }; + + const createProcess = createProcessFactoryWith([ + callbackDecorator, + createCallbackDecorator(callbackTwo), + createCallbackDecorator(logPointerCallback) + ]); + + const process = createProcess([ testCommandFactory('foo'), testCommandFactory('bar') ]); + const executor = process(store); + executor(); + assert.lengthOf(results, 2); + assert.strictEqual(results[0], 'callback two'); + assert.strictEqual(results[1], 'callback one'); + assert.deepEqual(store.get('/logs'), [ [ '/foo', '/bar' ] ]); + executor(); + assert.lengthOf(results, 4); + assert.strictEqual(results[0], 'callback two'); + assert.strictEqual(results[1], 'callback one'); + assert.strictEqual(results[2], 'callback two'); + assert.strictEqual(results[3], 'callback one'); + assert.deepEqual(store.get('/logs'), [ [ '/foo', '/bar' ], [ '/foo', '/bar' ] ]); + }); +}); diff --git a/tests/unit/query/CompoundQuery.ts b/tests/unit/query/CompoundQuery.ts deleted file mode 100644 index c9f7343..0000000 --- a/tests/unit/query/CompoundQuery.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import CompoundQuery from '../../../src/query/CompoundQuery'; -import { QueryType } from '../../../src/interfaces'; -import createSort from '../../../src/query/createSort'; -import createRange from '../../../src/query/createStoreRange'; -import { createData, ItemType } from '../support/createData'; -import createFilter from '../../../src/query/createFilter'; - -registerSuite({ - name: 'CompoundQuery', - - 'Should create a Query of type CompoundQuery.'() { - assert.strictEqual(new CompoundQuery( { query: null } ).queryType, QueryType.Compound); - }, - 'Should create a CompoundQuery from a filter.'() { - const data = createData(); - const query = new CompoundQuery( { - query: createFilter<{ value: number }>().lessThan('value', 2) - } ); - assert.deepEqual(query.apply(data), [data[0]]); - }, - 'Should create a CompoundQuery from a sort.'() { - const data = createData(); - const _data = createData(); - const query = new CompoundQuery( { - query: createSort('id', true) - } ); - assert.deepEqual(query.apply(data), [_data[2], _data[1], _data[0]]); - }, - 'Should create a CompoundQuery from a range.'() { - const data = createData(); - const query = new CompoundQuery( { - query: createRange(2, 1) - } ); - assert.deepEqual(query.apply(data), [data[2]]); - }, - 'Should compound another query.'() { - const data = createData(); - const _data = createData(); - const query = new CompoundQuery( { - query: createFilter<{ value: number }>().lessThan('value', 3) - } ) - .withQuery( createSort('id', true) ); - - assert.deepEqual(query.apply(data), [_data[1], _data[0]]); - }, - 'Should compound multiple queries.'() { - const data = createData(); - const _data = createData(); - const query = new CompoundQuery( { - query: createFilter<{ value: number }>().lessThan('value', 3) - } ) - .withQuery( createSort('id', true) ) - .withQuery( createRange(1, 1) ); - - assert.deepEqual(query.apply(data), [_data[0]]); - }, - 'Should compound another compound query.'() { - const data = createData(); - const _data = createData(); - const query1 = new CompoundQuery({ - query: createSort('id', true) - }) - .withQuery( createRange(1, 1) ); - - const query = new CompoundQuery( { - query: createFilter<{ value: number }>().lessThan('value', 3) - } ) - .withQuery(query1); - - assert.deepEqual(query.apply(data), [_data[0]]); - }, - 'Should have a toString that describes its properties'() { - const query = new CompoundQuery( { - query: createFilter<{ value: number }>().lessThan('value', 3) - } ) - .withQuery( createSort('id', true) ); - - assert.strictEqual( query.toString(), 'lt(value, 3)&sort(-id)' ); - }, - - 'Should return an empty string when serializing an empty compound query'(this: any) { - assert.strictEqual(new CompoundQuery().toString(), '', 'Should have returned an empty string'); - }, - - 'Should be able to identify whether this compound query is incremental'(this: any) { - const incrementalQuery = new CompoundQuery() - .withQuery(createFilter()) - .withQuery(createSort('any')) - .withQuery({ - queryType: QueryType.Filter, - toString() { - return ''; - }, - apply(data: any[]) { - return data; - }, - incremental: true - }); - - const nonIncrementalQuery = new CompoundQuery() - .withQuery(createFilter()) - .withQuery(createRange(0, 10)); - - assert.isTrue(incrementalQuery.incremental, 'Should have returned true for incremental query'); - assert.isFalse(nonIncrementalQuery.incremental, 'Should have returned false for non-incremental query'); - }, - - 'Should be able to return array of queries'(this: any) { - const queries = [ - createFilter(), - createSort('any'), - createRange(0, 10) - ]; - - const compoundQuery = queries.reduce((prev, next) => prev.withQuery(next), new CompoundQuery()); - - assert.deepEqual(compoundQuery.queries, queries, 'Should return queries'); - } -}); diff --git a/tests/unit/query/createFilter.ts b/tests/unit/query/createFilter.ts deleted file mode 100644 index d54374e..0000000 --- a/tests/unit/query/createFilter.ts +++ /dev/null @@ -1,584 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import createFilter, {Filter, FilterType, BooleanOp, SimpleFilter, FilterArray} from '../../../src/query/createFilter'; -import JsonPointer from '../../../src/patch/JsonPointer'; - -type SimpleObject = { key: number; id: string }; -type NestedObject = { key: { key2: number }; id: string}; -type ListWithLists = { id: string, list: number[] }; -const simpleList = [ - { - key: 5, - id: 'item-1' - }, - { - key: 7, - id: '2' - }, - { - key: 4, - id: '3' - } -]; -const nestedList = [ - { - key: { - key2: 5 - }, - id: 'item-1' - }, - { - key: { - key2: 7 - }, - id: '2' - }, - { - key: { - key2: 4 - }, - id: '3' - } -]; - -const listWithLists = [ - { - list: [ 1, 2, 3 ], - id: 'item-1' - }, - { - list: [ 3, 4, 5 ], - id: '2' - }, - { - list: [ 4, 5, 6 ], - id: '3' - } -]; -registerSuite({ - name: 'filter', - - 'basic filter operations': { - 'with string path': { - 'less than'() { - assert.deepEqual(createFilter().lessThan('key', 5).apply(simpleList), - [ { key: 4, id: '3' } ], 'Less than w/string path'); - }, - - 'less than or equal to'() { - assert.deepEqual(createFilter().lessThanOrEqualTo('key', 5).apply(simpleList), - [ { key: 5, id: 'item-1' }, { key: 4, id: '3' } ], 'Less than or equal to with string path'); - }, - - 'greater than'() { - assert.deepEqual(createFilter().greaterThan('key', 5).apply(simpleList), - [ { key: 7, id: '2' } ], 'Greater than with string path'); - }, - - 'greater than or equal to'() { - assert.deepEqual(createFilter().greaterThanOrEqualTo('key', 5).apply(simpleList), - [ { key: 5, id: 'item-1' }, { key: 7, id: '2' } ], 'Greater than or equal to with string path'); - }, - - 'matches'() { - assert.deepEqual(createFilter().matches('id', /[12]/).apply(simpleList), - [ { key: 5, id: 'item-1' }, { key: 7, id: '2' } ], 'Matches with string path'); - }, - - 'in'() { - assert.deepEqual(createFilter().in('key', [7, 4]).apply(simpleList), - simpleList.slice(1), 'In with string path'); - }, - - 'contains'() { - assert.deepEqual(createFilter().contains('key', 'key2').apply(nestedList), - nestedList, 'Contains with string path'); - - assert.deepEqual(createFilter().contains('key', 'key1').apply(nestedList), - [], 'Contains with string path'); - - assert.deepEqual(createFilter().contains('list', 4).apply(listWithLists), - listWithLists.slice(1), 'Contains with string path'); - }, - - 'equalTo'() { - assert.deepEqual(createFilter().equalTo('key', 5).apply(simpleList), - [ { key: 5, id: 'item-1' } ], 'Equal to with string path'); - }, - - 'notEqualTo'() { - assert.deepEqual(createFilter().notEqualTo('key', 5).apply(simpleList), - [ { key: 7, id: '2' }, { key: 4, id: '3' } ], 'Not equal to with string path'); - }, - - 'deepEqualTo'() { - assert.deepEqual(createFilter().deepEqualTo('key', { key2: 5 }).apply(nestedList), - [ nestedList[0] ], 'Deep equal with string path'); - }, - - 'notDeepEqualTo'() { - assert.deepEqual(createFilter().notDeepEqualTo('key', { key2: 5 }).apply(nestedList), - nestedList.slice(1), 'Not deep equal with string path') ; - }, - 'filterChain should keep all filters'() { - const filters = createFilter().lessThan('key', 5).greaterThan('key', 10).filterChain || []; - assert.lengthOf(filters, 3); - assert.strictEqual(filters[1], BooleanOp.And); - }, - 'SimpleFilter should have an apply that can be used individually.'() { - const filters = createFilter().lessThan('key', 5).filterChain || []; - const simpleFilter = > filters[0]; - - assert.deepEqual(simpleFilter.apply(simpleList), [ { key: 4, id: '3' } ]); - } - }, - - 'with json path': { - 'less than'() { - assert.deepEqual(createFilter().lessThan(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ { key: { key2: 4 }, id: '3' } ], 'Less than with JSON path'); - }, - - 'less than or equal to'() { - assert.deepEqual(createFilter().lessThanOrEqualTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ { key: { key2: 5 }, id: 'item-1' }, { key: { key2: 4 }, id: '3' } ], 'Less than or equal to with JSON path'); - }, - - 'greater than'() { - assert.deepEqual(createFilter().greaterThan(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ { key: { key2: 7 }, id: '2' } ], 'Greater than with JSON path'); - }, - - 'greater than or equal to'() { - assert.deepEqual(createFilter().greaterThanOrEqualTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ { key: { key2: 5 }, id: 'item-1' }, { key: { key2: 7 }, id: '2' }], 'Greater than or equal to with JSON path'); - }, - - 'matches'() { - assert.deepEqual(createFilter().matches(new JsonPointer('id'), /[12]/).apply(nestedList), - [ { key: { key2: 5 }, id: 'item-1' }, { key: { key2: 7 }, id: '2' } ], 'Matches with JSON path'); - }, - - 'in'() { - assert.deepEqual(createFilter().in(new JsonPointer('key', 'key2'), [7, 4]).apply(nestedList), - nestedList.slice(1), 'In with JSON path'); - }, - - 'contains'() { - assert.deepEqual(createFilter().contains(new JsonPointer('key'), 'key2').apply(nestedList), - nestedList, 'Contains with JSON path'); - - assert.deepEqual(createFilter().contains(new JsonPointer('key'), 'key1').apply(nestedList), - [], 'Contains with JSON path'); - }, - - 'equalTo'() { - assert.deepEqual(createFilter().equalTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [{key: { key2: 5 }, id: 'item-1'}], 'Equal to with json path'); - }, - - 'notEqualTo'() { - assert.deepEqual(createFilter().notEqualTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ { key: { key2: 7 }, id: '2' }, { key: { key2: 4 }, id: '3' } ], 'Not equal to with json path'); - }, - - 'deepEqualTo'() { - assert.deepEqual(createFilter().deepEqualTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - [ nestedList[0] ], 'Deep equal with JSON path'); - }, - - 'notDeepEqualTo'() { - assert.deepEqual(createFilter().notDeepEqualTo(new JsonPointer('key', 'key2'), 5).apply(nestedList), - nestedList.slice(1), 'Not deep equal with JSON path'); - } - }, - - 'custom'() { - assert.deepEqual(createFilter().custom((item: SimpleObject) => item.key === 4 ).apply(simpleList), - [ { key: 4, id: '3' } ], 'Not deep equal with custom filter'); - } - }, - - 'compound filters': { - 'chained': { - 'automatic chaining'() { - assert.deepEqual(createFilter().lessThanOrEqualTo('key', 5).equalTo('id', 'item-1').apply(simpleList), - [ simpleList[0] ], 'Sequential filters chain ands automatically'); - }, - - 'explicit chaining \'and\''() { - assert.deepEqual(createFilter().lessThanOrEqualTo('key', 5).and().equalTo('id', 'item-1').apply(simpleList), - [ simpleList[0] ], 'Chaining filters with and explicitly'); - }, - - 'explicit chaining \'or\''() { - assert.deepEqual(createFilter().lessThan('key', 5).or().greaterThan('key', 5).apply(simpleList), - simpleList.slice(1), 'Chaining filters with or explicitly'); - }, - - 'combining \'and\' and \'or\''() { - assert.deepEqual(createFilter() - // explicit chaining - .equalTo('key', 7) - .and() - .equalTo('id', '2') - .or() - // implicit chaining - .equalTo('key', 4) - .equalTo('id', '3') - .apply(simpleList), - simpleList.slice(1), 'Chaining \'and\' and \'or\' filters'); - } - }, - - 'nested'() { - const pickFirstItem = createFilter() - .lessThanOrEqualTo(new JsonPointer('key', 'key2'), 5) - .and() - .equalTo('id', 'item-1') - .or() - .greaterThanOrEqualTo(new JsonPointer('key', 'key2'), 5) - .equalTo('id', 'item-1') - .or() - .greaterThan(new JsonPointer('key', 'key2'), 5) - .equalTo('id', 'item-1'); - const pickAllItems = createFilter().lessThan(new JsonPointer('key', 'key2'), 100); - const pickNoItems = createFilter().greaterThan(new JsonPointer('key', 'key2'), 100); - - const pickLastItem = createFilter().equalTo('id', '3'); - - assert.deepEqual(pickFirstItem.apply(nestedList), [ nestedList[0] ], 'Should pick first item'); - assert.deepEqual(pickAllItems.apply(nestedList), nestedList, 'Should pick all items'); - assert.deepEqual(pickNoItems.apply(nestedList), [], 'Should pick no items'); - assert.deepEqual(pickLastItem.apply(nestedList), [ nestedList[2] ], 'Should pick last item'); - assert.deepEqual(pickFirstItem.and(pickLastItem).apply(nestedList), [], 'Shouldn\'t pick any items'); - assert.deepEqual(pickFirstItem.or(pickLastItem).apply(nestedList), [ nestedList[0], nestedList[2] ], - 'Should have picked first and last item'); - - assert.deepEqual(pickFirstItem.or(pickAllItems.and(pickNoItems)).or(pickLastItem).apply(nestedList), - [ nestedList[0], nestedList[2] ], 'Should have picked first and last item'); - } - }, - - 'from objects': { - 'nested'() { - const individualFilter = { - filterType: FilterType.EqualTo, - value: 5, - path: new JsonPointer('key', 'key2') - }; - const pickFirstItem: FilterArray = [ - { - filterType: FilterType.LessThanOrEqualTo, - value: 5, - path: new JsonPointer('key', 'key2') - }, - BooleanOp.And, - { - filterType: FilterType.EqualTo, - value: 'item-1', - path: 'id' - }, - BooleanOp.Or, - { - filterType: FilterType.GreaterThanOrEqualTo, - value: 5, - path: new JsonPointer('key', 'key2') - }, - { - filterType: FilterType.EqualTo, - value: 'item-1', - path: 'id' - }, - BooleanOp.Or, - { - filterType: FilterType.GreaterThan, - value: 5, - path: new JsonPointer('key', 'key2') - }, - { - filterType: FilterType.EqualTo, - value: 'item-1', - path: 'id' - } - ]; - - const pickAllItems = [ - { - filterType: FilterType.LessThan, - value: 100, - path: new JsonPointer('key', 'key2') - } - ]; - - const pickNoItems = [ - { - filterType: FilterType.GreaterThan, - value: 100, - path: new JsonPointer('key', 'key2') - } - ]; - - const pickLastItem: FilterArray = [ - { - filterType: FilterType.EqualTo, - value: '3', - path: 'id' - } - ]; - - assert.deepEqual(createFilter(pickFirstItem).apply(nestedList), [ nestedList[0] ], 'Should pick first item'); - assert.deepEqual(createFilter(pickAllItems).apply(nestedList), nestedList, 'Should pick all items'); - assert.deepEqual(createFilter(pickNoItems).apply(nestedList), [], 'Should pick no items'); - assert.deepEqual(createFilter(pickLastItem).apply(nestedList), [ nestedList[2] ], 'Should pick last item'); - assert.deepEqual( - createFilter([ pickFirstItem, BooleanOp.And, pickLastItem ]).apply(nestedList), - [], - 'Shouldn\'t pick any items' - ); - assert.deepEqual( - createFilter([ pickFirstItem, BooleanOp.Or, pickLastItem ]).apply(nestedList), - [ nestedList[0], nestedList[2] ], - 'Should have picked first and last item' - ); - - assert.deepEqual( - createFilter( - [ pickFirstItem, BooleanOp.Or, [ pickAllItems, BooleanOp.And, pickNoItems ], BooleanOp.Or, pickLastItem ] - ).apply(nestedList), - [ nestedList[0], nestedList[2] ], - 'Should have picked first and last item' - ); - - assert.deepEqual( - createFilter(individualFilter).apply(nestedList), [ nestedList[0] ], 'Should have picked first item' - ); - } - }, - - 'serializing': { - 'simple - no path': { - 'empty filter'() { - assert.strictEqual(createFilter().toString(), '', 'Didn\'t properly serialize empty filter'); - }, - - 'less than'() { - assert.strictEqual(createFilter().lessThan('key', 3).toString(), 'lt(key, 3)', - 'Didn\'t properly serialize less than'); - }, - - 'greater than'() { - assert.strictEqual(createFilter().greaterThan('key', 3).toString(), 'gt(key, 3)', - 'Didn\'t properly serialize greater than'); - }, - - 'equals'() { - assert.strictEqual(createFilter().equalTo('key', 'value').toString(), 'eq(key, "value")', - 'Didn\'t properly serialize equals'); - }, - - 'deep equals'() { - assert.strictEqual(createFilter().deepEqualTo('key', 'value').toString(), 'eq(key, "value")', - 'Didn\'t properly serialize deep equals'); - }, - - 'in'() { - assert.strictEqual(createFilter().in('key', [1, 2, 3]).toString(), 'in(key, [1,2,3])', - 'Didn\'t properly serialize in'); - }, - - 'contains'() { - assert.strictEqual(createFilter().contains('key', 'value').toString(), 'contains(key, "value")', - 'Didn\'t properly serialize contains'); - }, - - 'not equal'() { - assert.strictEqual(createFilter().notEqualTo('key', 'value').toString(), 'ne(key, "value")', - 'Didn\'t properly serialize not equal'); - }, - - 'not deep equal'() { - assert.strictEqual(createFilter().notDeepEqualTo('key', 'value').toString(), 'ne(key, "value")', - 'Didn\'t properly serialize not deep equal'); - }, - - 'less than or equal to'() { - assert.strictEqual(createFilter().lessThanOrEqualTo('key', 3).toString(), 'lte(key, 3)', - 'Didn\'t properly serialize less than or equal to'); - }, - - 'greater than or equal to'() { - assert.strictEqual(createFilter().greaterThanOrEqualTo('key', 3).toString(), 'gte(key, 3)', - 'Didn\'t properly serialize greater than or equal to'); - }, - - 'matches'() { - assert.throws(() => (createFilter().matches('key', /test/).toString()), 'Cannot parse this filter type to an RQL query string'); - }, - - 'custom'() { - assert.throws(() => (createFilter().custom((arg: any) => true).toString()), 'Cannot parse this filter type to an RQL query string'); - } - }, - - 'simple - path': { - 'less than'() { - assert.strictEqual(createFilter().lessThan(new JsonPointer('key', 'key2'), 3).toString(), - 'lt(key/key2, 3)', 'Didn\'t properly serialize less than'); - }, - - 'greater than'() { - assert.strictEqual(createFilter().greaterThan(new JsonPointer('key', 'key2'), 3).toString(), - 'gt(key/key2, 3)', 'Didn\'t properly serialize greater than'); - }, - - 'equals'() { - assert.strictEqual(createFilter().equalTo(new JsonPointer('key', 'key2'), 'value').toString(), - 'eq(key/key2, "value")', 'Didn\'t properly serialize equals'); - }, - - 'deep equals'() { - assert.strictEqual(createFilter().deepEqualTo(new JsonPointer('key', 'key2'), 'value').toString(), - 'eq(key/key2, "value")', 'Didn\'t properly serialize deep equals'); - }, - - 'in'() { - assert.strictEqual(createFilter().in(new JsonPointer('key', 'key2'), [ 1, 2, 3 ]).toString(), - 'in(key/key2, [1,2,3])', 'Didn\'t properly serialize in'); - }, - - 'contains'() { - assert.strictEqual(createFilter().contains(new JsonPointer('key', 'key2'), 'value').toString(), - 'contains(key/key2, "value")', 'Didn\'t properly serialize contains'); - }, - - 'not equal'() { - assert.strictEqual(createFilter().notEqualTo(new JsonPointer('key', 'key2'), 'value').toString(), - 'ne(key/key2, "value")', 'Didn\'t properly serialize not equal'); - }, - - 'not deep equal'() { - assert.strictEqual(createFilter().notDeepEqualTo(new JsonPointer('key', 'key2'), 'value').toString(), - 'ne(key/key2, "value")', 'Didn\'t properly serialize not deep equal'); - }, - - 'less than or equal to'() { - assert.strictEqual(createFilter().lessThanOrEqualTo(new JsonPointer('key', 'key2'), 3).toString(), - 'lte(key/key2, 3)', 'Didn\'t properly serialize less than or equal to'); - }, - - 'greater than or equal to'() { - assert.strictEqual(createFilter().greaterThanOrEqualTo(new JsonPointer('key', 'key2'), 3).toString(), - 'gte(key/key2, 3)', 'Didn\'t properly serialize greater than or equal to'); - }, - - 'matches'() { - assert.throws(() => (createFilter().matches(new JsonPointer('key', 'key2'), /test/).toString()), 'Cannot parse this filter type to an RQL query string'); - } - }, - - 'chained': { - 'ands'() { - assert.strictEqual(createFilter().greaterThan('key', 3).lessThan('key', 2).in('key', [ 3 ]).toString(), - 'gt(key, 3)<(key, 2)&in(key, [3])', 'Didn\'t properly chain filter with ands'); - }, - - 'ors'() { - assert.strictEqual(createFilter().greaterThan('key', 3).or().lessThan('key', 2).or().in('key', [ 3 ]).toString(), - 'gt(key, 3)|lt(key, 2)|in(key, [3])', 'Didn\'t properly chain filter with ors'); - }, - - 'combination'() { - assert.strictEqual(createFilter().greaterThan('key', 3).lessThan('key', 3).or().in('key', [ 3 ]).toString(), - 'gt(key, 3)<(key, 3)|in(key, [3])', 'Didn\'t properly chain filter with ands and ors'); - } - }, - - 'nested'() { - const filterOne = createFilter().greaterThan('key', 3).lessThan('key', 2).in('key', [ 3 ]); - const filterTwo = createFilter().greaterThan('key', 3).or().lessThan('key', 2).in('key', [ 3 ]); - const filterThree = createFilter().greaterThan('key', 3).or().lessThan('key', 2).or().in('key', [ 3 ]); - - const compoundFilter = createFilter() - .greaterThan('key', 3) - .or(filterOne) - .or() - .lessThan('key', 2) - .and(filterTwo.or(filterThree)) - .or() - .in('key', [ 3 ]); - - assert.strictEqual(compoundFilter.toString(), - 'gt(key, 3)|(' + /*filter one */ 'gt(key, 3)<(key, 2)&in(key, [3])' + /* close or */ ')' + '|lt(key, 2)&(' + - /*filterTwo*/ 'gt(key, 3)|lt(key, 2)&in(key, [3])|(' + /*filter three*/ 'gt(key, 3)|lt(key, 2)|in(key, [3])' + - /*close or */')' + /*close and */ ')' + '|in(key, [3])', 'Didn\'t properly serialize compound filter'); - } - }, - - 'provide custom serialization approach'() { - function serializeFilter(filter: Filter): string { - function recursivelySerialize(filter: Filter): string { - switch (filter.filterType) { - case FilterType.LessThan: - return (filter.path || '').toString() + ' is less than ' + (filter.value || ''); - case FilterType.GreaterThan: - return (filter.path || '').toString() + ' is greater than ' + (filter.value || ''); - case FilterType.EqualTo: - return (filter.path || '').toString() + ' is equal to ' + (filter.value || ''); - case FilterType.Compound: - return (filter.filterChain || []).reduce((prev, next) => { - if (next === BooleanOp.And) { - return prev + ' and'; - } else if (next === BooleanOp.Or) { - return prev + ' or'; - } else { - return prev + ' ' + recursivelySerialize(> next); - } - }, ''); - default: - return ''; - } - } - - return 'Return any item where' + recursivelySerialize(filter); - } - - assert.strictEqual(createFilter(undefined, serializeFilter) - .greaterThan('key', 3) - .lessThan('key', 5) - .or() - .equalTo('id', 'value') - .toString(), - 'Return any item where key is greater than 3 and key is less than 5 or id is equal to value', - 'Didn\'t use provided serialization function' - ); - }, - - 'ignore or at end of filter chain'(this: any) { - assert.deepEqual( - createFilter() - .custom((item) => true) - .notEqualTo('key', 5) - .or() - .apply(simpleList), - simpleList.slice(1), - 'Or at end of filter shouldn\'t have changed the result'); - }, - - 'empty ands or ors at beginning of chain should not throw errors'(this: any) { - const emptyAnd = createFilter().and(); - const emptyOr = createFilter().or(); - assert.doesNotThrow(() => { - emptyAnd.apply(simpleList); - emptyOr.apply(simpleList); - }, 'Should\'t have thrown any errors'); - }, - - 'filter on entire object'(this: any) { - assert.deepEqual( - createFilter().deepEqualTo('', simpleList[0]).apply(simpleList), - [ simpleList[0] ], - 'Should have returned the matching object' - ); - } -}); diff --git a/tests/unit/query/createSort.ts b/tests/unit/query/createSort.ts deleted file mode 100644 index ca64b22..0000000 --- a/tests/unit/query/createSort.ts +++ /dev/null @@ -1,156 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import createSort from '../../../src/query/createSort'; -import JsonPointer from '../../../src/patch/JsonPointer'; - -type SimpleObj = { key1: string; id: number }; -type NestedObj = { key1: { key2: string }; id: number}; - -function getSimpleList() { - return [ - { - key1: 'b', - id: 1 - }, - { - key1: 'c', - id: 2 - }, - { - key1: 'a', - id: 3 - } - ]; -}; -const nestedList = [ - { - key1: { - key2: 'b' - }, - id: 1 - }, - { - key1: { - key2: 'c' - }, - id: 2 - }, - { - key1: { - key2: 'a' - }, - id: 3 - } -]; - -registerSuite({ - name: 'sort', - - 'sort with property': { - 'sort in default order'() { - assert.deepEqual(createSort('key1').apply(getSimpleList()), - [ { key1: 'a', id: 3 }, { key1: 'b', id: 1 }, { key1: 'c', id: 2 } ]); - }, - 'sort in ascending order'() { - assert.deepEqual(createSort('key1', false).apply(getSimpleList()), - [ { key1: 'a', id: 3 }, { key1: 'b', id: 1 }, { key1: 'c', id: 2 } ]); - }, - 'sort in decending order'() { - assert.deepEqual(createSort('key1', true).apply(getSimpleList()), - [ { key1: 'c', id: 2 }, { key1: 'b', id: 1 }, { key1: 'a', id: 3 } ]); - }, - - 'sort with same order'() { - const sameKeyList = getSimpleList().map(({ id }) => ({ id, key1: 'd' })); - assert.deepEqual(createSort('key1', true).apply(sameKeyList), - [ { key1: 'd', id: 1 }, { key1: 'd', id: 2 }, { key1: 'd', id: 3 } ]); - }, - 'sort with null property value'() { - const sameKeyList = getSimpleList().map(({ id }) => ({ id, key1: null })); - assert.deepEqual(createSort<{ key1: null; id: number }>('key1', true).apply(sameKeyList), - [ { key1: null, id: 1 }, { key1: null, id: 2 }, { key1: null, id: 3 } ]); - }, - 'sort with partially null property value'() { - const list = [ { key1: null, id: 1 }, { key1: 'a', id: 2 }, { key1: undefined, id: 3 } ]; - assert.deepEqual(createSort<{ key1: undefined | null | string; id: number }>('key1').apply(list), - [ { key1: undefined, id: 3 }, { key1: null, id: 1 }, { key1: 'a', id: 2 } ]); - }, - 'sort with undefined property value'() { - const sameKeyList = getSimpleList().map(({ id }) => ({ id, key1: undefined })); - assert.deepEqual(createSort<{ key1: undefined; id: number }>('key1', true).apply(sameKeyList), - [ { key1: undefined, id: 1 }, { key1: undefined, id: 2 }, { key1: undefined, id: 3 } ]); - } - }, - 'sort with json path': { - 'sort with one path'() { - assert.deepEqual(createSort(new JsonPointer('key1')).apply(getSimpleList()), - [ { key1: 'a', id: 3 }, { key1: 'b', id: 1 }, { key1: 'c', id: 2 } ]); - }, - 'sort with multiple paths'() { - assert.deepEqual(createSort(new JsonPointer('key1', 'key2')).apply(nestedList), - [ { key1: { key2: 'a' }, id: 3 }, { key1: { key2: 'b' }, id: 1 }, { key1: { key2: 'c' }, id: 2 } ]); - } - }, - 'sort with comparator': { - 'sort with default order'() { - assert.deepEqual(createSort((a, b) => b.id - a.id).apply(getSimpleList()), - [ { key1: 'a', id: 3 }, { key1: 'c', id: 2 }, { key1: 'b', id: 1 } ]); - }, - 'sort with flipped order'() { - assert.deepEqual(createSort((a, b) => b.id - a.id, true).apply(getSimpleList()), - [ { key1: 'b', id: 1 }, { key1: 'c', id: 2 }, { key1: 'a', id: 3 } ]); - } - }, - 'sort with multiple properties': { - 'with no descending argument'() { - assert.deepEqual( - createSort(['key1', 'key2']).apply( - [ { key1: 2, key2: 2}, { key1: 2, key2: 1}, { key1: 1, key2: 2 }, { key1: 1, key2: 1} ] - ), - [ { key1: 1, key2: 1}, { key1: 1, key2: 2 }, { key1: 2, key2: 1}, { key1: 2, key2: 2} ], - 'Didn\'t sort properly with multiple properties' - ); - }, - - 'with one descending argument'() { - assert.deepEqual( - createSort(['key1', 'key2'], true).apply( - [ { key1: 1, key2: 1}, { key1: 1, key2: 2 }, { key1: 2, key2: 1}, { key1: 2, key2: 2} ] - ), - [ { key1: 2, key2: 2}, { key1: 2, key2: 1}, { key1: 1, key2: 2 }, { key1: 1, key2: 1} ], - 'Didn\'t sort properly with multiple properties with a single descending argument' - ); - }, - - 'with descending array'() { - assert.deepEqual( - createSort(['key1', 'key2'], [ true, false ]).apply( - [ { key1: 1, key2: 2}, { key1: 1, key2: 1 }, { key1: 2, key2: 2}, { key1: 2, key2: 1} ] - ), - [ { key1: 2, key2: 1}, { key1: 2, key2: 2}, { key1: 1, key2: 1 }, { key1: 1, key2: 2} ], - 'Didn\'t sort properly with multiple properties with multiple descending arguments' - ); - } - }, - 'toString': { - 'Should print plus sign when sort order is ascending.'(this: any) { - const result = createSort('key1').toString(); - assert.strictEqual(result, 'sort(+key1)'); - }, - 'Should print minus sign when sort order is decending.'(this: any) { - const result = createSort('key1', true).toString(); - assert.strictEqual(result, 'sort(-key1)'); - }, - 'Should throw an error when toString is called on a comparator based sort.'(this: any) { - const sort = createSort(() => 0); - assert.throws(() => { - sort.toString(); - }, /Cannot parse this sort type to an RQL query string/); - }, - - 'Should print JSON pointer.'(this: any) { - const result = createSort(new JsonPointer('key1', 'key2')).toString(); - assert.strictEqual(result, 'sort(+key1/key2)'); - } - } -}); diff --git a/tests/unit/query/createStoreRange.ts b/tests/unit/query/createStoreRange.ts deleted file mode 100644 index afff083..0000000 --- a/tests/unit/query/createStoreRange.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import createRange from '../../../src/query/createStoreRange'; -import { createData, ItemType } from '../support/createData'; - -registerSuite({ - name: 'StoreRange', - - 'Should return data in the specified range.'() { - const data = createData(); - assert.deepEqual(createRange(1, 2).apply(data), [ data[1], data[2] ]); - }, - 'Should return no data when count is 0.'() { - const data = createData(); - assert.lengthOf(createRange(1, 0).apply(data), 0); - }, - 'Should return last item when start from last item index and count is 1.'() { - const data = createData(); - const lastIndx = data.length - 1; - assert.deepEqual(createRange(lastIndx, 1).apply(data), [data[lastIndx]]); - }, - 'Should return last item only when start from last item index and count is more than 1.'() { - const data = createData(); - const lastIndx = data.length - 1; - assert.deepEqual(createRange(lastIndx, 3).apply(data), [data[lastIndx]]); - }, - 'Should have a toString that describes its properties.'() { - assert.strictEqual(createRange(1, 2).toString(), 'limit(2,1)'); - }, - 'Should exclude start if it is 0.'() { - assert.strictEqual(createRange(0, 2).toString(), 'limit(2)'); - } -}); diff --git a/tests/unit/state/Patch.ts b/tests/unit/state/Patch.ts new file mode 100644 index 0000000..f0932f2 --- /dev/null +++ b/tests/unit/state/Patch.ts @@ -0,0 +1,222 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { Pointer } from './../../../src/state/Pointer'; +import { Patch } from './../../../src/state/Patch'; +import * as ops from './../../../src/state/operations'; + +describe('state/Patch', () => { + + describe('add', () => { + + it('value to new path', () => { + const patch = new Patch(ops.add('/test', 'test')); + const obj = {}; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: 'test' }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test'), value: 'test'}, + {op: 'remove', path: new Pointer('/test') } + ]); + }); + + it('value to new nested path', () => { + const patch = new Patch(ops.add('/foo/bar/qux', 'test')); + const obj = {}; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { foo: { bar: { qux: 'test' } } }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/foo/bar/qux'), value: 'test'}, + {op: 'remove', path: new Pointer('/foo/bar/qux') } + ]); + }); + + it('value to existing path', () => { + const patch = new Patch(ops.add('/test', 'test')); + const obj = { test: true }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: 'test' }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test'), value: 'test'}, + {op: 'remove', path: new Pointer('/test') } + ]); + }); + + it('value to array index path', () => { + const patch = new Patch(ops.add('/test/0', 'test')); + const obj = { test: [] }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: ['test'] }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test/0'), value: 'test'}, + {op: 'remove', path: new Pointer('/test/0') } + ]); + }); + + }); + + describe('replace', () => { + + it('new path', () => { + const patch = new Patch(ops.replace('/test', 'test')); + const obj = {}; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: 'test' }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test'), value: 'test'}, + {op: 'replace', path: new Pointer('/test'), value: undefined } + ]); + }); + + it('value to new nested path', () => { + const patch = new Patch(ops.replace('/foo/bar/qux', 'test')); + const obj = {}; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { foo: { bar: { qux: 'test' } } }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/foo/bar/qux'), value: 'test'}, + {op: 'replace', path: new Pointer('/foo/bar/qux'), value: undefined } + ]); + }); + + it('existing path', () => { + const patch = new Patch(ops.replace('/test', 'test')); + const obj = { test: true }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: 'test' }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test'), value: 'test'}, + {op: 'replace', path: new Pointer('/test'), value: true } + ]); + }); + + it('array index path', () => { + const patch = new Patch(ops.replace('/test/1', 'test')); + const obj = { test: [ 'test', 'foo' ] }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: [ 'test', 'test' ] }); + assert.deepEqual(result.undoOperations, [ + {op: 'test', path: new Pointer('/test/1'), value: 'test'}, + {op: 'replace', path: new Pointer('/test/1'), value: 'foo' } + ]); + }); + + }); + + describe('remove', () => { + + it('new path', () => { + const patch = new Patch(ops.remove('/test')); + const obj = { other: true }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { other: true }); + assert.deepEqual(result.undoOperations, [ + {op: 'add', path: new Pointer('/test'), value: undefined } + ]); + }); + + it('existing path', () => { + const patch = new Patch(ops.remove('/test')); + const obj = { test: true }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { }); + assert.deepEqual(result.undoOperations, [ + {op: 'add', path: new Pointer('/test'), value: true } + ]); + }); + + it('array index path', () => { + const patch = new Patch(ops.remove('/test/1')); + const obj = { test: [ 'test', 'foo' ] }; + const result = patch.apply(obj); + assert.notStrictEqual(result.object, obj); + assert.deepEqual(result.object, { test: [ 'test' ] }); + assert.deepEqual(result.undoOperations, [ + {op: 'add', path: new Pointer('/test/1'), value: 'foo' } + ]); + }); + + }); + + describe('test', () => { + + it('success', () => { + const patch = new Patch(ops.test('/test', 'test')); + const obj = { test: 'test' }; + const result = patch.apply(obj); + assert.strictEqual(result.object, obj); + }); + + it('failure', () => { + const patch = new Patch(ops.test('/test', true)); + const obj = { test: 'test' }; + assert.throws(() => { + patch.apply(obj); + }, Error, 'Test operation failure. Unable to apply any operations.'); + }); + + it('nested path', () => { + const patch = new Patch(ops.test('/foo/0/bar/baz/0/qux', true)); + const obj = { + foo: [ { + bar: { + baz: [{ + qux: true + }] + } + }] + }; + const result = patch.apply(obj); + assert.strictEqual(result.object, obj); + }); + + it('complex value', () => { + const patch = new Patch(ops.test('/foo', [ { + bar: { + baz: [{ + qux: true + }] + } + }])); + const obj = { + foo: [ { + bar: { + baz: [{ + qux: true + }] + } + }] + }; + const result = patch.apply(obj); + assert.strictEqual(result.object, obj); + }); + + it('no value', () => { + const patch = new Patch(ops.test('/test', 'test')); + const obj = { test: 'test' }; + const result = patch.apply(obj); + assert.strictEqual(result.object, obj); + }); + }); + + it('unknown', () => { + const patch = new Patch({ + op: 'unknown', + path: new Pointer('/test') + } as any); + assert.throws(() => { + patch.apply({}); + }, Error, 'Unknown operation'); + }); + +}); diff --git a/tests/unit/state/Pointer.ts b/tests/unit/state/Pointer.ts new file mode 100644 index 0000000..7c12124 --- /dev/null +++ b/tests/unit/state/Pointer.ts @@ -0,0 +1,75 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { Pointer, walk } from './../../../src/state/Pointer'; + +describe('state/Pointer', () => { + + it('create pointer with string path', () => { + const pointer = new Pointer('/foo/bar'); + assert.strictEqual(pointer.path, '/foo/bar'); + assert.deepEqual(pointer.segments, [ 'foo', 'bar' ]); + }); + + it('create pointer with array path', () => { + const pointer = new Pointer([ 'foo', 'bar' ]); + assert.strictEqual(pointer.path, '/foo/bar'); + assert.deepEqual(pointer.segments, [ 'foo', 'bar' ]); + }); + + it('create with special characters', () => { + const pointer = new Pointer('/foo/bar~0~1'); + assert.strictEqual(pointer.path, '/foo/bar~0~1'); + assert.deepEqual(pointer.segments, [ 'foo', 'bar~/' ]); + }); + + it('create pointer for root should error', () => { + assert.throws(() => { + new Pointer(''); + }, Error, 'Access to the root is not supported.'); + assert.throws(() => { + new Pointer(['']); + }, Error, 'Access to the root is not supported.'); + assert.throws(() => { + new Pointer('/'); + }, Error, 'Access to the root is not supported.'); + assert.throws(() => { + new Pointer(['/']); + }, Error, 'Access to the root is not supported.'); + }); + + it('get', () => { + const pointer = new Pointer('/foo/bar/3'); + const obj = { foo: { bar: [ 1, 2, 3, 4, 5, 6, 7 ] } }; + assert.strictEqual(pointer.get(obj), 4); + }); + + it('get last item in array', () => { + const pointer = new Pointer('/foo/bar/-'); + const obj = { foo: { bar: [ 1, 2, 3, 4, 5, 6, 7 ] } }; + assert.strictEqual(pointer.get(obj), 7); + }); + + it('get deep path that does not exist', () => { + const pointer = new Pointer('/foo/bar/qux'); + const obj = { }; + assert.strictEqual(pointer.get(obj), undefined); + }); + + it('walk deep path that does not exist with clone', () => { + const pointer = new Pointer('/foo/bar/qux'); + const target = walk(pointer.segments, {}, true); + assert.deepEqual(target.object, { foo: { bar: {} } }); + assert.deepEqual(target.target, {}); + assert.deepEqual(target.segment, 'qux'); + }); + + it('walk deep path that does not exist not clone', () => { + const pointer = new Pointer('/foo/bar/qux'); + const target = walk(pointer.segments, {}, false); + assert.deepEqual(target.object, { foo: { bar: {} } }); + assert.deepEqual(target.target, {}); + assert.deepEqual(target.segment, 'qux'); + }); + +}); diff --git a/tests/unit/state/all.ts b/tests/unit/state/all.ts new file mode 100644 index 0000000..0092772 --- /dev/null +++ b/tests/unit/state/all.ts @@ -0,0 +1,3 @@ +import './operations'; +import './Patch'; +import './Pointer'; diff --git a/tests/unit/state/operations.ts b/tests/unit/state/operations.ts new file mode 100644 index 0000000..ab38bea --- /dev/null +++ b/tests/unit/state/operations.ts @@ -0,0 +1,45 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import * as operations from './../../../src/state/operations'; +import { OperationType } from './../../../src/state/Patch'; +import { Pointer } from './../../../src/state/Pointer'; + +describe('state/operations', () => { + + it('add()', () => { + const result = operations.add('/test', 'test'); + assert.deepEqual(result, { + op: OperationType.ADD, + path: new Pointer('/test'), + value: 'test' + }); + }); + + it('remove()', () => { + const result = operations.remove('/test'); + assert.deepEqual(result, { + op: OperationType.REMOVE, + path: new Pointer('/test') + }); + }); + + it('replace()', () => { + const result = operations.replace('/test', 'test'); + assert.deepEqual(result, { + op: OperationType.REPLACE, + path: new Pointer('/test'), + value: 'test' + }); + }); + + it('test()', () => { + const result = operations.test('/test', 'test'); + assert.deepEqual(result, { + op: OperationType.TEST, + path: new Pointer('/test'), + value: 'test' + }); + }); + +}); diff --git a/tests/unit/storage/InMemoryStorage.ts b/tests/unit/storage/InMemoryStorage.ts deleted file mode 100644 index fdb5ef1..0000000 --- a/tests/unit/storage/InMemoryStorage.ts +++ /dev/null @@ -1,244 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import Set from '@dojo/shim/Set'; -import InMemoryStorage from '../../../src/storage/InMemoryStorage'; -import Promise from '@dojo/shim/Promise'; -import { StoreOperation } from '../../../src/interfaces'; -import createFilter from '../../../src/query/createFilter'; -import createSort from '../../../src/query/createSort'; -import createRange from '../../../src/query/createStoreRange'; -import CompoundQuery from '../../../src/query/CompoundQuery'; -import { createData, createUpdates, ItemType, patches } from '../support/createData'; - -function getStorageAndDfd(test: any, option = {}) { - const dfd = test.async(1000); - const storage = new InMemoryStorage(option); - - return { dfd, storage, data: createData() }; -} - -registerSuite({ - name: 'InMemoryStorage', - 'identify': { - 'Should identify by idProperty if exists.'(this: any) { - const storage = new InMemoryStorage({ - idProperty: 'custId', - idFunction: (item: ItemType) => String(item.nestedProperty.value) - }); - const data = createData().map((item) => { - const newItem = { ...item }; - (newItem as any).custId = item.id + '-CID'; - return newItem as any; - }); - assert.deepEqual(storage.identify(data), ['item-1-CID', 'item-2-CID', 'item-3-CID']); - }, - 'Should identify by idFunction if idProperty doesn\'t exist.'(this: any) { - const storage = new InMemoryStorage({ - idFunction: (item: ItemType) => String(item.nestedProperty.value) - }); - assert.deepEqual(storage.identify(createData()), ['3', '2', '1']); - }, - 'Should default to `id` property if neither idProperty nor idFunction exists.'(this: any) { - const storage = new InMemoryStorage(); - assert.deepEqual(storage.identify(createData()), ['item-1', 'item-2', 'item-3']); - }, - 'Should accept identifying a single item.'(this: any) { - const storage = new InMemoryStorage(); - assert.deepEqual(storage.identify(createData()[2]), ['item-3']); - } - }, - - 'createId'() { - const storage = new InMemoryStorage(); - const ids: Promise[] = []; - const generateNIds = 1000; // reduced to 1,000 since IE 11 took minutes to run 100,000 - for (let i = 0; i < generateNIds; i++) { - ids.push(storage.createId()); - } - Promise.all(ids) - .then((ids) => { - assert.equal(new Set(ids).size, generateNIds, 'Not all generated IDs were unique'); - }); - }, - - 'add': { - 'Should add new items into storage.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data) - .then((result) => { - assert.deepEqual(result.successfulData, createData()); - }) - .then(dfd.resolve); - }, - 'Should return a result of type Add.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Add); - }) - .then(dfd.resolve); - }, - 'Should not allow adding existing items by default.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data); - storage.add(data).catch(dfd.callback((error: Error) => { - assert.strictEqual(error.message, 'Objects already exist in store'); - })); - } - }, - - 'put': { - 'Should put new items into storage.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - return storage.put(data) - .then((result) => { - assert.deepEqual(result.successfulData, createData()); - }); - }, - 'Should return a result of type Put.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.put(data) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Put); - }) - .then(dfd.resolve); - }, - 'Should allow adding existing items by default.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - const updates = createUpdates(); - - storage.put(data); - storage.put(updates[0]) - .then((result) => { - assert.deepEqual(result.successfulData, updates[0]); - }) - .then(dfd.resolve); - } - }, - - 'get': { - 'Should get single item.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - storage.add(data); - storage.get(['item-1']) - .then((items) => { - assert.deepEqual(items, [data[0]]); - }) - .then(dfd.resolve); - }, - 'Should get multiple items.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - storage.add(data); - storage.get(['item-1', 'item-3']) - .then((items) => { - assert.deepEqual(items, [data[0], data[2]]); - }) - .then(dfd.resolve); - }, - 'Should only get existing items back.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - storage.add(data); - const idNotExist = '4'; - storage.get(['item-1', 'item-3', idNotExist]) - .then((items) => { - assert.deepEqual(items, [data[0], data[2]]); - }) - .then(dfd.resolve); - } - }, - - 'fetch': { - 'Should fetch all the items when query is not provided.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - storage.add(data); - storage.fetch() - .then((items) => { - assert.deepEqual(items, data); - }) - .then(dfd.resolve); - }, - 'Should fetch queried items when a query is provided.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - const query = new CompoundQuery( { - query: createFilter().lessThan('value', 3) - } ) - .withQuery( createSort('id', true) ) - .withQuery( createRange(1, 1) ); - - storage.add(data); - storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [createData()[0]]); - }) - .then(dfd.resolve); - } - }, - - 'delete': { - 'Should delete items from storage.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - storage.add(data); - storage.delete(['item-1', 'item-3']) - .then((result) => { - assert.deepEqual(result.successfulData, ['item-1', 'item-3']); - }) - .then(dfd.resolve); - }, - 'Should return a result of type Delete.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data); - storage.delete(['item-1', 'item-3']) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Delete); - }) - .then(dfd.resolve); - }, - 'Should return empty array when deleting non-existing items.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data); - storage.delete(['4']) - .then((result) => { - assert.lengthOf(result.successfulData, 0); - }) - .then(dfd.resolve); - } - }, - - 'patch': { - 'Should return patched/updated items.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - const expectedItems = data.map(({ id, value, nestedProperty: { value: nestedValue } }) => ({ - id, - value: value + 2, - nestedProperty: { - value: nestedValue + 2 - } - })); - - storage.add(data); - storage.patch(patches) - .then((result) => { - assert.deepEqual(result.successfulData, expectedItems); - }) - .then(dfd.resolve); - }, - - 'Should return a result of type Patch.'(this: any) { - const { dfd, storage, data } = getStorageAndDfd(this); - - storage.add(data); - storage.patch(patches) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Patch); - }) - .then(dfd.resolve); - } - } -}); diff --git a/tests/unit/storage/IndexedDBStorage.ts b/tests/unit/storage/IndexedDBStorage.ts deleted file mode 100644 index 8f0b564..0000000 --- a/tests/unit/storage/IndexedDBStorage.ts +++ /dev/null @@ -1,729 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import * as sinon from 'sinon'; -import IndexedDBStorage, { createRequestPromise } from '../../../src/storage/IndexedDBStorage'; -import Promise from '@dojo/shim/Promise'; -import { StoreOperation, CrudOptions } from '../../../src/interfaces'; -import createFilter from '../../../src/query/createFilter'; -import createSort from '../../../src/query/createSort'; -import createRange from '../../../src/query/createStoreRange'; -import CompoundQuery from '../../../src/query/CompoundQuery'; -import { createData, createUpdates, ItemType, patches } from '../support/createData'; -import { QueryType, Query, Storage } from '../../../src/interfaces'; -import JsonPointer from '../../../src/patch/JsonPointer'; -import { BooleanOp } from '../../../src/query/createFilter'; - -registerSuite((() => { - const isIndexedDbAvailable = typeof indexedDB !== 'undefined'; - const storage: Storage = isIndexedDbAvailable ? new IndexedDBStorage({ - dbName: 'test-db' - }) : null; - - return { - name: 'IndexedDBStorage', - - setup(this: any) { - if (!isIndexedDbAvailable) { - this.skip(); - } - else { - storage.delete(['1', '2', '3']); - } - }, - - beforeEach() { - return storage.delete(createData().map((item) => item.id)); - }, - - 'add': { - 'Should add new items into storage.'(this: any) { - const data = createData(); - return storage.add(data) - .then((result) => { - assert.deepEqual(result.successfulData, data); - }); - }, - 'Should return a result of type Add.'(this: any) { - return storage.add(createData()) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Add); - }); - }, - 'Should not allow adding existing items by default.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.add(data) - .catch((error) => { - assert.ok(error, 'Should have thrown an error when adding an existing item'); - }) - ); - }, - 'Should default rejectOverwrite to true when options are provided and it is not specified'(this: any) { - const data = createData(); - return storage.add(data, {}) - .then(() => storage.add(data) - .catch((error) => { - assert.ok(error, 'Should have thrown an error when adding an existing item'); - }) - ); - }, - 'Should allow adding existing items when rejectOverwrite is set to false.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.add(data, { rejectOverwrite: false }) - .then(result => { - assert.deepEqual(result.successfulData, data); - }) - ); - } - }, - - 'put': { - beforeEach() { - return createRequestPromise(indexedDB.open('test-db')).then((db) => { - const objectStore = db.transaction('items', 'readwrite').objectStore('items'); - return Promise.all(createData().map((item) => createRequestPromise(objectStore.delete(item.id)))); - }); - }, - - 'Should put new items into storage.'(this: any) { - const data = createData(); - return storage.put(data) - .then((result) => { - assert.deepEqual(result.successfulData, data); - }); - }, - 'Should return a result of type Put.'(this: any) { - return storage.put(createData()) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Put); - }); - }, - 'Should allow adding existing items by default.'(this: any) { - const updates = createUpdates(); - - storage.put(createData()); - return storage.put(updates[0]) - .then((result) => { - assert.deepEqual(result.successfulData, updates[0]); - }); - } - }, - - 'get': { - 'Should get single item.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.get(['item-1']) - .then((items) => { - assert.deepEqual(items, [createData()[0]]); - }) - ); - }, - 'Should get multiple items.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.get(['item-1', 'item-3']) - .then((items) => { - assert.deepEqual(items, [data[0], data[2]]); - }) - ); - }, - 'Should only get existing items back.'(this: any) { - const data = createData(); - const nonexistentId = '4'; - return storage.add(data) - .then(() => storage.get(['item-1', 'item-3', nonexistentId]) - .then((items) => { - assert.deepEqual(items, [data[0], data[2]]); - }) - ); - } - }, - - 'fetch': { - 'Should fetch all the items when query is not provided.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.fetch() - .then((items) => { - assert.deepEqual(items, data); - }) - ); - }, - 'Should fetch queried items when a query is provided.'(this: any) { - const data = createData(); - const query = new CompoundQuery({ - query: createFilter().lessThan('value', 3) - }) - .withQuery(createSort('id', true)) - .withQuery(createRange(1, 1)); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[0]]); - }) - ); - }, - 'incremental query'(this: any) { - const data = createData(); - const query = createFilter() - .equalTo('id', 'item-1'); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[0]]); - }) - ); - }, - 'incremental query(not a filter)'(this: any) { - const data = createData(); - - return storage.add(data) - .then(() => storage.fetch(createSort('value', true)) - .then((items) => { - assert.deepEqual(items, data.reverse()); - }) - ); - }, - 'queries after non-incremental query'(this: any) { - const data = createData(); - const query = new CompoundQuery({ - query: createRange(0, 3) - }) - .withQuery(createFilter().greaterThan('value', 1)) - .withQuery(createFilter().lessThan('value', 3)); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[1]]); - }) - ); - }, - 'queries around non-incremental query'(this: any) { - const data = createData(); - const query = new CompoundQuery({ - query: createFilter().greaterThan('value', 1) - }) - .withQuery(createRange(0, 2)) - .withQuery(createFilter().lessThan('value', 3)); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[1]]); - }) - ); - }, - 'non-incremental query'(this: any) { - const data = createData(); - const query = createRange(0, 2); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[0], data[1]]); - }) - ); - }, - - 'incremental compound query'(this: any) { - const data = createData(); - const query = new CompoundQuery({ - query: createFilter().custom(() => true) - }) - .withQuery(createFilter().custom((item) => item.value === 3)); - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[2]], 'Didn\'t properly filter items'); - }) - ); - }, - - 'nested compound queries'(this: any) { - const data = createData(); - const query = new CompoundQuery({ - query: new CompoundQuery({ - query: createFilter().custom((item) => item.value === 1) - }) - }); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.deepEqual(items, [data[0]], 'Didn\'t property apply nested compund query'); - }) - ); - }, - - 'redundant filters should not be called'(this: any) { - const data = createData(); - const spy = sinon.spy(); - const query = new CompoundQuery({ - query: createFilter().custom((item) => false) - }) - .withQuery(new CompoundQuery({ - query: { - queryType: QueryType.Filter, - apply: spy, - toString() { - return ''; - }, - incremental: true - } - })); - - return storage.add(data) - .then(() => storage.fetch(query) - .then((items) => { - assert.strictEqual(items.length, 0, 'Shouldn\'t have returned any items'); - assert.isFalse(spy.called, 'Should not have called redundant query'); - }) - ); - } - }, - - 'delete': { - 'Should delete items from storage.'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.delete(['item-1', 'item-3']) - .then((result) => { - assert.deepEqual(result.successfulData, ['item-1', 'item-3']); - }) - ); - }, - 'Should return a result of type Delete.'(this: any) { - const data = createData(); - - return storage.add(data) - .then(() => storage.delete(['item-1', 'item-3']) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Delete); - }) - ); - }, - 'Should return ids even when deleting non-existing items.'(this: any) { - const data = createData(); - - return storage.add(data) - .then(() => storage.delete(['4']) - .then((result) => { - assert.deepEqual(result.successfulData, ['4']); - }) - ); - } - }, - - 'patch': { - 'Should return patched/updated items.'(this: any) { - const data = createData(); - const expectedItems = data.map(({id, value, nestedProperty: {value: nestedValue}}) => ({ - id, - value: value + 2, - nestedProperty: { - value: nestedValue + 2 - } - })); - - return storage.add(data) - .then(() => storage.patch(patches) - .then((result) => { - assert.deepEqual(result.successfulData, expectedItems); - }) - ); - }, - - 'Should return a result of type Patch.'(this: any) { - const data = createData(); - - return storage.add(data) - .then(() => storage.patch(patches) - .then((result) => { - assert.deepEqual(result.type, StoreOperation.Patch); - }) - ); - } - }, - - 'indices': { - 'should create indices for specified properties'(this: any) { - const dfd = this.async(1000); - const request = indexedDB.deleteDatabase('another-test-db-1'); - request.onsuccess = request.onerror = () => { - new IndexedDBStorage({ - dbName: 'another-test-db-1', - indices: { - value: true - } - }); - setTimeout(() => { - const request = indexedDB.open('another-test-db-1'); - request.onsuccess = dfd.callback((event: any) => { - const objectStore = event.target.result.transaction('items').objectStore('items'); - assert.isTrue(objectStore.indexNames.contains('value'), 'Didn\'t add specified index'); - }); - }, 100); - }; - }, - - 'should not error if specifying an existing index'(this: any) { - const dfd = this.async(1000); - const request = indexedDB.deleteDatabase('another-test-db-2'); - request.onsuccess = request.onerror = () => { - new IndexedDBStorage({ - dbName: 'another-test-db-2', - indices: { - value: true - } - }); - setTimeout(() => { - indexedDB.open('another-test-db-2').onsuccess = (event: any) => { - event.target.result.close(); - }; - setTimeout(dfd.rejectOnError(() => { - assert.doesNotThrow(() => { - new IndexedDBStorage({ - dbName: 'another-test-db-2', - version: 10, - indices: { - value: true - } - }); - }); - setTimeout(dfd.callback(() => { - }), 100); - }), 100); - }, 100); - }; - }, - - 'should search using indices if first filter is a simple comparator type'(this: any) { - const dfd = this.async(); - const request = indexedDB.deleteDatabase('another-test-db-3'); - const spy = sinon.spy(); - const queries: Query[] = [ - createFilter().equalTo('value', 1), - createFilter().greaterThanOrEqualTo('value', 2), - createFilter().greaterThan('value', 2), - createFilter().lessThanOrEqualTo('value', 2), - createFilter().lessThan('value', 2), - // With JSON Pointer - createFilter().equalTo(new JsonPointer('value'), 1), - createFilter().greaterThanOrEqualTo(new JsonPointer('value'), 2), - createFilter().greaterThan(new JsonPointer('value'), 2), - createFilter().lessThanOrEqualTo(new JsonPointer('value'), 2), - createFilter().lessThan(new JsonPointer('value'), 2) - ]; - queries.forEach((query) => { - query.apply = spy; - }); - request.onsuccess = request.onerror = () => { - const storage = new IndexedDBStorage({ - dbName: 'another-test-db-3', - indices: { - value: true - } - }); - - storage.add(createData()) - .then(() => { - Promise.all(queries.map((query) => storage.fetch(query))) - .then(dfd.callback((results: ItemType[][]) => { - assert.isFalse( - spy.called, 'Should not have called spy for any of the queries' - ); - const data = createData(); - assert.deepEqual( - results[0], [data[0]], 'First query should have returned first item' - ); - assert.deepEqual( - results[1], data.slice(1), 'Second query should have returned last two items' - ); - assert.deepEqual( - results[2], [data[2]], 'Third query should have returned last item' - ); - assert.deepEqual( - results[3], - data.slice(0, 2), - 'Fourth query should have returned first two items' - ); - assert.deepEqual( - results[4], [data[0]], 'Last query should have returned first item' - ); - - // With JSON Pointers - assert.deepEqual( - results[5], - [data[0]], - 'First query should have returned first item(JSON Pointer)' - ); - assert.deepEqual( - results[6], - data.slice(1), - 'Second query should have returned last two items(JSON Pointer)' - ); - assert.deepEqual( - results[7], - [data[2]], - 'Third query should have returned last item(JSON Pointer)' - ); - assert.deepEqual( - results[8], - data.slice(0, 2), - 'Fourth query should have returned first two items(JSON Pointer)' - ); - assert.deepEqual( - results[9], - [data[0]], - 'Last query should have returned first item(JSON Pointer)' - ); - })); - }); - }; - }, - - 'should search using indices with first two filters combined if appropriate'(this: any) { - const dfd = this.async(1000); - const request = indexedDB.deleteDatabase('another-test-db-4'); - const spy = sinon.spy(); - const queries: Query[] = [ - createFilter().greaterThanOrEqualTo('value', 2).lessThanOrEqualTo('value', 2), - createFilter().greaterThan('value', 1).lessThan('value', 3), - createFilter().lessThanOrEqualTo('value', 2).greaterThanOrEqualTo('value', 2), - createFilter().lessThan('value', 3).greaterThan('value', 1) - ]; - queries.forEach((query) => { - query.apply = spy; - }); - request.onsuccess = request.onerror = () => { - const storage = new IndexedDBStorage({ - dbName: 'another-test-db-4', - indices: { - value: true - } - }); - - storage.add(createData()) - .then(() => { - Promise.all(queries.map((query) => storage.fetch(query))) - .then(dfd.callback((results: ItemType[][]) => { - assert.isFalse(spy.called, 'Should not have called spy for any of the queries'); - const data = createData(); - assert.deepEqual(results[0], [data[1]], 'First query should have returned second item'); - assert.deepEqual(results[1], [data[1]], 'Second query should have returned second item'); - assert.deepEqual(results[2], [data[1]], 'Third query should have returned second item'); - assert.deepEqual(results[3], [data[1]], 'Fourth query should have returned second item'); - })); - }); - }; - }, - - 'should still apply subsequent filters in filter applied to index'(this: any) { - const dfd = this.async(1000); - const request = indexedDB.deleteDatabase('another-test-db-5'); - const queries: Query[] = [ - createFilter() - .greaterThanOrEqualTo('value', 1) - .lessThanOrEqualTo('value', 3) - .greaterThan('value', 1) - .lessThan('value', 3), - createFilter().greaterThan('value', 1).lessThan('value', 4).notEqualTo('value', 3), - createFilter() - .lessThanOrEqualTo('value', 3) - .greaterThanOrEqualTo('value', 1) - .equalTo('value', 2), - createFilter().lessThan('value', 4).greaterThan('value', 0).equalTo('value', 2) - ]; - request.onsuccess = request.onerror = () => { - const storage = new IndexedDBStorage({ - dbName: 'another-test-db-5', - indices: { - value: true - } - }); - - storage.add(createData()) - .then(() => { - Promise.all(queries.map((query) => storage.fetch(query))) - .then(dfd.callback((results: ItemType[][]) => { - const data = createData(); - assert.deepEqual(results[0], [data[1]], 'First query should have returned second item'); - assert.deepEqual(results[1], [data[1]], 'Second query should have returned second item'); - assert.deepEqual(results[2], [data[1]], 'Third query should have returned second item'); - assert.deepEqual(results[3], [data[1]], 'Fourth query should have returned second item'); - })); - }); - }; - } - }, - - 'shouldn\'t overwrite existing store'(this: any) { - const dbName = 'another-test-db'; - const storeName = 'another-test-store'; - const dfd = this.async(); - const request = indexedDB.deleteDatabase(dbName); - request.onsuccess = request.onerror = () => { - const request = indexedDB.open(dbName); - request.onupgradeneeded = (event: any) => { - const db = event.target.result; - db.createObjectStore(storeName); - }; - request.onsuccess = (event: any) => { - storage.add(createData()); - const db = event.target.result; - const objectStore = db - .transaction(storeName, 'readwrite') - .objectStore(storeName); - const item = createData()[0]; - const request = objectStore.add(item, 'item-1'); - request.onerror = () => { - dfd.reject(Error('Unable to add item to db')); - }; - request.onsuccess = () => { - db.close(); - const newStorage = new IndexedDBStorage({ - dbName: dbName, - objectStoreName: storeName, - version: 10 - }); - - return newStorage.fetch() - .then(dfd.callback((data: ItemType[]) => { - assert.deepEqual(data, [item], 'Shouldn\'t have deleted data'); - }), dfd.reject); - }; - }; - - request.onerror = () => { - dfd.reject('Unable to create database'); - }; - }; - }, - - 'should fail operations if database startup fails'(this: any) { - const dfd = this.async(); - const stub = sinon.stub(indexedDB, 'open', () => { - const requestStub: any = {}; - setTimeout(() => { - requestStub.error = { message: 'Failed to open database' }; - requestStub.onerror(); - }, 1000); - return requestStub; - }); - - new IndexedDBStorage({ - dbName: 'test-db' - }) - .add(createData()) - .then(() => { - stub.restore(); - dfd.reject('Should have failed'); - }, dfd.callback((error: any) => { - stub.restore(); - assert.strictEqual( - error.message, 'Failed to open database', 'Should have failed with DB error message' - ); - })); - }, - - 'should use a default db name if none is provided'(this: any) { - const request = indexedDB.deleteDatabase('store-database'); - const dfd = this.async(); - const data = createData(); - request.onsuccess = request.onerror = () => { - const storage = new IndexedDBStorage(); - storage.add(data) - .then(() => { - storage.fetch() - .then(dfd.callback((fetchedData: ItemType[]) => { - assert.deepEqual(fetchedData, data, 'Didn\'t use default database and store data'); - }), dfd.reject); - }, dfd.reject); - }; - }, - - 'should have totalLength property on fetch results': { - 'fetch all'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.fetch() - .totalLength - .then((totalLength) => { - assert.equal(3, totalLength, 'Didn\'t return the correct total length'); - }) - ); - }, - - 'filtered fetch'(this: any) { - const data = createData(); - return storage.add(data) - .then(() => storage.fetch(createFilter().lessThan('value', 2)) - .totalLength - .then((totalLength) => { - assert.equal(3, totalLength, 'Didn\'t return the correct total length'); - }) - ); - } - }, - - 'should handle unusually formed filters'(this: any) { - const dfd = this.async(1000); - const request = indexedDB.deleteDatabase('another-test-db-6'); - const filterWithNoPath = createFilter().greaterThan('value', 2); - const filterStartingWithBooleanOp = createFilter(); - filterStartingWithBooleanOp.filterChain!.push(BooleanOp.And); - ( filterWithNoPath.filterChain![0]).path = undefined; - const queries: Query[] = [ - // Second filter exists but can't be used - createFilter().greaterThanOrEqualTo('value', 2).equalTo('value', 3), - // First filter targets the correct property but is not an - // appropriate filter type - createFilter().notEqualTo('value', 1).lessThan('value', 3), - filterWithNoPath, - // Both have the correct path but cannot be cast to a - // range - createFilter().notEqualTo('value', 1).notEqualTo('value', 3), - // Empty filter chain starting with boolean op, - filterStartingWithBooleanOp - ]; - request.onsuccess = request.onerror = () => { - const storage = new IndexedDBStorage({ - dbName: 'another-test-db-6', - indices: { - value: true - } - }); - - storage.add(createData()) - .then(() => { - Promise.all(queries.map((query) => storage.fetch(query))) - .then(dfd.callback((results: ItemType[][]) => { - const data = createData(); - assert.deepEqual(results[0], [data[2]], 'First query should have returned last item'); - assert.deepEqual(results[1], [data[1]], 'Second query should have returned second item'); - assert.deepEqual(results[2], [data[2]], 'Third query should have returned last item'); - assert.deepEqual(results[3], [data[1]], 'Fourth query should have returned second item'); - assert.deepEqual(results[4], data, 'Fourth query should have returned all items'); - })); - }); - }; - }, - - 'should throw an error if database doesn\'t exist when trying to create a transaction'(this: any) { - assert.throws(() => { - class TestGetTransactionThrows extends IndexedDBStorage { - constructor() { - try { super(); } catch (ignore) { } - this._getTransactionAndObjectStore(); - } - } - new TestGetTransactionThrows(); - }, 'Can\'t create transaction because database does not exist'); - } - }; -})()); diff --git a/tests/unit/store/ObservableStore.ts b/tests/unit/store/ObservableStore.ts deleted file mode 100644 index 93cc125..0000000 --- a/tests/unit/store/ObservableStore.ts +++ /dev/null @@ -1,820 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import ObservableStore, { ObservableStoreInterface, StoreDelta, ItemUpdate } from '../../../src/store/ObservableStore'; -import { ItemType, createData, createUpdates, patches, patchedItems } from '../support/createData'; -import { CrudOptions, UpdateResults } from '../../../src/interfaces'; -import AsyncStorage from '../support/AsyncStorage'; -import InMemoryStorage from '../../../src/storage/InMemoryStorage'; -import Set from '@dojo/shim/Set'; -import Promise from '@dojo/shim/Promise'; - -function getStoreAndDfd(test: any) { - const dfd = test.async(1000); - const observableStore: ObservableStoreInterface> = new ObservableStore({ data: createData() } ); - const emptyObservableStore = new ObservableStore(); - const fetchingObservableStore: ObservableStoreInterface> = new ObservableStore( { - data: createData(), - fetchAroundUpdates: true - }); - - return { dfd, observableStore, emptyObservableStore, data: createData(), fetchingObservableStore}; -} -function getStoreWithAsyncStorage(test: any, asyncOptions?: {}, useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const asyncStorage = new AsyncStorage(asyncOptions); - const observableStore = new ObservableStore({ storage: asyncStorage }); - - return { dfd, observableStore, asyncStorage }; -} - -registerSuite({ - name: 'ObservableStore', - - 'with basic store': (() => { - const ids = createData().map((item) => item.id); - return { - 'should be able to observe the whole store': { - 'initial updates'(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - let firstUpdate = true; - observableStore.observe().subscribe((update: StoreDelta) => { - try { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update, { - updates: [], - deletes: [], - adds: [], - beforeAll: [], - afterAll: [] - }, 'Didn\'t send the proper initial update'); - } - else { - assert.deepEqual(update, { - updates: [], - deletes: [], - adds: data, - beforeAll: [], - afterAll: data - }, 'Didn\'t send the proper initial update'); - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - }, - put(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - const updates = createUpdates(); - let ignoreFirst = 2; - observableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - updates: [ updates[0][0] ], - deletes: [], - beforeAll: data, - afterAll: [ updates[0][0], data[1], data[2] ], - adds: [] - }, 'Didn\'t send the proper update'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.put(updates[0][0]); - }, - - patch(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - let ignoreFirst = 2; - observableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - // Patch operate on the item itself in a memory store. This means that any references - // to that item will be updated immediately - const item = patches[0].patch.apply(createData()[0]); - try { - assert.deepEqual(update, { - updates: [ item ], - deletes: [], - beforeAll: data, - afterAll: [ item, data[1], data[2] ], - adds: [] - }, 'Didn\'t send the proper update'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.patch(patches[0]); - }, - - add(this: any) { - const { dfd, emptyObservableStore: observableStore, data } = getStoreAndDfd(this); - let ignoreFirst = true; - observableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - updates: [], - deletes: [], - beforeAll: [], - afterAll: [ data[0] ], - adds: [ data[0] ] - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.add(data[0]); - }, - - delete(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - let ignoreFirst = 2; - observableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - updates: [], - deletes: [ ids[0] ], - beforeAll: data, - afterAll: [ data[1], data[2] ], - adds: [] - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.delete(ids[0]); - }, - - 'with fetch around updates': { - 'initial update'(this: any) { - const { dfd, fetchingObservableStore, data } = getStoreAndDfd(this); - fetchingObservableStore.observe().subscribe(dfd.callback((update: StoreDelta) => { - assert.deepEqual(update, { - updates: [], - adds: [], - deletes: [], - afterAll: data, - beforeAll: [] - }, 'Didn\'t send initial update with data'); - })); - }, - put(this: any) { - const { dfd, fetchingObservableStore, data } = getStoreAndDfd(this); - const updates = createUpdates(); - let ignoreFirst = true; - fetchingObservableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - updates: [ updates[0][0] ], - deletes: [], - beforeAll: data, - afterAll: [ updates[0][0], data[1], data[2] ], - adds: [] - }, 'Didn\'t send the proper update'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - fetchingObservableStore.put(updates[0][0]); - }, - - patch(this: any) { - const { dfd, fetchingObservableStore, data} = getStoreAndDfd(this); - patches[0].patch.apply(data[0]); - let ignoreFirst = true; - fetchingObservableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - // Patch operate on the item itself in a memory store. This means that any references - // to that item will be updated immediately - assert.deepEqual(update, { - updates: [ data[0] ], - deletes: [], - beforeAll: createData(), - afterAll: data, - adds: [] - }, 'Didn\'t send the proper update'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - fetchingObservableStore.patch(patches[0]); - }, - - add(this: any) { - const { dfd, data } = getStoreAndDfd(this); - const fetchingObservableStore = new ObservableStore({ - fetchAroundUpdates: true - }); - let ignoreFirst = true; - fetchingObservableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - updates: [], - deletes: [], - beforeAll: [], - afterAll: [ data[0] ], - adds: [ data[0] ] - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - fetchingObservableStore.add(data[0]); - }, - - delete(this: any) { - const { dfd, fetchingObservableStore, data } = getStoreAndDfd(this); - let ignoreFirst = true; - fetchingObservableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - updates: [], - deletes: [ids[0]], - beforeAll: data, - afterAll: [data[1], data[2]], - adds: [] - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - fetchingObservableStore.delete(ids[0]); - } - } - }, - - 'should be able to observe items by ids': { - 'observing a single id': { - put(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - const updatedItem = createUpdates()[0][0]; - let firstUpdate = true; - observableStore.observe(ids[0]).subscribe((update: ItemType) => { - try { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update, createData()[0], 'Didn\'t send the initial notification for item'); - } else { - assert.deepEqual(update, updatedItem, 'Didn\'t send the correct update'); - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - observableStore.put(updatedItem); - }, - - patch(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - const patch = patches[0]; - let firstUpdate = true; - observableStore.observe(ids[0]).subscribe((update: ItemType) => { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update, createData()[0], 'Didn\'t send the initial notification for item'); - } - else { - assert.deepEqual(update, patchedItems[0], 'Didn\'t send the correct update'); - dfd.resolve(); - } - }); - observableStore.patch(patch); - }, - - delete(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - let updatePassed = false; - let firstUpdate = true; - observableStore.observe(ids[0]).subscribe((update: ItemType) => { - try { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update, createData()[0], 'Didn\'t send the initial notification for item'); - updatePassed = true; - } - else { - throw Error('Shouldn\'t have sent another update before completing'); - } - } catch (error) { - dfd.reject(error); - } - }, undefined, dfd.callback(() => { - assert.isTrue(updatePassed, 'Should have updated before completing'); - })); - observableStore.delete(ids[0]); - } - }, - 'observing a single id multiple times'(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - const updatedItem = createUpdates()[0][0]; - let count = 0; - function next(update: ItemType) { - count++; - if (count <= 2) { - // skip initial updates. - return; - } - assert.deepEqual(update, updatedItem, 'Didn\'t send the correct update'); - if (count === 4) { - dfd.resolve(); - } - } - - observableStore.observe(ids[0]).subscribe(next); - observableStore.observe(ids[0]).subscribe(next); - observableStore.put(updatedItem); - }, - 'observing multiple ids'(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - const data = createData(); - - let initialUpdate = 0; - - let putUpdate = false; - const put = createUpdates()[0][0]; - - let patchUpdate = false; - const patched = patches[1].patch.apply(createData()[1]); - const patch = patches[1]; - - let firstDelete = false; - let secondDelete = false; - let thirdDelete = false; - observableStore.observe(ids).subscribe((update: ItemUpdate) => { - try { - if (initialUpdate < 3) { - if (initialUpdate !== 1) { - assert.deepEqual(update, { - item: data[initialUpdate], - id: data[initialUpdate].id - }, 'Didn\'t send proper initial update' - ); - } - initialUpdate++; - return; - } - if (!putUpdate) { - assert.deepEqual(update, { - item: put, - id: put.id - }, 'Didn\'t send the right update for put operation' - ); - putUpdate = true; - } - else if (!patchUpdate) { - assert.deepEqual(update, { - item: patched, - id: patched.id - }, 'Didn\'t send the right update for patch operation' - ); - patchUpdate = true; - } - else if (!firstDelete) { - assert.deepEqual(update, { - item: undefined, - id: data[0].id - }, 'Didn\'t send the right update for first delete operation' - ); - firstDelete = true; - } - else if (!secondDelete) { - assert.deepEqual(update, { - item: undefined, - id: data[1].id - }, 'Didn\'t send the right update for second delete operation' - ); - secondDelete = true; - } - else if (!thirdDelete) { - assert.deepEqual(update, { - item: undefined, - id: data[2].id - }, 'Didn\'t send the right update for third delete operation' - ); - thirdDelete = true; - } - else { - throw Error('Shouldn\'t have received another update'); - } - - } catch (error) { - dfd.reject(error); - } - }, undefined, dfd.callback(() => { - assert.isTrue( - putUpdate && patchUpdate && firstDelete && secondDelete && thirdDelete, - 'Didn\'t send all updates before completing _observable' - ); - })); - observableStore.put(put); - observableStore.patch(patch); - observableStore.delete([ ids[0], ids[1] ]); - observableStore.delete(ids[2]); - }, - 'observing multiple ids multiple times'(this: any) { - const { dfd, observableStore } = getStoreAndDfd(this); - const updatedItem = createUpdates()[0][0]; - let count = 0; - function next(update: ItemUpdate) { - count++; - if (count <= 6) { - // skip initial updates. - return; - } - assert.deepEqual(update.item, updatedItem, 'Didn\'t send the correct update'); - if (count === 8) { - dfd.resolve(); - } - } - - observableStore.observe(ids).subscribe(next); - observableStore.observe(ids).subscribe(next); - observableStore.put(updatedItem); - } - - }, - - 'should receive an update when subscribed before initial items are stored'(this: any) { - const { dfd, emptyObservableStore: observableStore, data } = getStoreAndDfd(this); - - let ignoreFirst = true; - observableStore.observe().subscribe((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update.adds, data, 'Should have received an update for all three items added'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.add(data); - }, - - 'should not allow observing on non-existing ids'(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - const idNotExist = '4'; - - observableStore.observe(idNotExist).subscribe(() => { - dfd.reject(new Error('Should not call success callback.')); - }, () => { - dfd.resolve(); - }); - }, - 'should include non-existing ids in the error message.'(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - const idNotExist = '4'; - const idExisting = 'item-2'; - observableStore.observe([idExisting, idNotExist]).subscribe( - () => {}, - dfd.callback((error: Error) => { - assert.isTrue(error.message.indexOf(idExisting) === -1, `${idExisting} should not be included in the error message`); - assert.isTrue(error.message.indexOf(idNotExist) !== -1, `${idNotExist} should be included in the error message`); - })); - }, - 'should overwrite dirty data by default'(this: any) { - const { dfd, observableStore, data } = getStoreAndDfd(this); - const updates = createUpdates(); - observableStore.put(updates[0][0]); - observableStore.put(updates[1][0]).subscribe(dfd.callback((result: UpdateResults) => { - assert.deepEqual(result.successfulData[0], updates[1][0], 'Should have taken the second update'); - })); - }, - - 'when operation fails in an ordered store, the error should be sent the observable way.'(this: any) { - const dfd = this.async(1000); - const store = new ObservableStore({ - data: createData() - }); - - const updates = createUpdates(); - - store.add(updates[0][2]) - .then(dfd.reject, dfd.callback((error: Error) => { - assert.equal( - error.message, - 'Objects already exist in store', - 'Didn\'t reject with appropriate error message' - ); - })); - }, - - 'deleting ID not in local cache'(this: any) { - const dfd = this.async(1000); - const preLoadedStorage = new InMemoryStorage(); - preLoadedStorage.add(createData()); - const store = new ObservableStore({ - storage: preLoadedStorage - }); - - let ignoreFirst = true; - store.observe().subscribe(dfd.rejectOnError((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - assert.deepEqual(update, { - updates: [], - deletes: [ 'item-1' ], - adds: [], - afterAll: [], - beforeAll: [] - }); - dfd.resolve(); - })); - store.delete('item-1'); - }, - - 'unsubscribing and resubscribing'(this: any) { - const store = new ObservableStore({ - fetchAroundUpdates: true, - data: createData() - }); - const dfd = this.async(1000); - store.observe().subscribe(() => { - dfd.reject(Error('Shouldn\'t receive updates for unsubscribed observer')); - }).unsubscribe(); - - const newObject = { - id: 'new', - value: 10, - nestedProperty: { - value: 10 - } - }; - let ignoreFirst = true; - store.observe().subscribe((delta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - try { - assert.deepEqual(delta.adds, [ newObject ], 'Observable not functioning properly after unsubscribing'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - store.add(newObject); - }, - - 'unsubscribing in update'(this: any) { - const store = new ObservableStore({ - fetchAroundUpdates: true, - data: createData() - }); - const dfd = this.async(1000); - let ignoreUnsubscribeFirst = true; - let unsubscribed = false; - const subscription = store.observe().subscribe(() => { - if (ignoreUnsubscribeFirst) { - ignoreUnsubscribeFirst = false; - return; - } - if (!unsubscribed) { - unsubscribed = true; - } - subscription.unsubscribe(); - }); - - let ignoreFirst = true; - store.observe().subscribe((delta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - try { - assert.deepEqual(delta.adds, [ newObject ], 'Observable not functioning properly after unsubscribing'); - assert.isTrue(unsubscribed, 'Should have unsubscribed'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - const newObject = { - id: 'new', - value: 10, - nestedProperty: { - value: 10 - } - }; - store.add(newObject); - } - }; - })(), - - 'ignore errors in observable mixin but propagate back to caller'(this: any) { - const failingStorage: any = { - add() { - return Promise.reject(Error('Add failed')); - }, - delete() { - return Promise.reject(Error('Delete failed')); - }, - put() { - return Promise.reject(Error('Put failed')); - }, - patch() { - return Promise.reject(Error('Patch failed')); - }, - fetch() { - const rejected = Promise.reject(Error('Fetch failed')); - ( rejected).totalLength = Promise.resolve(0); - return rejected; - } - }; - const observableStore = new ObservableStore({ - storage: failingStorage - }); - - return observableStore.add(createData()) - .then(() => { - throw Error('Promise should not have resolved for add'); - }, (error) => { - assert.equal('Add failed', error.message, 'Wrong error message'); - return observableStore.delete('1'); - }) - .then(() => { - throw Error('Promise should not have resolved for delete'); - }, (error) => { - assert.equal('Delete failed', error.message, 'Wrong error message'); - return observableStore.put(createData()); - }) - .then(() => { - throw Error('Promise should not have resolved for put'); - }, (error) => { - assert.equal('Put failed', error.message, 'Wrong error message'); - return observableStore.patch(patches); - }) - .then(() => { - throw Error('Promise should not have resolved for patch'); - }, (error) => { - assert.equal('Patch failed', error.message, 'Wrong error message'); - return observableStore.fetch(); - }) - .then(() => { - throw Error('Promise should not have resolved for fetch'); - }, (error) => { - assert.equal('Fetch failed', error.message, 'Wrong error message'); - }); - }, - - 'async storage': { - 'filtered subcollection async operations should be done in the order specified by the user.'(this: any) { - const { observableStore } = getStoreWithAsyncStorage(this, undefined, false); - const data = createData(); - const updates = createUpdates(); - - return observableStore.add(createData()) - .then((result) => { - assert.deepEqual(result, data, 'Should have returned all added items'); - return observableStore.put(updates[0]); - }) - .then((result) => { - assert.deepEqual(result, updates[0], 'Should have returned all updated items'); - return observableStore.delete(['item-2']); - }) - .then((result) => { - assert.deepEqual(result, ['item-2'], 'Should have returned all deleted ids'); - return observableStore.fetch(); - }) - .then((result) => { - assert.deepEqual(result, [updates[0][0], updates[0][2]], 'Should have returned all filtered items'); - }); - }, - - 'should be able to observe the whole store'(this: any) { - const { dfd, observableStore } = getStoreWithAsyncStorage(this); - const data = createData(); - - let ignoreFirst = true; - observableStore.observe().subscribe((update) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - updates: [], - deletes: [], - beforeAll: [], - afterAll: [data[0]], - adds: [data[0]] - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - observableStore.add(data[0]); - }, - 'should be able to observe single id'(this: any) { - const { dfd, observableStore } = getStoreWithAsyncStorage(this, { get: 50, put: 10 }); - const data = createData(); - const updatedItem = createUpdates()[0][0]; - let firstUpdate = true; - - observableStore.add(data[0]) - .then(() => { - observableStore.observe('item-1') - .subscribe((update: ItemType) => { - try { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update, createUpdates()[0][0], 'Didn\'t send the updated item in the initial notification'); - setTimeout(dfd.resolve, 100); - } - else { - throw Error('Should not have received a second update'); - } - } catch (error) { - dfd.reject(error); - } - }); - - observableStore.put(updatedItem); - }); - }, - 'should be able to observe with initial items'(this: any) { - const { dfd, asyncStorage } = getStoreWithAsyncStorage(this, { put: 50, get: 10 }); - const observableStore = new ObservableStore({ storage: asyncStorage, data: createData() }); - const data = createData(); - - observableStore.observe('item-1').subscribe((update: ItemType) => { - try { - assert.deepEqual(update, data[0], 'Didn\'t send the initial notification for item'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - }, - 'should only send one update if all items were updated before get finished'(this: any) { - const { dfd, observableStore } = getStoreWithAsyncStorage(this, { put: 10, get: 80 }); - observableStore.add(createData()) - .then(() => { - const updates = createUpdates()[0]; - const numbersFound = new Set(); - let updatesReceived = 0; - observableStore.observe(['item-1', 'item-2', 'item-3']) - .subscribe(dfd.rejectOnError(({ id, item }: ItemUpdate) => { - assert.deepEqual(item, updates[Number(id[id.length - 1]) - 1], 'Didn\'t receive correct initial update for item one'); - updatesReceived++; - numbersFound.add(id); - if (numbersFound.size === 3) { - setTimeout(dfd.resolve, 100); - } - else if (updatesReceived >= 3) { - dfd.reject(Error('Shouldn\'t have received any more updates')); - } - })); - observableStore.put(updates); - }); - } - } -}); diff --git a/tests/unit/store/QueryableStore/querying.ts b/tests/unit/store/QueryableStore/querying.ts deleted file mode 100644 index 802a22c..0000000 --- a/tests/unit/store/QueryableStore/querying.ts +++ /dev/null @@ -1,693 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import * as sinon from 'sinon'; -import { createData, ItemType, patches, createUpdates } from '../../support/createData'; -import createFilter from '../../../../src/query/createFilter'; -import createRange from '../../../../src/query/createStoreRange'; -import createSort from '../../../../src/query/createSort'; -import AsyncStorage from '../../support/AsyncStorage'; -import QueryStore from '../../../../src/store/QueryableStore'; -import { QueryResult } from '../../../../src/store/QueryResult'; -import { diff } from '../../../../src/patch/Patch'; -import Promise from '@dojo/shim/Promise'; -import { CrudOptions } from '../../../../src/interfaces'; - -function getStoreAndDfd(test: any, useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const queryStore = new QueryStore({ - data: createData() - }); - - const emptyStore = new QueryStore(); - - return { dfd, queryStore, emptyStore }; -} -function getStoreWithAsyncStorage(test: any, asyncOptions?: {}, useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const asyncStorage = new AsyncStorage(asyncOptions); - const queryStore = new QueryStore({ storage: asyncStorage }); - - return { dfd, queryStore, asyncStorage }; -} - -registerSuite({ - name: 'Queryable Store - Querying', - 'single query'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item: ItemType) => String(item.id) === 'item-1') - .fetch() - .then((items) => { - assert.deepEqual(items, [createData()[0]], 'Didn\'t filter items propertly'); - }); - }, - - 'nested queries'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item) => String(item.id) === 'item-1' || String(item.id) === 'item-2') - .filter((item: ItemType) => String(item.id) === 'item-2') - .fetch() - .then((items) => { - assert.deepEqual(items, [createData()[1]], 'Didn\'t filter items properly with nested query'); - }); - }, - - 'get'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item) => item.value > 1).get('item-1') - .then((item) => { - assert.isUndefined(item, 'Shouldn\'t have returned item'); - }); - }, - - 'get multiple items'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item) => item.value > 1) - .get(['item-1', 'item-2']) - .then((items) => { - assert.deepEqual(items, [], 'Shouldn\'t have returned items before they are added to local storage'); - }); - }, - - 'get with initial fetch'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const queriedView = queryStore.filter((item) => item.value > 1); - queriedView.fetch(); - return queriedView.get('item-1') - .then((item) => { - assert.isUndefined(item, 'Shouldn\'t have returned filtered item with updated collection'); - return queriedView.get('item-2') - .then((item) => { - assert.deepEqual(item, createData()[1], 'Should have returned item in filtered collection'); - }); - }); - }, - - 'get multiple items with initial fetch'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const queriedView = queryStore.filter((item) => item.value > 1); - queriedView.fetch(); - return queriedView.get(['item-1', 'item-2']) - .then((items) => { - assert.deepEqual(items, [createData()[1]], 'Should only return item in filtered collection'); - }); - }, - - 'accessing source'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const newItem = { - id: '4', - value: 4, - nestedProperty: { - value: 4 - } - }; - - queryStore.filter((item) => true).source.add(newItem); - - return queryStore.filter((item) => item.value < 100) - .fetch() - .then((data) => { - assert.deepEqual(data, [ - ...createData(), - newItem - ], 'Didn\'t properly add item to source store'); - }); - }, - - 'fetch with query and nested queries'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - queryStore.filter((item: ItemType) => Boolean(item.id)) - .filter((item: ItemType) => String(item.id) === 'item-2' || String(item.id) === 'item-1') - .fetch(createFilter().equalTo('id', 'item-1')) - .then((items) => { - assert.deepEqual( - items, [createData()[0]], 'Didn\'t filter items properly with nested query and query in fetch' - ); - }) - .then(dfd.resolve); - }, - - 'should retrieve source collection\'s data with queries'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - return queryStore - .filter(createFilter().lessThan('value', 3)) - .sort('value', true) - .fetch() - .then((fetchedData) => { - assert.deepEqual(fetchedData, [createData()[1], createData()[0]]); - }); - }, - - 'should be notified of changes in parent collection on items in query or just moved from query'(this: any) { - const dfd = this.async(2000); - const store = new QueryStore(); - const data = createData(); - const updates = createUpdates(); - const calls: Array<() => any> = [ - // If an item is moved out of range we should still get notified - () => store.put({ - id: 'item-1', - value: 4, - nestedProperty: { - value: 10 - } - }), - () => store.patch(patches[1]), - // updating or deleting items in query should create notifications - () => store.put(updates[0]), - () => store.put(data), - () => store.patch(patches[2]), - () => store.delete(data[0].id) - ]; - const subCollection = store.filter(createFilter().lessThanOrEqualTo('value', 3)); - subCollection.observe().subscribe(() => { - let nextCall: (() => any) | undefined = calls.shift(); - if (nextCall) { - nextCall(); - } else { - dfd.resolve(); - } - }); - setTimeout(() => { - dfd.reject(Error('Had ' + calls.length + ' operations remaining out of 5')); - }, 1000); - // Should get notified for adding items - store.add(data); - }, - - 'shouldn\'t get notifications for updates outside of query'(this: any) { - const dfd = this.async(2000); - const store = new QueryStore<{ id: string, value: number }>(); - const filteredView = store.filter(createFilter().lessThan('value', 5)); - - let ignoreFirst = true; - filteredView.observe().subscribe(() => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - dfd.reject(Error('Received a notification')); - }); - - store.add([ - { - id: 'item-1', - value: 6 - }, - { - id: 'item-2', - value: 7 - } - ]); - store.add({ id: 'item-3', value: 8 }); - store.put([ - { - id: 'item-1', - value: 7 - }, - { - id: 'item-2', - value: 6 - } - ]); - store.put({ id: 'item-3', value: 7 }); - - store.patch([ - { id: 'item-1', patch: diff({ id: 'item-1', value: 8 }) }, - { id: 'item-2', patch: diff({ id: 'item-2', value: 8 }) } - ]); - store.patch({ id: 'item-3', patch: diff({ id: 'item-3', value: 10 }) }); - store.delete(['item-1', 'item-2']); - store.delete('item-3'); - - setTimeout(dfd.resolve, 1000); - }, - - 'notification for item deleted from initial data'(this: any) { - const dfd = this.async(1000); - const store = new QueryStore({ - data: createData() - }); - const data = createData(); - const filtered = store.filter(createFilter().greaterThan('value', 0)); - let ignoreFirst = 2; - filtered.observe().subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - deletes: [ 'item-1' ], - updates: [], - beforeAll: data, - afterAll: [ data[1], data[2] ], - adds: [], - removedFromTracked: [ - { - id: 'item-1', - item: data[0], - previousIndex: 0 - } - ], - movedInTracked: [ - { - id: 'item-2', - index: 0, - previousIndex: 1, - item: data[1] - }, - { - id: 'item-3', - index: 1, - previousIndex: 2, - item: data[2] - } - ], - addedToTracked: [] - }, 'Didn\'t send proper update for delete'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - store.delete('item-1'); - }, - - 'notification on item deleted from initial data after fetch'(this: any) { - const dfd = this.async(1000); - const data = createData(); - const store = new QueryStore({ - data: data - }); - const filtered = store.filter(createFilter().greaterThan('value', 0)); - let ignoreFirst = 2; - filtered.observe().subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - deletes: [ 'item-1' ], - updates: [], - beforeAll: data, - afterAll: [ data[1], data[2] ], - adds: [], - removedFromTracked: [ - { - id: 'item-1', - item: data[0], - previousIndex: 0 - } - ], - movedInTracked: [ - { - id: 'item-2', - index: 0, - previousIndex: 1, - item: data[1] - }, - { - id: 'item-3', - index: 1, - previousIndex: 2, - item: data[2] - } - ], - addedToTracked: [] - }, 'Didn\'t send proper update for delete'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - filtered.fetch() - .then(() => { - store.delete('item-1'); - }); - }, - - 'should allow fetch with sort on a sorted store'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - const data = createData(); - - return queryStore.sort(createSort('id', true)) - .fetch(createSort('id', false)) - .then((fetchedData) => { - assert.deepEqual(fetchedData, [ data[0], data[1], data[2] ], 'Data fetched with sort was incorrect'); - }); - }, - - 'should allow fetch with filter on a filtered store'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - const data = createData(); - - return queryStore.filter(createFilter().greaterThanOrEqualTo('value', 2)) - .fetch(createFilter().lessThan('value', 3)) - .then((fetchedData: ItemType[]) => { - assert.deepEqual(fetchedData, [ data[1] ], 'Data fetched with filter was incorrect'); - }); - }, - - 'should allow fetch with range on a range filtered store'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - const data = createData(); - - return queryStore.range(1, 2) - .fetch(createRange(1, 1)) - .then((fetchedData: ItemType[]) => { - assert.deepEqual(fetchedData, [ data[2] ], 'Data fetched with range was incorrect'); - }); - }, - - 'all query mixin APIs should work together'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - const data = createData(); - - return queryStore.range(createRange(0, 3)) - .filter(createFilter().greaterThanOrEqualTo('value', 2)) - .filter( (item) => item.value >= 2 ) - .sort( createSort('id', true) ) - .range(createRange(1, 1)) - .range(0, 1) - .fetch() - .then((fetchedData: ItemType[]) => { - assert.deepEqual(fetchedData, [ data[1] ], 'Data fetched with multiple queries was incorrect'); - }); - }, - - 'unsubscribing and resubscribing'(this: any) { - const queryStore = new QueryStore({ - fetchAroundUpdates: true, - data: createData() - }); - const dfd = this.async(1000); - const filteredView = queryStore.filter((item) => item.value > 1); - filteredView.observe().subscribe(() => { - }).unsubscribe(); - setTimeout(() => { - let ignoreFirst = true; - const newObject = { - id: 'new', - value: 10, - nestedProperty: { - value: 10 - } - }; - filteredView.observe().subscribe((delta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - try { - assert.deepEqual(delta.adds, [ newObject ], 'Observable not functioning properly after unsubscribing'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - queryStore.add(newObject); - }, 100); - - }, - - 'unsubscribing with another subscriber'(this: any) { - const queryStore = new QueryStore({ - fetchAroundUpdates: true, - data: createData() - }); - const dfd = this.async(); - const filteredView = queryStore.filter((item) => item.value > 1); - const newObject = { - id: 'new', - value: 10, - nestedProperty: { - value: 10 - } - }; - let ignoreFirst = true; - filteredView.observe().subscribe(() => { - }).unsubscribe(); - setTimeout(() => { - filteredView.observe().subscribe((delta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - try { - assert.deepEqual(delta.adds, [ newObject ], 'Observable not functioning properly after unsubscribing'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - setTimeout(() => { - queryStore.add(newObject); - }); - }); - - }, - - 'unsubscribing in update'(this: any) { - const store = new QueryStore({ - fetchAroundUpdates: true, - data: createData() - }); - const dfd = this.async(1000); - const filtered = store.filter(() => true); - let ignoreUnsubscribeFirst = 2; - let unsubscribed = false; - const subscription = filtered.observe().subscribe(() => { - if (ignoreUnsubscribeFirst) { - ignoreUnsubscribeFirst--; - return; - } - if (!unsubscribed) { - unsubscribed = true; - } - subscription.unsubscribe(); - }); - - let ignoreFirst = 2; - filtered.observe().subscribe((delta) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - - try { - assert.deepEqual(delta.adds, [ newObject ], 'Observable not functioning properly after unsubscribing'); - assert.isTrue(unsubscribed, 'Should have unsubscribed'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - const newObject = { - id: 'new', - value: 10, - nestedProperty: { - value: 10 - } - }; - store.add(newObject); - }, - - 'should throw if created without a source'(this: any) { - assert.throw( - () => new QueryResult(), 'Query Transform result cannot be created without providing a source store' - ); - }, - - 'totalLength and dataLength': { - 'totalLength should return the total number of items in storage': { - 'fetch all'(this: any) { - const queryStore = new QueryStore({ - data: createData() - }).filter(() => false); - const fetchResult = queryStore.fetch(); - return fetchResult.then((data) => { - assert.equal(0, data.length, 'Should not return any items for this query'); - return fetchResult.totalLength.then((totalLength) => { - assert.equal(3, totalLength, 'Didn\'t return the correct totalLength'); - }); - }); - }, - - 'fetch with query'(this: any) { - const queryStore = new QueryStore({ - data: createData() - }).filter(() => false); - const fetchResult = queryStore.fetch(createFilter().custom(() => false)); - return fetchResult.then((data) => { - assert.equal(0, data.length, 'Should not return any items for this query'); - return fetchResult.totalLength.then((totalLength) => { - assert.equal(3, totalLength, 'Didn\'t return the correct totalLength'); - }); - }); - } - }, - - 'dataLength should return the number of items matching the Query Transform result\'s own queries': { - 'fetch all'(this: any) { - const queryStore = new QueryStore({ - data: createData() - }).filter((item) => item.value < 3); - const fetchResult = queryStore.fetch(); - return fetchResult.then((data) => { - assert.deepEqual(createData().slice(0, 2), data, 'Didn\'t return expected data'); - return fetchResult.dataLength.then((dataLength) => { - assert.equal(2, dataLength, 'Didn\'t return the correct dataLength'); - }); - }); - }, - - 'fetch with query'(this: any) { - const queryStore = new QueryStore({ - data: createData() - }).filter((item) => item.value < 3); - const fetchResult = queryStore.fetch(createFilter().custom((item) => item.value < 2)); - return fetchResult.then((data) => { - assert.deepEqual(data, [ createData()[0] ], 'Didn\'t return expected data'); - return fetchResult.dataLength.then((dataLength) => { - assert.equal(2, dataLength, 'Didn\'t return the correct dataLength'); - }); - }); - }, - - 'should be rejected if fetch errors'(this: any) { - const queryStore = new QueryStore({ - storage: { - fetch() { - const result = Promise.reject(Error('Fetch failed')); - ( result).totalLength = Promise.resolve(0); - return result; - } - } - }); - - const fetchResult = queryStore.filter(() => true).fetch(); - // Handle the error on the fetch result itself to avoid warnings - fetchResult.then(undefined, () => {}); - return fetchResult.dataLength.then(() => { - throw Error('Data length should not have resolved'); - }, (error: any) => { - assert.equal('Fetch failed', error.message); - }); - } - } - }, - - 'should continue to report correct data after multiple updates'(this: any) { - const dfd = this.async(); - const queryStore = new QueryStore({ - data: createData() - }); - - let updateCount = 0; - const updates = [ - { - id: 'item-1', - value: 3, - nestedProperty: { - value: 3 - } - }, - { - id: 'item-1', - value: 4, - nestedProperty: { - value: 3 - } - } - ]; - const sorted = queryStore.sort('value'); - - const data = createData(); - sorted.observe().subscribe(dfd.rejectOnError(({ afterAll }: any) => { - switch (updateCount) { - case 0: - case 1: - break; - case 2: - assert.deepEqual(afterAll, [ data[1], updates[0], data[2] ], 'Wrong first update'); - break; - case 3: - assert.deepEqual(afterAll, [ data[1], data[2], updates[1] ], 'Wrong second update'); - dfd.resolve(); - break; - } - updateCount++; - })); - queryStore.put(updates[0]) - .then(() => { - queryStore.put(updates[1]); - }); - }, - - 'async storage': { - 'filtered subcollection fetch should not return items when it is done before add.'(this: any) { - const { queryStore: store } = getStoreWithAsyncStorage(this, { put: 25, fetch: 1 }, false); - const subcollection = store.filter(createFilter().greaterThanOrEqualTo('value', 2)); - - store.add(createData()); - return subcollection.fetch() - .then((storeData) => { - assert.lengthOf(storeData, 0, 'should not have retrieved items'); - }); - }, - 'should complete initial add before subsequent operations'(this: any) { - const asyncStorage = new AsyncStorage(); - const store = new QueryStore({ - storage: asyncStorage, - data: createData() - }); - - return store.get(['item-1', 'item-2', 'item-3']) - .then((items: ItemType[]) => { - assert.deepEqual(items, createData(), 'Didn\'t retrieve items from async add'); - }); - }, - 'failed initial add should not prevent subsequent operations'(this: any) { - let fail = true; - const stub = sinon.stub(console, 'error'); - const asyncStorage = new (class extends AsyncStorage { - add(items: any[], options?: CrudOptions): any { - if (fail) { - fail = false; - return Promise.reject(Error('error')); - } - else { - return super.add(items, options); - } - } - })(); - const data = createData(); - const store = new QueryStore({ - storage: asyncStorage, - data: data - }); - - return store.add(data) - .then(() => store.get(['item-1', 'item-2', 'item-3']) - .then((items) => { - assert.isTrue(stub.called, 'Didn\'t log error for failed add'); - assert.equal('error', stub.args[0][0].message, 'Didn\'t log expected error'); - stub.restore(); - assert.isFalse(fail, 'Didn\'t fail for first operation'); - assert.deepEqual(items, data, 'Didn\'t retrieve items from add following failed initial add'); - }) - ); - } - } -}); diff --git a/tests/unit/store/QueryableStore/tracking.ts b/tests/unit/store/QueryableStore/tracking.ts deleted file mode 100644 index 423107e..0000000 --- a/tests/unit/store/QueryableStore/tracking.ts +++ /dev/null @@ -1,609 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import { createData, ItemType, createUpdates } from '../../support/createData'; -import createFilter from '../../../../src/query/createFilter'; -import QueryStore from '../../../../src/store/QueryableStore'; -import { TrackableStoreDelta, MappedQueryResultInterface } from '../../../../src/store/QueryResult'; -import Promise from '@dojo/shim/Promise'; - -registerSuite(() => { - let trackableQueryStore: QueryStore; - - function testFetchingQueryStore( - trackedCollection: QueryStore, - trackResult: MappedQueryResultInterface>, - dfd: any, - isFetchingAroundUpdates = false - ) { - return new Promise((resolve) => { - let ignoreFirst = true; - trackResult - .track() - .observe() - .subscribe((update) => { - try { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - if (isFetchingAroundUpdates) { - isFetchingAroundUpdates = false; - return; - } - // Check deleted item - assert.equal(update.removedFromTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.removedFromTracked[0], { - previousIndex: 0, - item: { id: 'item-2', value: 2, nestedProperty: { value: 2 } }, - id: 'item-2' - }, 'Didn\'t send proper delete notification'); - - // Check added items - assert.equal(update.addedToTracked.length, 2, 'Had wrong number of additions'); - assert.deepEqual(update.addedToTracked[0], { - index: 1, - item: { id: 'new', value: 4, nestedProperty: { value: 10 } }, - id: 'new' - }, 'Didn\'t send correct update for added item'); - assert.deepEqual(update.addedToTracked[1], { - index: 2, - item: { id: 'ignored', value: 5, nestedProperty: { value: 5 } }, - id: 'ignored' - }, 'Didn\'t send correct update for item moved into tracking by update'); - - // Check moved item - assert.equal(update.movedInTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.movedInTracked[0], { - index: 0, - previousIndex: 1, - item: { id: 'item-3', value: 3, nestedProperty: { value: 1 }}, - id: 'item-3' - }, 'Didn\'t send correct update for item moved within tracking'); - resolve(); - } catch (error) { - dfd.reject(error); - } - }); - - trackedCollection.delete('item-2'); - trackedCollection.add({ id: 'new', value: 10, nestedProperty: { value: 10 }}); - // Shouldn't create a notification - trackedCollection.add({ id: 'ignored', value: -1, nestedProperty: { value: -1 } }); - trackedCollection.put({ - id: 'ignored', - value: 5, - nestedProperty: { - value: 5 - } - }); - trackedCollection.put({ - id: 'new', - value: 4, - nestedProperty: { - value: 10 - } - }); - }); - } - function testFetchingQueryStoreWithDelayedOperations( - trackedCollection: QueryStore, - trackResult: MappedQueryResultInterface>, - dfd: any - ) { - return new Promise((resolve) => { - let notifiedOnDelete = false; - let notifiedOnAddition = false; - let notifiedOnUpdateIntoCollection = false; - let notifiedOnMoveInCollection = false; - let ignoreFirst = true; - - trackResult - .track() - .observe() - .subscribe((update) => { - try { - if (ignoreFirst) { - ignoreFirst = false; - } - else if (!notifiedOnDelete) { - assert.equal(update.removedFromTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.removedFromTracked[0], { - previousIndex: 0, - item: { id: 'item-2', value: 2, nestedProperty: { value: 2 } }, - id: 'item-2' - }, 'Didn\'t send proper delete notification'); - notifiedOnDelete = true; - } - else if (!notifiedOnAddition) { - assert.equal(update.addedToTracked.length, 1, 'Had wrong number of additions'); - assert.deepEqual(update.addedToTracked[0], { - index: 1, - item: { id: 'new', value: 10, nestedProperty: { value: 10 } }, - id: 'new' - }, 'Didn\'t send correct update for added item'); - notifiedOnAddition = true; - } - else if (!notifiedOnUpdateIntoCollection) { - assert.equal(update.addedToTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.addedToTracked[0], { - index: 1, - item: { id: 'ignored', value: 5, nestedProperty: { value: 5 } }, - id: 'ignored' - }, 'Didn\'t send correct update for item moved into tracking by update'); - notifiedOnUpdateIntoCollection = true; - } - else if (!notifiedOnMoveInCollection) { - assert.equal(update.movedInTracked.length, 2, 'Had wrong number of updates'); - assert.deepEqual(update.movedInTracked[0], { - index: 2, - previousIndex: 1, - item: { id: 'ignored', value: 5, nestedProperty: { value: 5 }}, - id: 'ignored' - }, 'Didn\'t send correct update for item moved within tracking'); - assert.deepEqual(update.movedInTracked[1], { - index: 1, - previousIndex: 2, - item: { id: 'new', value: 4, nestedProperty: { value: 10 }}, - id: 'new' - }, 'Didn\'t send correct update for item moved within tracking'); - notifiedOnMoveInCollection = true; - resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - - trackedCollection.delete('item-2'); - setTimeout(() => { - trackedCollection.add({ id: 'new', value: 10, nestedProperty: { value: 10 }}); - // Shouldn't create a notification - trackedCollection.add({ id: 'ignored', value: -1, nestedProperty: { value: -1 } }); - setTimeout(() => { - trackedCollection.put({ - id: 'ignored', - value: 5, - nestedProperty: { - value: 5 - } - }); - setTimeout(() => { - trackedCollection.put({ - id: 'new', - value: 4, - nestedProperty: { - value: 10 - } - }); - }, 300); - }, 300); - }, 300); - }); - } - - return { - name: 'Queryable Store - Tracking', - beforeEach() { - trackableQueryStore = new QueryStore({ - data: createData() - }); - }, - - 'updating in place': { - 'put()'(this: any) { - const dfd = this.async(1000); - const trackedCollection = trackableQueryStore - .filter((item) => item.value > 1) - .sort('value') - .track(); - - trackableQueryStore.put({ - id: 'item-1', - value: 5, - nestedProperty: { - value: 5 - } - }); - - let ignoreFirst = true; - trackedCollection.observe().subscribe((update: TrackableStoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.equal(update.removedFromTracked.length, 0, 'Shouldn\'t have any removals'); - assert.equal(update.addedToTracked.length, 1, 'Should have one addition'); - assert.equal(update.movedInTracked.length, 0, 'Shouldn\'t have any moves within tracked'); - - assert.deepEqual(update.addedToTracked[0], { - index: 2, - item: {id: 'item-1', value: 5, nestedProperty: {value: 5}}, - id: 'item-1' - }); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - } - }, - - 'not tracking or fetching with simple queries'(this: any) { - const dfd = this.async(1000); - const trackedCollection = trackableQueryStore - .filter((item) => item.value > 1) - .sort('value') - .track(); - - let ignoreFirst = true; - - trackedCollection.observe().subscribe((update) => { - try { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - // Check deleted item - assert.equal(update.removedFromTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.removedFromTracked[0], { - previousIndex: 0, - item: { id: 'item-2', value: 2, nestedProperty: { value: 2 } }, - id: 'item-2' - }, 'Didn\'t send proper delete notification'); - - // Check added items - assert.equal(update.addedToTracked.length, 1, 'Had wrong number of additions'); - assert.deepEqual(update.addedToTracked[0], { - index: 1, - item: { id: 'ignored', value: 5, nestedProperty: { value: 5 } }, - id: 'ignored' - }, 'Didn\'t send correct update for item moved into tracking by update'); - - // Check moved item - assert.equal(update.movedInTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.movedInTracked[0], { - index: 0, - previousIndex: 1, - item: { id: 'item-3', value: 3, nestedProperty: { value: 1 }}, - id: 'item-3' - }, 'Didn\'t send correct update for item moved within tracking'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - - trackableQueryStore.delete('item-2'); - trackableQueryStore.add({ id: 'new', value: 10, nestedProperty: { value: 10 }}); - trackableQueryStore.add({ id: 'ignored', value: -1, nestedProperty: { value: -1 } }); - trackableQueryStore.put({ - id: 'ignored', - value: 5, - nestedProperty: { - value: 5 - } - }); - trackableQueryStore.put({ - id: 'new', - value: 4, - nestedProperty: { - value: 10 - } - }); - trackableQueryStore.put({ - id: 'new', - value: -1, - nestedProperty: { - value: -1 - } - }); - }, - - 'tracking with a range query': { - 'full range'(this: any) { - const dfd = this.async(1000); - trackableQueryStore = new QueryStore({ - data: createData(), - fetchAroundUpdates: true - }); - const trackedCollection = trackableQueryStore - .filter((item) => item.value > 1) - .sort('value') - .range(0, 100) - .track(); - testFetchingQueryStore(trackableQueryStore, trackedCollection, dfd, true).then(dfd.resolve); - }, - 'released with range query should only filter "afterAll"'(this: any) { - const dfd = this.async(1000); - const trackableQueryStore = new QueryStore(); - const untrackedCollection = trackableQueryStore.range(0, 1).track().release(); - const data = createData(); - - let ignoreFirst = true; - untrackedCollection.observe().subscribe(dfd.rejectOnError(({ adds, afterAll }: TrackableStoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - assert.deepEqual(afterAll, [ data[0] ], 'Should have applied query to afterAll'); - assert.deepEqual(adds, data, 'Shouldn\'t have applied range query to adds'); - dfd.resolve(); - })); - trackableQueryStore.add(data); - - }, - 'full range - delay between operations'(this: any) { - const dfd = this.async(5000); - trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .filter((item) => item.value > 1) - .sort('value') - .range(0, 100) - .track(); - testFetchingQueryStoreWithDelayedOperations(trackableQueryStore, trackedCollection, dfd) - .then(dfd.resolve); - }, - 'full range, not initially fetching around updates'(this: any) { - const dfd = this.async(1000); - trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .filter((item) => item.value > 1) - .sort('value') - .range(0, 100) - .track(); - testFetchingQueryStore(trackableQueryStore, trackedCollection, dfd).then(dfd.resolve); - }, - 'item pushed into collection'(this: any) { - const dfd = this.async(1000); - trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .sort('value') - .range(1, 2) - .track(); - - let ignoreFirst = true; - trackedCollection.observe().subscribe((update) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.equal(update.removedFromTracked.length, 1, 'Had wrong number of updates'); - assert.deepEqual(update.removedFromTracked[0], { - previousIndex: 1, - item: { id: 'item-3', value: 3, nestedProperty: { value: 1 } }, - id: 'item-3' - }, 'Didn\'t send proper delete notification'); - assert.equal(update.addedToTracked.length, 1, 'Had wrong number of additions'); - assert.deepEqual(update.addedToTracked[0], { - index: 0, - item: { id: 'item-1', value: 1, nestedProperty: { value: 3 } }, - id: 'item-1' - }, 'Didn\'t send correct update for added item'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - trackableQueryStore.put({ - id: '0', - value: 0, - nestedProperty: { value: 10 } - }); - } - }, - - 'should be able to track a store with a query'(this: any) { - const dfd = this.async(1000); - let firstUpdate = true; - const data = createData(); - trackableQueryStore.query(createFilter().custom(() => true)).track().observe().subscribe(update => { - try { - if (firstUpdate) { - firstUpdate = false; - assert.deepEqual(update.addedToTracked, [ - { - id: 'item-1', - index: 0, - item: data[0] - }, - { - id: 'item-2', - index: 1, - item: data[1] - }, - { - id: 'item-3', - index: 2, - item: data[2] - } - ], 'Should contain initial items'); - } - else { - assert.deepEqual(update, { - addedToTracked: [], - adds: [], - afterAll: [ data[1], data[2], data[0] ], - beforeAll: data, - deletes: [], - movedInTracked: [ - { - id: 'item-1', - previousIndex: 0, - index: 2, - item: data[0] - }, - { - id: 'item-2', - previousIndex: 1, - index: 0, - item: data[1] - }, - { - id: 'item-3', - previousIndex: 2, - index: 1, - item: data[2] - } - ], - removedFromTracked: [], - updates: [ - data[0] - ] - }, 'Wrong update for delete followed by addition'); - - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - trackableQueryStore.delete('item-1'); - trackableQueryStore.add(createData()[0]); - }, - - 'add data after initialization'(this: any) { - const dfd = this.async(1000); - trackableQueryStore = new QueryStore(); - let ignoreFirst = true; - trackableQueryStore.query(createFilter().custom(() => true)).track().observe().subscribe(update => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update.adds, createData(), 'Should contain added items'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - trackableQueryStore.add(createData()); - }, - - 'should receive a notification of initial data'(this: any) { - const dfd = this.async(1000); - trackableQueryStore = new QueryStore({ - data: createData() - }); - trackableQueryStore - .filter((item) => item.value > 0) - .range(0, 100) - .track() - .observe() - .subscribe(dfd.callback((update: TrackableStoreDelta) => { - const data = createData(); - assert.deepEqual(update.addedToTracked, [ - { - id: 'item-1', - index: 0, - item: data[0] - }, - { - id: 'item-2', - index: 1, - item: data[1] - }, - { - id: 'item-3', - index: 2, - item: data[2] - } - ], 'Should contain initially added items'); - })); - }, - - 'tracked collection should filter out items that are not being tracked and remove updates that cancel out'(this: any) { - const dfd = this.async(1000); - const data = createData(); - - trackableQueryStore.delete(data.map((item) => item.id)) - .then(() => { - const trackedCollection = trackableQueryStore.filter((item: ItemType) => item.value > 1) - .sort('value') - .track(); - - let ignoreFirst = true; - trackedCollection.observe() - .subscribe((update: any) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - try { - assert.deepEqual(update, { - adds: [data[2]], - updates: [], - deletes: [], - movedInTracked: [], - removedFromTracked: [], - addedToTracked: [{ id: 'item-3', index: 0, item: data[2] }], - beforeAll: [], - afterAll: [data[2]] - }, 'Should have cancelled out delete and add, and filtered out the other non-tracked delete'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - trackableQueryStore.add(createData()); - trackableQueryStore.delete(['item-1', 'item-2']); - }); - }, - - 'should not receive same update data twice'(this: any) { - const dfd = this.async(1000); - const data = createData(); - const updates = createUpdates()[0]; - const trackableQueryStore = new QueryStore(); - const trackedCollection = trackableQueryStore.filter((item) => item.value > 1).sort('value').track(); - - let ignoreFirst = true; - let add = true; - trackedCollection.observe() - .subscribe(dfd.rejectOnError( - ({ adds, updates, addedToTracked, movedInTracked }: TrackableStoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - if (add) { - assert.deepEqual(adds, data.slice(1), 'Should have included adds'); - assert.deepEqual(addedToTracked, data.slice(1).map((item, index) => ({ - id: item.id, item: item, index: index - })), 'Should have included items added to tracked'); - add = false; - } - else { - assert.strictEqual(adds.length, 0, 'Shouldn\'t have included any adds'); - assert.deepEqual(updates, updates, 'Should have included updates'); - assert.deepEqual(addedToTracked, [updates[0]].map((item, index) => ({ - id: item.id, item: item, index: 0 - })), 'Should have included items added to tracked'); - assert.deepEqual(movedInTracked, updates.slice(1).map((item, index) => ({ - id: item.id, item: item, previousIndex: index, index: index + 1 - })), 'Should have included items moved in tracked'); - dfd.resolve(); - } - } - )); - - trackableQueryStore.add(data) - .then(() => { - setTimeout(() => { - trackableQueryStore.put(updates); - }, 50); - }); - } - }; -}); diff --git a/tests/unit/store/QueryableStore/transforming.ts b/tests/unit/store/QueryableStore/transforming.ts deleted file mode 100644 index 4e255b6..0000000 --- a/tests/unit/store/QueryableStore/transforming.ts +++ /dev/null @@ -1,823 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import { createData, patches } from '../../support/createData'; -import QueryStore from '../../../../src/store/QueryableStore'; -import createFilter from '../../../../src/query/createFilter'; -import Set from '@dojo/shim/Set'; - -function getStoreAndDfd(test: any, useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const queryStore = new QueryStore({ - data: createData() - }); - - const emptyStore = new QueryStore(); - - return { dfd, queryStore, emptyStore }; -} - -registerSuite({ - name: 'Queryable Store - Transform', - 'single transformations'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .fetch() - .then((data) => { - assert.deepEqual(data, [ - { newValue: 'item-1-1' }, - { newValue: 'item-2-2' }, - { newValue: 'item-3-3' } - ], 'Didn\'t transform data'); - }); - }, - - 'chained transformations'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .transform((item) => ({ newerValue: `${item.newValue}-+` })) - .fetch() - .then((data) => { - assert.deepEqual(data, [ - { newerValue: 'item-1-1-+' }, - { newerValue: 'item-2-2-+' }, - { newerValue: 'item-3-3-+' } - ], 'Didn\'t transform data'); - }); - }, - - 'queries and transformations'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item) => item.value >= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 })) - .filter((item) => item._value >= 4) - .transform((item) => ({ id: item.id, __value: item._value + 1 })) - .fetch() - .then((data) => { - assert.deepEqual(data, [{ id: 'item-3', __value: 5 }], 'Didn\'t work with queries and transformations'); - }); - }, - - 'get'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedView = queryStore.filter((item) => item.value <= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 })) - .filter((item) => item._value <= 4) - .transform((item) => ({ id: item.id, __value: item._value + 1 })); - - return transformedView.fetch() - .then(() => transformedView.get('item-1') - .then((data) => { - assert.deepEqual( - data, - { id: 'item-1', __value: 3 }, - 'Didn\'t work with queries and transformations' - ); - }) - ); - }, - - 'get non-existent item'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedView = queryStore.transform((item) => ({ id: item.id, _value: item.value + 1 })); - - return transformedView.fetch() - .then(() => transformedView.get('no-item') - .then((data) => { - assert.isUndefined(data, 'Should have returned undefined when fetching an undefined item'); - }) - ); - }, - - 'get with multiple ids'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedView = queryStore.filter((item) => item.value <= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 })) - .filter((item) => item._value <= 4) - .transform((item) => ({ id: item.id, __value: item._value + 1 })); - - return transformedView.fetch() - .then(() => transformedView.get(['item-1', 'item-2']) - .then((data) => { - assert.deepEqual( - data, - [ { id: 'item-1', __value: 3 }, { id: 'item-2', __value: 4 }], - 'Didn\'t work with queries and transformations' - ); - }) - ); - - }, - - 'get with mapped transformation'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedView = queryStore.filter((item) => item.value <= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 }), 'id') - .filter((item) => item._value <= 4) - .transform((item) => ({ id: item.id, __value: item._value + 1 }), 'id'); - - return transformedView.fetch() - .then(() => transformedView.get('item-1') - .then((data) => { - assert.deepEqual( - data, { id: 'item-1', __value: 3 }, 'Didn\'t work with queries and transformations' - ); - }) - ); - }, - - 'get multiple ids with mapped transformation'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedView = queryStore.filter((item) => item.value <= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 }), 'id') - .filter((item) => item._value <= 4) - .transform((item) => ({ id: item.id, __value: item._value + 1 }), 'id'); - - return transformedView.fetch() - .then(() => transformedView.get(['item-1', 'item-2']) - .then((data) => { - assert.deepEqual( - data, - [ { id: 'item-1', __value: 3 }, { id: 'item-2', __value: 4 } ], - 'Didn\'t work with queries and transformations' - ); - }) - ); - }, - 'applying additional queries in fetch'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.filter((item) => item.value >= 2) - .transform((item) => ({ id: item.id, _value: item.value + 1 })) - .fetch(createFilter().custom((item) => item._value >= 4)) - .then((data) => { - assert.deepEqual(data, [{ id: 'item-3', _value: 4 }], 'Didn\'t work with query in fetch'); - }); - }, - - 'observing items': { - 'single item': { - 'single transformation': { - 'initial update'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .observe('item-1') - .subscribe(dfd.callback((update: { newValue: string }) => { - assert.deepEqual(update, { - newValue: 'item-1-1' - }, 'Didn\'t receive once transformed update for single observed item'); - })); - }, - 'mutation update'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - let ignoreFirst = true; - queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .observe('item-1') - .subscribe((update: { newValue: string }) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - newValue: 'item-1-100' - }, 'Didn\'t receive once transformed update for single observed item'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - - queryStore.put({ - id: 'item-1', - value: 100, - nestedProperty: { - value: 1 - } - }); - }, - 'completion'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .observe('item-1') - .subscribe(() => {}, undefined, dfd.resolve); - - queryStore.delete('item-1'); - } - }, - 'chained transformations': { - 'initial update'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - chainedTransformation.observe('item-1') - .subscribe(dfd.callback((update: { newerValue: string }) => { - assert.deepEqual(update, { - newerValue: 'item-1-1-+' - }, 'Didn\'t receive chained transformed update for single observed item'); - })); - }, - 'mutation update'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let ignoreFirst = true; - chainedTransformation.observe('item-1') - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - newerValue: 'item-1-100-+' - }, 'Didn\'t receive chained transformed update for single observed item'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - - queryStore.put({ - id: 'item-1', - value: 100, - nestedProperty: { - value: 1 - } - }); - }, - 'completion'(this: any) { - const { dfd, queryStore } = getStoreAndDfd(this); - - queryStore.transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .transform((item) => ({ newerValue: `${item.newValue}-+` })) - .observe('item-1') - .subscribe(() => {}, undefined, dfd.resolve); - - queryStore.delete('item-1'); - } - } - }, - - 'multiple items': { - 'single transformation': { - 'initial updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const singleTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ); - - let idsUpdated = new Set(); - singleTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newValue: 'item-1-1' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newValue: 'item-2-2' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - }, - 'mutation updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const singleTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ); - - let idsUpdated = new Set(); - let ignoreFirst = 2; - singleTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newValue: 'item-1-100' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newValue: 'item-2-200' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - queryStore.put([ { - id: 'item-1', - value: 100, - nestedProperty: { - value: 1 - } - }, { - id: 'item-2', - value: 200, - nestedProperty: { - value: 1 - } - } ]); - }, - 'delete update'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const singleTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ); - - let ignoreFirst = 2; - singleTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - id: 'item-1', - item: undefined - }, 'Didn\'t receive once transformed update for first observed item'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - queryStore.delete('item-1'); - }, - 'completion'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const singleTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ); - - singleTransformation.observe(['item-1', 'item-2']).subscribe(() => {}, undefined, dfd.resolve); - queryStore.delete([ 'item-1', 'item-2' ]); - } - }, - 'chained transformations': { - 'initial updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let idsUpdated = new Set(); - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newerValue: 'item-1-1-+' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newerValue: 'item-2-2-+' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - }, - 'mutation updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let idsUpdated = new Set(); - let ignoreFirst = 2; - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newerValue: 'item-1-100-+' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newerValue: 'item-2-200-+' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - queryStore.put([ { - id: 'item-1', - value: 100, - nestedProperty: { - value: 1 - } - }, { - id: 'item-2', - value: 200, - nestedProperty: { - value: 1 - } - } ]); - }, - 'delete update'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let ignoreFirst = 2; - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - id: 'item-1', - item: undefined - }, 'Didn\'t receive once transformed update for first observed item'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - queryStore.delete('item-1'); - }, - 'completion'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }) - ); - - chainedTransformation.observe(['item-1', 'item-2']).subscribe(() => {}, undefined, dfd.resolve); - queryStore.delete([ 'item-1', 'item-2' ]); - } - }, - 'queries shouldn\'t affect targeted observation': { - 'initial updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value < 2) - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.charAt(0) === 'item-1') - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let idsUpdated = new Set(); - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newerValue: 'item-1-1-+' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newerValue: 'item-2-2-+' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - }, - 'mutation updates'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value < 2) - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.charAt(0) === 'item-1') - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let idsUpdated = new Set(); - let ignoreFirst = 2; - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - if (update.id === 'item-1') { - idsUpdated.add('item-1'); - assert.deepEqual(update, { - id: 'item-1', - item: { - newerValue: 'item-1-100-+' - } - }, 'Didn\'t receive once transformed update for first observed item'); - } - else if (update.id === 'item-2') { - idsUpdated.add('item-2'); - assert.deepEqual(update, { - id: 'item-2', - item: { - newerValue: 'item-2-200-+' - } - }, 'Didn\'t receive once transformed update for second observed item'); - } - if (idsUpdated.has('item-1') && idsUpdated.has('item-2')) { - dfd.resolve(); - } - } catch (error) { - dfd.reject(error); - } - }); - queryStore.put([ { - id: 'item-1', - value: 100, - nestedProperty: { - value: 1 - } - }, { - id: 'item-2', - value: 200, - nestedProperty: { - value: 1 - } - } ]); - }, - 'delete update'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value < 2) - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.charAt(0) === 'item-1') - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let ignoreFirst = 2; - chainedTransformation.observe(['item-1', 'item-2']) - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst--; - return; - } - try { - assert.deepEqual(update, { - id: 'item-1', - item: undefined - }, 'Didn\'t receive once transformed update for first observed item'); - dfd.resolve(); - } catch (error) { - dfd.reject(error); - } - }); - queryStore.delete('item-1'); - }, - 'completion'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value < 2) - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.charAt(0) === 'item-1') - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - chainedTransformation.observe(['item-1', 'item-2']).subscribe(() => {}, undefined, dfd.resolve); - queryStore.delete([ 'item-1', 'item-2' ]); - } - } - } - }, - - 'observing the whole result': { - 'initial update'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value <= 2) - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.indexOf('item-1') > -1) - .transform((item) => ({ newerValue: `${item.newValue}-+` })); - - let ignoreFirst = true; - chainedTransformation.observe() - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - adds: [{ newerValue: 'item-1-1-+' }], - updates: [], - deletes: [], - beforeAll: [], - afterAll: [{ newerValue: 'item-1-1-+' }] - }, 'Didn\'t properly filter before and after transforms when observing the whole result'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - }, - - 'initial update with idTransforms'(this: any) { - const {dfd, queryStore} = getStoreAndDfd(this); - - const chainedTransformation = queryStore.filter((item) => item.value <= 2) - // Loses 'mapped' status when no idTransform is specified - .transform((item) => ({ newValue: `${item.id}-${item.value}` })) - .filter((item) => item.newValue.indexOf('item-1') > -1) - // Can become mapped again by specifying idTransform on last transformation, since all that matters - // is that the final state can be identified - .transform((item) => ({ newerValue: `${item.newValue}-+` }), 'newerValue'); - - let ignoreFirst = true; - chainedTransformation.observe() - .subscribe((update) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - try { - assert.deepEqual(update, { - adds: [{ newerValue: 'item-1-1-+' }], - updates: [], - deletes: [], - beforeAll: [], - afterAll: [{ newerValue: 'item-1-1-+' }], - addedToTracked: [ - { - item: { newerValue: 'item-1-1-+' }, - index: 0, - id: 'item-1-1-+' - } - ], - removedFromTracked: [], - movedInTracked: [] - }, 'Didn\'t properly filter before and after transforms when observing the whole result'); - } catch (error) { - dfd.reject(error); - } - dfd.resolve(); - }); - } - }, - - 'id transform': { - 'idTransform function'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedStore = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }), (item) => item.newValue + '-+' - ); - return transformedStore.fetch() - .then((data) => transformedStore.identify(data)) - .then((ids) => { - assert.deepEqual( - ids, - ['item-1-1-+', 'item-2-2-+', 'item-3-3-+'], - 'Didn\'t properly identify items using idTransform function' - ); - }); - }, - - 'idTransform property'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedStore = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }), 'newValue' - ); - return transformedStore.fetch() - .then((data) => transformedStore.identify(data)) - .then((ids) => { - assert.deepEqual( - ids, - ['item-1-1', 'item-2-2', 'item-3-3'], - 'Didn\'t properly identify items using idTransform property' - ); - }); - }, - - 'multiple transformations'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - const transformedStore = queryStore.transform( - (item) => ({ newValue: `${item.id}-${item.value}` }), 'newValue' - ) - .transform((item) => ({ newerValue: `${item.newValue}-+` }), 'newerValue'); - - return transformedStore.fetch() - .then((data) => transformedStore.identify(data)) - .then((ids) => { - assert.deepEqual( - ids, - ['item-1-1-+', 'item-2-2-+', 'item-3-3-+'], - 'Didn\'t properly identify items using idTransform property in chained transforms' - ); - }); - } - }, - - 'transform with a patch'(this: any) { - const { queryStore } = getStoreAndDfd(this, false); - - return queryStore.transform(patches[0].patch) - .fetch() - .then((data) => { - assert.deepEqual( - data, - createData().map((item) => patches[0].patch.apply(item)), - 'Didn\'t use patch properly for transformation' - ); - }); - } -}); diff --git a/tests/unit/store/StoreBase.ts b/tests/unit/store/StoreBase.ts deleted file mode 100644 index 56c9664..0000000 --- a/tests/unit/store/StoreBase.ts +++ /dev/null @@ -1,618 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import * as sinon from 'sinon'; -import Map from '@dojo/shim/Map'; -import Promise from '@dojo/shim/Promise'; -import Set from '@dojo/shim/Set'; -import StoreBase from '../../../src/store/StoreBase'; -import createRange from '../../../src/query/createStoreRange'; -import createFilter from '../../../src/query/createFilter'; -import JsonPointer from '../../../src/patch/JsonPointer'; -import Patch from '../../../src/patch/Patch'; -import createSort from '../../../src/query/createSort'; -import createOperation, { OperationType } from '../../../src/patch/createOperation'; -import CompoundQuery from '../../../src/query/CompoundQuery'; -import InMemoryStorage from '../../../src/storage/InMemoryStorage'; -import { createData, ItemType, createUpdates, patches, patchedItems } from '../support/createData'; -import AsyncStorage from '../support/AsyncStorage'; -import { StoreOperation, CrudOptions } from '../../../src/interfaces'; - -function getStoreAndDfd(test: any, data = createData(), useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const store = new StoreBase( { data: data } ); - const emptyStore = new StoreBase(); - - return { dfd, store, emptyStore, data: createData() }; -} - -function getStoreWithAsyncStorage(test: any, asyncOptions?: {}, useAsync = true) { - const dfd = useAsync ? test.async(1000) : null; - const asyncStorage = new AsyncStorage(asyncOptions); - const store = new StoreBase({ storage: asyncStorage }); - - return { dfd, store, asyncStorage }; -} - -const ids = createData().map((item) => item.id); - -registerSuite({ - name: 'StoreBase', - - 'initialize store'(this: any) { - const { store, data } = getStoreAndDfd(this, createData(), false); - - return store.fetch() - .then((fetchedData) => { - assert.deepEqual(fetchedData, data, 'Fetched data didn\'t match provided data'); - }); - }, - - 'basic operations': { - 'get': { - 'get one item by id should return the item'(this: any) { - const { store } = getStoreAndDfd(this, undefined, true); - return store.get('item-1') - .then(item => { - assert.deepEqual(item, createData()[0]); - }); - }, - 'get multiple items by an array of ids should return the items array'(this: any) { - const { store } = getStoreAndDfd(this, undefined, true); - return store.get(['item-1', 'item-2', 'item-3']) - .then(items => { - assert.isTrue(Array.isArray(items)); - assert.deepEqual(items, createData()); - }); - } - }, - - 'add': { - 'should add new items'(this: any) { - const { emptyStore: store, data } = getStoreAndDfd(this, undefined, false); - // Add items - store.add([ data[0], data[1] ]); - store.add(data[2]); - return store.fetch() - .then((storeData) => { - assert.deepEqual(storeData, data, 'Didn\'t add items'); - }); - }, - - 'add action with existing items should fail'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - const updates = createUpdates(); - - return store.add(updates[0][2]) - .then() - .catch((error: any) => { - assert.equal(error.message, 'Objects already exist in store', - 'Didn\'t reject with appropriate error message'); - }); - }, - - 'add action with `rejectOverwrite=false` in options should overwrite existing data'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - const updates = createUpdates(); - // Update items with add - return store.add(updates[0][2], { rejectOverwrite: false }) - .then((items) => { - assert.deepEqual(items, [updates[0][2]], 'Didn\'t successfully return item'); - }); - } - }, - 'put': { - 'should add new items'(this: any) { - const { data, emptyStore: store } = getStoreAndDfd(this, undefined, false); - // Add items with put - store.put([ data[0], data[1] ]); - store.put(data[2]); - return store.fetch() - .then((storeData) => { - assert.deepEqual(storeData, data, 'Didn\'t add items'); - }); - }, - - 'should update existing items'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - const updates = createUpdates(); - // Add items with put - store.put([ updates[0][0], updates[0][1] ]); - store.put(updates[0][2]); - return store.fetch() - .then((storeData) => { - assert.deepEqual(storeData, updates[0], 'Didn\'t update items'); - }); - }, - 'put action with existing items should fail with `rejectOverwrite=true`'(this: any) { - const { dfd, store } = getStoreAndDfd(this); - const updates = createUpdates(); - // Update existing items with put - store.put([ updates[0][0], updates[0][1] ], { rejectOverwrite: true }) - .then(dfd.reject, dfd.callback((error: Error) => { - assert.equal(error.message, 'Objects already exist in store', 'Didn\'t reject with appropriate error message'); - })); - } - }, - - 'patch': { - 'should allow patching with a single update'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - store.patch(patches[0]); - return store.fetch() - .then((storeData) => { - assert.deepEqual( - storeData[0], - patches[0].patch.apply(createData()[0]), - 'Should have patched item' - ); - }); - }, - - 'should allow patching with a single object'(this: any) { - const { dfd, store } = getStoreAndDfd(this); - const update = createUpdates()[0][0]; - store.patch(update); - store.fetch() - .then((storeData) => { - const updateCopy = createUpdates()[0][0]; - assert.deepEqual(update, updateCopy, 'Shouldn\'t have modified passed object'); - assert.deepEqual(storeData[0], updateCopy, 'Should have patched item'); - }) - .then(dfd.resolve); - }, - - 'should allow patching with an array'(this: any) { - const { store, data: copy } = getStoreAndDfd(this, undefined, false); - store.patch(patches); - return store.fetch() - .then((storeData) => { - assert.deepEqual( - storeData, - patches.map((patchObj, i) => patchObj.patch.apply(copy[i])), - 'Should have patched all items' - ); - }); - }, - - 'should allow patching with a Map'(this: any) { - const { store, data: copy } = getStoreAndDfd(this, undefined, false); - - const map = new Map>(); - patches.forEach(patch => map.set(patch.id, patch.patch)); - - store.patch(map); - return store.fetch() - .then((storeData) => { - assert.deepEqual( - storeData, - patches.map((patchObj, i) => patchObj.patch.apply(copy[i])), - 'Should have patched all items' - ); - }); - }, - - 'should allow patching with an array of items - if default id property is used'(this: any) { - const { dfd, store } = getStoreAndDfd(this); - store.patch(createUpdates()[0]); - store.fetch() - .then((data) => { - assert.deepEqual( - data, createUpdates()[0], 'Should have patched objects based on provided items' - ); - }) - .then(dfd.resolve); - }, - - 'should use id property to identify patches'(this: any) { - type IdProp = { idProp: string; value: number }; - const data: IdProp[] = [ - { idProp: 'item-1', value: 1 }, { idProp: 'item-2', value: 2 }, { idProp: 'item-3', value: 3 } - ]; - const store = new StoreBase({ - data: data, - idProperty: 'idProp' - }); - - return store.patch([ { id: 'item-1', value: 2} ]) - .then(() => store.fetch()) - .then((data) => { - assert.deepEqual(data, [ - { idProp: 'item-1', value: 2 }, - { idProp: 'item-2', value: 2 }, - { idProp: 'item-3', value: 3 } - ], 'Didn\'t patch record properly'); - }); - }, - - 'should allow patching with an object and id in options'(this: any) { - const { dfd, store } = getStoreAndDfd(this); - const update = createUpdates()[0][0]; - // This should never really be done - update.id = 'new id'; - store.patch(update, { id: 'item-1' }); - store.fetch().then((storeData) => { - const updateCopy = createUpdates()[0][0]; - updateCopy.id = 'new id'; - assert.deepEqual(storeData[0], updateCopy, 'Should have patched item'); - }).then(dfd.resolve); - }, - - 'should fail when patch is not applicable.'(this: any) { - const { dfd, store } = getStoreAndDfd(this); - const operation = createOperation(OperationType.Replace, ['prop1'], undefined, 2); - const patch = new Patch([operation]); - - store.patch({ id: 'item-1', patch }) - .then(dfd.rejectOnError(() => { - assert(false, 'Should not have resolved'); - }), dfd.callback((error: Error) => { - assert.equal( - error.message, - 'Cannot replace undefined path: prop1 on object', - 'Didn\'t reject with appropriate error message' - ); - })); - } - }, - 'delete': { - 'should allow deleting a single item'(this: any) { - const { store, data: copy } = getStoreAndDfd(this, undefined, false); - store.delete(ids[0]); - return store.fetch() - .then((data: ItemType[]) => { - assert.deepEqual(data, [copy[1], copy[2]], 'Didn\'t delete item'); - }); - }, - - 'should allow deleting multiple items'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - store.delete(ids); - return store.fetch() - .then((data) => { - assert.deepEqual(data, [], 'Didn\'t delete items'); - }); - }, - - 'should fail when storage deletion fails.'(this: any) { - const dfd = this.async(1000); - - const storage = new InMemoryStorage(); - sinon.stub(storage, 'delete').returns(Promise.reject(Error('failed'))); - const store = new StoreBase({ storage }); - - store.delete(ids[0]) - .then(dfd.rejectOnError(() => { - assert(false, 'Should not have resolved'); - }), dfd.callback((error: Error) => { - assert.equal(error.message, 'failed', 'Didn\'t reject with appropriate error message'); - }) - ); - } - }, - 'identify': { - 'identify one item should return its id'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - const id = store.identify(data[0]); - assert.strictEqual(id, data[0].id); - }, - 'identify multiple items should return an array of ids'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - const ids = store.identify(data); - assert.isTrue(Array.isArray(ids)); - assert.deepEqual(ids, data.map(({id}) => id)); - } - } - }, - - 'fetch': { - 'should fetch with sort applied'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - - return store.fetch(createSort('id', true)) - .then((fetchedData) => { - assert.deepEqual(fetchedData, [ data[2], data[1], data[0] ], 'Data fetched with sort was incorrect'); - }); - }, - - 'should fetch with filter applied'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - - return store.fetch(createFilter() - .lessThan('value', 2)) - .then((fetchedData) => { - assert.deepEqual(fetchedData, [ data[0] ], 'Data fetched with filter was incorrect'); - }); - }, - - 'should fetch with range applied'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - - return store.fetch(createRange(1, 2)) - .then((fetchedData) => { - assert.deepEqual(fetchedData, [ data[1], data[2] ], 'Data fetched with range was incorrect'); - }); - }, - - 'should fetch with CompoundQuery applied'(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - - return store.fetch( - new CompoundQuery({ - query: createFilter() - .deepEqualTo(new JsonPointer('nestedProperty', 'value'), 2) - .or() - .deepEqualTo(new JsonPointer('nestedProperty', 'value'), 3) - }).withQuery(createSort(new JsonPointer('nestedProperty', 'value'))) - ) - .then((fetchedData) => { - assert.deepEqual(fetchedData, [ data[1], data[0] ], 'Data fetched with queries was incorrect'); - }); - } - }, - - 'crud operations should return an observable'(this: any) { - const data = createData(); - const { dfd, store } = getStoreAndDfd(this, [data[0]]); - - store.add(data[1]).subscribe((updateResults) => { - assert.equal(updateResults.type, StoreOperation.Add, 'Update results had wrong type'); - assert.deepEqual(updateResults.successfulData, [ data[1] ], 'Update results had wrong item'); - - store.put(data[2]).subscribe((updateResults) => { - assert.equal(updateResults.type, StoreOperation.Put, 'Update results had wrong type'); - assert.deepEqual(updateResults.successfulData, [ data[2] ], 'Update results had wrong item'); - - store.patch(patches[0]).subscribe((updateResults) => { - assert.equal(updateResults.type, StoreOperation.Patch, 'Update results had wrong type'); - assert.deepEqual(updateResults.successfulData, [ patchedItems[0] ], 'Update results had wrong item'); - - store.delete(data[0].id).subscribe((updateResults) => { - assert.equal(updateResults.type, StoreOperation.Delete, 'Update results had wrong type'); - assert.deepEqual(updateResults.successfulData, [ data[0].id ], 'Update results had wrong id'); - }, dfd.reject, dfd.resolve); - }); - }); - }); - }, - - 'should allow a property or function to be specified as the id'(this: any) { - const data = createData(); - const updates = createUpdates(); - const store = new StoreBase({ - data: updates[0], - idProperty: 'value' - }); - const idFunctionStore = new StoreBase({ - idFunction: (item: ItemType) => item.id + '-id', - data: data - }); - - assert.deepEqual(store.identify(updates[0]), [ '2', '3', '4' ], 'Should have used value property as the id'); - assert.deepEqual(idFunctionStore.identify(data), [ 'item-1-id', 'item-2-id', 'item-3-id' ], 'Should have used id function to create item ids'); - }, - - 'should execute calls in order in which they are called'(this: any) { - const { dfd, data, emptyStore: store } = getStoreAndDfd(this); - const updates = createUpdates(); - let retrievalCount = 0; - - store.add(data[0]); - store.get(data[0].id) - .then(item => { - retrievalCount++; - try { - assert.deepEqual(item, data[0], 'Should have received initial item'); - } catch (e) { - dfd.reject(e); - } - }); - store.put(updates[0][0]); - store.get(data[0].id) - .then(item => { - retrievalCount++; - try { - assert.deepEqual(item, updates[0][0], 'Should have received updated item'); - } catch (e) { - dfd.reject(e); - } - }); - - store.put(updates[1][0]); - store.get(data[0].id) - .then(item => { - try { - assert.equal(retrievalCount, 2, 'Didn\'t perform gets in order'); - assert.deepEqual(item, updates[1][0], 'Should have received second updated item'); - } catch (e) { - dfd.reject(e); - } - dfd.resolve(); - }); - }, - - 'should generate unique ids'(this: any) { - const ids: Promise[] = []; - const store = new StoreBase(); - const generateNIds = 1000; // reduced to 1,000 since IE 11 took minutes to run 100,000 - for (let i = 0; i < generateNIds; i++) { - ids.push(store.createId()); - } - Promise.all(ids) - .then((ids) => { - assert.equal(new Set(ids).size, generateNIds, 'Not all generated IDs were unique'); - }); - }, - - 'should be able to get all updates by treating as a promise': { - add(this: any) { - const { emptyStore: store, data } = getStoreAndDfd(this, undefined, false); - return store.add(data) - .then((result) => { - assert.deepEqual(result, data, 'Should have returned all added items'); - }); - - }, - - 'use catch'(this: any) { - const { dfd, data } = getStoreAndDfd(this); - const store = new StoreBase({ - data: [ data[0], data[1] ] - }); - const catchSpy = sinon.spy(); - store.add(data[2]) - .catch(catchSpy) - .then((results) => { - assert.isFalse(catchSpy.called, 'Shouldn\'t have called error handler'); - assert.deepEqual(results, [data[2]], 'Didn\'t add item'); - }) - .then(dfd.resolve); - }, - - 'add with conflicts should fail'(this: any) { - const { dfd, data } = getStoreAndDfd(this); - const store = new StoreBase({ - data: [ data[0], data[1] ] - }); - store.add(data).then(dfd.reject, dfd.resolve); - }, - - put(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - store.put(data) - .then((result) => assert.deepEqual(result, data, 'Should have returned all updated items')); - }, - - 'put with conflicts should override'(this: any) { - const { data } = getStoreAndDfd(this, undefined, false); - const store = new StoreBase({ - data: [ data[0], data[1] ] - }); - return store.put(data) - .then((result) => { - assert.deepEqual(result, data, 'Should have returned all updated items'); - }); - }, - - patch(this: any) { - const { store, data } = getStoreAndDfd(this, undefined, false); - const expectedResult = data.map((item) => { - item.value += 2; - item.nestedProperty.value += 2; - return item; - }); - return store.patch(patches) - .then((result) => { - assert.deepEqual(result, expectedResult, 'Should have returned all patched items'); - }); - }, - delete(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - return store.delete(ids) - .then((result) => { - assert.deepEqual(result, ids, 'Should have returned all deleted ids'); - }); - } - }, - - 'should have totalLength and dataLength properties on fetch results': { - 'fetch all'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - const fetchResult = store.fetch(); - return Promise.all([ fetchResult.totalLength, fetchResult.dataLength ]) - .then((lengths) => { - assert.equal(3, lengths[0], 'Didn\'t return the correct total length'); - assert.equal(3, lengths[1], 'Didn\'t return the correct data length'); - }); - }, - - 'filtered fetch'(this: any) { - const { store } = getStoreAndDfd(this, undefined, false); - const fetchResult = store.fetch(createFilter().lessThan('value', 2)); - return Promise.all([ fetchResult.totalLength, fetchResult.dataLength ]) - .then((lengths) => { - assert.equal(3, lengths[0], 'Didn\'t return the correct total length'); - assert.equal(3, lengths[1], 'Didn\'t return the correct data length'); - }); - } - }, - - 'async storage': { - 'async operation should not be done immediately.'(this: any) { - const { store } = getStoreWithAsyncStorage(this, { put: 50 }, false); - - const start = Date.now(); - return store.add(createData()) - .then(() => { - const finish = Date.now(); - assert.isAbove(finish - start, 25); - }); - }, - 'should complete initial add before subsequent operations'(this: any) { - const asyncStorage = new AsyncStorage(); - const store = new StoreBase({ - storage: asyncStorage, - data: createData() - }); - - return store.get(['item-1', 'item-2', 'item-3']) - .then((items) => { - assert.deepEqual(items, createData(), 'Didn\'t retrieve items from async add'); - }); - }, - 'failed initial add should not prevent subsequent operations'(this: any) { - let fail = true; - const stub = sinon.stub(console, 'error'); - const asyncStorage = new (class extends AsyncStorage { - add(items: any[], options?: CrudOptions): any { - if (fail) { - fail = false; - return Promise.reject(Error('error')); - } - else { - return super.add(items, options); - } - } - })(); - const data = createData(); - const store = new StoreBase({ - storage: asyncStorage, - data: data - }); - - return store.add(data) - .then(() => store.get(['item-1', 'item-2', 'item-3']) - .then((items) => { - assert.isTrue(stub.calledOnce); - assert.equal('error', stub.args[0][0].message, 'Didn\'t log expected error'); - stub.restore(); - assert.isFalse(fail, 'Didn\'t fail for first operation'); - assert.deepEqual(items, data, 'Didn\'t retrieve items from add following failed initial add'); - }) - ); - }, - 'fetch should not return items when it is done before add.'(this: any) { - const { store } = getStoreWithAsyncStorage(this, { put: 20, fetch: 10 }, false); - store.add(createData()); - return store.fetch() - .then((storeData) => { - assert.lengthOf(storeData, 0, 'should not have retrieved items'); - }); - }, - 'async operations should be done in the order specified by the user.'(this: any) { - const{ store } = getStoreWithAsyncStorage(this, undefined, false); - - return store.add(createData()) - .then((result) => { - assert.deepEqual(result, createData(), 'Should have returned all added items'); - return store.put(createUpdates()[0]); - }) - .then((result) => { - assert.deepEqual(result, createUpdates()[0], 'Should have returned all updated items'); - return store.delete(ids[0]); - }) - .then((result) => { - assert.deepEqual(result, [ids[0]], 'Should have returned all deleted ids'); - }); - } - } -}); diff --git a/tests/unit/store/materialize.ts b/tests/unit/store/materialize.ts deleted file mode 100644 index b71d741..0000000 --- a/tests/unit/store/materialize.ts +++ /dev/null @@ -1,263 +0,0 @@ -import * as registerSuite from 'intern!object'; -import * as assert from 'intern/chai!assert'; -import * as sinon from 'sinon'; -import { createData, ItemType, createUpdates } from '../support/createData'; -import QueryStore from '../../../src/store/QueryableStore'; -import ObservableStore, { StoreDelta } from '../../../src/store/ObservableStore'; -import materialize from '../../../src/store/materialize'; -import { Store } from '../../../src/interfaces'; -import { delay } from '@dojo/core/async/timing'; -import { QueryResultInterface } from '../../../src/store/QueryResult'; - -type TransformedObject = { - _value: number; - _nestedProperty: { - _value: number; - }; - _id: string; -}; - -registerSuite({ - name: 'materialize', - - 'Should apply updates to target store'(this: any) { - const dfd = this.async(); - const targetStore = new ObservableStore({ - idProperty: '_id' - }); - const trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .transform((item) => ({ - _value: item.value, - _nestedProperty: { - _value: item.nestedProperty.value - }, - _id: item.id - }), '_id') - .filter((item) => item._value > 1) - .range(0, 100) - .track(); - - let ignoreFirst = true; - let initialUpdateFromSource = false; - targetStore.observe().subscribe(dfd.rejectOnError(({ adds, deletes }: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - if (!initialUpdateFromSource) { - initialUpdateFromSource = true; - assert.deepEqual(adds, [ - { - _id: 'item-2', - _value: 2, - _nestedProperty: { - _value: 2 - } - }, - { - _id: 'item-3', - _value: 3, - _nestedProperty: { - _value: 1 - } - } - ], 'Didn\'t add initial data to target store'); - } - else { - assert.deepEqual(deletes, [ 'item-2' ]); - dfd.resolve(); - } - })); - - materialize< - TransformedObject, QueryResultInterface, Store - >({ source: trackedCollection, target: targetStore }); - trackableQueryStore.delete('item-2'); - }, - - 'Should stop applying updates after destroying handle'(this: any) { - const dfd = this.async(); - const targetStore = new ObservableStore({ - idProperty: '_id' - }); - const trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .transform((item) => ({ - _value: item.value, - _nestedProperty: { - _value: item.nestedProperty.value - }, - _id: item.id - }), '_id') - .filter((item) => item._value > 1) - .range(0, 100) - .track(); - - let ignoreFirst = true; - let initialUpdateFromSource = false; - targetStore.observe().subscribe(dfd.rejectOnError(({ adds }: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - - if (!initialUpdateFromSource) { - initialUpdateFromSource = true; - assert.deepEqual(adds, [ - { - _id: 'item-2', - _value: 2, - _nestedProperty: { - _value: 2 - } - }, - { - _id: 'item-3', - _value: 3, - _nestedProperty: { - _value: 1 - } - } - ], 'Didn\'t add initial data to target store'); - handle.destroy(); - trackableQueryStore.delete('item-2'); - setTimeout(dfd.resolve, 300); - } - else { - dfd.reject(Error('Shouldn\'t have received another update after handle was destroyed')); - } - })); - - const handle = materialize< - TransformedObject, QueryResultInterface, Store - >({ source: trackedCollection, target: targetStore }); - }, - - 'Should use apply function if provided'(this: any) { - const dfd = this.async(); - const targetStore = new ObservableStore({ - idProperty: '_id' - }); - const trackableQueryStore = new QueryStore({ - data: createData() - }); - const trackedCollection = trackableQueryStore - .transform((item) => ({ - _value: item.value, - _nestedProperty: { - _value: item.nestedProperty.value - }, - _id: item.id - }), '_id') - .filter((item) => item._value > 1) - .range(0, 100) - .track(); - - let ignoreFirst = true; - targetStore.observe().subscribe(dfd.rejectOnError((update: StoreDelta) => { - if (ignoreFirst) { - ignoreFirst = false; - return; - } - dfd.reject(Error('Shouldn\'t have received another update since apply function was used')); - })); - - let initialUpdateFromSource = false; - materialize< - TransformedObject, QueryResultInterface, Store - >({ - source: trackedCollection, - target: targetStore, - apply: dfd.rejectOnError((target: Store, { afterAll, deletes }: StoreDelta, source: any) => { - if (!initialUpdateFromSource) { - initialUpdateFromSource = true; - assert.deepEqual(afterAll, [ - { - _id: 'item-2', - _value: 2, - _nestedProperty: { - _value: 2 - } - }, - { - _id: 'item-3', - _value: 3, - _nestedProperty: { - _value: 1 - } - } - ], 'Didn\'t add initial data to target store'); - } - else { - assert.deepEqual(deletes, [ 'item-2' ]); - dfd.resolve(); - } - }) - }); - - trackableQueryStore.delete('item-2'); - }, - - 'Shouldn\'t make any updates if initial update is empty'(this: any) { - const dfd = this.async(1000); - const targetStore = new QueryStore(); - const trackableQueryStore = new QueryStore(); - const trackedCollection = trackableQueryStore.filter(() => true).track(); - - targetStore.add = dfd.reject.bind(dfd, Error('Shouldn\'t have called add on targetStore')); - - materialize({ - source: trackedCollection, - target: targetStore - }); - - setTimeout(dfd.resolve, 200); - }, - - 'Should add new items to targetStore'(this: any) { - const targetStore = new QueryStore(); - const trackableQueryStore = new QueryStore(); - const trackedCollection = trackableQueryStore.filter(() => true).track(); - - const spy = sinon.spy(targetStore, 'add'); - - materialize({ - source: trackedCollection, - target: targetStore - }); - - trackableQueryStore.add(createData()); - - return delay(100)(() => { - assert.isTrue(spy.calledOnce, 'Should have called add on target store once'); - assert.deepEqual(spy.args[0][0], createData()); - }); - }, - - 'Should update items in target store'(this: any) { - const targetStore = new QueryStore(); - const trackableQueryStore = new QueryStore(); - const trackedCollection = trackableQueryStore.filter(() => true).track(); - - const spy = sinon.spy(targetStore, 'put'); - - materialize({ - source: trackedCollection, - target: targetStore - }); - - trackableQueryStore.add(createData()); - trackableQueryStore.put(createUpdates()[0]); - - return delay(100)(() => { - assert.isTrue(spy.calledOnce, 'Should have called add on target store once'); - assert.deepEqual(spy.args[0][0], createUpdates()[0]); - }); - } -}); diff --git a/tests/unit/support/AsyncStorage.ts b/tests/unit/support/AsyncStorage.ts deleted file mode 100644 index 8497ca9..0000000 --- a/tests/unit/support/AsyncStorage.ts +++ /dev/null @@ -1,65 +0,0 @@ -import InMemoryStorage from '../../../src/storage/InMemoryStorage'; -import Promise from '@dojo/shim/Promise'; -import { delay } from '@dojo/core/async/timing'; -import { Query, FetchResult, CrudOptions } from '../../../src/interfaces'; -import Patch from '../../../src/patch/Patch'; - -function getRandomInt(max = 100) { - return Math.floor(Math.random() * max); -} - -export default class AsyncStorage extends InMemoryStorage { - timing: { [ index: string ]: number | undefined }; - - constructor(options?: any) { - super(options); - this.timing = options || {}; - } - - get(ids: string[]): Promise { - return delay(this.timing['get'] || getRandomInt())(() => super.get(ids)); - } - - createId() { - return delay(this.timing['createId'] || getRandomInt())(() => super.createId()); - } - - put(items: T[], options?: CrudOptions) { - return delay(this.timing['put'] || getRandomInt())(() => super.put(items, options)); - } - - add(items: T[], options?: CrudOptions) { - return delay(this.timing['put'] || getRandomInt())(() => super.add(items, options)); - } - - delete(ids: string[]) { - return delay(this.timing['delete'] || getRandomInt())(() => super.delete(ids)); - } - - patch(updates: { id: string; patch: Patch }[]) { - return delay(this.timing['patch'] || getRandomInt())(() => super.patch(updates)); - } - - fetch(query?: Query) { - let totalLengthResolve: () => void; - let totalLengthReject: () => void; - let fetchResultResolve: () => void; - let fetchResultReject: () => void; - const totalLengthPromise = new Promise((resolve, reject) => { - totalLengthResolve = resolve; - totalLengthReject = reject; - }); - const fetchResult: FetchResult = new Promise((resolve, reject) => { - fetchResultResolve = resolve; - fetchResultReject = reject; - }); - fetchResult.totalLength = fetchResult.dataLength = totalLengthPromise; - setTimeout(() => { - const result = super.fetch(); - result.then(fetchResultResolve, fetchResultReject); - result.totalLength.then(totalLengthResolve, totalLengthReject); - }, this.timing['fetch'] || getRandomInt()); - - return fetchResult; - } -} diff --git a/tests/unit/support/createData.ts b/tests/unit/support/createData.ts deleted file mode 100644 index 5ce4810..0000000 --- a/tests/unit/support/createData.ts +++ /dev/null @@ -1,65 +0,0 @@ -import Patch, { diff } from '../../../src/patch/Patch'; - -export interface ItemType { - id: string; - value: number; - nestedProperty: { value: number }; -} - -export function createData(): ItemType[] { - return [ - { - id: 'item-1', - value: 1, - nestedProperty: { value: 3 } - }, - { - id: 'item-2', - value: 2, - nestedProperty: { value: 2 } - }, - { - id: 'item-3', - value: 3, - nestedProperty: { value: 1 } - } - ]; -} - -export function createUpdates(): ItemType[][] { - return [ - createData().map(({ id, value, nestedProperty: { value: nestedValue } }) => ({ - id: id, - value: value + 1, - nestedProperty: { - value: nestedValue - } - })), - createData().map(({ id, value, nestedProperty: { value: nestedValue } }) => ({ - id: id, - value: value + 1, - nestedProperty: { - value: nestedValue + 1 - } - })) - ]; -} -export const patches: { id: string; patch: Patch }[] = - createData().map(({ id, value, nestedProperty: { value: nestedValue } }, index) => ({ - id: id, - patch: diff({ - id: id, - value: value + 2, - nestedProperty: { - value: nestedValue + 2 - } - }, createData()[index]) - })); - -export const patchedItems: ItemType[] = createData().map(({ id, value, nestedProperty: { value: nestedValue } }) => ({ - id, - value: value + 2, - nestedProperty: { - value: nestedValue + 2 - } -})); diff --git a/tsconfig.json b/tsconfig.json index 363ed0a..f08cb39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,12 @@ { "compilerOptions": { "declaration": false, + "experimentalDecorators": true, "lib": [ "dom", "es5", "es2015.iterable", + "es2015.promise", "es2015.symbol", "es2015.symbol.wellknown" ], @@ -12,15 +14,15 @@ "moduleResolution": "node", "noImplicitAny": true, "noImplicitThis": true, - "strictNullChecks": true, + "noUnusedLocals": true, "outDir": "_build/", "removeComments": false, "sourceMap": true, + "strictNullChecks": true, "target": "es5", "types": [ "intern" ] }, "include": [ - "./typings/index.d.ts", "./src/**/*.ts", "./tests/**/*.ts" ] diff --git a/tslint.json b/tslint.json index d2e0e9a..0c5b6fd 100644 --- a/tslint.json +++ b/tslint.json @@ -8,10 +8,10 @@ "eofline": true, "forin": false, "indent": [ true, "tabs" ], - "interface-name": false, + "interface-name": [ true, "never-prefix" ], "jsdoc-format": true, "label-position": true, - "max-line-length": false, + "max-line-length": 120, "member-access": false, "member-ordering": false, "no-any": false, @@ -24,12 +24,12 @@ "no-duplicate-variable": true, "no-empty": false, "no-eval": true, + "no-inferrable-types": [ true, "ignore-params" ], "no-shadowed-variable": false, "no-string-literal": false, "no-switch-case-fall-through": false, "no-trailing-whitespace": true, "no-unused-expression": false, - "no-unused-variable": true, "no-use-before-declare": false, "no-var-keyword": true, "no-var-requires": false, @@ -37,7 +37,7 @@ "one-line": [ true, "check-open-brace", "check-whitespace" ], "quotemark": [ true, "single" ], "radix": true, - "semicolon": true, + "semicolon": [ true, "always" ], "trailing-comma": [ true, { "multiline": "never", "singleline": "never" @@ -50,9 +50,14 @@ "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" + }, { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" } ], - "use-strict": false, - "variable-name": false, + "variable-name": [ true, "check-format", "allow-leading-underscore", "ban-keywords", "allow-pascal-case" ], "whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-module", "check-separator", "check-type", "check-typecast" ] } }