[
  {
    "path": ".dockerignore",
    "content": "**/node_modules\ndocs\nexamples\nserver/test\n\nclient/test\nclient/lib/\nclient/build\nclient/dist\n\n**/README.md\ndocker-compose.yml\n\n**/rethinkdb_data_test\n**/rethinkdb_data/\n\n**/*.log\n.#*\n**/*-key.pem\n**/*-cert.pem\n\n**/.DS_Store\n\n.hz/\nconfig.toml\nDockerfile*\n.git\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n  extends: \"eslint:recommended\",\n  rules: {\n    \"arrow-body-style\": [ERROR, \"as-needed\"],\n    \"array-bracket-spacing\": [ ERROR, \"always\" ],\n    \"arrow-parens\": [ ERROR, \"always\" ],\n    \"arrow-spacing\": [ ERROR ],\n    \"block-spacing\": [ ERROR, \"always\" ],\n    \"brace-style\": [ ERROR, \"1tbs\", { \"allowSingleLine\": true } ],\n    \"comma-dangle\": [ ERROR, \"always-multiline\" ],\n    \"comma-spacing\": [ ERROR ],\n    \"comma-style\": [ ERROR, \"last\" ],\n    \"constructor-super\": [ ERROR ],\n    \"curly\": [ ERROR, \"all\" ],\n    \"dot-notation\": [ ERROR ],\n    \"eqeqeq\": [ ERROR, \"allow-null\" ],\n    \"func-style\": [ ERROR, \"declaration\", { \"allowArrowFunctions\": true } ],\n    \"indent\": [ ERROR, 2 ],\n    \"key-spacing\": [ ERROR ],\n    \"keyword-spacing\": [ ERROR ],\n    \"linebreak-style\": [ ERROR, \"unix\" ],\n    \"new-parens\": [ ERROR ],\n    \"max-len\": [ ERROR, 80 ],\n    \"no-array-constructor\": [ ERROR ],\n    \"no-case-declarations\": [ ERROR ],\n    \"no-class-assign\": [ ERROR ],\n    \"no-confusing-arrow\": [ ERROR, { \"allowParens\": true } ],\n    \"no-console\": [ OFF ],\n    \"no-const-assign\": [ ERROR ],\n    \"no-constant-condition\": [ ERROR ],\n    \"no-dupe-class-members\": [ ERROR ],\n    \"no-eval\": [ ERROR ],\n    \"no-extend-native\": [ ERROR ],\n    \"no-extra-semi\": [ ERROR ],\n    \"no-floating-decimal\": [ ERROR ],\n    \"no-implicit-coercion\": [ ERROR ],\n    \"no-implied-eval\": [ ERROR ],\n    \"no-invalid-this\": [ ERROR ],\n    \"no-labels\": [ ERROR ],\n    \"no-lonely-if\": [ ERROR ],\n    \"no-mixed-requires\": [ ERROR ],\n    \"no-multi-spaces\": [ ERROR ],\n    \"no-multi-str\": [ ERROR ],\n    \"no-multiple-empty-lines\": [ ERROR, { \"max\": 2, \"maxEOF\": 1 } ],\n    \"no-native-reassign\": [ ERROR ],\n    \"no-new-func\": [ ERROR ],\n    \"no-new-object\": [ ERROR ],\n    \"no-new-require\": [ ERROR ],\n    \"no-new-wrappers\": [ ERROR ],\n    \"no-param-reassign\": [ ERROR ],\n    \"no-proto\": [ ERROR ],\n    \"no-return-assign\": [ ERROR ],\n    \"no-self-compare\": [ ERROR ],\n    \"no-sequences\": [ ERROR ],\n    \"no-shadow\": [ ERROR ],\n    \"no-shadow-restricted-names\": [ ERROR ],\n    \"no-this-before-super\": [ ERROR ],\n    \"no-throw-literal\": [ ERROR ],\n    \"no-trailing-spaces\": [ ERROR ],\n    \"no-unexpected-multiline\": [ ERROR ],\n    \"no-unneeded-ternary\": [ ERROR ],\n    \"no-unreachable\": [ ERROR ],\n    \"no-use-before-define\": [ ERROR, \"nofunc\" ],\n    \"no-var\": [ ERROR ],\n    \"no-void\": [ ERROR ],\n    \"no-with\": [ ERROR ],\n    \"object-curly-spacing\": [ ERROR, \"always\" ],\n    \"one-var\": [ ERROR, { \"uninitialized\": \"always\", \"initialized\": \"never\" } ],\n    \"operator-assignment\": [ ERROR, \"always\" ],\n    \"operator-linebreak\": [ ERROR, \"after\" ],\n    \"padded-blocks\": [ ERROR, \"never\" ],\n    \"prefer-const\": [ ERROR ],\n    \"prefer-template\": [ ERROR ],\n    \"quote-props\": [ ERROR, \"as-needed\" ],\n    \"quotes\": [ ERROR, \"single\", \"avoid-escape\" ],\n    \"semi\": [ ERROR, \"always\" ],\n    \"semi-spacing\": [ ERROR ],\n    \"space-before-blocks\": [ ERROR, \"always\" ],\n    \"space-before-function-paren\": [ ERROR, \"never\" ],\n    \"space-in-parens\": [ ERROR, \"never\" ],\n    \"space-infix-ops\": [ ERROR ],\n    \"space-unary-ops\": [ ERROR ],\n    \"spaced-comment\": [ ERROR, \"always\" ],\n    \"strict\": [ ERROR, \"global\" ],\n    \"wrap-iife\": [ ERROR, \"inside\" ],\n    \"yoda\": [ ERROR, \"never\" ],\n  },\n  env: {\n    \"es6\": true,\n    \"node\": true,\n    \"mocha\": true,\n  },\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "client/dist/\nclient/lib/\nrethinkdb_data_test\nrethinkdb_data/\n**/*.log\n.#*\n**/*-key.pem\n**/*-cert.pem\nnode_modules/\n**/.DS_Store\n.hz/\nconfig.toml\n**/.vscode\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWe're happy you want to contribute! You can help us in different ways:\n\n- [Open an issue][1] with suggestions for improvements and errors you're facing\n- Fork this repository and submit a pull request\n- Improve the <a>documentation</a> (coming soon, see [Resources](#resources) below for now)\n\n[1]: https://github.com/rethinkdb/horizon/issues\n\nTo submit a pull request, fork the [Horizon repository][3] and then clone your fork:\n\n    git clone git@github.com:<your-name>/horizon.git\n\n[3]: https://github.com/rethinkdb/horizon\n\nMake your suggested changes, `git push` and then [submit a pull request][4]. Note that before we can accept your pull requests, you need to sign our [Contributor License Agreement][5].\n\n[4]: https://github.com/rethinkdb/horizon/compare/\n[5]: http://rethinkdb.com/community/cla/\n\n## Resources\n\nSome useful resources to get started:\n* [Getting started with Horizon][getting-started]\n* [The Horizon client library API][client-api]\n* [Configuring the `hz` command-line tool][cli-config]\n\n[cli-config]: /cli/README.md\n[client-api]: /client/README.md\n[getting-started]: GETTING-STARTED.md\n"
  },
  {
    "path": "Dockerfile",
    "content": "# REQUIREMENTS\n# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_URI=HOST:PORT\n# * Your Horizon app needs to be mounted into /usr/app using -v /path/to/app:/usr/app\n\nFROM node:5-slim\n\nRUN yes '' | adduser --disabled-password horizon && \\\n    mkdir -p /usr/horizon /usr/app /usr/certs\n\nRUN apt update && apt install -y git\n\nCOPY . /usr/horizon/\nWORKDIR /usr/horizon\nRUN cd test; ./setupDev.sh\n\nEXPOSE 8181\n\nVOLUME /usr/app\n\nCMD [\"su\", \"-s\", \"/bin/sh\", \"horizon\", \"-c\", \"hz serve --bind all --connect $RETHINKDB_URI /usr/app\"]\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "# REQUIREMENTS\n# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_URI=HOST:PORT\n# * Your Horizon app needs to be mounted into /usr/app using -v /path/to/app:/usr/app\n\nFROM rethinkdb/horizon\nENV HZ_DEV = yes\n"
  },
  {
    "path": "GETTING-STARTED.md",
    "content": "![](/horizon.png)\n\n# Getting Started with Horizon\n\n**Getting Started**\n* [Installation](#installation)\n* [Creating your first app](#creating-your-first-app)\n* [Starting Horizon Server](#starting-horizon-server)\n * [Configuring Horizon Server](#configuring-horizon-server)\n * [Adding OAuth authentication](#adding-oauth-authentication)\n* [Intro to the Horizon Client Library](#the-horizon-client-library)\n * [Storing documents](#storing-documents)\n * [Retrieving documents](#retrieving-documents)\n * [Removing documents](#removing-documents)\n * [Watching for changes](#watching-for-changes)\n* [Putting it all together](#putting-it-all-together)\n* [Using an already existing application with Horizon](#bringing-your-app-to-horizon)\n * [Do I need to move all my files into the `dist` folder?](#do-i-need-to-output-all-my-files-into-the-dist-folder)\n * [How do I add Horizon to X?](#how-do-i-add-horizon-to-x)\n\n\n\n**Examples**\n* [Example Horizon Applications](#example-applications)\n* [Extending Horizon Server examples](#extending-horizon-server)\n\n---\n\n## Installation\n\nFirst, install horizon from npm:\n\n```sh\n$ npm install -g horizon\n```\n\n## Creating your first app\n\nNow you can initialize a new horizon project:\n\n```sh\n$ hz init example-app\n```\n\nThis will create a directory with the following files:\n\n```sh\n$ tree -aF example-app/\nexample-app/\n├── dist/\n│   └── index.html\n├── .hz/\n│   └── config.toml\n└── src/\n```\n\nThe `dist` directory is where you should output your static\nfiles. Horizon doesn't have any opinions about what front-end build\nsystem you use, just that the files to serve end up in `dist`. Your\nsource files would go into `src` but that's just a convention.\nHorizon doesn't touch anything in `src`.\n\nIf you want, you can `npm init` or `bower init` in the `example-app`\ndirectory to set up dependencies etc.\n\n`.hz/config.toml` is a [toml](https://github.com/toml-lang/toml) configuration file where you can set all the different options for Horizon Server. [Read more about available configuration options here](/cli/README.md#hzconfigtoml-file).\n\nBy default, horizon creates a basic `index.html` to serve so you can verify everything is working:\n\n```html\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <script src=\"/horizon/horizon.js\"></script>\n    <script>\n      var horizon = Horizon();\n      horizon.onReady(function() {\n        document.querySelector('h1').innerHTML = 'It works!'\n      });\n      horizon.connect();\n    </script>\n  </head>\n  <body>\n   <marquee><h1></h1></marquee>\n  </body>\n</html>\n```\n\n---\n\n## Starting Horizon Server\n\nWe now need to start Horizon Server. Running `hz serve` does three main things:\n\n1. Starts the Horizon Server node app which serves the Horizon Client API / WebSocket endpoint.\n1. Serves the `horizon.js` client library.\n1. Serves everything in the `dist` folder, _if it exists in the current working directory_.\n\n*[RethinkDB](https://www.rethinkdb.com/docs/install/) needs to be installed first and accessible from the Path.*\n\nNormally, running `hz serve` requires a running instance of RethinkDB as well as pre-created tables in your RethinkDB instance.\n\nLuckily, running `hz serve --dev` has all that covered for you. Here's a comparison of what happens with and without `--dev`:\n\n|  | `hz serve`| `hz serve --dev` | Command-line Flag                 |\n|----------------------------|:-----------:|:-----:|----------------------|\n|Starts Horizon Server       | ✅        | ✅  |                      |\n|Starts RethinkDB Server     | ❌        | ✅  | `--start-rethinkdb`  |\n|Insecure Mode (no HTTPS/WSS)| ❌        | ✅  | `--insecure`         |\n|Auto creates tables         | ❌        | ✅  | `--auto-create-table`|\n|Auto creates indexes        | ❌        | ✅  | `--auto-create-index`|\n\nSo when using `hz serve --dev`, you don't have to worry about explicitly creating tables, or  worry about creating indexes to ensure your Horizon queries are always fast. As well, Horizon will start an instance of RethinkDB specifically for Horizon and create a `rethinkdb_data` folder in your current directory when you start `hz serve --dev`\n\n> Using authentication _requires_ that you use TLS. To setup authentication for your app you will have to use `hz serve` without `--dev` and with `--key-file` and `--cert-file` flags as well as any other options you require.\n\nHere you can find\n<a href=\"https://github.com/rethinkdb/horizon/tree/next/cli#hz-serve\">the complete list of command line flags</a> for `hz serve` ➡️.\n\nOn your local dev machine, you will usually use `hz serve --dev` which will begin a new instance of RethinkDB for you and will automatically create tables and indexes making your development workflow easy. In a production environment, you will want to just use `hz serve` and make use of the `.hz/config.toml` file.\n\n### Configuring Horizon Server\n\nHorizon Server is configurable via the `.hz/config.toml` file which is in the [toml](https://github.com/toml-lang/toml) config format. By default, `hz serve` will look for this file\nin the current working directory. Here is [an example `.hz/config.toml` file from the Horizon CLI documentation](/cli/README.md#hzconfigtoml-file) ➡️.\n\n> Be warned that there is a precedence to config file setting in the order of:\n> environment variables > config file > command-line flags\n\n### Adding OAuth authentication\n\nWith Horizon, we wanted to make it easy to allow your users to authenticate with the accounts\nthey already have with the most popular services.\n\nYou can find [a full list of OAuth implementations we support here](/server/src/auth).\n\nThe first thing you need to do is create an application with the provider you'd like to authenticate with, usually at the developer portal portion of their website. Here are links\n to a the providers we currently support.\n\n* 😵📖 - [Facebook](https://developers.facebook.com/apps/)\n* 💻🏦 - [Github](https://github.com/settings/applications/new)\n* 🔟<sup>100</sup> - [Google](https://console.developers.google.com/project)\n* 🎮📹 - [Twitch](https://www.twitch.tv/kraken/oauth2/clients/new)\n* 🐦💬 - [Twitter](https://apps.twitter.com/app/new)\n\nFrom each of these providers you will eventually have a `client_id` and `client_secret`\n(sometimes just `id` and `secret`) that you will need to put into the `.hz/config.toml`\nconfiguration file.\n\nNear the bottom of the automatically generated `.hz/config.toml` file you'll see commented out\nsample OAuth settings, you'll just need to uncomment them out and replace the values with your `client_id` and `client_secret`. Adding Github OAuth configuration would look like this:\n\n```toml\n# [auth.facebook]\n# id = \"000000000000000\"\n# secret = \"00000000000000000000000000000000\"\n#\n# [auth.google]\n# id = \"00000000000-00000000000000000000000000000000.apps.googleusercontent.com\"\n# secret = \"000000000000000000000000\"\n#\n# [auth.twitter]\n# id = \"0000000000000000000000000\"\n# secret = \"00000000000000000000000000000000000000000000000000\"\n#\n\n[auth.github]\nid = \"your_client_id\"\nsecret = \"your_client_secret\"\n```\n\nOnce you've added the lines in your `.hz/config.toml` you're basically all set. To verify that\nHorizon Server picked them up, run `hz serve` then go to\n`https://localhost:8181/horizon/auth_methods` (or where ever you are running Horizon Server) to\nsee a list of currently active authentication options.\n\n> At this point, ensure that you're using `--key-file` and `--cert-file` with `hz serve` as you cannot have authentication without also using TLS to serve assets via HTTPS/WSS. Also ensure that you are now using `https://` for all your URLs.\n\nYou should see `github` included in the object of available auth methods, if you just see a blank object like so `{ }`, ensure that you restarted Horizon Server and that it is using the `.hz/config.toml` you edited. It should look like this:\n\n```js\n{\n  github: \"/horizon/github\"\n}\n```\n\nNow the value of the property `github` is the path to replace on the current `window.location`\nthat will begin the authentication process. Or, just type in\n`https://localhost:8181/horizon/github` in your browser to test it out.\n\nAs a result of a successful authentication, the browser will be redirected to the root of the\ndev server (`https://localhost:8181/`) with the `?horizon_token=` in the query parameters and you\ncan now consider the user properly authenticated at this point. If an error occurs somewhere\nduring the authentication process, the browser will be redirected back to the root of the dev server with an error message in the query parameters.\n\nA couple notes to mention:\n\n* ***Where is the user data from authenticating with OAuth?***: At the moment we just\nallow users to prove they have an account with the given provider. But obviously part of the\npower of OAuth is the convenience of sharing controlled slices of user data. For example, I may want users to allow my app to have access to their friends list, or see who they're following on Github. This is coming soon, and in the future, we will allow developers to specify the requested authentication scopes and give developer access to the returned data via the Users table.\n\n* ***Why can't I configure the final redirect url?***: Customizing the final redirect_url on the\noriginal domain will be possible in the future.\n\n* ***Why doesn't Horizon use Passport?***: Passport was definitely considered for Horizon but\nultimately was too heavily tied with Express to achieve the amount of extensibility we wanted.\nTo ensure this extensibility we decided to implement our own handling of OAuth routes for\nthe different providers. If you're still convinced we should use Passport, feel free to\n[open an issue](https://github.com/rethinkdb/horizon/issues/new) and direct your comments\nto @Tryneus.\n\n---\n\n## The Horizon Client Library\n\nIn the boilerplate created by `hz init`, you can see that the Horizon client library is being\nimported from the path `/horizon/horizon.js` served by Horizon Server. If you\n\n\n```html\n...\n<head>\n  ...\n  <script src=\"/horizon/horizon.js\"></script>\n</head>\n...\n```\n\nAfter this script is loaded, you can connect to your running instance of Horizon Server.\n\n\n```js\nconst horizon = Horizon();\n```\n\nFrom here you can start to interact with Horizon collections. Having `--dev` mode enabled on\nthe Horizon Server creates collections and indexes automatically so you can get your\napplication setup with as little hassle as possible.\n\n> **Note:** With `--dev` mode enabled or `--auto-create-index`, indices will\nbe created automatically for queries that are run that don't already match\na pre-existing query.\n\n```js\n// This automatically creates\nconst chat = horizon(\"messages\");\n```\n\nNow, `chat` is a Horizon collection of documents. You can perform a\nvariety of operations on this collection to filter them down to the ones\nyou need. This most basic operations are [`.store`][store] and [`.fetch`][fetch]:\n\n### Storing documents\n\nTo store documents into the collection, we use [`.store`][store].\n\n```js\n// Object being stored\nlet message = {\n  text: \"What a beautiful horizon 🌄!\",\n  datetime: new Date(),\n  author: \"@dalanmiller\"\n}\n\n// Storing a document\nchat.store(message);\n```\n\nIf we wanted, we could also add `.subscribe` at the end of [`.store`][store] and handle the document `id`s created by the server as well as any errors that occur with storing. Check out [`.store`](https://github.com/rethinkdb/horizon/tree/next/client#store-------) in the [Horizon Client docs](https://github.com/rethinkdb/horizon/tree/next/client) ➡️.\n\n### Retrieving documents\n\nTo retrieve messages from the collection we use [`.fetch`][fetch]. In this case, `.subscribe` takes a result and error handler function.\n\n```js\nchat.fetch().subscribe(\n  (items) => {\n    items.subscribe((item) => {\n      // Each result from the chat collection\n      //  will pass through this function\n      console.log(item);\n    })\n  },\n  // If an error occurs, this function\n  //  will execute with the `err` message\n  (err) => {\n    console.log(err);\n  })\n```\n\n### Removing documents\n\nTo remove documents from a collection, you can use either [`.remove`][remove] or [`.removeAll`][removeAll]:\n\n```js\n// These two queries are equivalent and will remove the document with id: 1.\nchat.remove(1).subscribe((id) => { console.log(id) })\nchat.remove({id: 1}).subscribe((id) => {console.log(id)})\n```\n\nOr, if you have a set of documents that you'd like to remove you can pass them in as an array to [`.removeAll`][removeAll].\n\n```js\n\n// Will remove documents with ids 1, 2, and 3 from the collection.\nchat.removeAll([1, 2, 3])\n```\nAs with the other functions, you can chain `.subscribe` onto the remove functions and provide response and error handlers.\n\n### Watching for changes\n\nWe can also \"listen\" to an entire collection, query, or a single document by using [`.watch`][watch].\nThis is very convenient for building apps that want to update state immediately as data changes\nin the database. Here are a few variations of how you can use [`.watch`][watch]:\n\n```js\n// Watch all documents, if any of them change, call the handler function.\nchat.watch().subscribe((docs) => { console.log(docs)  })\n\n// Query all documents and sort them in ascending order by datetime,\n//  then if any of them change, the handler function is called.\nchat.order(\"datetime\").watch().subscribe((docs) => { console.log(docs)  })\n\n// Find a single document in the collection, if it changes, call the handler function\nchat.find({author: \"@dalanmiller\"}).watch().subscribe((doc) => { console.log(doc) })\n```\n\nBy default, the handler you pass to `.subscribe` chained on [`.watch`][watch] will receive\nthe entire collection of documents when one of them changes. This makes it easy when\nusing frameworks such as [Vue](https://vuejs.org/) or [React](https://facebook.github.io/react/)\nallowing you to replace the current state with the new array given to you by Horizon.\n\n```js\n\n// Our current state of chat messages\nlet chats = [];\n\n// Query chats with `.order` which by default\n//  is in ascending order.\nchat.order(\"datetime\").watch().subscribe(\n\n  // Returns the entire array\n  (newChats) => {\n\n    // Here we replace the old value of `chats` with the new\n    //  array. Frameworks such as React will re-render based\n    //  on the new values inserted into the array. Preventing you\n    //  from having to do modifications on the original array.\n    //\n    // In short, it's this easy! :cool:\n    chats = newChats;\n  },\n\n  (err) => {\n    console.log(err);\n  })\n```\n\nTo learn more about how Horizon works with React, check out [this complete Horizon & React example](https://github.com/rethinkdb/horizon/tree/next/examples/react-chat-app) ➡️.\n\n## Putting it all together\n\nNow that we have the basics covered, let's pretend we are building a\nsimple chat application where the messages are displayed\nin ascending order. Here are some basic functions that would allow\nyou to build such an app.\n\n```js\n\nlet chats = [];\n\n// Retrieve all messages from the server\nconst retrieveMessages = () => {\n  chat.order('datetime')\n  // fetch all results as an array\n  .fetch()\n  // Retrieval successful, update our model\n  .subscribe((newChats) => {\n      chats = chats.concat(newChats);\n    },\n    // Error handler\n    error => console.log(error),\n    // onCompleted handler\n    () => console.log('All results received!')\n    )\n};\n\n// Retrieve an single item by id\nconst retrieveMessage = id => {\n  chat.find(id).fetch()\n    // Retrieval successful\n    .subscribe(result => {\n      chats.push(result);\n    },\n    // Error occurred\n    error => console.log(error))\n};\n\n// Store new item\nconst storeMessage = (message) => {\n   chat.store(message)\n    .subscribe(\n      // Returns id of saved objects\n      result => console.log(result),\n      // Returns server error message\n      error => console.log(error)\n      // called when store is complete\n      () => console.log('completed store')\n    )\n};\n\n// Replace item that has equal `id` field\n//  or insert if it doesn't exist.\nconst updateMessage = message => {\n  chat.replace(message);\n};\n\n// Remove item from collection\nconst deleteMessage = message => {\n  chat.remove(message);\n};\n```\n\nAnd lastly, the [`.watch`][watch] method basically creates a listener on the chat collection. Using just `chat.watch()`, and the new updated results will be pushed to you any time they change on the server. You can also [`.watch`][watch] changes on a query or a single document.\n\n\n```js\n\nchat.watch().subscribe(chats => {\n  // Each time through it will returns all results of your query\n    renderChats(allChats)\n  },\n\n  // When error occurs on server\n  error => console.log(error)\n)\n```\n\nYou can also get notifications when the client connects and disconnects from the server\n\n``` js\n  // Triggers when client successfully connects to server\n  horizon.onReady().subscribe(() => console.log(\"Connected to Horizon Server\"))\n\n  // Triggers when disconnected from server\n  horizon.onDisconnected().subscribe(() => console.log(\"Disconnected from Horizon Server\"))\n```\n\nFrom here, you could take any framework and add these functions to create a realtime chat application\nwithout writing a single line of backend code.\n\nThere's also plenty of other functions in the Horizon Client library to meet your needs, including:\n[above][above], [below][below], [limit][limit], [replace][replace], and [upsert][upsert].\n\n\n## Bringing your app to Horizon\n\nWe expect many people to already have an application in place but want to leverage\nthe power of Horizon for their realtime data. Here are a few scenarios that will\nbe relevant to you:\n\n### Do I need to output all my files into the `dist` folder?\n\nThe short and long answer is, **_no_**.\n\nIf you are already using some other process to serve your static files, you absolutely\ndo not need to now do Yet Another Refactor™️ just to get the power of Horizon. From your already existing code base you have two options to get include and then `require` the Horizon Client library:\n\n1. Use `horizon.js` served by Horizon Server (simplest option)\n1. Install `@horizon/client` as a dependency in your project\n\nWe recommend using the `horizon.js` library as served by Horizon Server for solely the\nreason that there will be no mismatches between your client library version and your\ncurrent running version of Horizon Server.\n\nThis means somewhere in your application, you'll need to have:\n\n```html\n<script src=\"localhost:8181/horizon/horizon.js\"></script>\n```\n\nAnd then when you init the Horizon connection you need to specify the `host` property:\n\n```js\nconst horizon = Horizon({host: 'localhost:8181'});\n```\n\nHowever, if requesting the .js library at page load time isn't desirable, or you are using [webpack](https://webpack.github.io/) and similar build setups for your front-end code, just add `npm install @horizon/client` to your project, and dependency wise, you'll be good to go.\n\nJust remember that when you make connections to Horizon Server to specify the port number (which is by default `8181`) when connecting.\n\n> **Note:** This will likely require setting CORS headers on the Horizon Server responses, which is a feature in progress, refer to [issue #239 for progress](https://github.com/rethinkdb/horizon/issues/239).\n\n### How do I add Horizon to X?\n\nIf you already have a React, Angular, or Whatever Is Cool These Days:tm: application, you should first check our [examples directory](/examples) for different ways on how we have integrated Horizon into these frameworks.\n\n---\n\n## Example Applications\n\nTo show how Horizon fits with your framework of choice, we've put together a handful of\nexample applications to help you get started.\n\n<img src=\"https://i.imgur.com/XFostB8.gif\" align=\"right\" width=\"450px\">\n\n* [Horizon Repo Examples Directory](https://github.com/rethinkdb/horizon/tree/next/examples)\n * [CycleJS Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/cyclejs-chat-app)\n * [RiotJS Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/riotjs-chat-app)\n * [React Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/react-chat-app)\n * [React TodoMVC App](https://github.com/rethinkdb/horizon/tree/next/examples/react-todo-app)\n * [Vue Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/vue-chat-app)\n * [Vue TodoMVC App](https://github.com/rethinkdb/horizon/tree/next/examples/vue-todo-app)\n\n\n## Extending Horizon Server\n\nWe also have a few examples of how you can extend Horizon Server. We imagine that once your application\ngrows beyond the needs of simply providing the Horizon Client API, you'll want to expand and build upon\nHorizon Server. Here are a few examples of how to extend Horizon Server with some popular Node web frameworks.\n\n* [Extending with Koa Server](https://github.com/rethinkdb/horizon/tree/next/examples/koa-server)\n* [Extending with Hapi Server](https://github.com/rethinkdb/horizon/tree/next/examples/hapi-server)\n* [Extending with Express Server](https://github.com/rethinkdb/horizon/tree/next/examples/express-server)\n\n[above]: https://github.com/rethinkdb/horizon/tree/next/client#above-limit-integer--key-value-closed-string-\n[below]: https://github.com/rethinkdb/horizon/tree/next/client#below-limit-integer--key-value-closed-string-\n[Collection]: https://github.com/rethinkdb/horizon/tree/next/client#collection\n[fetch]: https://github.com/rethinkdb/horizon/tree/next/client#fetch\n[find]: https://github.com/rethinkdb/horizon/tree/next/client#find---id-any-\n[findAll]: https://github.com/rethinkdb/horizon/tree/next/client#findall--id-any----id-any--\n[Horizon]: https://github.com/rethinkdb/horizon/tree/next/client#horizon\n[limit]: https://github.com/rethinkdb/horizon/tree/next/client#limit-num-integer-\n[order]: https://github.com/rethinkdb/horizon/tree/next/client#order---directionascending-\n[remove]: https://github.com/rethinkdb/horizon/tree/next/client#remove-id-any--id-any-\n[removeAll]: https://github.com/rethinkdb/horizon/tree/next/client#removeall--id-any--id-any-----id-any---id-any---\n[replace]: https://github.com/rethinkdb/horizon/tree/next/client#replace--\n[store]: https://github.com/rethinkdb/horizon/tree/next/client#store-------\n[store]: https://github.com/rethinkdb/horizon/tree/next/client#store-------\n[upsert]: https://github.com/rethinkdb/horizon/tree/next/client#upsert------\n[watch]: https://github.com/rethinkdb/horizon/tree/next/client#watch--rawchanges-false--\n"
  },
  {
    "path": "ISSUE_TEMPLATE.md",
    "content": "If you're reporting a bug please include the server version and client version.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\nCopyright (c) 2016 RethinkDB, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img style=\"width:100%;\" src=\"/github-banner.png\">\n\n# Horizon\n\n[Official Repository](https://github.com/rethinkdb/horizon)\n\n## What is Horizon?\n\nHorizon is an open-source developer platform for building sophisticated realtime\napps. It provides a complete backend that makes it dramatically simpler to\nbuild, deploy, manage, and scale engaging JavaScript web and mobile apps.\nHorizon is extensible, integrates with the Node.js stack, and allows building\nmodern, arbitrarily complex applications.\n\nHorizon is built on top of [RethinkDB](https://www.rethinkdb.com) and consists of\nfour components:\n\n- [__Horizon server__](/server) -- a middleware server that connects to/is built on\n  top of RethinkDB, and exposes a simple API/protocol to front-end\n  applications.\n- [__Horizon client library__](/client) -- a JavaScript client library that wraps\n  Horizon server's protocol in a convenient API for front-end\n  developers.\n- [__Horizon CLI - `hz`__](/cli) -- a command-line tool aiding in scaffolding, development, and deployment\n- [__GraphQL support__](https://github.com/rethinkdb/horizon/issues/125) -- the server will have a GraphQL adapter so anyone can get started building React/Relay apps without writing any backend code at the beginning. This will not ship in v1, but we'll follow up with a GraphQL adapter quickly after launch.\n\nHorizon currently has all the following services available to developers:\n\n- ✅ __Subscribe__ -- a streaming API for building realtime apps directly from the\n  browser without writing any backend code.\n- ✅ __Auth__ -- an authentication API that connects to common auth providers\n  (e.g. Facebook, Google, GitHub).\n- ✅ __Identity__ -- an API for listing and manipulating user accounts.\n- ✅ __Permissions__ -- a security model that allows the developer to protect\n   data from unauthorized access.\n\nUpcoming versions of Horizon will likely expose the following\nadditional services:\n\n- __Session management__ -- manage browser session and session\n  information.\n- __Geolocation__ -- an API that makes it very easy to build\n  location-aware apps.\n- __Presence__ -- an API for detecting presence information for a given\n  user and sharing it with others.\n- __Plugins__ -- a system for extending Horizon with user-defined services\n  in a consistent, discoverable way.\n- __Backend__ -- an API/protocol to integrate custom backend code with\n  Horizon server/client-libraries.\n\n## Why Horizon?\n\nWhile technologies like [RethinkDB](http://www.rethinkdb.com) and\n[WebSocket](https://en.wikipedia.org/wiki/WebSocket) make it possible to build\nengaging realtime apps, empirically there is still too much friction for most\ndevelopers. Building realtime apps now requires understanding and manually\norchestrating multiple systems across the software stack, understanding\ndistributed stream processing, and learning how to deploy and scale realtime systems. The\nlearning curve is quite steep, and most of the initial work involves boilerplate\ncode that is far removed from the primary task of building a realtime app.\n\nHorizon sets out to solve this problem. Developers can start building\napps using their favorite front-end framework using Horizon's APIs\nwithout having to write any backend code.\n\nSince Horizon stores data in RethinkDB, once the app gets sufficiently\ncomplex to need custom business logic on the backend, developers can\nincrementally add backend code at any time in the development cycle of\ntheir app.\n\n## Get Involved\n\nWe'd love for you to help us build Horizon. If you'd like to be a contributor,\ncheck out our [Contributing guide](/CONTRIBUTING.md).\n\nAlso, to stay up-to-date on all Horizon related news and the community you should\ndefinitely [join us on Slack](http://slack.rethinkdb.com) or [follow us on Twitter](https://twitter.com/horizonjs).\n\n![](/assets/Lets-go.png)\n\n## FAQ\n\nCheck out our FAQ at [horizon.io/faq](https://horizon.io/faq/)\n\n### How will Horizon be licensed?\n\nThe Horizon server, client and cli are available under the MIT license\n"
  },
  {
    "path": "circle.yml",
    "content": "## Customize the test machine\nmachine:\n\n  #timezone:\n  #  America/Los_Angeles # Set the timezone\n\n  # Set version of node to use\n  #node:\n  #  version:\n  #    5.7.0\n\n  post:\n    - source /etc/lsb-release && echo \"deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main\" | sudo tee /etc/apt/sources.list.d/rethinkdb.list\n    - wget -qO- https://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add -\n    - sudo apt-get update -o Dir::Etc::sourcelist=\"/etc/apt/sources.list.d/rethinkdb.list\" -o Dir::Etc::sourceparts=\"-\" -o APT::Get::List-Cleanup=\"0\"\n    - sudo apt-get install rethinkdb\n\n## Set artifacts\n# general:\n#   artifacts:\n#     - \"client/npm-debug.log\"\n#     - \"server/npm-debug.log\"\n#     - \"cli/npm-debug.log\"\n\n## Customize dependencies\ndependencies:\n  # Cache directories for speed\n  cache_directories:\n    - client/node_modules\n    - server/node_modules\n    - cli/node_modules\n  override:\n    # Stop default services\n    #- sudo service redis-server stop\n    #- sudo service postgresql stop\n    #- sudo service mysql stop\n\n    # Prepare for client tests\n    #- npm prune --production:\n    #    pwd: client\n    # Prepare for server tests\n    #- npm prune --production:\n    #    pwd: server\n    #- npm prune --production:\n    #    pwd: cli\n    - ./setupDev.sh:\n        pwd: test\n\n## Customize test commands\ntest:\n  pre:\n    - ./test/serve.js:\n        background: true\n    # - mkdir -p $CIRCLE_TEST_REPORTS/xunit\n    # - touch $CIRCLE_TEST_REPORTS/xunit/cli-tests.xml\n    # - touch $CIRCLE_TEST_REPORTS/xunit/client-tests.xml\n    # - touch $CIRCLE_TEST_REPORTS/xunit/server-tests.xml\n  override:\n    # Run client tests\n    - ./node_modules/.bin/mocha --timeout 100000 dist/test.js:\n        pwd: client\n        parallel: false\n    # Run server tests\n    - ./node_modules/.bin/mocha --timeout 100000 test/test.js test/schema.js:\n        pwd: server\n        parallel: false\n    # Run cli tests\n    - ./node_modules/.bin/mocha --timeout 100000 test:\n        pwd: cli\n        parallel: false\n"
  },
  {
    "path": "cli/.eslintrc.js",
    "content": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n  extends: \"../.eslintrc.js\",\n  rules: {\n    \"max-len\": [ ERROR, 100 ],\n    \"no-invalid-this\": [ OFF ]\n  },\n  env: {\n    \"es6\": true,\n    \"node\": true,\n    \"mocha\": true,\n  },\n};\n"
  },
  {
    "path": "cli/.gitignore",
    "content": "# Coverage directory used by tools like istanbul\ncoverage\n"
  },
  {
    "path": "cli/README.md",
    "content": "# **Horizon** is a realtime, open-source backend for JavaScript apps.\n\nRapidly build and deploy web or mobile apps using a simple JavaScript API. Scale your apps to millions of users without any backend code.\n\nHorizon consists of three components:\n\n* Horizon server: a middleware server that connects to/is built on top of RethinkDB, and exposes a simple API/protocol to front-end applications.\n* Horizon client: a JavaScript client library that wraps Horizon server’s protocol in a convenient API for front-end developers.\n* Horizon CLI: a command line tool, hz, aiding in scaffolding, development, and deployment.\n\nBuilt by the [RethinkDB](https://rethinkdb.com) team and an open-source community, Horizon lets you build sophisticated apps with lightning speed.\n\n## Installing Horizon \n\nhttps://horizon.io/install/\n\n## Getting Started\n\nhttps://horizon.io/docs/getting-started/\n\n\n"
  },
  {
    "path": "cli/package.json",
    "content": "{\n  \"name\": \"horizon\",\n  \"version\": \"2.0.0\",\n  \"description\": \"An open-source developer platform for building realtime, scalable web apps.\",\n  \"main\": \"src/main.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rethinkdb/horizon.git\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint src test\",\n    \"test\": \"mocha test test/unit --timeout 10000\",\n    \"coverage\": \"istanbul cover _mocha test\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"MIT\",\n  \"bin\": {\n    \"hz\": \"src/main.js\",\n    \"horizon\": \"src/main.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/rethinkdb/horizon/issues\"\n  },\n  \"homepage\": \"https://github.com/rethinkdb/horizon#readme\",\n  \"dependencies\": {\n    \"@horizon/server\": \"2.0.0\",\n    \"argparse\": \"^1.0.3\",\n    \"bluebird\": \"^3.4.1\",\n    \"chalk\": \"^1.1.3\",\n    \"hasbin\": \"^1.2.1\",\n    \"joi\": \"^8.0.5\",\n    \"jsonwebtoken\": \"^5.5.4\",\n    \"mime-types\": \"^2.0.4\",\n    \"open\": \"7.0.4\",\n    \"rethinkdb\": \"^2.1.1\",\n    \"toml\": \"^2.3.0\"\n  },\n  \"devDependencies\": {\n    \"chai\": \"^3.5.0\",\n    \"eslint\": \"^7.3.1\",\n    \"istanbul\": \"^0.4.3\",\n    \"mocha\": \"2.4.5\",\n    \"mock-fs\": \"^3.10.0\",\n    \"sinon\": \"1.17.3\",\n    \"strip-ansi\": \"^3.0.1\",\n    \"toml\": \"^2.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=4.0.0\",\n    \"npm\": \">=3.0.0\"\n  },\n  \"preferGlobal\": true\n}\n"
  },
  {
    "path": "cli/src/create-cert.js",
    "content": "'use strict';\nconst hasbin = require('hasbin');\nconst spawn = require('child_process').spawn;\n\nconst run = (args) => {\n  if (args.length) {\n    throw new Error('create-cert takes no arguments');\n  }\n\n  // TODO: user configuration?\n  const settings = {\n    binaryName: 'openssl',\n    keyOutName: 'horizon-key.pem',\n    certOutName: 'horizon-cert.pem',\n    algo: 'rsa',\n    bits: '2048',\n    days: '365',\n  };\n\n  // generate the arguments to the command\n  const binArgs = [ 'req', '-x509', '-nodes', '-batch',\n    '-newkey', `${settings.algo}:${settings.bits}`,\n    '-keyout', settings.keyOutName,\n    '-out', settings.certOutName,\n    '-days', settings.days,\n  ];\n\n  return new Promise((resolve, reject) => {\n    hasbin(settings.binaryName, (hasOpenSSL) => {\n      // show the invocation that's about to be run\n      console.log(`> ${settings.binaryName} ${binArgs.join(' ')}`);\n\n      // if we don't have openssl, bail\n      if (!hasOpenSSL) {\n        reject(new Error(`Missing ${settings.binaryName}. Make sure it is on the path.`));\n      }\n\n      // otherwise start openssl\n      const sslProc = spawn(settings.binaryName, binArgs);\n\n      // pipe output appropriately\n      sslProc.stdout.pipe(process.stdout, { end: false });\n      sslProc.stderr.pipe(process.stderr, { end: false });\n\n      // say nice things to the user when it's done\n      sslProc.on('error', reject);\n      sslProc.on('close', (code) => {\n        if (code) {\n          reject(new Error(`OpenSSL failed with code ${code}.`));\n        } else {\n          console.log('Everything seems to be fine. ' +\n                      'Remember to add your shiny new certificates to your Horizon config!');\n          resolve();\n        }\n      });\n    });\n  });\n};\n\nmodule.exports = {\n  run,\n  description: 'Generate a certificate',\n};\n"
  },
  {
    "path": "cli/src/init.js",
    "content": "/* global require, module */\n\n'use strict';\n\nconst fs = require('fs');\nconst crypto = require('crypto');\nconst process = require('process');\nconst argparse = require('argparse');\nconst checkProjectName = require('./utils/check-project-name');\nconst rethrow = require('./utils/rethrow');\n\nconst makeIndexHTML = (projectName) => `\\\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <script src=\"/horizon/horizon.js\"></script>\n    <script>\n      var horizon = Horizon();\n      horizon.onReady(function() {\n        document.querySelector('h1').innerHTML = '${projectName} works!'\n      });\n      horizon.connect();\n    </script>\n  </head>\n  <body>\n   <marquee direction=\"left\"><h1></h1></marquee>\n  </body>\n</html>\n`;\n\nconst makeDefaultConfig = (projectName) => `\\\n# This is a TOML file\n\n###############################################################################\n# IP options\n# 'bind' controls which local interfaces will be listened on\n# 'port' controls which port will be listened on\n#------------------------------------------------------------------------------\n# bind = [ \"localhost\" ]\n# port = 8181\n\n\n###############################################################################\n# HTTPS Options\n# 'secure' will disable HTTPS and use HTTP instead when set to 'false'\n# 'key_file' and 'cert_file' are required for serving HTTPS\n#------------------------------------------------------------------------------\n# secure = true\n# key_file = \"horizon-key.pem\"\n# cert_file = \"horizon-cert.pem\"\n\n\n###############################################################################\n# App Options\n# 'project_name' sets the name of the RethinkDB database used to store the\n#                application state\n# 'serve_static' will serve files from the given directory over HTTP/HTTPS\n#------------------------------------------------------------------------------\nproject_name = \"${projectName}\"\n# serve_static = \"dist\"\n\n\n###############################################################################\n# Data Options\n# WARNING: these should probably not be enabled on a publically accessible\n# service.  Tables and indexes are not lightweight objects, and allowing them\n# to be created like this could open the service up to denial-of-service\n# attacks.\n# 'auto_create_collection' creates a collection when one is needed but does not exist\n# 'auto_create_index' creates an index when one is needed but does not exist\n#------------------------------------------------------------------------------\n# auto_create_collection = false\n# auto_create_index = false\n\n\n###############################################################################\n# RethinkDB Options\n# 'connect' and 'start_rethinkdb' are mutually exclusive\n# 'connect' will connect to an existing RethinkDB instance\n# 'start_rethinkdb' will run an internal RethinkDB instance\n# 'rdb_timeout' is the number of seconds to wait when connecting to RethinkDB\n#------------------------------------------------------------------------------\n# connect = \"localhost:28015\"\n# start_rethinkdb = false\n# rdb_timeout = 30\n\n\n###############################################################################\n# Debug Options\n# 'debug' enables debug log statements\n#------------------------------------------------------------------------------\n# debug = false\n\n\n###############################################################################\n# Authentication Options\n# Each auth subsection will add an endpoint for authenticating through the\n# specified provider.\n# 'token_secret' is the key used to sign jwts\n# 'allow_anonymous' issues new accounts to users without an auth provider\n# 'allow_unauthenticated' allows connections that are not tied to a user id\n# 'auth_redirect' specifies where users will be redirected to after login\n# 'access_control_allow_origin' sets a host that can access auth settings\n#   (typically your frontend host)\n#------------------------------------------------------------------------------\n# allow_anonymous = false\n# allow_unauthenticated = false\n# auth_redirect = \"/\"\n# access_control_allow_origin = \"\"\n#\n`;\n\nconst makeDefaultSchema = () => `\\\n[groups.admin]\n[groups.admin.rules.carte_blanche]\ntemplate = \"any()\"\n`;\n\nconst makeDefaultSecrets = () => `\\\ntoken_secret = \"${crypto.randomBytes(64).toString('base64')}\"\n\n###############################################################################\n# RethinkDB Options\n# 'rdb_user' is the user account to log in with when connecting to RethinkDB\n# 'rdb_password' is the password for the user account specified by 'rdb_user'\n#------------------------------------------------------------------------------\n# rdb_user = 'admin'\n# rdb_password = ''\n\n# [auth.auth0]\n# host = \"0000.00.auth0.com\"\n# id = \"0000000000000000000000000\"\n# secret = \"00000000000000000000000000000000000000000000000000\"\n#\n# [auth.facebook]\n# id = \"000000000000000\"\n# secret = \"00000000000000000000000000000000\"\n#\n# [auth.google]\n# id = \"00000000000-00000000000000000000000000000000.apps.googleusercontent.com\"\n# secret = \"000000000000000000000000\"\n#\n# [auth.twitter]\n# id = \"0000000000000000000000000\"\n# secret = \"00000000000000000000000000000000000000000000000000\"\n#\n# [auth.github]\n# id = \"00000000000000000000\"\n# secret = \"0000000000000000000000000000000000000000\"\n#\n# [auth.twitch]\n# id = \"0000000000000000000000000000000\"\n# secret = \"0000000000000000000000000000000\"\n#\n# [auth.slack]\n# id = \"0000000000000000000000000000000\"\n# secret = \"0000000000000000000000000000000\"\n`;\n\nconst gitignore = () => `\\\nrethinkdb_data\n**/*.log\n.hz/secrets.toml\nnode_modules\n`;\n\nconst parseArguments = (args) => {\n  const parser = new argparse.ArgumentParser({ prog: 'hz init' });\n  parser.addArgument([ 'projectName' ],\n    { action: 'store',\n      help: 'Name of directory to create. Defaults to current directory',\n      nargs: '?',\n    }\n  );\n  return parser.parseArgs(args);\n};\n\nconst fileExists = (pathName) => {\n  try {\n    fs.statSync(pathName);\n    return true;\n  } catch (e) {\n    return false;\n  }\n};\n\nconst maybeMakeDir = (createDir, dirName) => {\n  if (createDir) {\n    try {\n      fs.mkdirSync(dirName);\n      console.info(`Created new project directory ${dirName}`);\n    } catch (e) {\n      throw rethrow(e,\n        `Couldn't make directory ${dirName}: ${e.message}`);\n    }\n  } else {\n    console.info(`Initializing in existing directory ${dirName}`);\n  }\n};\n\nconst maybeChdir = (chdirTo) => {\n  if (chdirTo) {\n    try {\n      process.chdir(chdirTo);\n    } catch (e) {\n      if (e.code === 'ENOTDIR') {\n        throw rethrow(e, `${chdirTo} is not a directory`);\n      } else {\n        throw rethrow(e, `Couldn't chdir to ${chdirTo}: ${e.message}`);\n      }\n    }\n  }\n};\n\nconst populateDir = (projectName, dirWasPopulated, chdirTo, dirName) => {\n  const niceDir = chdirTo ? `${dirName}/` : '';\n  if (!dirWasPopulated && !fileExists('src')) {\n    fs.mkdirSync('src');\n    console.info(`Created ${niceDir}src directory`);\n  }\n  if (!dirWasPopulated && !fileExists('dist')) {\n    fs.mkdirSync('dist');\n    console.info(`Created ${niceDir}dist directory`);\n\n    fs.appendFileSync('./dist/index.html', makeIndexHTML(projectName));\n    console.info(`Created ${niceDir}dist/index.html example`);\n  }\n\n  if (!fileExists('.hz')) {\n    fs.mkdirSync('.hz');\n    console.info(`Created ${niceDir}.hz directory`);\n  }\n\n  // Default permissions\n  const permissionGeneral = {\n    encoding: 'utf8',\n    mode: 0o666,\n  };\n\n  const permissionSecret = {\n    encoding: 'utf8',\n    mode: 0o600, // Secrets are put in this config, so set it user, read/write only\n  };\n\n  // Create .gitignore if it doesn't exist\n  if (!fileExists('.gitignore')) {\n    fs.appendFileSync(\n      '.gitignore',\n      gitignore(),\n      permissionGeneral\n    );\n    console.info(`Created ${niceDir}.gitignore`);\n  } else {\n    console.info('.gitignore already exists, not touching it.');\n  }\n\n  // Create .hz/config.toml if it doesn't exist\n  if (!fileExists('.hz/config.toml')) {\n    fs.appendFileSync(\n      '.hz/config.toml',\n      makeDefaultConfig(projectName),\n      permissionGeneral\n    );\n    console.info(`Created ${niceDir}.hz/config.toml`);\n  } else {\n    console.info('.hz/config.toml already exists, not touching it.');\n  }\n\n  // Create .hz/schema.toml if it doesn't exist\n  if (!fileExists('.hz/schema.toml')) {\n    fs.appendFileSync(\n      '.hz/schema.toml',\n      makeDefaultSchema(),\n      permissionGeneral\n    );\n    console.info(`Created ${niceDir}.hz/schema.toml`);\n  } else {\n    console.info('.hz/schema.toml already exists, not touching it.');\n  }\n\n  // Create .hz/secrets.toml if it doesn't exist\n  if (!fileExists('.hz/secrets.toml')) {\n    fs.appendFileSync(\n      '.hz/secrets.toml',\n      makeDefaultSecrets(),\n      permissionSecret\n    );\n    console.info(`Created ${niceDir}.hz/secrets.toml`);\n  } else {\n    console.info('.hz/secrets.toml already exists, not touching it.');\n  }\n};\n\nconst run = (args) =>\n  Promise.resolve(args)\n    .then(parseArguments)\n    .then((parsed) => {\n      const check = checkProjectName(\n        parsed.projectName,\n        process.cwd(),\n        fs.readdirSync('.')\n      );\n      const projectName = check.projectName;\n      const dirName = check.dirName;\n      const chdirTo = check.chdirTo;\n      const createDir = check.createDir;\n      maybeMakeDir(createDir, dirName);\n      maybeChdir(chdirTo);\n\n      // Before we create things, check if the directory is empty\n      const dirWasPopulated = fs.readdirSync(process.cwd()).length !== 0;\n      populateDir(projectName, dirWasPopulated, chdirTo, dirName);\n    });\n\nmodule.exports = {\n  run,\n  description: 'Initialize a horizon app directory',\n};\n"
  },
  {
    "path": "cli/src/main.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\n// To support `pidof horizon`, by default it shows in `pidof node`\nprocess.title = 'horizon';\n\nconst chalk = require('chalk');\nconst path = require('path');\n\nconst initCommand = require('./init');\nconst serveCommand = require('./serve');\nconst versionCommand = require('./version');\nconst createCertCommand = require('./create-cert');\nconst schemaCommand = require('./schema');\nconst makeTokenCommand = require('./make-token');\nconst migrateCommand = require('./migrate');\n\nconst NiceError = require('./utils/nice_error');\n// Mapping from command line strings to modules. To add a new command,\n// add an entry in this object, and create a module with the following\n// exported:\n// - run: main function for the command\n// - description: a string to display in the hz help text\nconst commands = {\n  init: initCommand,\n  serve: serveCommand,\n  version: versionCommand,\n  'create-cert': createCertCommand,\n  'make-token': makeTokenCommand,\n  schema: schemaCommand,\n  migrate: migrateCommand,\n};\n\nconst programName = path.basename(process.argv[1]);\n\nconst help = () => {\n  console.log(`Usage: ${programName} subcommand [args...]`);\n  console.log('Available subcommands:');\n  Object.keys(commands).forEach((cmdName) =>\n    console.log(`  ${cmdName} - ${commands[cmdName].description}`)\n  );\n};\n\nconst allArgs = process.argv.slice(2);\nif (allArgs.length === 0) {\n  help();\n  process.exit(1);\n}\n\nconst cmdName = allArgs[0];\nconst cmdArgs = allArgs.slice(1);\n\nif (cmdName === '-h' || cmdName === '--help' || cmdName === 'help') {\n  help();\n  process.exit(0);\n}\n\nconst command = commands[cmdName];\nif (!command) {\n  console.error(chalk.red.bold(\n    `No such subcommand ${cmdName}, run with -h for help`));\n  process.exit(1);\n}\n\nconst done = (err) => {\n  if (err) {\n    const errMsg = (err instanceof NiceError) ?\n            err.niceString({ contextSize: 2 }) : err.message;\n    console.error(chalk.red.bold(errMsg));\n    process.exit(1);\n  } else {\n    process.exit(0);\n  }\n};\n\ntry {\n  command.run(cmdArgs).then(() => done()).catch(done);\n} catch (err) {\n  done(err);\n}\n"
  },
  {
    "path": "cli/src/make-token.js",
    "content": "'use strict';\n\nconst interrupt = require('./utils/interrupt');\nconst config = require('./utils/config');\nconst horizon_server = require('@horizon/server');\n\nconst path = require('path');\nconst jwt = require('jsonwebtoken');\n\nconst r = horizon_server.r;\nconst logger = horizon_server.logger;\nconst argparse = require('argparse');\n\nconst parseArguments = (args) => {\n  const parser = new argparse.ArgumentParser({ prog: 'hz make-token' });\n\n  parser.addArgument(\n    [ '--token-secret' ],\n    { type: 'string', metavar: 'SECRET',\n      help: 'Secret key for signing the token.' });\n\n  parser.addArgument(\n    [ 'user' ],\n    { type: 'string', metavar: 'USER_ID',\n      help: 'The ID of the user to issue a token for.' });\n\n  return parser.parseArgs(args);\n};\n\nconst processConfig = (parsed) => {\n  let options;\n\n  options = config.default_options();\n\n  options = config.merge_options(\n    options, config.read_from_config_file(parsed.project_path));\n  options = config.merge_options(\n    options, config.read_from_secrets_file(parsed.project_path));\n  options = config.merge_options(options, config.read_from_env());\n  options = config.merge_options(options, config.read_from_flags(parsed));\n\n  if (options.project_name === null) {\n    options.project_name = path.basename(path.resolve(options.project_path));\n  }\n\n  return Object.assign(options, { user: parsed.user });\n};\n\nconst run = (args) => Promise.resolve().then(() => {\n  const options = processConfig(parseArguments(args));\n\n  if (options.token_secret === null) {\n    throw new Error('No token secret specified, unable to sign the token.');\n  }\n  const token = jwt.sign(\n    { id: options.user, provider: null },\n    new Buffer(options.token_secret, 'base64'),\n    { expiresIn: '1d', algorithm: 'HS512' }\n  );\n  console.log(`${token}`);\n});\n\nmodule.exports = {\n  run,\n  description: 'Generate a token to log in as a user',\n};\n"
  },
  {
    "path": "cli/src/migrate.js",
    "content": "'use strict';\nconst chalk = require('chalk');\nconst r = require('rethinkdb');\nconst Promise = require('bluebird');\nconst argparse = require('argparse');\nconst runSaveCommand = require('./schema').runSaveCommand;\nconst fs = require('fs');\nconst accessAsync = Promise.promisify(fs.access);\nconst config = require('./utils/config');\nconst procPromise = require('./utils/proc-promise');\nconst interrupt = require('./utils/interrupt');\nconst change_to_project_dir = require('./utils/change_to_project_dir');\nconst parse_yes_no_option = require('./utils/parse_yes_no_option');\nconst start_rdb_server = require('./utils/start_rdb_server');\nconst NiceError = require('./utils/nice_error.js');\n\nconst VERSION_2_0 = [ 2, 0, 0 ];\n\nfunction run(cmdArgs) {\n  const options = processConfig(cmdArgs);\n  interrupt.on_interrupt(() => teardown());\n  return Promise.resolve().bind({ options })\n    .then(setup)\n    .then(validateMigration)\n    .then(makeBackup)\n    .then(renameUserTables)\n    .then(moveInternalTables)\n    .then(renameIndices)\n    .then(rewriteHzCollectionDocs)\n    .then(exportNewSchema)\n    .finally(teardown);\n}\n\nfunction green() {\n  const args = Array.from(arguments);\n  args[0] = chalk.green(args[0]);\n  console.log.apply(console, args);\n}\n\nfunction white() {\n  const args = Array.from(arguments);\n  args[0] = chalk.white(args[0]);\n  console.log.apply(console, args);\n}\n\nfunction processConfig(cmdArgs) {\n  // do boilerplate to get config args :/\n  const parser = new argparse.ArgumentParser({ prog: 'hz migrate' });\n\n  parser.addArgument([ 'project_path' ], {\n    default: '.',\n    nargs: '?',\n    help: 'Change to this directory before migrating',\n  });\n\n  parser.addArgument([ '--project-name', '-n' ], {\n    help: 'Name of the Horizon project server',\n  });\n\n  parser.addArgument([ '--connect', '-c' ], {\n    metavar: 'host:port',\n    default: undefined,\n    help: 'Host and port of the RethinkDB server to connect to.',\n  });\n\n  parser.addArgument([ '--rdb-user' ], {\n    default: 'admin',\n    metavar: 'USER',\n    help: 'RethinkDB User',\n  });\n\n  parser.addArgument([ '--rdb-password' ], {\n    default: undefined,\n    metavar: 'PASSWORD',\n    help: 'RethinkDB Password',\n  });\n\n  parser.addArgument([ '--start-rethinkdb' ], {\n    metavar: 'yes|no',\n    default: 'yes',\n    constant: 'yes',\n    nargs: '?',\n    help: 'Start up a RethinkDB server in the current directory',\n  });\n\n  parser.addArgument([ '--skip-backup' ], {\n    metavar: 'yes|no',\n    default: 'no',\n    constant: 'yes',\n    nargs: '?',\n    help: 'Whether to perform a backup of rethinkdb_data' +\n      ' before migrating',\n  });\n\n  parser.addArgument([ '--nonportable-backup' ], {\n    metavar: 'yes|no',\n    default: 'no',\n    constant: 'yes',\n    nargs: '?',\n    help: 'Allows creating a backup that is not portable, ' +\n      \"but doesn't require the RethinkDB Python driver to be \" +\n      'installed.',\n  });\n\n  const parsed = parser.parseArgs(cmdArgs);\n  const confOptions = config.read_from_config_file(parsed.project_path);\n  const envOptions = config.read_from_env();\n  config.merge_options(confOptions, envOptions);\n  // Pull out the relevant settings from the config file\n  const options = {\n    project_path: parsed.project_path || '.',\n    project_name: parsed.project_name || confOptions.project_name,\n    rdb_host: parsed.rdb_host || confOptions.rdb_host || 'localhost',\n    rdb_port: parsed.rdb_port || confOptions.rdb_port || 28015,\n    rdb_user: parsed.rdb_user || confOptions.rdb_user || 'admin',\n    rdb_password: parsed.rdb_password || confOptions.rdb_password || '',\n    start_rethinkdb: parse_yes_no_option(parsed.start_rethinkdb),\n    skip_backup: parse_yes_no_option(parsed.skip_backup),\n    nonportable_backup: parse_yes_no_option(parsed.nonportable_backup),\n  };\n  // sets rdb_host and rdb_port from connect if necessary\n  if (parsed.connect) {\n    config.parse_connect(parsed.connect, options);\n  }\n\n  if (options.project_name == null) {\n    throw new NiceError('No project_name given', {\n      description: `\\\nThe project_name is needed to migrate from the v1.x format the v.2.0 format. \\\nIt wasn't passed on the command line or found in your config.`,\n      suggestions: [\n        'pass the --project-name option to hz migrate',\n        'add the \"project_name\" key to your .hz/config.toml',\n      ] });\n  }\n  return options;\n}\n\nfunction setup() {\n  // Start rethinkdb server if necessary\n  // Connect to whatever rethinkdb server we're using\n  white('Setup');\n  return Promise.resolve().then(() => {\n    if (this.options.project_path && this.options.project_path !== '.') {\n      green(` ├── Changing to directory ${this.options.project_path}`);\n      change_to_project_dir(this.options.project_path);\n    }\n  }).then(() => {\n    // start rethinkdb server if necessary\n    if (this.options.start_rethinkdb) {\n      green(' ├── Starting RethinkDB server');\n      return start_rdb_server({ quiet: true }).then((server) => {\n        this.rdb_server = server;\n        this.options.rdb_host = 'localhost';\n        this.options.rdb_port = server.driver_port;\n      });\n    }\n  }).then(() => {\n    green(' ├── Connecting to RethinkDB');\n    return r.connect({\n      host: this.options.rdb_host,\n      port: this.options.rdb_port,\n      user: this.options.rdb_user,\n      password: this.options.rdb_password,\n    });\n  }).then((conn) => {\n    green(' └── Successfully connected');\n    this.conn = conn;\n  });\n}\n\nfunction teardown() {\n  return Promise.resolve().then(() => {\n    white('Cleaning up...');\n    // close the rethinkdb connection\n    if (this.conn) {\n      green(' ├── Closing rethinkdb connection');\n      return this.conn.close();\n    }\n  }).then(() => {\n    // shut down the rethinkdb server if we started it\n    if (this.rdb_server) {\n      green(' └── Shutting down rethinkdb server');\n      return this.rdb_server.close();\n    }\n  });\n}\n\nfunction validateMigration() {\n  // check that `${project}_internal` exists\n  const project = this.options.project_name;\n  const internalNotFound = `Database named '${project}_internal' wasn't found`;\n  const tablesHaveHzPrefix = `Some tables in ${project} have an hz_ prefix`;\n  const checkForHzTables = r.db('rethinkdb')\n          .table('table_config')\n          .filter({ db: project })('name')\n          .contains((x) => x.match('^hz_'))\n          .branch(r.error(tablesHaveHzPrefix), true);\n  const waitForCollections = r.db(`${project}_internal`)\n          .table('collections')\n          .wait({ timeout: 30 })\n          .do(() => r.db(project).tableList())\n          .forEach((tableName) =>\n            r.db(project).table(tableName).wait({ timeout: 30 })\n          );\n\n  return Promise.resolve().then(() => {\n    white('Validating current schema version');\n    return r.dbList().contains(`${project}_internal`)\n      .branch(true, r.error(internalNotFound))\n      .do(() => checkForHzTables)\n      .do(() => waitForCollections)\n      .run(this.conn)\n      .then(() => green(' └── Pre-2.0 schema found'))\n      .catch((e) => {\n        if (e.msg === internalNotFound) {\n          throw new NiceError(e.msg, {\n            description: `\\\nThis could happen if you don't have a Horizon app in this database, or if \\\nyou've already migrated this database to the v2.0 format.`,\n          });\n        } else if (e.msg === tablesHaveHzPrefix) {\n          throw new NiceError(e.msg, {\n            description: `This could happen if you've already migrated \\\nthis database to the v2.0 format.`,\n          });\n        } else {\n          throw e;\n        }\n      });\n  });\n}\n\nfunction makeBackup() {\n  // shell out to rethinkdb dump\n  const rdbHost = this.options.rdb_host;\n  const rdbPort = this.options.rdb_port;\n\n  if (this.options.skip_backup) {\n    return Promise.resolve();\n  }\n\n  white('Backing up rethinkdb_data directory');\n\n  if (this.options.nonportable_backup) {\n    return nonportableBackup();\n  }\n\n  return procPromise('rethinkdb', [\n    'dump',\n    '--connect',\n    `${rdbHost}:${rdbPort}`,\n  ]).then(() => {\n    green(' └── Backup completed');\n  }).catch((e) => {\n    if (e.message.match(/Python driver/)) {\n      throw new NiceError('The RethinkDB Python driver is not installed.', {\n        description: `Before we migrate to the v2.0 format, we should do a \\\nbackup of your RethinkDB database in case anything goes wrong. Unfortunately, \\\nwe can't use the rethinkdb dump command to do a backup because you don't have \\\nthe RethinkDB Python driver installed on your system.`,\n        suggestions: [\n          `Install the Python driver with the instructions found at: \\\nhttp://www.rethinkdb.com/docs/install-drivers/python/`,\n          `Pass the --nonportable-backup flag to hz migrate. This flag uses \\\nthe tar command to make a backup, but the backup is not safe to use on \\\nanother machine or to create replicas from. This option should not be used \\\nif RethinkDB is currently running. It should also not be used if the \\\nrethinkdb_data/ directory is not in the current directory.`,\n        ] });\n    } else {\n      throw e;\n    }\n  });\n}\n\nfunction nonportableBackup() {\n  // Uses tar to do an unsafe backup\n  const timestamp = new Date().toISOString().replace(/:/g, '_');\n  return procPromise('tar', [\n    '-zcvf', // gzip, compress, verbose, filename is...\n    `rethinkdb_data.nonportable-backup.${timestamp}.tar.gz`,\n    'rethinkdb_data', // directory to back up\n  ]).then(() => {\n    green(' └── Nonportable backup completed');\n  });\n}\n\nfunction renameUserTables() {\n  // for each table listed in ${project}_internal.collections\n  // rename the table name to the collection name\n  const project = this.options.project_name;\n  return Promise.resolve().then(() => {\n    white('Removing suffix from user tables');\n    return r.db(`${project}_internal`).wait({ timeout: 30 }).\n      do(() => r.db(`${project}_internal`).table('collections')\n         .forEach((collDoc) => r.db('rethinkdb').table('table_config')\n                  .filter({ db: project, name: collDoc('table') })\n                  .update({ name: collDoc('id') }))\n        ).run(this.conn)\n      .then(() => green(' └── Suffixes removed'));\n  });\n}\n\nfunction moveInternalTables() {\n  // find project_internal\n  // move all tables from ${project}_internal.${table} to ${project}.hz_${table}\n  //   - except for users, don't add hz_prefix, but move its db\n  const project = this.options.project_name;\n  return Promise.resolve().then(() => {\n    white(`Moving internal tables from ${project}_internal to ${project}`);\n    return r.db('rethinkdb').table('table_config')\n      .filter({ db: `${project}_internal` })\n      .update((table) => ({\n        db: project,\n        name: r.branch(\n          table('name').ne('users'),\n          r('hz_').add(table('name')),\n          'users'),\n      })).run(this.conn)\n      .then(() => green(' ├── Internal tables moved'));\n  }).then(() => {\n    // delete project_internal\n    green(` └── Deleting empty \"${project}_internal\" database`);\n    return r.dbDrop(`${project}_internal`).run(this.conn);\n  });\n}\n\nfunction renameIndices() {\n  // for each user $table in ${project}\n  //    for each index in ${table}\n  //        parse the old name into array of field names.\n  //        rename to `hz_${JSON.stringify(fields)}`\n  const project = this.options.project_name;\n  return Promise.resolve().then(() => {\n    white('Renaming indices to new JSON format');\n    return r.db(project).tableList().forEach((tableName) =>\n      r.db(project).table(tableName).indexList().forEach((indexName) =>\n        r.db(project).table(tableName)\n          .indexRename(indexName, rename(indexName))\n      )\n    ).run(this.conn)\n    .then(() => green(' └── Indices renamed.'));\n  });\n\n  function rename(name) {\n    // ReQL to rename the index name to the new format\n    const initialState = {\n      escaped: false,\n      field: '',\n      fields: [ ],\n    };\n    return name.split('')\n      .fold(initialState, (acc, c) =>\n        r.branch(\n          acc('escaped'),\n            acc.merge({\n              escaped: false,\n              field: acc('field').add(c),\n            }),\n          c.eq('\\\\'),\n            acc.merge({ escaped: true }),\n          c.eq('_'),\n            acc.merge({\n              fields: acc('fields').append(acc('field')),\n              field: '',\n            }),\n          acc.merge({ field: acc('field').add(c) })\n        )\n      ).do((state) =>\n          // last field needs to be appended to running list\n          state('fields').append(state('field'))\n          // wrap each field in an array\n          .map((field) => [ field ])\n         )\n      .toJSON()\n      .do((x) => r('hz_').add(x));\n  }\n}\n\nfunction rewriteHzCollectionDocs() {\n  // for each document in ${project}.hz_collections\n  //   delete the table field\n  const project = this.options.project_name;\n  return Promise.resolve().then(() => {\n    white('Rewriting hz_collections to new format');\n    return r.db(project).table('hz_collections')\n      .update({ table: r.literal() })\n      .run(this.conn);\n  }).then(() => green(' ├── \"table\" field removed'))\n    .then(() => r.db(project).table('hz_collections')\n          .insert({ id: 'users' })\n          .run(this.conn))\n    .then(() => green(' ├── Added document for \"users\" table'))\n    .then(() => r.db(project).table('hz_collections')\n          .insert({ id: 'hz_metadata', version: VERSION_2_0 })\n          .run(this.conn))\n    .then(() => green(' └── Adding the metadata document with schema version:' +\n                      `${JSON.stringify(VERSION_2_0)}`));\n}\n\nfunction exportNewSchema() {\n  // Import and run schema save process, giving it a different\n  // filename than schema.toml\n  const timestamp = new Date().toISOString().replace(/:/g, '_');\n  return accessAsync('.hz/schema.toml', fs.R_OK | fs.F_OK)\n    .then(() => `.hz/schema.toml.migrated.${timestamp}`)\n    .catch(() => '.hz/schema.toml') // if no schema.toml\n    .then((schemaFile) => {\n      white(`Exporting the new schema to ${schemaFile}`);\n      return runSaveCommand({\n        rdb_host: this.options.rdb_host,\n        rdb_port: this.options.rdb_port,\n        rdb_user: this.options.rdb_user,\n        rdb_password: this.options.rdb_password,\n        out_file: schemaFile,\n        project_name: this.options.project_name,\n      });\n    }).then(() => green(' └── Schema exported'));\n}\n\nmodule.exports = {\n  run,\n  description: 'migrate an older version of horizon to a newer one',\n};\n"
  },
  {
    "path": "cli/src/schema.js",
    "content": "'use strict';\n\nconst horizon_server = require('@horizon/server');\nconst horizon_index = require('@horizon/server/src/metadata/index');\nconst horizon_metadata = require('@horizon/server/src/metadata/metadata');\n\nconst config = require('./utils/config');\nconst interrupt = require('./utils/interrupt');\nconst start_rdb_server = require('./utils/start_rdb_server');\nconst parse_yes_no_option = require('./utils/parse_yes_no_option');\nconst change_to_project_dir = require('./utils/change_to_project_dir');\nconst initialize_joi = require('./utils/initialize_joi');\n\nconst fs = require('fs');\nconst Joi = require('joi');\nconst path = require('path');\n\nconst argparse = require('argparse');\nconst toml = require('toml');\n\nconst r = horizon_server.r;\nconst create_collection = horizon_metadata.create_collection;\nconst initialize_metadata = horizon_metadata.initialize_metadata;\n\ninitialize_joi(Joi);\n\nconst parseArguments = (args) => {\n  const parser = new argparse.ArgumentParser({ prog: 'hz schema' });\n\n  const subparsers = parser.addSubparsers({\n    title: 'subcommands',\n    dest: 'subcommand_name',\n  });\n\n  const apply = subparsers.addParser('apply', { addHelp: true });\n  const save = subparsers.addParser('save', { addHelp: true });\n\n  // Set options shared between both subcommands\n  [ apply, save ].map((subcmd) => {\n    subcmd.addArgument([ 'project_path' ],\n      { type: 'string', nargs: '?',\n        help: 'Change to this directory before serving' });\n\n    subcmd.addArgument([ '--project-name', '-n' ],\n      { type: 'string', action: 'store', metavar: 'NAME',\n        help: 'Name of the Horizon Project server' });\n\n    subcmd.addArgument([ '--connect', '-c' ],\n      { type: 'string', metavar: 'HOST:PORT',\n        help: 'Host and port of the RethinkDB server to connect to.' });\n\n    subcmd.addArgument([ '--rdb-timeout' ],\n      { type: 'int', metavar: 'TIMEOUT',\n        help: 'Timeout period in seconds for the RethinkDB connection to be opened' });\n\n    subcmd.addArgument([ '--rdb-user' ],\n      { type: 'string', metavar: 'USER',\n        help: 'RethinkDB User' });\n\n    subcmd.addArgument([ '--rdb-password' ],\n      { type: 'string', metavar: 'PASSWORD',\n        help: 'RethinkDB Password' });\n\n    subcmd.addArgument([ '--start-rethinkdb' ],\n      { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n        help: 'Start up a RethinkDB server in the current directory' });\n\n    subcmd.addArgument([ '--debug' ],\n      { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n        help: 'Enable debug logging.' });\n  });\n\n  // Options exclusive to HZ SCHEMA APPLY\n  apply.addArgument([ '--update' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Only add new items and update existing, no removal.' });\n\n  apply.addArgument([ '--force' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Allow removal of existing collections.' });\n\n  apply.addArgument([ 'schema_file' ],\n    { type: 'string', metavar: 'SCHEMA_FILE_PATH',\n      help: 'File to get the horizon schema from, use \"-\" for stdin.' });\n\n  // Options exclusive to HZ SCHEMA SAVE\n  save.addArgument([ '--out-file', '-o' ],\n    { type: 'string', metavar: 'PATH', defaultValue: '.hz/schema.toml',\n      help: 'File to write the horizon schema to, defaults to .hz/schema.toml.' });\n\n  return parser.parseArgs(args);\n};\n\nconst schema_schema = Joi.object().unknown(false).keys({\n  collections: Joi.object().unknown(true).pattern(/.*/,\n    Joi.object().unknown(false).keys({\n      indexes: Joi.array().items(\n        Joi.alternatives(\n          Joi.string(),\n          Joi.object().unknown(false).keys({\n            fields: Joi.array().items(Joi.array().items(Joi.string())).required(),\n          })\n        )\n      ).optional().default([ ]),\n    })\n  ).optional().default({ }),\n  groups: Joi.object().unknown(true).pattern(/.*/,\n    Joi.object().keys({\n      rules: Joi.object().unknown(true).pattern(/.*/,\n        Joi.object().unknown(false).keys({\n          template: Joi.string().required(),\n          validator: Joi.string().optional(),\n        })\n      ).optional().default({ }),\n    })\n  ).optional().default({ }),\n});\n\n// Preserved for interpreting old schemas\nconst v1_0_name_to_fields = (name) => {\n  let escaped = false;\n  let field = '';\n  const fields = [ ];\n  for (const c of name) {\n    if (escaped) {\n      if (c !== '\\\\' && c !== '_') {\n        throw new Error(`Unexpected index name: \"${name}\"`);\n      }\n      escaped = false;\n      field += c;\n    } else if (c === '\\\\') {\n      escaped = true;\n    } else if (c === '_') {\n      fields.push(field);\n      field = '';\n    } else {\n      field += c;\n    }\n  }\n  if (escaped) {\n    throw new Error(`Unexpected index name: \"${name}\"`);\n  }\n  fields.push([ field ]);\n  return fields;\n};\n\nconst parse_schema = (schema_toml) => {\n  const parsed = Joi.validate(toml.parse(schema_toml), schema_schema);\n  const schema = parsed.value;\n\n  if (parsed.error) {\n    throw parsed.error;\n  }\n\n  const collections = [ ];\n  for (const name in schema.collections) {\n    collections.push({\n      id: name,\n      indexes: schema.collections[name].indexes.map((index) => {\n        if (typeof index === 'string') {\n          return { fields: v1_0_name_to_fields(index), multi: false, geo: false };\n        } else {\n          return { fields: index.fields, multi: false, geo: false };\n        }\n      }),\n    });\n  }\n\n  // Make sure the 'users' collection is present, as some things depend on\n  // its existence.\n  if (!schema.collections || !schema.collections.users) {\n    collections.push({ id: 'users', indexes: [ ] });\n  }\n\n  const groups = [ ];\n  for (const name in schema.groups) {\n    groups.push(Object.assign({ id: name }, schema.groups[name]));\n  }\n\n  return { groups, collections };\n};\n\nconst processApplyConfig = (parsed) => {\n  let options, in_file;\n\n  options = config.default_options();\n  options = config.merge_options(options,\n    config.read_from_config_file(parsed.project_path));\n  options = config.merge_options(options, config.read_from_env());\n  options = config.merge_options(options, config.read_from_flags(parsed));\n\n  if (parsed.schema_file === '-') {\n    in_file = process.stdin;\n  } else {\n    in_file = fs.createReadStream(parsed.schema_file, { flags: 'r' });\n  }\n\n  if (options.project_name === null) {\n    options.project_name = path.basename(path.resolve(options.project_path));\n  }\n\n  return {\n    subcommand_name: 'apply',\n    start_rethinkdb: options.start_rethinkdb,\n    rdb_host: options.rdb_host,\n    rdb_port: options.rdb_port,\n    rdb_user: options.rdb_user || undefined,\n    rdb_password: options.rdb_password || undefined,\n    project_name: options.project_name,\n    project_path: options.project_path,\n    debug: options.debug,\n    update: parse_yes_no_option(parsed.update),\n    force: parse_yes_no_option(parsed.force),\n    in_file,\n  };\n};\n\nconst processSaveConfig = (parsed) => {\n  let options, out_file;\n\n  options = config.default_options();\n  options.start_rethinkdb = true;\n\n  options = config.merge_options(options,\n    config.read_from_config_file(parsed.project_path));\n  options = config.merge_options(options, config.read_from_env());\n  options = config.merge_options(options, config.read_from_flags(parsed));\n\n  if (parsed.out_file === '-') {\n    out_file = process.stdout;\n  } else {\n    out_file = parsed.out_file;\n  }\n\n  if (options.project_name === null) {\n    options.project_name = path.basename(path.resolve(options.project_path));\n  }\n\n  return {\n    subcommand_name: 'save',\n    start_rethinkdb: options.start_rethinkdb,\n    rdb_host: options.rdb_host,\n    rdb_port: options.rdb_port,\n    rdb_user: options.rdb_user || undefined,\n    rdb_password: options.rdb_password || undefined,\n    project_name: options.project_name,\n    project_path: options.project_path,\n    debug: options.debug,\n    out_file,\n  };\n};\n\nconst schema_to_toml = (collections, groups) => {\n  const res = [ '# This is a TOML document' ];\n\n  for (const c of collections) {\n    res.push('');\n    res.push(`[collections.${c.id}]`);\n    c.indexes.forEach((index) => {\n      const info = horizon_index.name_to_info(index);\n      res.push(`[[collections.${c.id}.indexes]]`);\n      res.push(`fields = ${JSON.stringify(info.fields)}`);\n    });\n  }\n\n  for (const g of groups) {\n    res.push('');\n    res.push(`[groups.${g.id}]`);\n    if (g.rules) {\n      for (const key in g.rules) {\n        const template = g.rules[key].template;\n        const validator = g.rules[key].validator;\n        res.push(`[groups.${g.id}.rules.${key}]`);\n        res.push(`template = ${JSON.stringify(template)}`);\n        if (validator) {\n          res.push(`validator = ${JSON.stringify(validator)}`);\n        }\n      }\n    }\n  }\n\n  res.push('');\n  return res.join('\\n');\n};\n\nconst runApplyCommand = (options) => {\n  let conn, schema, rdb_server;\n  let obsolete_collections = [ ];\n  const db = options.project_name;\n\n  const cleanup = () =>\n    Promise.all([\n      conn ? conn.close() : Promise.resolve(),\n      rdb_server ? rdb_server.close() : Promise.resolve(),\n    ]);\n\n  interrupt.on_interrupt(() => cleanup());\n\n  return Promise.resolve().then(() => {\n    if (options.start_rethinkdb) {\n      change_to_project_dir(options.project_path);\n    }\n\n    return new Promise((resolve, reject) => {\n      let schema_toml = '';\n      options.in_file.on('data', (buffer) => (schema_toml += buffer));\n      options.in_file.on('end', () => resolve(schema_toml));\n      options.in_file.on('error', reject);\n    });\n  }).then((schema_toml) => {\n    schema = parse_schema(schema_toml);\n\n    if (options.start_rethinkdb) {\n      return start_rdb_server({ quiet: !options.debug }).then((server) => {\n        rdb_server = server;\n        options.rdb_host = 'localhost';\n        options.rdb_port = server.driver_port;\n      });\n    }\n  }).then(() =>\n    r.connect({ host: options.rdb_host,\n                port: options.rdb_port,\n                user: options.rdb_user,\n                password: options.rdb_password,\n                timeout: options.rdb_timeout })\n  ).then((rdb_conn) => {\n    conn = rdb_conn;\n    return initialize_metadata(db, conn);\n  }).then((initialization_result) => {\n    if (initialization_result.tables_created) {\n      console.log('Initialized new application metadata.');\n    }\n    // Wait for metadata tables to be writable\n    return r.expr([ 'hz_collections', 'hz_groups' ])\n      .forEach((table) =>\n        r.db(db).table(table)\n          .wait({ waitFor: 'ready_for_writes', timeout: 30 }))\n      .run(conn);\n  }).then(() => {\n    // Error if any collections will be removed\n    if (!options.update) {\n      return r.db(db).table('hz_collections')\n        .filter((row) => row('id').match('^hz_').not())\n        .getField('id')\n        .coerceTo('array')\n        .setDifference(schema.collections.map((c) => c.id))\n        .run(conn)\n        .then((res) => {\n          if (!options.force && res.length > 0) {\n            throw new Error('Run with \"--force\" to continue.\\n' +\n                            'These collections would be removed along with their data:\\n' +\n                            `${res.join(', ')}`);\n          }\n          obsolete_collections = res;\n        });\n    }\n  }).then(() => {\n    if (options.update) {\n      // Update groups\n      return Promise.all(schema.groups.map((group) => {\n        const literal_group = JSON.parse(JSON.stringify(group));\n        Object.keys(literal_group.rules).forEach((key) => {\n          literal_group.rules[key] = r.literal(literal_group.rules[key]);\n        });\n\n        return r.db(db).table('hz_groups')\n          .get(group.id).replace((old_row) =>\n            r.branch(old_row.eq(null),\n                     group,\n                     old_row.merge(literal_group)))\n          .run(conn).then((res) => {\n            if (res.errors) {\n              throw new Error(`Failed to update group: ${res.first_error}`);\n            }\n          });\n      }));\n    } else {\n      // Replace and remove groups\n      const groups_obj = { };\n      schema.groups.forEach((g) => { groups_obj[g.id] = g; });\n\n      return Promise.all([\n        r.expr(groups_obj).do((groups) =>\n          r.db(db).table('hz_groups')\n            .replace((old_row) =>\n              r.branch(groups.hasFields(old_row('id')),\n                       old_row,\n                       null))\n          ).run(conn).then((res) => {\n            if (res.errors) {\n              throw new Error(`Failed to write groups: ${res.first_error}`);\n            }\n          }),\n        r.db(db).table('hz_groups')\n          .insert(schema.groups, { conflict: 'replace' })\n          .run(conn).then((res) => {\n            if (res.errors) {\n              throw new Error(`Failed to write groups: ${res.first_error}`);\n            }\n          }),\n      ]);\n    }\n  }).then(() => {\n    // Ensure all collections exist and remove any obsolete collections\n    const promises = [ ];\n    for (const c of schema.collections) {\n      promises.push(\n        create_collection(db, c.id, conn).then((res) => {\n          if (res.error) {\n            throw new Error(res.error);\n          }\n        }));\n    }\n\n    for (const c of obsolete_collections) {\n      promises.push(\n        r.db(db)\n          .table('hz_collections')\n          .get(c)\n          .delete({ returnChanges: 'always' })('changes')(0)\n          .do((res) =>\n            r.branch(res.hasFields('error'),\n                     res,\n                     res('old_val').eq(null),\n                     res,\n                     r.db(db).tableDrop(res('old_val')('id')).do(() => res)))\n          .run(conn).then((res) => {\n            if (res.error) {\n              throw new Error(res.error);\n            }\n          }));\n    }\n\n    return Promise.all(promises);\n  }).then(() => {\n    const promises = [ ];\n\n    // Ensure all indexes exist\n    for (const c of schema.collections) {\n      for (const info of c.indexes) {\n        const name = horizon_index.info_to_name(info);\n        promises.push(\n          r.branch(r.db(db).table(c.id).indexList().contains(name), { },\n                   r.db(db).table(c.id).indexCreate(name, horizon_index.info_to_reql(info),\n                     { geo: Boolean(info.geo), multi: (info.multi !== false) }))\n            .run(conn)\n            .then((res) => {\n              if (res.errors) {\n                throw new Error(`Failed to create index ${name} ` +\n                                `on collection ${c.id}: ${res.first_error}`);\n              }\n            }));\n      }\n    }\n\n    // Remove obsolete indexes\n    if (!options.update) {\n      for (const c of schema.collections) {\n        const names = c.indexes.map(horizon_index.info_to_name);\n        promises.push(\n          r.db(db).table(c.id).indexList().filter((name) => name.match('^hz_'))\n            .setDifference(names)\n            .forEach((name) => r.db(db).table(c.id).indexDrop(name))\n            .run(conn)\n            .then((res) => {\n              if (res.errors) {\n                throw new Error('Failed to remove old indexes ' +\n                                `on collection ${c.id}: ${res.first_error}`);\n              }\n            }));\n      }\n    }\n\n    return Promise.all(promises);\n  }).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));\n};\n\nconst file_exists = (filename) => {\n  try {\n    fs.accessSync(filename);\n  } catch (e) {\n    return false;\n  }\n  return true;\n};\n\nconst runSaveCommand = (options) => {\n  let conn, rdb_server;\n  const db = options.project_name;\n\n  const cleanup = () =>\n    Promise.all([\n      conn ? conn.close() : Promise.resolve(),\n      rdb_server ? rdb_server.close() : Promise.resolve(),\n    ]);\n\n  interrupt.on_interrupt(() => cleanup());\n\n  return Promise.resolve().then(() => {\n    if (options.start_rethinkdb) {\n      change_to_project_dir(options.project_path);\n    }\n  }).then(() => {\n    if (options.start_rethinkdb) {\n      return start_rdb_server({ quiet: !options.debug }).then((server) => {\n        rdb_server = server;\n        options.rdb_host = 'localhost';\n        options.rdb_port = server.driver_port;\n      });\n    }\n  }).then(() =>\n    r.connect({ host: options.rdb_host,\n                port: options.rdb_port,\n                user: options.rdb_user,\n                password: options.rdb_password,\n                timeout: options.rdb_timeout })\n  ).then((rdb_conn) => {\n    conn = rdb_conn;\n    return r.db(db).wait({ waitFor: 'ready_for_reads', timeout: 30 }).run(conn);\n  }).then(() =>\n    r.object('collections',\n             r.db(db).table('hz_collections')\n               .filter((row) => row('id').match('^hz_').not())\n               .coerceTo('array')\n               .map((row) =>\n                 row.merge({ indexes: r.db(db).table(row('id')).indexList() })),\n             'groups', r.db(db).table('hz_groups').coerceTo('array'))\n      .run(conn)\n  ).then((res) =>\n    new Promise((resolve) => {\n      // Only rename old file if saving to default .hz/schema.toml\n      if (options.out_file === '.hz/schema.toml' &&\n          file_exists(options.out_file)) {\n        // Rename existing file to have the current time appended to its name\n        const oldPath = path.resolve(options.out_file);\n        const newPath = `${path.resolve(options.out_file)}.${new Date().toISOString()}`;\n        fs.renameSync(oldPath, newPath);\n      }\n\n      const output = (options.out_file === '-') ? process.stdout :\n        fs.createWriteStream(options.out_file, { flags: 'w', defaultEncoding: 'utf8' });\n\n      // Output toml_str to schema.toml\n      const toml_str = schema_to_toml(res.collections, res.groups);\n      output.end(toml_str, resolve);\n    })\n  ).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));\n};\n\nconst processConfig = (options) => {\n  // Determine if we are saving or applying and use appropriate config processing\n  switch (options.subcommand_name) {\n  case 'apply':\n    return processApplyConfig(options);\n  case 'save':\n    return processSaveConfig(options);\n  default:\n    throw new Error(`Unrecognized schema subcommand: \"${options.subcommand_name}\"`);\n  }\n};\n\n// Avoiding cyclical depdendencies\nmodule.exports = {\n  run: (args) =>\n    Promise.resolve().then(() => {\n      const options = processConfig(parseArguments(args));\n      // Determine if we are saving or applying and use appropriate run function\n      switch (options.subcommand_name) {\n      case 'apply':\n        return runApplyCommand(options);\n      case 'save':\n        return runSaveCommand(options);\n      default:\n        throw new Error(`Unrecognized schema subcommand: \"${options.subcommand_name}\"`);\n      }\n    }),\n  description: 'Apply and save the schema from a horizon database',\n  processApplyConfig,\n  runApplyCommand,\n  runSaveCommand,\n  parse_schema,\n};\n"
  },
  {
    "path": "cli/src/serve.js",
    "content": "'use strict';\n\nconst chalk = require('chalk');\nconst crypto = require('crypto');\nconst fs = require('fs');\nconst get_type = require('mime-types').contentType;\nconst http = require('http');\nconst https = require('https');\nconst open = require('open');\nconst path = require('path');\nconst argparse = require('argparse');\nconst url = require('url');\n\nconst config = require('./utils/config');\nconst start_rdb_server = require('./utils/start_rdb_server');\nconst change_to_project_dir = require('./utils/change_to_project_dir');\nconst NiceError = require('./utils/nice_error.js');\nconst interrupt = require('./utils/interrupt');\nconst schema = require('./schema');\n\nconst horizon_server = require('@horizon/server');\nconst logger = horizon_server.logger;\n\nconst TIMEOUT_30_SECONDS = 30 * 1000;\n\nconst default_rdb_host = 'localhost';\nconst default_rdb_port = 28015;\nconst default_rdb_timeout = 20;\n\nconst parseArguments = (args) => {\n  const parser = new argparse.ArgumentParser({ prog: 'hz serve' });\n\n  parser.addArgument([ 'project_path' ],\n    { type: 'string', nargs: '?',\n      help: 'Change to this directory before serving' });\n\n  parser.addArgument([ '--project-name', '-n' ],\n    { type: 'string', action: 'store', metavar: 'NAME',\n      help: 'Name of the Horizon project. Determines the name of ' +\n            'the RethinkDB database that stores the project data.' });\n\n  parser.addArgument([ '--bind', '-b' ],\n    { type: 'string', action: 'append', metavar: 'HOST',\n      help: 'Local hostname to serve horizon on (repeatable).' });\n\n  parser.addArgument([ '--port', '-p' ],\n    { type: 'int', metavar: 'PORT',\n      help: 'Local port to serve horizon on.' });\n\n  parser.addArgument([ '--connect', '-c' ],\n    { type: 'string', metavar: 'HOST:PORT',\n      help: 'Host and port of the RethinkDB server to connect to.' });\n\n  parser.addArgument([ '--rdb-timeout' ],\n    { type: 'int', metavar: 'TIMEOUT',\n      help: 'Timeout period in seconds for the RethinkDB connection to be opened' });\n\n  parser.addArgument([ '--rdb-user' ],\n    { type: 'string', metavar: 'USER',\n      help: 'RethinkDB User' });\n\n  parser.addArgument([ '--rdb-password' ],\n    { type: 'string', metavar: 'PASSWORD',\n      help: 'RethinkDB Password' });\n\n  parser.addArgument([ '--key-file' ],\n    { type: 'string', metavar: 'PATH',\n      help: 'Path to the key file to use, defaults to \"./horizon-key.pem\".' });\n\n  parser.addArgument([ '--cert-file' ],\n    { type: 'string', metavar: 'PATH',\n      help: 'Path to the cert file to use, defaults to \"./horizon-cert.pem\".' });\n\n  parser.addArgument([ '--token-secret' ],\n    { type: 'string', metavar: 'SECRET',\n      help: 'Key for signing jwts' });\n\n  parser.addArgument([ '--allow-unauthenticated' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Whether to allow unauthenticated Horizon connections.' });\n\n  parser.addArgument([ '--allow-anonymous' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Whether to allow anonymous Horizon connections.' });\n\n  parser.addArgument([ '--max-connections' ],\n    { type: 'int', metavar: 'MAX_CONNECTIONS',\n      help: 'Maximum number of simultaneous connections server will accept.' });\n\n  parser.addArgument([ '--debug' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Enable debug logging.' });\n\n  parser.addArgument([ '--secure' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Serve secure websockets, requires --key-file and ' +\n      '--cert-file if true, on by default.' });\n\n  parser.addArgument([ '--start-rethinkdb' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Start up a RethinkDB server in the current directory' });\n\n  parser.addArgument([ '--auto-create-collection' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Create collections used by requests if they do not exist.' });\n\n  parser.addArgument([ '--auto-create-index' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Create indexes used by requests if they do not exist.' });\n\n  parser.addArgument([ '--permissions' ],\n    { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',\n      help: 'Enables or disables checking permissions on requests.' });\n\n  parser.addArgument([ '--serve-static' ],\n    { type: 'string', metavar: 'PATH', nargs: '?', constant: './dist',\n      help: 'Serve static files from a directory, defaults to \"./dist\".' });\n\n  parser.addArgument([ '--dev' ],\n    { action: 'storeTrue',\n      help: 'Runs the server in development mode, this sets ' +\n      '--secure=no, ' +\n      '--permissions=no, ' +\n      '--auto-create-collection=yes, ' +\n      '--auto-create-index=yes, ' +\n      '--start-rethinkdb=yes, ' +\n      '--allow-unauthenticated=yes, ' +\n      '--allow-anonymous=yes ' +\n      'and --serve-static=./dist.' });\n\n  parser.addArgument([ '--schema-file' ],\n    { type: 'string', metavar: 'SCHEMA_FILE_PATH',\n      help: 'Path to the schema file to use, ' +\n      'will attempt to apply schema before starting Horizon server\".' });\n\n  parser.addArgument([ '--auth' ],\n    { type: 'string', action: 'append', metavar: 'PROVIDER,ID,SECRET', defaultValue: [ ],\n      help: 'Auth provider and options comma-separated, e.g. \"facebook,<id>,<secret>\".' });\n\n  parser.addArgument([ '--auth-redirect' ],\n    { type: 'string', metavar: 'URL',\n      help: 'The URL to redirect to upon completed authentication, defaults to \"/\".' });\n\n  parser.addArgument([ '--access-control-allow-origin' ],\n    { type: 'string', metavar: 'URL',\n      help: 'The URL of the host that can access auth settings, defaults to \"\".' });\n\n  parser.addArgument([ '--open' ],\n    { action: 'storeTrue',\n      help: 'Open index.html in the static files folder once Horizon is ready to' +\n      ' receive connections' });\n\n  return parser.parseArgs(args);\n};\n\n// Simple file server. 404s if file not found, 500 if file error,\n// otherwise serve it with a mime-type suggested by its file extension.\nconst serve_file = (filePath, res) => {\n  fs.access(filePath, fs.R_OK | fs.F_OK, (exists) => {\n    if (exists) {\n      res.writeHead(404, { 'Content-Type': 'text/plain' });\n      res.end(`File \"${filePath}\" not found\\n`);\n    } else {\n      fs.lstat(filePath, (err, stats) => {\n        if (err) {\n          res.writeHead(500, { 'Content-Type': 'text/plain' });\n          res.end(`${err}\\n`);\n        } else if (stats.isFile()) {\n          fs.readFile(filePath, 'binary', (err2, file) => {\n            if (err2) {\n              res.writeHead(500, { 'Content-Type': 'text/plain' });\n              res.end(`${err2}\\n`);\n            } else {\n              const type = get_type(path.extname(filePath)) || false;\n              if (type) {\n                res.writeHead(200, { 'Content-Type': type });\n              } else {\n                res.writeHead(200);\n              }\n              res.end(file, 'binary');\n            }\n          });\n        } else if (stats.isDirectory()) {\n          serve_file(path.join(filePath, 'index.html'), res);\n        }\n      });\n    }\n  });\n};\n\nconst file_server = (distDir) => (req, res) => {\n  const reqPath = url.parse(req.url).pathname;\n  // Serve client files directly\n  if (reqPath === '/' || reqPath === '') {\n    serve_file(path.join(distDir, 'index.html'), res);\n  } else if (!reqPath.match(/\\/horizon\\/.*$/)) {\n    // All other static files come from the dist directory\n    serve_file(path.join(distDir, reqPath), res);\n  }\n  // Fall through otherwise. Should be handled by horizon server\n};\n\nconst initialize_servers = (ctor, opts) => {\n  const servers = [ ];\n  let numReady = 0;\n  return new Promise((resolve, reject) => {\n    opts.bind.forEach((host) => {\n      const srv = ctor().listen(opts.port, host);\n      servers.push(srv);\n      if (opts.serve_static) {\n        if (opts.serve_static === 'dist') {\n          // do nothing, this is the default\n        } else if (opts.project_path !== '.') {\n          const pth = path.join(opts.project_path, opts.serve_static);\n          console.info(`Static files being served from ${pth}`);\n        } else {\n          console.info(`Static files being served from ${opts.serve_static}`);\n        }\n        srv.on('request', file_server(opts.serve_static));\n      } else {\n        srv.on('request', (req, res) => {\n          res.writeHead(404);\n          res.end('404 Not Found');\n        });\n      }\n      srv.on('listening', () => {\n        const protocol = opts.secure ? 'https' : 'http';\n        console.info(`App available at ${protocol}://${srv.address().address}:` +\n                    `${srv.address().port}`);\n        if (++numReady === servers.length) {\n          resolve(servers);\n        }\n      });\n      srv.on('error', (err) => {\n        reject(new Error(`HTTP${opts.secure ? 'S' : ''} server: ${err}`));\n      });\n    });\n  });\n};\n\nconst create_insecure_servers = (opts) => {\n  if (!opts._dev_flag_used) {\n    console.error(chalk.red.bold('WARNING: Serving app insecurely.'));\n  }\n  return initialize_servers(() => new http.Server(), opts);\n};\n\nconst read_cert_file = (file, type) => {\n  try {\n    return fs.readFileSync(path.resolve(file));\n  } catch (err) {\n    const wasDefault = file.endsWith(`horizon-${type}.pem`);\n    let description;\n    const suggestions = [\n      `If you're running horizon for the first time, we recommend \\\nrunning horizon like ${chalk.white('hz serve --dev')} to get started without \\\nhaving to configure certificates.`,\n    ];\n    if (wasDefault) {\n      suggestions.push(\n        `If you have a ${type} file you'd like to use but they aren't in the \\\ndefault location, pass them in with the \\\n${chalk.white(`hz serve --${type}-file`)} option.`,\n        `You can explicitly disable security by passing \\\n${chalk.white('--secure=no')} to ${chalk.white('hz serve')}.`,\n        `You can generate a cert and key file for development by using the \\\n${chalk.white('hz create-cert')} command. Note that these certs won't be \\\nsigned by a certificate authority, so you will need to explicitly authorize \\\nthem in your browser.`\n      );\n      description = `In order to run the server in secure mode (the default), \\\nHorizon needs both a certificate file and a key file to encrypt websockets. \\\nBy default, it looks for horizon-key.pem and horizon-cert.pem \\\nfiles in the current directory.`;\n    } else {\n      // They supplied a cert or key file, so don't give the long\n      // explanation and irrelevant suggestions.\n      suggestions.unshift(`See if the ${type} filename was misspelled.`);\n      description = null;\n    }\n    throw new NiceError(\n      `Could not access the ${type} file ${file}`, {\n        description,\n        suggestions,\n      });\n  }\n};\n\nconst create_secure_servers = (opts) => {\n  const cert = read_cert_file(opts.cert_file, 'cert');\n  const key = read_cert_file(opts.key_file, 'key');\n  return initialize_servers(() => new https.Server({ key, cert }), opts);\n};\n\n// Command-line flags have the highest precedence, followed by environment variables,\n// then the config file, and finally the default values.\nconst processConfig = (parsed) => {\n  let options;\n\n  options = config.default_options();\n\n  options = config.merge_options(options,\n    config.read_from_config_file(parsed.project_path));\n\n  options = config.merge_options(options,\n    config.read_from_secrets_file(parsed.project_path));\n\n  options = config.merge_options(options, config.read_from_env());\n\n  options = config.merge_options(options, config.read_from_flags(parsed));\n\n  if (options.project_name === null) {\n    options.project_name = path.basename(path.resolve(options.project_path));\n  }\n\n  if (options.bind.indexOf('all') !== -1) {\n    options.bind = [ '0.0.0.0' ];\n  }\n\n  if (!options.rdb_host) {\n    options.rdb_host = default_rdb_host;\n  }\n\n  if (!options.rdb_port) {\n    options.rdb_port = default_rdb_port;\n  }\n\n  if (!options.rdb_timeout) {\n    options.rdb_timeout = default_rdb_timeout;\n  }\n\n  return options;\n};\n\nconst start_horizon_server = (http_servers, opts) =>\n  new horizon_server.Server(http_servers, {\n    auto_create_collection: opts.auto_create_collection,\n    auto_create_index: opts.auto_create_index,\n    permissions: opts.permissions,\n    project_name: opts.project_name,\n    access_control_allow_origin: opts.access_control_allow_origin,\n    auth: {\n      token_secret: opts.token_secret,\n      allow_unauthenticated: opts.allow_unauthenticated,\n      allow_anonymous: opts.allow_anonymous,\n      success_redirect: opts.auth_redirect,\n      failure_redirect: opts.auth_redirect,\n    },\n    rdb_host: opts.rdb_host,\n    rdb_port: opts.rdb_port,\n    rdb_user: opts.rdb_user || null,\n    rdb_password: opts.rdb_password || null,\n    rdb_timeout: opts.rdb_timeout || null,\n    max_connections: opts.max_connections || null,\n  });\n\n// `interruptor` is meant for use by tests to stop the server without relying on SIGINT\nconst run = (args, interruptor) => {\n  let opts, http_servers, hz_server, rdb_server;\n  const old_log_level = logger.level;\n\n  const cleanup = () => {\n    logger.level = old_log_level;\n\n    return Promise.all([\n      hz_server ? hz_server.close() : Promise.resolve(),\n      rdb_server ? rdb_server.close() : Promise.resolve(),\n      http_servers ? Promise.all(http_servers.map((s) =>\n        new Promise((resolve) => s.close(resolve)))) : Promise.resolve(),\n    ]);\n  };\n\n  interrupt.on_interrupt(() => cleanup());\n\n  return Promise.resolve().then(() => {\n    opts = processConfig(parseArguments(args));\n    logger.level = opts.debug ? 'debug' : 'warn';\n\n    if (!opts.secure && opts.auth && Array.from(Object.keys(opts.auth)).length > 0) {\n      logger.warn('Authentication requires that the server be accessible via HTTPS. ' +\n                  'Either specify \"secure=true\" or use a reverse proxy.');\n    }\n\n    change_to_project_dir(opts.project_path);\n\n    if (opts.secure) {\n      return create_secure_servers(opts);\n    } else {\n      return create_insecure_servers(opts);\n    }\n  }).then((servers) => {\n    http_servers = servers;\n\n    if (opts.start_rethinkdb) {\n      return start_rdb_server().then((server) => {\n        rdb_server = server;\n\n        // Don't need to check for host, always localhost.\n        opts.rdb_host = 'localhost';\n        opts.rdb_port = server.driver_port;\n\n        console.log('RethinkDB');\n        console.log(`   ├── Admin interface: http://localhost:${server.http_port}`);\n        console.log(`   └── Drivers can connect to port ${server.driver_port}`);\n      });\n    }\n  }).then(() => {\n    // Ensure schema from schema.toml file is set\n    if (opts.schema_file) {\n      console.log(`Ensuring schema \"${opts.schema_file}\" is applied`);\n      try {\n        fs.accessAsync(opts.schema_file, fs.R_OK | fs.F_OK);\n      } catch (e) {\n        console.error(\n          chalk.yellow.bold('No .hz/schema.toml file found'));\n        return;\n      }\n      const schemaOptions = schema.processApplyConfig({\n        project_name: opts.project_name,\n        schema_file: opts.schema_file,\n        start_rethinkdb: false,\n        connect: `${opts.rdb_host}:${opts.rdb_port}`,\n        update: true,\n        force: false,\n      });\n      return schema.runApplyCommand(schemaOptions);\n    }\n  }).then(() => {\n    console.log('Starting Horizon...');\n    hz_server = start_horizon_server(http_servers, opts);\n\n    return new Promise((resolve, reject) => {\n      const timeoutObject = setTimeout(() => {\n        reject(new Error('Horizon failed to start after 30 seconds.\\n' +\n                         'Try running hz serve again with the --debug flag'));\n      }, TIMEOUT_30_SECONDS);\n\n      hz_server.ready().then(() => {\n        clearTimeout(timeoutObject);\n        console.log(chalk.green.bold('🌄 Horizon ready for connections'));\n        resolve(hz_server);\n      }).catch(reject);\n    });\n  }).then(() => {\n    if (opts.auth) {\n      for (const name in opts.auth) {\n        const provider = horizon_server.auth[name];\n        if (!provider) {\n          throw new Error(`Unrecognized auth provider \"${name}\"`);\n        }\n        hz_server.add_auth_provider(provider,\n                                    Object.assign({}, { path: name }, opts.auth[name]));\n      }\n    }\n  }).then(() => {\n    // Automatically open up index.html in the `dist` directory only if\n    //  `--open` flag specified and an index.html exists in the directory.\n    if (opts.open && opts.serve_static) {\n      try {\n        // Check if index.html exists and readable in serve static_static directory\n        fs.accessSync(`${opts.serve_static}/index.html`, fs.R_OK | fs.F_OK);\n        // Determine scheme from options\n        const scheme = opts.secure ? 'https://' : 'http://';\n        // Open up index.html in default browser\n        console.log('Attempting open of index.html in default browser');\n        open(`${scheme}${opts.bind}:${opts.port}/index.html`);\n      } catch (open_err) {\n        console.log(chalk.red('Error occurred while trying to open ' +\n                              `${opts.serve_static}/index.html`));\n        console.log(open_err);\n      }\n    }\n\n    return Promise.race([\n      hz_server._interruptor.catch(() => { }),\n      interruptor ? interruptor.catch(() => { }) : new Promise(() => { }),\n    ]);\n  }).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));\n};\n\nmodule.exports = {\n  run,\n  description: 'Serve a Horizon app',\n  parseArguments,\n  processConfig,\n};\n"
  },
  {
    "path": "cli/src/utils/change_to_project_dir.js",
    "content": "'use strict';\n\nconst is_directory = require('./is_directory');\n\nmodule.exports = (project_path) => {\n  if (is_directory(project_path)) {\n    process.chdir(project_path);\n  } else {\n    throw new Error(`${project_path} is not a directory`);\n  }\n  if (!is_directory('.hz')) {\n    const nice_path = (project_path === '.' ? 'this directory' : project_path);\n    throw new Error(`${nice_path} doesn't contain an .hz directory`);\n  }\n};\n"
  },
  {
    "path": "cli/src/utils/check-project-name.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst basename = path.basename;\nconst join = path.join;\n\nconst fixableProjectName = /^[A-Za-z0-9_-]+$/;\nconst unfixableChars = /[^A-Za-z0-9_-]/g;\n\nconst dehyphenate = (name) => name.replace(/-/g, '_');\n\nconst shouldCreateDir = (prospectiveName, dirList) => {\n  if (prospectiveName === '.' ||\n      prospectiveName == null ||\n      !fixableProjectName.test(prospectiveName)) {\n    return false;\n  } else if (dirList.indexOf(prospectiveName) === -1) {\n    return true;\n  } else {\n    return false;\n  }\n};\n\nmodule.exports = (prospectiveName, cwd, dirList) => {\n  let chdirTo = prospectiveName != null ?\n        join(cwd, prospectiveName) : cwd;\n  const createDir = shouldCreateDir(prospectiveName, dirList);\n  if (prospectiveName === '.' || prospectiveName == null) {\n    // eslint-disable-next-line no-param-reassign\n    prospectiveName = basename(cwd);\n    chdirTo = false;\n  }\n  if (fixableProjectName.test(prospectiveName)) {\n    return {\n      dirName: prospectiveName,\n      projectName: dehyphenate(prospectiveName),\n      chdirTo,\n      createDir,\n    };\n  } else {\n    const invalids = prospectiveName.match(unfixableChars).join('');\n    throw new Error(`Invalid characters in project name: ${invalids}`);\n  }\n};\n"
  },
  {
    "path": "cli/src/utils/config.js",
    "content": "'use strict';\n\nconst parse_yes_no_option = require('./parse_yes_no_option');\nconst NiceError = require('./nice_error.js');\n\nconst fs = require('fs');\nconst url = require('url');\n\nconst toml = require('toml');\nconst chalk = require('chalk');\n\nconst default_config_file = '.hz/config.toml';\nconst default_secrets_file = '.hz/secrets.toml';\nconst default_rdb_port = 28015;\n\nconst make_default_options = () => ({\n  config: null,\n  debug: false,\n  // Default to current directory for path\n  project_path: '.',\n  // Default to current directory name for project name\n  project_name: null,\n\n  bind: [ 'localhost' ],\n  port: 8181,\n\n  start_rethinkdb: false,\n  serve_static: null,\n  open: false,\n\n  secure: true,\n  permissions: true,\n  key_file: './horizon-key.pem',\n  cert_file: './horizon-cert.pem',\n  schema_file: null,\n\n  auto_create_collection: false,\n  auto_create_index: false,\n\n  rdb_host: null,\n  rdb_port: null,\n  rdb_user: null,\n  rdb_password: null,\n  rdb_timeout: null,\n\n  token_secret: null,\n  allow_anonymous: false,\n  allow_unauthenticated: false,\n  auth_redirect: '/',\n  access_control_allow_origin: '',\n  max_connections: null,\n\n  auth: { },\n});\n\nconst default_options = make_default_options();\n\nconst yes_no_options = [ 'debug',\n                         'secure',\n                         'permissions',\n                         'start_rethinkdb',\n                         'auto_create_index',\n                         'auto_create_collection',\n                         'allow_unauthenticated',\n                         'allow_anonymous' ];\n\nconst parse_connect = (connect, config) => {\n  // support rethinkdb:// style connection uri strings\n  // expects rethinkdb://host:port` at a minimum but can optionally take a user:pass and db\n  // e.g. rethinkdb://user:pass@host:port/db\n  const rdb_uri = url.parse(connect);\n  if (rdb_uri.protocol === 'rethinkdb:') {\n    if (rdb_uri.hostname) {\n      config.rdb_host = rdb_uri.hostname;\n      config.rdb_port = rdb_uri.port || default_rdb_port;\n\n      // check for user/pass\n      if (rdb_uri.auth) {\n        const user_pass = rdb_uri.auth.split(':');\n        config.rdb_user = user_pass[0];\n        config.rdb_password = user_pass[1];\n      }\n\n      // set the project name based on the db\n      if (rdb_uri.path && rdb_uri.path.replace('/', '') !== '') {\n        config.project_name = rdb_uri.path.replace('/', '');\n      }\n    } else {\n      throw new Error(`Expected --connect rethinkdb://HOST, but found \"${connect}\".`);\n    }\n  } else {\n    // support legacy HOST:PORT connection strings\n    const host_port = connect.split(':');\n    if (host_port.length === 1) {\n      config.rdb_host = host_port[0];\n      config.rdb_port = default_rdb_port;\n    } else if (host_port.length === 2) {\n      config.rdb_host = host_port[0];\n      config.rdb_port = parseInt(host_port[1]);\n      if (isNaN(config.rdb_port) || config.rdb_port < 0 || config.rdb_port > 65535) {\n        throw new Error(`Invalid port: \"${host_port[1]}\".`);\n      }\n    } else {\n      throw new Error(`Expected --connect HOST:PORT, but found \"${connect}\".`);\n    }\n  }\n};\n\nconst read_from_config_file = (project_path) => {\n  const config = { auth: { } };\n\n  let fileData, configFilename, fileConfig;\n\n  if (project_path) {\n    configFilename = `${project_path}/${default_config_file}`;\n  } else {\n    configFilename = default_config_file;\n  }\n\n  try {\n    fileData = fs.readFileSync(configFilename);\n  } catch (err) {\n    return config;\n  }\n\n  try {\n    fileConfig = toml.parse(fileData);\n  } catch (e) {\n    if (e.name === 'SyntaxError') {\n      throw new NiceError(\n        `There was a syntax error when parsing ${configFilename}`, {\n          description: `Something was wrong with the format of \\\n${configFilename}, causing it not be a valid TOML file.`,\n          src: {\n            filename: configFilename,\n            contents: fileData,\n            line: e.line,\n            column: e.column,\n          },\n          suggestions: [\n            'Check that all strings values have quotes around them',\n            'Check that key/val pairs use equals and not colons',\n            'See https://github.com/toml-lang/toml#user-content-spec',\n          ],\n        });\n    } else {\n      throw e;\n    }\n  }\n  for (const field in fileConfig) {\n    if (field === 'connect') {\n      parse_connect(fileConfig.connect, config);\n    } else if (yes_no_options.indexOf(field) !== -1) {\n      config[field] = parse_yes_no_option(fileConfig[field], field);\n    } else if (default_options[field] !== undefined) {\n      config[field] = fileConfig[field];\n    } else {\n      throw new Error(`Unknown config parameter: \"${field}\".`);\n    }\n  }\n  return config;\n};\n\nconst read_from_secrets_file = (projectPath) => {\n  const config = { auth: { } };\n\n  let fileData, secretsFilename;\n\n  if (projectPath) {\n    secretsFilename = `${projectPath}/${default_secrets_file}`;\n  } else {\n    secretsFilename = default_secrets_file;\n  }\n\n  try {\n    fileData = fs.readFileSync(secretsFilename);\n  } catch (err) {\n    return config;\n  }\n\n  const fileConfig = toml.parse(fileData);\n  for (const field in fileConfig) {\n    if (field === 'connect') {\n      parse_connect(fileConfig.connect, config);\n    } else if (yes_no_options.indexOf(field) !== -1) {\n      config[field] = parse_yes_no_option(fileConfig[field], field);\n    } else if (default_options[field] !== undefined) {\n      config[field] = fileConfig[field];\n    } else {\n      throw new Error(`Unknown config parameter: \"${field}\".`);\n    }\n  }\n\n  return config;\n};\n\nconst env_regex = /^HZ_([A-Z]+([_]?[A-Z]+)*)$/;\nconst read_from_env = () => {\n  const config = { auth: { } };\n  for (const env_var in process.env) {\n    const matches = env_regex.exec(env_var);\n    if (matches && matches[1]) {\n      const destVarName = matches[1].toLowerCase();\n      const varPath = destVarName.split('_');\n      const value = process.env[env_var];\n\n      if (destVarName === 'connect') {\n        parse_connect(value, config);\n      } else if (destVarName === 'bind') {\n        config[destVarName] = value.split(',');\n      } else if (varPath[0] === 'auth') {\n        if (varPath.length !== 3) {\n          console.log(`Ignoring malformed Horizon environment variable: \"${env_var}\", ` +\n                      'should be HZ_AUTH_{PROVIDER}_{OPTION}.');\n        } else {\n          config.auth[varPath[1]] = config.auth[varPath[1]] || { };\n          config.auth[varPath[1]][varPath[2]] = value;\n        }\n      } else if (yes_no_options.indexOf(destVarName) !== -1) {\n        config[destVarName] = parse_yes_no_option(value, destVarName);\n      } else if (default_options[destVarName] !== undefined) {\n        config[destVarName] = value;\n      }\n    }\n  }\n\n  return config;\n};\n\n// Handles reading configuration from the parsed flags\nconst read_from_flags = (parsed) => {\n  const config = { auth: { } };\n\n  // Dev mode\n  if (parsed.dev) {\n    config.access_control_allow_origin = '*';\n    config.allow_unauthenticated = true;\n    config.allow_anonymous = true;\n    config.secure = false;\n    config.permissions = false;\n    config.start_rethinkdb = true;\n    config.auto_create_collection = true;\n    config.auto_create_index = true;\n    config.serve_static = 'dist';\n    config._dev_flag_used = true;\n\n    if (parsed.start_rethinkdb === null || parsed.start_rethinkdb === undefined) {\n      config._start_rethinkdb_implicit = true;\n    }\n  }\n\n  for (const key in parsed) {\n    if (key === 'auth' && parsed.auth != null) {\n      // Auth options\n      parsed.auth.forEach((auth_options) => {\n        const params = auth_options.split(',');\n        if (params.length !== 3) {\n          throw new Error(`Expected --auth PROVIDER,ID,SECRET, but found \"${auth_options}\"`);\n        }\n        config.auth[params[0]] = { id: params[1], secret: params[2] };\n      });\n    } else if (key === 'connect' && parsed.connect != null) {\n      // Normalize RethinkDB connection options\n      parse_connect(parsed.connect, config);\n    } else if (yes_no_options.indexOf(key) !== -1 && parsed[key] != null) {\n      // Simple 'yes' or 'no' (or 'true' or 'false') flags\n      config[key] = parse_yes_no_option(parsed[key], key);\n    } else if (parsed[key] != null) {\n      config[key] = parsed[key];\n    }\n  }\n\n  return config;\n};\n\nconst merge_options = (old_options, new_options) => {\n  // Disable start_rethinkdb if it was enabled by dev mode but we already have a host\n  if (new_options._start_rethinkdb_implicit) {\n    if (old_options.rdb_host) {\n      delete new_options.start_rethinkdb;\n    }\n  } else if (new_options.start_rethinkdb && new_options.rdb_host) {\n    throw new Error('Cannot provide both --start-rethinkdb and --connect.');\n  }\n\n  for (const key in new_options) {\n    if (key === 'rdb_host') {\n      old_options.start_rethinkdb = false;\n    }\n\n    if (key === 'auth') {\n      for (const provider in new_options.auth) {\n        old_options.auth[provider] = old_options.auth[provider] || { };\n        for (const field in new_options.auth[provider]) {\n          old_options.auth[provider][field] = new_options.auth[provider][field];\n        }\n      }\n    } else {\n      old_options[key] = new_options[key];\n    }\n  }\n\n  return old_options;\n};\n\nmodule.exports = {\n  default_config_file,\n  default_secrets_file,\n  default_options: make_default_options,\n  read_from_config_file,\n  read_from_secrets_file,\n  read_from_env,\n  read_from_flags,\n  merge_options,\n  parse_connect,\n};\n"
  },
  {
    "path": "cli/src/utils/each_line_in_pipe.js",
    "content": "'use strict';\n\nmodule.exports = (pipe, callback) => {\n  let buffer = '';\n  pipe.on('data', (data) => {\n    buffer += data.toString();\n\n    let endline_pos = buffer.indexOf('\\n');\n    while (endline_pos !== -1) {\n      const line = buffer.slice(0, endline_pos);\n      buffer = buffer.slice(endline_pos + 1);\n      callback(line);\n      endline_pos = buffer.indexOf('\\n');\n    }\n  });\n};\n"
  },
  {
    "path": "cli/src/utils/initialize_joi.js",
    "content": "'use strict';\n\n// Issues a dummy joi validation to force joi to initialize its scripts.\n// This is used because tests will mock the filesystem, and the lazy\n// `require`s done by joi will no longer work at that point.\nmodule.exports = (joi) =>\n  joi.validate('', joi.any().when('', { is: '', then: joi.any() }));\n"
  },
  {
    "path": "cli/src/utils/interrupt.js",
    "content": "'use strict';\n\nconst handlers = [ ];\n\nconst on_interrupt = (cb) => {\n  handlers.push(cb);\n};\n\nconst run_handlers = () => {\n  if (handlers.length > 0) {\n    return handlers.shift()().then(() => run_handlers);\n  }\n};\n\nconst interrupt = () => {\n  process.removeAllListeners('SIGTERM');\n  process.removeAllListeners('SIGINT');\n  process.on('SIGTERM', () => process.exit(1));\n  process.on('SIGINT', () => process.exit(1));\n\n  return run_handlers();\n};\n\nprocess.on('SIGTERM', interrupt);\nprocess.on('SIGINT', interrupt);\n\nmodule.exports = { on_interrupt, interrupt };\n"
  },
  {
    "path": "cli/src/utils/is_directory.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst fs = require('fs');\n\nmodule.exports = (dirname) => {\n  try {\n    return fs.statSync(path.resolve(dirname)).isDirectory();\n  } catch (e) {\n    return false;\n  }\n};\n"
  },
  {
    "path": "cli/src/utils/nice_error.js",
    "content": "'use strict';\n\n/*\nA nice error type that allows you to associate a longer description, a\nsource file and suggestions with it.\n*/\n\nconst chalk = require('chalk');\n\nclass NiceError extends Error {\n  constructor(message, options) {\n    super(message);\n    const opts = options || {};\n    this.description = opts.description || null;\n    this.suggestions = opts.suggestions || null;\n    // TODO: maybe allow multiple source locations and spans of text\n    // instead of a single column offset\n    this.src = (opts.src) ? Object.assign({}, opts.src) : null;\n  }\n  toString() {\n    return this.message;\n  }\n\n  niceString(options) {\n    const opts = options || {};\n    const cSize = opts.contextSize != null ? opts.contextSize : 2;\n    const results = [ this.message ];\n    if (this.description) {\n      results.push('', this.description);\n    }\n    if (this.src != null) {\n      const formattedSrc = NiceError._formatContext(\n        this.src.contents,\n        this.src.line,\n        this.src.column,\n        cSize\n      );\n      if (formattedSrc.length > 0) {\n        results.push(`\\nIn ${this.src.filename}, ` +\n                     `line ${this.src.line}, ` +\n                     `column ${this.src.column}:`);\n        results.push.apply(results, formattedSrc);\n      }\n    }\n    if (this.suggestions) {\n      results.push(\n        '', // extra newline before suggestions\n        chalk.red(\n          this.suggestions.length > 1 ? 'Suggestions:' : 'Suggestion:'));\n      results.push.apply(\n        results, this.suggestions.map((note) => `  ➤ ${note}`));\n    }\n    results.push(''); // push a final newline on\n    return results.join('\\n');\n  }\n\n  static _sourceLine(ln) {\n    return `${chalk.blue(`${ln.line}:`)} ${chalk.white(ln.src)}`;\n  }\n\n  static _extractContext(sourceContents, line, contextSize) {\n    const lines = sourceContents.toString().split('\\n');\n    const minLine = Math.max(line - contextSize - 1, 0);\n    const maxLine = Math.min(line + contextSize, lines.length);\n    if (line > lines.length) {\n      return [];\n    } else {\n      return lines.slice(minLine, maxLine).map((src, i) => ({\n        line: i + minLine + 1,\n        src,\n      }));\n    }\n  }\n\n  static _formatContext(sourceContents, line, col, contextSize) {\n    return this._extractContext(sourceContents, line, contextSize)\n      .map((srcLine) => {\n        let formatted = this._sourceLine(srcLine);\n        if (srcLine.line === line) {\n          const prefix = `${line}: `;\n          formatted +=\n            `\\n${' '.repeat(prefix.length + col - 1)}${chalk.green('^')}`;\n        }\n        return formatted;\n      });\n  }\n\n}\n\nmodule.exports = NiceError;\n"
  },
  {
    "path": "cli/src/utils/parse_yes_no_option.js",
    "content": "'use strict';\n\nmodule.exports = (value, option_name) => {\n  if (value !== undefined && value !== null) {\n    const lower = value.toLowerCase ? value.toLowerCase() : value;\n    if (lower === true || lower === 'true' || lower === 'yes') {\n      return true;\n    } else if (lower === false || lower === 'false' || lower === 'no') {\n      return false;\n    }\n    throw new Error(`Unexpected value \"${option_name}=${value}\", should be yes or no.`);\n  }\n};\n"
  },
  {
    "path": "cli/src/utils/proc-promise.js",
    "content": "'use strict';\nconst Promise = require('bluebird');\nconst childProcess = require('child_process');\n\nfunction procPromise() {\n  // Takes the same arguments as child_process.spawn\n  const args = Array.prototype.slice.call(arguments);\n  return new Promise((resolve, reject) => {\n    const proc = childProcess.spawn.apply(childProcess, args);\n    proc.stderr.setEncoding('utf8');\n    proc.stdout.setEncoding('utf8');\n    proc.on('exit', (code) => {\n      if (code === 0) {\n        resolve(proc);\n      } else {\n        const err = new Error(proc.stderr.read());\n        err.exitCode = code;\n        reject(err);\n      }\n    });\n  });\n}\n\nmodule.exports = procPromise;\n"
  },
  {
    "path": "cli/src/utils/rethrow.js",
    "content": "'use strict';\n\n// Returns a new Error with the given message. Combines the stack\n// traces with the old error, and removes itself from the stack trace.\nmodule.exports = (e, newMessage) => {\n  let e2;\n  if (typeof newMessage === 'string') {\n    e2 = new Error(newMessage);\n    e2.stack = e2.stack.split('\\n');\n    e2.stack.splice(1, 1); // Remove rethrow from stack trace\n  } else {\n    e2 = newMessage;\n  }\n  e2.stack += '\\n\\n  ==== Original stack trace ====\\n\\n';\n  e2.stack += e.stack;\n  return e2;\n};\n"
  },
  {
    "path": "cli/src/utils/rm_sync_recursive.js",
    "content": "'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst rmdirSyncRecursive = (dir) => {\n  try {\n    fs.readdirSync(dir).forEach((item) => {\n      const full_path = path.join(dir, item);\n      if (fs.statSync(full_path).isDirectory()) {\n        rmdirSyncRecursive(full_path);\n      } else {\n        fs.unlinkSync(full_path);\n      }\n    });\n    fs.rmdirSync(dir);\n  } catch (err) { /* Do nothing */ }\n};\n\nmodule.exports = rmdirSyncRecursive;\n"
  },
  {
    "path": "cli/src/utils/start_rdb_server.js",
    "content": "'use strict';\n\nconst each_line_in_pipe = require('./each_line_in_pipe');\nconst horizon_server = require('@horizon/server');\n\nconst execSync = require('child_process').execSync;\nconst spawn = require('child_process').spawn;\nconst hasbinSync = require('hasbin').sync;\n\nconst defaultDatadir = 'rethinkdb_data';\n\nconst infoLevelLog = (msg) => /^Running/.test(msg) || /^Listening/.test(msg);\n\nconst r = horizon_server.r;\nconst logger = horizon_server.logger;\nconst version_check = horizon_server.utils.rethinkdb_version_check;\n\nclass RethinkdbServer {\n  constructor(options) {\n    const quiet = Boolean(options.quiet);\n    const bind = options.bind || [ '127.0.0.1' ];\n    const dataDir = options.dataDir || defaultDatadir;\n    const driverPort = options.rdbPort;\n    const httpPort = options.rdbHttpPort;\n    const cacheSize = options.cacheSize || 200;\n\n    // Check if `rethinkdb` in PATH\n    if (!hasbinSync('rethinkdb')) {\n      throw new Error('`rethinkdb` not found in $PATH, please install RethinkDB.');\n    }\n\n    // Check if RethinkDB is sufficient version for Horizon\n    version_check(execSync('rethinkdb --version', { timeout: 5000 }).toString());\n\n    const args = [ '--http-port', String(httpPort || 0),\n                   '--cluster-port', '0',\n                   '--driver-port', String(driverPort || 0),\n                   '--cache-size', String(cacheSize),\n                   '--directory', dataDir,\n                   '--no-update-check' ];\n    bind.forEach((host) => args.push('--bind', host));\n\n    this.proc = spawn('rethinkdb', args);\n\n    this.ready_promise = new Promise((resolve, reject) => {\n      this.proc.once('error', reject);\n      this.proc.once('exit', (exit_code) => {\n        if (exit_code !== 0) {\n          reject(new Error(`RethinkDB process terminated with error code ${exit_code}.`));\n        }\n      });\n\n      process.on('exit', () => {\n        if (this.proc.exitCode === null) {\n          logger.error('Unclean shutdown - killing RethinkDB child process');\n          this.proc.kill('SIGKILL');\n        }\n      });\n\n      const maybe_resolve = () => {\n        // Once we have both ports determined, callback with all settings.\n        if (this.http_port !== undefined &&\n            this.driver_port !== undefined) {\n          resolve(this);\n        }\n      };\n\n      each_line_in_pipe(this.proc.stdout, (line) => {\n        if (!quiet) {\n          if (infoLevelLog(line)) {\n            logger.info('RethinkDB', line);\n          } else {\n            logger.debug('RethinkDB stdout:', line);\n          }\n        }\n        if (this.driver_port === undefined) {\n          const matches = line.match(\n              /^Listening for client driver connections on port (\\d+)$/);\n          if (matches !== null && matches.length === 2) {\n            this.driver_port = parseInt(matches[1]);\n            maybe_resolve();\n          }\n        }\n        if (this.http_port === undefined) {\n          const matches = line.match(\n              /^Listening for administrative HTTP connections on port (\\d+)$/);\n          if (matches !== null && matches.length === 2) {\n            this.http_port = parseInt(matches[1]);\n            maybe_resolve();\n          }\n        }\n      });\n\n      each_line_in_pipe(this.proc.stderr, (line) =>\n                        logger.error(`rethinkdb stderr: ${line}`));\n    });\n  }\n\n  ready() {\n    return this.ready_promise;\n  }\n\n  // This is only used by tests - cli commands use a more generic method as\n  // the database may be launched elsewhere.\n  connect() {\n    return r.connect({ host: 'localhost', port: this.driver_port });\n  }\n\n  close() {\n    return new Promise((resolve) => {\n      if (this.proc.exitCode !== null) {\n        resolve();\n      } else {\n        this.proc.kill('SIGTERM');\n\n        this.proc.once('exit', () => {\n          resolve();\n        });\n\n        setTimeout(() => {\n          this.proc.kill('SIGKILL');\n          resolve();\n        }, 20000).unref();\n      }\n    });\n  }\n}\n\n// start_rdb_server\n// Options:\n// quiet: boolean, suppresses rethinkdb log messages\n// bind: array of ip addresses to bind to, or 'all'\n// dataDir: name of rethinkdb data directory. Defaults to `rethinkdb_data`\n// driverPort: port number for rethinkdb driver connections. Auto-assigned by default.\n// httpPort: port number for webui. Auto-assigned by default.\n// cacheSize: cacheSize to give to rethinkdb in MB. Default 200.\nmodule.exports = (options) => new RethinkdbServer(options || { }).ready();\nmodule.exports.r = r;\n"
  },
  {
    "path": "cli/src/version.js",
    "content": "'use strict';\n\nconst package_json = require('../package.json');\n\nconst run = (args) =>\n  Promise.resolve().then(() => {\n    if (args && args.length) {\n      throw new Error('create-cert takes no arguments');\n    }\n    console.info(package_json.version);\n  });\n\nmodule.exports = {\n  run,\n  description: 'Print the version number of horizon',\n};\n"
  },
  {
    "path": "cli/test/config.js",
    "content": "'use strict';\n\nconst serve = require('../src/serve');\nconst processConfig = serve.processConfig;\n\nconst assert = require('assert');\n\nconst mockFs = require('mock-fs');\n\nconst make_flags = (flags) => Object.assign({}, serve.parseArguments([]), flags);\n\nconst write_config = (config) => {\n  let data = '';\n  const recursive_add = (obj, path) => {\n    const value_keys = [ ];\n    const object_keys = [ ];\n    for (const key in obj) {\n      const val = obj[key];\n      if (typeof val === 'object' && val !== null && !Array.isArray(val)) {\n        object_keys.push(key);\n      } else {\n        value_keys.push(key);\n      }\n    }\n\n    if (value_keys.length > 0) {\n      if (path) {\n        data += `[${path}]\\n`;\n      }\n      value_keys.forEach((key) => {\n        data += `${key} = ${JSON.stringify(obj[key])}\\n`;\n      });\n    }\n\n    object_keys.forEach((key) => {\n      recursive_add(obj[key], `${path}${path ? '.' : ''}${key}`);\n    });\n  };\n\n  recursive_add(config, '');\n  mockFs({ '.hz': { 'config.toml': data } });\n};\n\ndescribe('Config', () => {\n  let original_env;\n\n  before('Save env', () => {\n    original_env = Object.assign({}, process.env);\n  });\n\n  beforeEach('Create empty config file', () => {\n    write_config({ });\n  });\n\n  after('Restore fs', () => {\n    mockFs.restore();\n  });\n\n  afterEach('Restore env', () => {\n    process.env = Object.assign({}, original_env);\n  });\n\n  it('precedence', () => {\n    // Test a yes/no flag (in this case `secure`)\n    // Make a list of all possible states to test, each item contains\n    // the state of the config file, the env, and the flags\n    const states = [ ];\n    const values = [ 'yes', 'no', 'false', 'true', false, true, null ];\n\n    for (let i = 0; i < Math.pow(values.length, 3); ++i) {\n      states.push([ values[i % 3], values[Math.floor(i / 3) % 3], values[Math.floor(i / 9) % 3] ]);\n    }\n\n    states.forEach((state) => {\n      const parsed = { };\n      let expected = true; // default value\n\n      if (state[0] !== null) {\n        write_config({ secure: state[0] });\n        expected = state[0];\n      } else {\n        write_config({ });\n      }\n\n      if (state[1] !== null) {\n        process.env.HZ_INSECURE = `${state[1]}`;\n        expected = state[1];\n      } else {\n        delete process.env.HZ_INSECURE;\n      }\n\n      if (state[2] !== null) {\n        parsed.secure = state[2];\n        expected = state[2];\n      }\n\n      expected = (expected === 'yes' || expected === 'true') || expected;\n      expected = (expected === 'no' || expected === 'false') ? false : expected;\n\n      assert.strictEqual(processConfig(make_flags(parsed)).secure, expected);\n    });\n  });\n\n  // An unrecognized parameter in a config file should cause an error\n  it('unknown field in file', () => {\n    write_config({ fake_field: 'foo' });\n    assert.throws(() => processConfig(make_flags({ })),\n                  /Unknown config parameter: \"fake_field\"./);\n  });\n\n  // An unrecognized environment variable that matches the pattern should be ignored\n  it('unknown field in env', () => {\n    process.env.HZ_FAKE_FIELD = 'foo';\n    const config = processConfig(make_flags({ }));\n    assert.strictEqual(config.fake_field, undefined);\n  });\n\n  // The port parameter should always be stored as a number\n  describe('connect', () => {\n    it('valid in file', () => {\n      write_config({ connect: 'localhost:123' });\n      const config = processConfig(make_flags({ }));\n      assert.strictEqual(config.rdb_port, 123);\n    });\n\n    it('valid in env', () => {\n      process.env.HZ_CONNECT = 'localhost:456';\n      const config = processConfig(make_flags({ }));\n      assert.strictEqual(config.rdb_port, 456);\n    });\n\n    // Make sure an error is thrown if the format is wrong\n    it('invalid format in file', () => {\n      write_config({ connect: 'local:host:111' });\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Expected --connect HOST:PORT, but found \"local:host:111\"./);\n    });\n\n    it('invalid format in env', () => {\n      process.env.HZ_CONNECT = 'local:host:111';\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Expected --connect HOST:PORT, but found \"local:host:111\"./);\n    });\n\n    it('invalid format in flags', () => {\n      assert.throws(() => processConfig(make_flags({ connect: 'local:host:111' })),\n                    /Expected --connect HOST:PORT, but found \"local:host:111\"./);\n    });\n\n    // Make sure an error is thrown if the port cannot be parsed\n    it('invalid port in file', () => {\n      write_config({ connect: 'localhost:cat' });\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Invalid port: \"cat\"./);\n    });\n\n    it('invalid port in env', () => {\n      process.env.HZ_CONNECT = 'localhost:dog';\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Invalid port: \"dog\"./);\n    });\n\n    it('invalid port in flags', () => {\n      assert.throws(() => processConfig(make_flags({ connect: 'localhost:otter' })),\n                    /Invalid port: \"otter\"./);\n    });\n\n    it('with start_rethinkdb in file', () => {\n      write_config({ connect: 'localhost:123', start_rethinkdb: true });\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Cannot provide both --start-rethinkdb and --connect./);\n    });\n\n    it('with start_rethinkdb in env', () => {\n      process.env.HZ_CONNECT = 'localhost:123';\n      process.env.HZ_START_RETHINKDB = 'true';\n      assert.throws(() => processConfig(make_flags({ })),\n                    /Cannot provide both --start-rethinkdb and --connect./);\n    });\n\n    it('with start_rethinkdb in flags', () => {\n      assert.throws(() => processConfig(make_flags({ connect: 'localhost:123',\n                                                     start_rethinkdb: true })),\n                    /Cannot provide both --start-rethinkdb and --connect./);\n    });\n\n    it('with enabling and disabling start_rethinkdb', () => {\n      write_config({ start_rethinkdb: true });\n      process.env.HZ_CONNECT = 'example:123';\n      const config = processConfig(make_flags({ start_rethinkdb: false }));\n\n      assert.strictEqual(config.start_rethinkdb, false);\n      assert.strictEqual(config.rdb_host, 'example');\n      assert.strictEqual(config.rdb_port, 123);\n    });\n\n    it('with start_rethinkdb across configs', () => {\n      let config;\n\n      write_config({ connect: 'example:123' });\n      config = processConfig(\n        make_flags({ start_rethinkdb: true }));\n      assert.strictEqual(config.start_rethinkdb, true);\n\n      write_config({ start_rethinkdb: true });\n      config = processConfig(\n        make_flags({ connect: 'example:123' }));\n      assert.strictEqual(config.start_rethinkdb, false);\n      assert.strictEqual(config.rdb_host, 'example');\n      assert.strictEqual(config.rdb_port, 123);\n    });\n\n    it('with dev mode and start_rethinkdb across configs', () => {\n      let config;\n\n      write_config({ connect: 'example:123' });\n      config = processConfig(make_flags({ dev: true }));\n      assert.strictEqual(config.start_rethinkdb, false);\n      assert.strictEqual(config.rdb_host, 'example');\n      assert.strictEqual(config.rdb_port, 123);\n\n      write_config({ connect: 'example:123' });\n      config = processConfig(\n        make_flags({ start_rethinkdb: false, dev: true }));\n      assert.strictEqual(config.start_rethinkdb, false);\n      assert.strictEqual(config.rdb_host, 'example');\n      assert.strictEqual(config.rdb_port, 123);\n\n      write_config({ connect: 'example:123' });\n      config = processConfig(\n        make_flags({ start_rethinkdb: true, dev: true }));\n      assert.strictEqual(config.start_rethinkdb, true);\n\n      write_config({ start_rethinkdb: true });\n      config = processConfig(make_flags({ dev: true }));\n      assert.strictEqual(config.start_rethinkdb, true);\n\n      write_config({ start_rethinkdb: true });\n      config = processConfig(\n        make_flags({ connect: 'example:123', dev: true }));\n      assert.strictEqual(config.start_rethinkdb, false);\n      assert.strictEqual(config.rdb_host, 'example');\n      assert.strictEqual(config.rdb_port, 123);\n    });\n  });\n\n  // The bind parameter must be stored as an array of hostnames\n  describe('bind', () => {\n    it('in file', () => {\n      write_config({ bind: [ 'foo', 'bar' ] });\n      const config = processConfig(make_flags({ }));\n      assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);\n    });\n\n    it('in env', () => {\n      process.env.HZ_BIND = 'foo,bar';\n      const config = processConfig(make_flags({ }));\n      assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);\n    });\n\n    it('in flags', () => {\n      const config = processConfig(make_flags({ bind: [ 'foo', 'bar' ] }));\n      assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);\n    });\n  });\n\n  // Auth parameters are handled slightly differently to other parameters\n  // They add to an object, rather than having an explicit option name\n  // for each auth provider.\n  it('auth', () => {\n    // provider 'foo' and 'far' through config file\n    write_config({ auth: {\n      foo: { id: 'foo_id', secret: 'foo_secret' },\n      far: { id: 'far_id', secret: 'far_secret' },\n    } });\n\n    // provider 'bar' and 'baz' through env\n    process.env.HZ_AUTH_BAR_ID = 'bar_id';\n    process.env.HZ_AUTH_BAR_SECRET = 'bar_secret';\n    process.env.HZ_AUTH_BAZ_ID = 'baz_id';\n    process.env.HZ_AUTH_BAZ_SECRET = 'baz_secret';\n\n    // overwrite 'far' through env\n    process.env.HZ_AUTH_FAR_ID = 'far_id_new';\n    process.env.HZ_AUTH_FAR_SECRET = 'far_secret_new';\n\n    // provider 'bamf' through command-line\n    // overwrite 'baz' through command-line\n    const config = processConfig(make_flags({\n      auth: [ 'bamf,bamf_id,bamf_secret',\n              'baz,baz_id_new,baz_secret_new' ] }));\n\n    assert.deepStrictEqual(config.auth, {\n      foo: { id: 'foo_id', secret: 'foo_secret' },\n      far: { id: 'far_id_new', secret: 'far_secret_new' },\n      bar: { id: 'bar_id', secret: 'bar_secret' },\n      baz: { id: 'baz_id_new', secret: 'baz_secret_new' },\n      bamf: { id: 'bamf_id', secret: 'bamf_secret' },\n    });\n  });\n});\n"
  },
  {
    "path": "cli/test/init.spec.js",
    "content": "/* global beforeEach, describe, process, afterEach, it, require */\n\n'use strict';\n\nconst init = require('../src/init');\nconst assert = require('chai').assert;\nconst fs = require('fs');\nconst mockFs = require('mock-fs');\nconst path = require('path');\nconst sinon = require('sinon');\nconst toml = require('toml');\n\nconst original_dir = path.resolve(process.cwd());\nconst hz_dir = `${original_dir}/.hz`;\n\nconst assertNameExists = (baseDir, fileName) => {\n  const files = fs.readdirSync(baseDir);\n  assert.include(files, fileName);\n};\n\nconst assertNameDoesntExist = (baseDir, fileName) => {\n  const files = fs.readdirSync(baseDir);\n  assert.notInclude(files, fileName);\n};\n\nconst assertDirExists = (baseDir, dirName) => {\n  assertNameExists(baseDir, dirName);\n  assert.isTrue(fs.statSync(`${baseDir}/${dirName}`).isDirectory());\n};\n\nconst assertDirDoesntExist = (baseDir, dirName) => {\n  assertNameDoesntExist(baseDir, dirName);\n};\n\nconst assertFileExists = (baseDir, fileName) => {\n  assertNameExists(baseDir, fileName);\n  assert.isTrue(fs.statSync(`${baseDir}/${fileName}`).isFile());\n};\n\nconst assertFileDoesntExist = (baseDir, fileName) => {\n  assertNameDoesntExist(baseDir, fileName);\n};\n\nconst getFileString = (filepath) =>\n  fs.readFileSync(filepath, { encoding: 'utf8' });\n\nconst readToml = (filepath) => {\n  const tomlData = getFileString(filepath);\n  return toml.parse(tomlData);\n};\n\nconst assertValidConfig = (filepath) => {\n  const configObject = readToml(filepath);\n  // Need an uncommented project name\n  assert.property(configObject, 'project_name');\n};\n\nconst assertValidSecrets = (filepath) => {\n  const secretsObject = readToml(filepath);\n  // Need an uncommented token_secret\n  assert.property(secretsObject, 'token_secret');\n};\n\ndescribe('hz init', () => {\n  beforeEach('redirect console out', () => {\n    sinon.stub(console, 'error');\n    sinon.stub(console, 'info');\n  });\n\n  afterEach('restore console out', () => {\n    console.error.restore();\n    console.info.restore();\n  });\n\n  afterEach('restore cwd', () => process.chdir(original_dir));\n  afterEach('clear mockfs', () => mockFs.restore());\n\n  describe('when passed a project name', () => {\n    const testDirName = 'test-app';\n    const projectDir = `${original_dir}/${testDirName}`;\n    const args = [ testDirName ];\n\n    it(\"creates the project dir if it doesn't exist\", () => {\n      mockFs();\n      return init.run(args).then(() =>\n        assertDirExists(original_dir, testDirName)\n      );\n    });\n\n    it('moves into the project dir', () => {\n      mockFs({ [testDirName]: { } });\n      return init.run(args).then(() =>\n        assert.equal(process.cwd(), `${original_dir}/${testDirName}`)\n      );\n    });\n\n    describe('when the project dir is empty', () => {\n      beforeEach('initialize mockfs', () => {\n        mockFs({ [projectDir]: {} });\n      });\n\n      it('creates the src dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(projectDir, 'src')));\n\n      it('creates the dist dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(projectDir, 'dist')));\n\n      it('creates an example dist/index.html', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${projectDir}/dist`, 'index.html')));\n\n      it('creates the .hz dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(projectDir, '.hz')));\n\n      it('creates the .gitignore file', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${projectDir}`, '.gitignore')));\n\n      it('creates the .hz/config.toml file', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${projectDir}/.hz`, 'config.toml')));\n\n      it('creates the .hz/secrets.toml file', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${projectDir}/.hz`, 'secrets.toml')));\n\n      it('creates the .hz/schema.toml file', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${projectDir}/.hz`, 'schema.toml')));\n    });\n\n    describe('when the project dir is not empty', () => {\n      beforeEach('initialize mockfs', () => {\n        mockFs({\n          [projectDir]: {\n            lib: { 'someFile.html': '<blink>Something</blink>' },\n          },\n        });\n      });\n\n      it(\"doesn't create the src dir\", () =>\n        init.run(args).then(() =>\n          assertDirDoesntExist(projectDir, 'src')));\n\n      it(\"doesn't create the dist dir\", () =>\n        init.run(args).then(() =>\n          assertDirDoesntExist(projectDir, 'dist')));\n\n      it(\"doesn't create an example dist/index.html\", () => {\n        fs.mkdirSync(`${projectDir}/dist`);\n        return init.run(args).then(() =>\n          assertFileDoesntExist(`${projectDir}/dist`, 'index.html')\n        );\n      });\n\n      it(\"still creates the .hz dir if it doesn't exist\", () =>\n        init.run(args).then(() =>\n          assertDirExists(projectDir, '.hz')));\n\n      it(\"doesn't create the .hz dir if it already exists\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        const beforeMtime = fs.statSync(`${projectDir}/.hz`).birthtime.getTime();\n\n        return init.run(args).then(() => {\n          const afterMtime = fs.statSync(`${projectDir}/.hz`).birthtime.getTime();\n          assert.equal(beforeMtime, afterMtime, '.hz was modified');\n        });\n      });\n\n      it(\"creates a valid config.toml if it doesn't exist\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        return init.run(args).then(() => {\n          assertFileExists(`${projectDir}/.hz`, 'config.toml');\n          assertValidConfig(`${projectDir}/.hz/config.toml`);\n        });\n      });\n\n      it(\"creates a valid secrets.toml if it doesn't exist\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        return init.run(args).then(() => {\n          assertFileExists(`${projectDir}/.hz`, 'secrets.toml');\n          assertValidSecrets(`${projectDir}/.hz/secrets.toml`);\n        });\n      });\n\n      it(\"creates a valid schema.toml if it doesn't exist\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        return init.run(args).then(() => {\n          assertFileExists(`${projectDir}/.hz`, 'schema.toml');\n          // TODO: assertValidSchema(`${projectDir}/.hz/schema.toml`);\n        });\n      });\n\n      it(\"doesn't touch the config.toml if it already exists\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        const filename = `${projectDir}/.hz/config.toml`;\n        fs.appendFileSync(filename, '#Hoo\\n');\n        const beforeMtime = fs.statSync(filename).mtime.getTime();\n\n        return init.run(args).then(() => {\n          const afterMtime = fs.statSync(filename).mtime.getTime();\n          assert.equal(beforeMtime, afterMtime);\n          const afterContents = getFileString(filename);\n          assert.equal('#Hoo\\n', afterContents);\n        });\n      });\n\n      it(\"doesn't touch the secrets.toml if it already exists\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        const filename = `${projectDir}/.hz/secrets.toml`;\n        fs.appendFileSync(filename, '#Hoo\\n');\n        const beforeMtime = fs.statSync(filename).mtime.getTime();\n\n        return init.run(args).then(() => {\n          const afterMtime = fs.statSync(filename).mtime.getTime();\n          assert.equal(beforeMtime, afterMtime);\n          const afterContents = getFileString(filename);\n          assert.equal('#Hoo\\n', afterContents);\n        });\n      });\n\n      it(\"doesn't touch the schema.toml if it already exists\", () => {\n        fs.mkdirSync(`${projectDir}/.hz`);\n        const filename = `${projectDir}/.hz/schema.toml`;\n        fs.appendFileSync(filename, '#Hoo\\n');\n        const beforeMtime = fs.statSync(filename).mtime.getTime();\n\n        return init.run(args).then(() => {\n          const afterMtime = fs.statSync(filename).mtime.getTime();\n          assert.equal(beforeMtime, afterMtime);\n          const afterContents = getFileString(filename);\n          assert.equal('#Hoo\\n', afterContents);\n        });\n      });\n    });\n  });\n\n  describe('when not passed a project name', () => {\n    const args = [ ];\n\n    it('stays in the current directory', () => {\n      mockFs();\n      return init.run(args).then(() => {\n        const afterCwd = process.cwd();\n        assert.equal(original_dir, afterCwd, 'init changed directories');\n      });\n    });\n\n    describe('in an empty directory', () => {\n      beforeEach('initialize mockfs', () => mockFs({}));\n\n      it('creates the src dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(original_dir, 'src')));\n\n      it('creates the dist dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(original_dir, 'dist')));\n\n      it('creates an example dist/index.html', () =>\n        init.run(args).then(() =>\n          assertFileExists(`${original_dir}/dist`, 'index.html')));\n\n      it('creates the .hz dir', () =>\n        init.run(args).then(() =>\n          assertDirExists(original_dir, '.hz')));\n\n      it('creates the .hz/config.toml file', () =>\n        init.run(args).then(() => {\n          assertFileExists(hz_dir, 'config.toml');\n          assertValidConfig(`${hz_dir}/config.toml`);\n        }));\n    });\n\n    describe('in a directory with files in it', () => {\n      beforeEach('initialize mocks', () => {\n        mockFs({\n          lib: { 'some_file.txt': 'Some file content' },\n        });\n      });\n\n      it(\"doesn't create the src dir\", () =>\n        init.run(args).then(() =>\n          assertDirDoesntExist(original_dir, 'src')));\n\n      it(\"doesn't create the dist dir\", () =>\n        init.run(args).then(() =>\n          assertDirDoesntExist(original_dir, 'dist')));\n\n      it(\"doesn't create an example dist/index.html\", () => {\n        fs.mkdirSync(`${original_dir}/dist`);\n        return init.run(args).then(() =>\n          assertFileDoesntExist(`${original_dir}/dist`, 'index.html')\n        );\n      });\n\n      it(\"creates the .hz dir if it doesn't exist\", () =>\n        init.run(args).then(() =>\n          assertDirExists(original_dir, '.hz')));\n\n      it(\"doesn't create the .hz dir if it exists\", () => {\n        fs.mkdirSync(hz_dir);\n        const beforeTime = fs.statSync(hz_dir).birthtime.getTime();\n\n        return init.run(args).then(() => {\n          assertDirExists(original_dir, '.hz');\n          const afterTime = fs.statSync(hz_dir).birthtime.getTime();\n          assert.equal(beforeTime, afterTime, '.hz birthtime changed');\n        });\n      });\n\n      it(\"creates the config.toml if it doesn't exist\", () =>\n        init.run(args).then(() => {\n          assertFileExists(hz_dir, 'config.toml');\n          assertValidConfig(`${hz_dir}/config.toml`);\n        })\n      );\n\n      it(\"doesn't touch the config.toml if it already exists\", () => {\n        fs.mkdirSync(hz_dir);\n        const filename = `${hz_dir}/config.toml`;\n        fs.appendFileSync(filename, '#Hoo\\n');\n        const beforeMtime = fs.statSync(filename).mtime.getTime();\n\n        return init.run(args).then(() => {\n          const afterMtime = fs.statSync(filename).mtime.getTime();\n          assert.equal(beforeMtime, afterMtime);\n          const afterContents = getFileString(filename);\n          assert.equal('#Hoo\\n', afterContents);\n        });\n      });\n    });\n  });\n});\n\n"
  },
  {
    "path": "cli/test/schema.spec.js",
    "content": "'use strict';\n\nconst processApplyConfig = require('../src/schema').processApplyConfig;\nconst runApplyCommand = require('../src/schema').runApplyCommand;\nconst runSaveCommand = require('../src/schema').runSaveCommand;\nconst parse_schema = require('../src/schema').parse_schema;\nconst start_rdb_server = require('../src/utils/start_rdb_server');\nconst rm_sync_recursive = require('../src/utils/rm_sync_recursive');\n\nconst assert = require('assert');\nconst fs = require('fs');\n\nconst r = start_rdb_server.r;\nconst mockFs = require('mock-fs');\nconst tmpdir = require('os').tmpdir;\n\nconst project_name = 'schema_test';\n\nconst v1_schema = ` \n[collections.test_messages]\nindexes = [\"datetime\"]\n\n[groups.admin]\n[groups.admin.rules.carte_blanche]\ntemplate = \"any()\"\n`;\n\nconst v2_schema = `# This is a TOML document\n\n[collections.users]\n\n[collections.test_messages]\n[[collections.test_messages.indexes]]\nfields = [[\"a\",\"b\"],[\"c\"]]\n[[collections.test_messages.indexes]]\nfields = [[\"datetime\"]]\n\n[groups.admin]\n[groups.admin.rules.carte_blanche]\ntemplate = \"any()\"\n`;\n\nconst brokenTestSchema = `\n[collectiosfklajsfns.test_messages]\nindexes = [\"datetime\"]\n\n[groups.adminasklfjasf]\n[groups.admin.rules.carte_blanche]\ntemplate = \"any()a;lkdfjlakjf;ladkfjal;kfj\"\n`;\n\nconst fs_with_schema = (schema) => {\n  mockFs({ '.hz': { 'schema.toml': schema } });\n};\n\ndescribe('hz schema', () => {\n  const rdb_data_dir = `${tmpdir()}/horizon-test-${process.pid}`;\n  let rdb_conn, rdb_server;\n\n  before('start rethinkdb', () =>\n    start_rdb_server({\n      quiet: true,\n      dataDir: rdb_data_dir,\n    }).then((server) => {\n      rdb_server = server;\n    })\n  );\n\n  after('stop rethinkdb', () => rdb_server && rdb_server.close());\n  after('delete rethinkdb data directory', () => rm_sync_recursive(rdb_data_dir));\n\n  before('connect to rethinkdb', () =>\n    rdb_server.connect().then((conn) => {\n      rdb_conn = conn;\n    })\n  );\n\n  beforeEach('initialize mockfs', () => fs_with_schema(v2_schema));\n  afterEach('restore fs', () => mockFs.restore());\n\n  describe('save', () => {\n    before('initialize database', () => {\n      fs_with_schema(v2_schema);\n      return runApplyCommand(processApplyConfig({\n        start_rethinkdb: false,\n        schema_file: '.hz/schema.toml',\n        project_name,\n        connect: `localhost:${rdb_server.driver_port}`,\n      }));\n    });\n\n    after('clear database', () =>\n      r.branch(\n        r.dbList().contains(project_name),\n        r.dbDrop(project_name),\n        null\n      ).run(rdb_conn)\n    );\n\n    it('renames previous schema.toml if it already exists', () =>\n      runSaveCommand({\n        start_rethinkdb: false,\n        rdb_host: 'localhost',\n        rdb_port: rdb_server.driver_port,\n        out_file: '.hz/schema.toml',\n        project_name,\n      }).then(() =>\n        assert.equal(fs.readdirSync('.hz').length, 2, 'backup schema file not created')\n      )\n    );\n\n    it('saves schema to schema.toml from rdb', () =>\n      runSaveCommand({\n        start_rethinkdb: false,\n        rdb_host: 'localhost',\n        rdb_port: rdb_server.driver_port,\n        out_file: 'out.toml',\n        project_name,\n      }).then(() =>\n        assert.strictEqual(fs.readFileSync('out.toml', 'utf8'), v2_schema)\n      )\n    );\n  });\n\n  describe('apply', () => {\n    afterEach('clear database', () =>\n      r.branch(\n        r.dbList().contains(project_name),\n        r.dbDrop(project_name),\n        null\n      ).run(rdb_conn)\n    );\n\n    it('applies v1.x schema to rdb from schema.toml', () => {\n      fs_with_schema(v1_schema);\n      const config = processApplyConfig({\n        connect: `localhost:${rdb_server.driver_port}`,\n        schema_file: '.hz/schema.toml',\n        start_rethinkdb: false,\n        update: true,\n        force: true,\n        secure: false,\n        permissions: false,\n        project_name,\n      });\n\n      // Apply settings into RethinkDB\n      return runApplyCommand(config).then(() =>\n        // Check that the project database exists\n        r.dbList().contains(project_name).run(rdb_conn)\n      ).then((res) =>\n        assert(res, `${project_name} database is missing.`)\n      ).then(() =>\n        r.db(project_name).table('test_messages').indexList().run(rdb_conn)\n      ).then((indexes) => {\n        // Check that the expected indexes exist on the expected table\n        assert(indexes.indexOf('hz_[[\"datetime\"]]') !== -1, '\"datetime\" index is missing');\n      });\n    });\n\n    it('applies v2.x schema to rdb from schema.toml', () => {\n      const config = processApplyConfig({\n        connect: `localhost:${rdb_server.driver_port}`,\n        schema_file: '.hz/schema.toml',\n        start_rethinkdb: false,\n        update: true,\n        force: true,\n        secure: false,\n        permissions: false,\n        project_name,\n      });\n\n      // Apply settings into RethinkDB\n      return runApplyCommand(config).then(() =>\n        // Check that the project database exists\n        r.dbList().contains(project_name).run(rdb_conn)\n      ).then((res) =>\n        assert(res, `${project_name} database is missing.`)\n      ).then(() =>\n        r.db(project_name).table('test_messages').indexList().run(rdb_conn)\n      ).then((indexes) => {\n        // Check that the expected indexes exist on the expected table\n        assert(indexes.indexOf('hz_[[\"datetime\"]]') !== -1, '\"datetime\" index is missing');\n        assert(indexes.indexOf('hz_[[\"a\",\"b\"],[\"c\"]]') !== -1, '[[\"a\",\"b\"],[\"c\"]] index is missing');\n      });\n    });\n  });\n\n  describe('given a schema.toml file', () => {\n    it('can parse a valid v1.x schema.toml file', () => {\n      parse_schema(v1_schema);\n    });\n\n    it('can parse a valid v2.x schema.toml file', () => {\n      parse_schema(v2_schema);\n    });\n\n    it('fails parsing invalid schema.toml file', () => {\n      assert.throws(() => {\n        parse_schema(brokenTestSchema);\n      }, /\"collectiosfklajsfns\" is not allowed/);\n    });\n\n    it('can read a vaild schema.toml file', () =>\n      processApplyConfig({\n        start_rethinkdb: true,\n        schema_file: '.hz/schema.toml',\n        update: true,\n        force: true,\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "cli/test/serve.spec.js",
    "content": "'use strict';\n\nconst serve = require('../src/serve');\nconst schema = require('../src/schema');\nconst start_rdb_server = require('../src/utils/start_rdb_server');\nconst rm_sync_recursive = require('../src/utils/rm_sync_recursive');\n\nconst tmpdir = require('os').tmpdir;\n\nconst assert = require('chai').assert;\nconst mockFs = require('mock-fs');\n\nconst project_name = 'test_app';\nconst serve_args = [\n  '--secure=false',\n  '--port=0',\n  '--auto-create-collection',\n  `--project-name=${project_name}`,\n  `--token-secret=test-token`\n];\nconst make_args = (args) => serve_args.concat(args);\n\nconst valid_project_data = {\n  '.hz': {\n    'config.toml': 'project_name = \"projectName\"\\n',\n  },\n};\n\ndescribe('hz serve', () => {\n  const rdb_data_dir = `${tmpdir()}/horizon-test-${process.pid}`;\n  let rdb_server;\n\n  before('start rethinkdb', () =>\n    start_rdb_server({\n      quiet: true,\n      dataDir: rdb_data_dir,\n    }).then((server) => {\n      rdb_server = server;\n      serve_args.push(`--connect=localhost:${rdb_server.driver_port}`);\n    })\n  );\n\n  // Run schema apply with a blank schema\n  before('initialize rethinkdb', () => {\n    mockFs({ 'schema.toml': '' });\n    return schema.run([ 'apply', 'schema.toml',\n                        `--connect=localhost:${rdb_server.driver_port}`,\n                        `--project-name=${project_name}` ])\n      .then(() => mockFs.restore());\n  });\n\n  after('stop rethinkdb', () => rdb_server && rdb_server.close());\n  after('delete rethinkdb data directory', () => rm_sync_recursive(rdb_data_dir));\n\n  afterEach('restore mockfs', () => mockFs.restore());\n\n  describe('with a project path', () => {\n    beforeEach('initialize mockfs', () => mockFs({ [project_name]: valid_project_data }));\n\n    it('changes to the project directory', () => {\n      const before_dir = process.cwd();\n      return serve.run(make_args([ project_name ]), Promise.resolve()).then(() =>\n        assert.strictEqual(`${before_dir}/${project_name}`, process.cwd(),\n                           'directory should have changed')\n      );\n    });\n\n    it('fails if the .hz dir does not exist', () => {\n      mockFs({ [project_name]: {} });\n      return serve.run(make_args([ project_name ]), Promise.resolve()).then(() =>\n        assert(false, 'should have failed because the .hz directory is missing')\n      ).catch((err) =>\n        assert.throws(() => { throw err; }, /doesn't contain an .hz directory/)\n      );\n    });\n\n    it('continues if .hz dir does exist', () =>\n      serve.run(make_args([ project_name ]), Promise.resolve())\n    );\n  });\n\n  describe('without a project path', () => {\n    beforeEach('initialize mockfs', () => mockFs(valid_project_data));\n\n    it('does not change directories', () => {\n      const before_dir = process.cwd();\n      return serve.run(make_args([ '.' ]), Promise.resolve()).then(() =>\n        assert.strictEqual(before_dir, process.cwd(), 'directory should not have changed')\n      );\n    });\n\n    it('fails if the .hz dir does not exist', () => {\n      mockFs({ });\n      return serve.run(make_args([ '.' ]), Promise.resolve()).then(() =>\n        assert(false, 'should have failed because the .hz directory is missing')\n      ).catch((err) =>\n        assert.throws(() => { throw err; }, /doesn't contain an .hz directory/)\n      );\n    });\n\n    it('continues if .hz dir does exist', () =>\n      serve.run(make_args([ '.' ]), Promise.resolve())\n    );\n  });\n});\n"
  },
  {
    "path": "cli/test/unit/check-project-name.js",
    "content": "/* global describe, it */\n\n'use strict';\n\nconst checkProjectName = require('../../src/utils/check-project-name');\nconst assert = require('chai').assert;\n\ndescribe('checkProjectName', () => {\n  describe('when passed null for a directory', () => {\n    const prospectiveName = null;\n    const dirList = [];\n    it(\"doesn't change directory\", (done) => {\n      const goodCwd = '/foo/bar/Ba_z';\n      const res = checkProjectName(prospectiveName, goodCwd, dirList);\n      assert.propertyVal(res, 'chdirTo', false);\n      assert.propertyVal(res, 'dirName', 'Ba_z');\n      assert.propertyVal(res, 'projectName', 'Ba_z');\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n    it('throws if the cwd has invalid chars', (done) => {\n      const badCwd = '/foo/bar/b*a&z';\n      assert.throws(() => {\n        checkProjectName(prospectiveName, badCwd, dirList);\n      }, '*&');\n      done();\n    });\n    it('sets projectName to dehyphenated cwd if fixable', (done) => {\n      const fixableCwd = '/foo/bar/ba-z';\n      const res = checkProjectName(\n        prospectiveName, fixableCwd, dirList);\n      assert.propertyVal(res, 'projectName', 'ba_z');\n      assert.propertyVal(res, 'dirName', 'ba-z');\n      assert.propertyVal(res, 'chdirTo', false);\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n  });\n  describe('when passed \".\" as a directory', () => {\n    const prospectiveName = '.';\n    const dirList = [];\n    it(\"doesn't change directory\", (done) => {\n      const goodCwd = '/foo/bar/Ba_z';\n      const res = checkProjectName(prospectiveName, goodCwd, dirList);\n      assert.propertyVal(res, 'chdirTo', false);\n      assert.propertyVal(res, 'dirName', 'Ba_z');\n      assert.propertyVal(res, 'projectName', 'Ba_z');\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n    it('throws if the cwd has invalid chars', (done) => {\n      const badCwd = '/foo/bar/b*a&z';\n      assert.throws(() => {\n        checkProjectName(prospectiveName, badCwd, dirList);\n      }, '*&');\n      done();\n    });\n    it('sets projectName to dehyphenated cwd if fixable', (done) => {\n      const fixableCwd = '/foo/bar/ba-z';\n      const res = checkProjectName(prospectiveName, fixableCwd, dirList);\n      assert.propertyVal(res, 'projectName', 'ba_z');\n      assert.propertyVal(res, 'dirName', 'ba-z');\n      assert.propertyVal(res, 'chdirTo', false);\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n  });\n  describe('when passed a non-existing directory', () => {\n    const dirList = [ 'a', 'b', 'c' ];\n    const cwd = '/foo/bar';\n    it('creates the directory when name is valid', (done) => {\n      const results = checkProjectName('Ba_z9', cwd, dirList);\n      assert.propertyVal(results, 'projectName', 'Ba_z9');\n      assert.propertyVal(results, 'createDir', true);\n      assert.propertyVal(results, 'chdirTo', '/foo/bar/Ba_z9');\n      assert.propertyVal(results, 'dirName', 'Ba_z9');\n      done();\n    });\n    it('creates the directory when name is fixable', (done) => {\n      const results = checkProjectName('Ba-z9', cwd, dirList);\n      assert.propertyVal(results, 'projectName', 'Ba_z9');\n      assert.propertyVal(results, 'createDir', true);\n      assert.propertyVal(results, 'chdirTo', '/foo/bar/Ba-z9');\n      assert.propertyVal(results, 'dirName', 'Ba-z9');\n      done();\n    });\n    it('throws an error if the name is not fixable', (done) => {\n      assert.throws(() => {\n        checkProjectName('Some*Bad+Name', cwd, dirList);\n      }, '*+');\n      done();\n    });\n  });\n  describe('when passed an existing directory', () => {\n    const dirList = [ 'a', 'Ba-z', 'B^a%z', 'Ba_z9' ];\n    const cwd = '/foo/bar';\n    it('errors if given an invalid projectName', (done) => {\n      assert.throws(() => {\n        checkProjectName('B^%z', cwd, dirList);\n      }, '^%');\n      done();\n    });\n    it('changes directory  if the name is good', (done) => {\n      const res = checkProjectName('Ba_z9', cwd, dirList);\n      assert.propertyVal(res, 'dirName', 'Ba_z9');\n      assert.propertyVal(res, 'projectName', 'Ba_z9');\n      assert.propertyVal(res, 'chdirTo', '/foo/bar/Ba_z9');\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n    it('changes directory if the name is fixable', (done) => {\n      const res = checkProjectName('Ba-z', cwd, dirList);\n      assert.propertyVal(res, 'dirName', 'Ba-z');\n      assert.propertyVal(res, 'projectName', 'Ba_z');\n      assert.propertyVal(res, 'chdirTo', '/foo/bar/Ba-z');\n      assert.propertyVal(res, 'createDir', false);\n      done();\n    });\n  });\n});\n"
  },
  {
    "path": "cli/test/unit/nice_error.js",
    "content": "'use strict';\nconst stripAnsi = require('strip-ansi');\nconst assert = require('chai').assert;\nconst NiceError = require('../../src/utils/nice_error');\n\nconst fakeFile = `\\\nsome = fake, syntax\nnext := some(1, 2, 3)\ndef foo(bar) {\n  -- what language is this?\n}\n`;\n\ndescribe('NiceError', () => {\n  describe('._sourceLine', () => {\n    it('should have blue line number and white source text', (done) => {\n      const inputs = [\n        { src: 'foo bar', line: 2 },\n        { src: 'baz wux', line: 200 },\n        { src: ' a b c d e', line: 2000 },\n      ];\n      const expected = [\n        '\\u001b[34m2:\\u001b[39m' + ' ' + '\\u001b[37mfoo bar\\u001b[39m',\n        '\\u001b[34m200:\\u001b[39m' + ' ' + '\\u001b[37mbaz wux\\u001b[39m',\n        '\\u001b[34m2000:\\u001b[39m' + ' ' + '\\u001b[37m a b c d e\\u001b[39m',\n      ];\n      const results = inputs.map(NiceError._sourceLine.bind(NiceError));\n      assert.deepEqual(results, expected);\n      done();\n    });\n  });\n  describe('._extractContext', () => {\n    it('can get one line of context from the middle', (done) => {\n      const line = 3;\n      const contextSize = 1;\n      const expected = [\n        { line: 2, src: 'next := some(1, 2, 3)' },\n        { line: 3, src: 'def foo(bar) {' },\n        { line: 4, src: '  -- what language is this?' },\n      ];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n    it('can get a size 2 context', (done) => {\n      const line = 3;\n      const contextSize = 2;\n      const expected = [\n        { line: 1, src: 'some = fake, syntax' },\n        { line: 2, src: 'next := some(1, 2, 3)' },\n        { line: 3, src: 'def foo(bar) {' },\n        { line: 4, src: '  -- what language is this?' },\n        { line: 5, src: '}' },\n      ];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n    it('can gets a size 2 context with 1 line below it', (done) => {\n      const line = 2;\n      const contextSize = 2;\n      const expected = [\n        { line: 1, src: 'some = fake, syntax' },\n        { line: 2, src: 'next := some(1, 2, 3)' },\n        { line: 3, src: 'def foo(bar) {' },\n        { line: 4, src: '  -- what language is this?' },\n      ];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n    it('can gets a size 3 context with 0 lines below it', (done) => {\n      const line = 1;\n      const contextSize = 3;\n      const expected = [\n        { line: 1, src: 'some = fake, syntax' },\n        { line: 2, src: 'next := some(1, 2, 3)' },\n        { line: 3, src: 'def foo(bar) {' },\n        { line: 4, src: '  -- what language is this?' },\n      ];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n    it('can gets a size 3 context with 0 lines after it', (done) => {\n      const line = 6;\n      const contextSize = 3;\n      const expected = [\n        { line: 3, src: 'def foo(bar) {' },\n        { line: 4, src: '  -- what language is this?' },\n        { line: 5, src: '}' },\n        { line: 6, src: '' },\n      ];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n    it('returns an empty array if line out of bounds', (done) => {\n      const line = 7;\n      const contextSize = 3;\n      const expected = [];\n      const results = NiceError._extractContext(fakeFile, line, contextSize);\n      assert.deepEqual(results, expected);\n      done();\n    });\n  });\n  describe('.toString', () => {\n    const message = 'This is an error message';\n    it('is compatible with a basic Error', (done) => {\n      const error = new NiceError(message);\n      const result = error.toString();\n      assert.deepEqual(result, message);\n      done();\n    });\n    it('only displays the basic message', (done) => {\n      const error = new NiceError(message, {\n        description: 'Some long description',\n        suggestions: [\n          'Suggestion A',\n          'Suggestion B',\n        ],\n        src: {\n          filename: 'fakety.txt',\n          contents: 'File contents',\n          line: 0,\n          column: 0,\n        },\n      });\n      const result = error.toString();\n      assert.deepEqual(result, message);\n      done();\n    });\n  });\n  describe('.niceString', () => {\n    const message = 'Some kinda message';\n    const description = `A much longer description here that may span \\\nmany lines and be just really ridiculously long in order to completely \\\nexplain what's going on.`\n    const filename = './fake.dx';\n    const line = 2;\n    const column = 6;\n    const suggestions = [\n      'Always call your mother',\n      'Never lie to your mother about being robbed in Rio',\n    ];\n    let error;\n    beforeEach(() => {\n      error = new NiceError(message, {\n        description: description,\n        src: {\n          filename,\n          contents: fakeFile,\n          line,\n          column,\n        },\n        suggestions,\n      });\n    });\n    it('shows the description if present', (done) => {\n      error.src = null;\n      error.suggestions = null;\n      const expected = `\\\n${message}\n\n${description}\n`;\n      const result = stripAnsi(error.niceString());\n      assert.deepEqual(result, expected);\n      done();\n    });\n    it('returns a carrot in the right place with source', (done) => {\n      error.suggestions = null;\n      const expected = `\\\n${message}\n\n${description}\n\nIn ./fake.dx, line 2, column 6:\n1: some = fake, syntax\n2: next := some(1, 2, 3)\n        ^\n3: def foo(bar) {\n4:   -- what language is this?\n`;\n      const result = stripAnsi(error.niceString());\n      assert.deepEqual(result, expected);\n      done();\n    });\n\n    it('returns a list of suggestions if present', (done) => {\n      error.src = null;\n      error.description = null;\n      const expected = `\\\n${message}\n\nSuggestions:\n  ➤ ${suggestions[0]}\n  ➤ ${suggestions[1]}\n`;\n      const results = stripAnsi(error.niceString());\n      assert.deepEqual(results, expected);\n      done();\n    });\n\n    it('shows both suggestions and source if present', (done) => {\n      error.description = null;\n      error.suggestions.shift();\n      error.src.line = 5;\n      error.src.column = 1;\n      const expected = `\\\n${message}\n\nIn ./fake.dx, line 5, column 1:\n2: next := some(1, 2, 3)\n3: def foo(bar) {\n4:   -- what language is this?\n5: }\n   ^\n6: \\\n\n\nSuggestion:\n  ➤ ${suggestions[0]}\n`;\n      /* Note: there's an extra space in the string literal above on\n       * the line starting with \"6:\". This could be removed, but a lot\n       * of text editors are set to remove trailing spaces on save, so\n       * a backslash and an extra newline are a workaround to avoid\n       * the editor mucking with it. */\n      const results = stripAnsi(error.niceString({ contextSize: 3 }));\n      assert.deepEqual(results, expected);\n      done();\n    });\n  });\n});\n"
  },
  {
    "path": "cli/test/version.spec.js",
    "content": "'use strict';\n\nconst versionCommand = require('../src/version');\nconst assert = require('assert');\nconst sinon = require('sinon');\nconst pkg = require('../package.json');\n\ndescribe('hz version', () => {\n  beforeEach(() => sinon.stub(console, 'info'));\n  afterEach(() => console.info.restore());\n\n  it('prints the version and exits', () =>\n    versionCommand.run().then(() =>\n      assert.equal(console.info.args[0][0], pkg.version)));\n});\n"
  },
  {
    "path": "client/.eslintrc.js",
    "content": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n  extends: \"../.eslintrc.js\",\n  rules: {\n    \"arrow-parens\": [ ERROR, \"as-needed\" ],\n    \"no-confusing-arrow\": [ OFF ],\n    \"no-use-before-define\": [ OFF ],\n    \"semi\": [ ERROR, \"never\" ],\n    \"max-len\": [ ERROR, 80, 2 ],\n  },\n  env: {\n    \"browser\": true,\n    \"commonjs\": true,\n    \"es6\": true,\n    \"mocha\": true,\n  },\n  parser: \"babel-eslint\",\n};\n"
  },
  {
    "path": "client/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\nbuild/\n\n# Dependency directory\n# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git\nnode_modules\n\n# database stuff\nrethinkdb_data/\nrethinkdb_data_test/\n"
  },
  {
    "path": "client/README.md",
    "content": "# Horizon Client Library\n\nThe Horizon client library. Built to interact with the [Horizon Server](/server) API. Provides all the tooling to build a fully-functional and reactive front-end web application.\n\n## Building\n\nRunning `npm install` for the first time will build the browser bundle and lib files.\n\n1. `npm install`\n2. `npm run dev` (or `npm run build` or `npm run compile`, see below)\n\n### Build Options\n\nCommand             | Description\n--------------------|----------------------------\nnpm run build       | Build dist/horizon.js minified production browser bundle\nnpm run builddebug  | Build with webpack and output debug logging\nnpm run compile     | Compile src to lib for CommonJS module loaders (such as webpack, browserify)\nnpm run coverage    | Run code coverage tool - `istanbul`\nnpm run dev         | Watch directory for changes, build dist/horizon.js unminified browser bundle\nnpm run devtest     | Serve `dist` directory to build app and continuously run tests\nnpm test            | Run tests in node\nnpm run lint -s     | Lint src\nnpm run test        | Run tests\n\n## Running tests\n\n* `npm test` or open `dist/test.html` in your browser after getting setup and while you also have Horizon server with the `--dev` flag running on `localhost`.\n* You can spin up a dev server by cloning the horizon repo and running `node serve.js` in `test` directory in repo root. Then tests can be accessed from <http://localhost:8181/test.html>. Source maps work properly when served via http, not from file system. You can test the production version via `NODE_ENV=production node serve.js`. You may want to use `test/setupDev.sh` to set the needed local npm links for development.\n\n## Docs\n\n\n### Getting Started\n\n[samuelhughes.com/rethinkdb/horizon-docs/docs/getting-started.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/getting-started.html)\n\n### APIs\n\n* Horizon API - [samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html](http://samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html)\n* Collection API - [samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html](http://samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html)\n\n## Users and Groups\n\n[samuelhughes.com/rethinkdb/horizon-docs/docs/users.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/users.html)\n\n## Setting Permissions\n\n[samuelhughes.com/rethinkdb/horizon-docs/docs/permissions.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/permissions.html)\n\n### Clearing tokens\n\nSometimes you may wish to delete all authentication tokens from localStorage. You can do that with:\n\n``` js\n// Note the 'H'\nHorizon.clearAuthTokens()\n```\n"
  },
  {
    "path": "client/index.d.ts",
    "content": "\nimport { Observable } from 'rxjs';\n\ndeclare namespace hz {\n\n    interface Feed {\n        watch (options?: { rawChanges: boolean }): Observable<any>;\n        fetch (): Observable<any>;\n    }\n\n    type Bound = 'open' | 'closed';\n    type Direction = 'ascending' | 'descending';\n    type Primitive = boolean | number | string | Date;\n    type IdValue = Primitive | Primitive[] | { id: Primitive };\n    type WriteOp = Object | Object[];\n\n    interface TermBase extends Feed {\n        find (value: IdValue): TermBase;\n        findAll (...values: IdValue[]): TermBase;\n\n        order (fields: string[], direction?: Direction): TermBase;\n        limit (size: Number): TermBase;\n        above (spec: any, bound?: Bound): TermBase;\n        below (spec: any, bound?: Bound): TermBase;\n    }\n\n    interface Collection extends TermBase {\n        store (docs: WriteOp): Observable<any>;\n        upsert (docs: WriteOp): Observable<any>;\n        insert (docs: WriteOp): Observable<any>;\n        replace (docs: WriteOp): Observable<any>;\n        update (docs: WriteOp): Observable<any>;\n\n        remove (docs: IdValue): Observable<any>;\n        removeAll (docs: IdValue[]): Observable<any>;\n    }\n\n    interface User extends Feed {}\n\n    interface HorizonInstance {\n        (name: string): Collection;\n\n        currentUser (): User;\n\n        hasAuthToken (): boolean;\n        authEndpoint (name: string): Observable<string>;\n\n        aggregate (aggs: any): TermBase;\n        model (fn: Function): TermBase;\n\n        disconnect (): void;\n        connect (): void;\n\n        status (): Observable<any>;\n        onReady (): Observable<any>;\n        onDisconnected (): Observable<any>;\n        onSocketError (): Observable<any>;\n    }\n\n    interface HorizonOptions {\n        host?: string;\n        path?: string;\n        secure?: boolean;\n\n        authType?: string;\n        lazyWrites?: boolean;\n        keepalive?: number;\n\n        WebSocketCtor?: any;\n    }\n\n    interface Horizon {\n        (options: HorizonOptions): HorizonInstance;\n\n        clearAuthTokens (): void;\n    }\n}\n\nexport type HorizonOptions = hz.HorizonOptions;\nexport type HorizonInstance = hz.HorizonInstance;\nexport type TermBase = hz.TermBase;\nexport type Collection = hz.Collection;\nexport type User = hz.User;\n\ndeclare var Horizon: hz.Horizon;\nexport default Horizon;\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"@horizon/client\",\n  \"version\": \"2.0.0\",\n  \"description\": \"RethinkDB Horizon is an open-source developer platform for building realtime, scalable web apps.\",\n  \"scripts\": {\n    \"coverage\": \"cross-env NODE_ENV=test nyc mocha test/test.js\",\n    \"dev\": \"webpack --watch --progress --colors\",\n    \"devtest\": \"nodemon --watch dist --exec 'npm test -- --reporter dot && npm run lint -s'\",\n    \"builddebug\": \"webpack --progress --colors --display-modules --display-reasons\",\n    \"build\": \"cross-env NODE_ENV=production webpack --progress --colors\",\n    \"compile\": \"node ./scripts/compile.js\",\n    \"lint\": \"eslint src\",\n    \"test\": \"mocha dist/test.js --inline-diffs --timeout 10000\",\n    \"prepublish\": \"npm run compile && npm run build\"\n  },\n  \"dependencies\": {\n    \"babel-runtime\": \"^6.6.1\",\n    \"core-js\": \"^2.1.0\",\n    \"deep-equal\": \"^1.0.1\",\n    \"es6-promise\": \"^3.2.1\",\n    \"is-plain-object\": \"^2.0.1\",\n    \"rxjs\": \"5.0.0-beta.11\",\n    \"snake-case\": \"^2.1.0\",\n    \"ws\": \"^1.1.0\"\n  },\n  \"engines\": {\n    \"node\": \">=4.0.0\"\n  },\n  \"devDependencies\": {\n    \"babel-cli\": \"^6.11.4\",\n    \"babel-core\": \"^6.10.4\",\n    \"babel-eslint\": \"^6.0.0-beta\",\n    \"babel-loader\": \"^6.2.4\",\n    \"babel-plugin-istanbul\": \"^1.0.3\",\n    \"babel-plugin-transform-runtime\": \"^6.6.0\",\n    \"babel-preset-es2015\": \"^6.6.0\",\n    \"babel-preset-es2015-loose\": \"^7.0.0\",\n    \"babel-register\": \"^6.9.0\",\n    \"chai\": \"^3.5.0\",\n    \"copy-webpack-plugin\": \"^3.0.1\",\n    \"cross-env\": \"^2.0.0\",\n    \"eslint\": \"^7.3.1\",\n    \"exports-loader\": \"^0.6.3\",\n    \"imports-loader\": \"^0.6.5\",\n    \"istanbul\": \"^0.4.2\",\n    \"lodash.clonedeep\": \"^4.4.1\",\n    \"lodash.sortby\": \"^4.6.1\",\n    \"mocha\": \"^2.5.3\",\n    \"nodemon\": \"^1.9.1\",\n    \"nyc\": \"^7.0.0\",\n    \"shelljs\": \"^0.7.0\",\n    \"source-map-support\": \"^0.4.0\",\n    \"webpack\": \"^1.12.14\"\n  },\n  \"main\": \"lib/index.js\",\n  \"jsmain:next\": \"src/index.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/rethinkdb/horizon.git\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/rethinkdb/horizon/issues\"\n  },\n  \"homepage\": \"https://github.com/rethinkdb/horizon\",\n  \"files\": [\n    \"dist/horizon.js\",\n    \"dist/horizon.js.map\",\n    \"dist/horizon-dev.js\",\n    \"dist/horizon-dev.js.map\",\n    \"dist/horizon-core.js\",\n    \"dist/horizon-core.js.map\",\n    \"dist/horizon-core-dev.js\",\n    \"dist/horizon-core-dev.js.map\",\n    \"lib/*\"\n  ],\n  \"babel\": {\n    \"presets\": [\n      \"es2015-loose\",\n      {\n        \"plugins\": [\n          [\n            \"transform-runtime\",\n            {\n              \"polyfill\": false\n            }\n          ]\n        ]\n      }\n    ],\n    \"env\": {\n      \"test\": {\n        \"plugins\": [\n          \"istanbul\"\n        ]\n      }\n    }\n  },\n  \"nyc\": {\n    \"all\": true,\n    \"statements\": 82.02,\n    \"branches\": 72.51,\n    \"functions\": 87.42,\n    \"lines\": 81.89,\n    \"cache\": true,\n    \"check-coverage\": true,\n    \"include\": [\n      \"src/**/*.js\"\n    ],\n    \"require\": [\n      \"babel-register\"\n    ],\n    \"reporter\": [\n      \"lcov\",\n      \"text-summary\",\n      \"html\"\n    ],\n    \"sourceMap\": false,\n    \"instrument\": false\n  }\n}\n"
  },
  {
    "path": "client/scripts/compile.js",
    "content": "require('shelljs/global')\n\n// remove existing lib files\nrm('-rf', 'lib/**/*')\n\n// compile with babel\nif (exec('babel src --out-dir lib --extends src/.babelrc --source-maps true').code !== 0) {\n  echo('error: babel couldn\\'t build source, if EACCESS error, check access rights')\n  exit(1)\n}\n"
  },
  {
    "path": "client/src/ast.js",
    "content": "import { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/observable/empty'\n\nimport 'rxjs/add/operator/publishReplay'\nimport 'rxjs/add/operator/scan'\nimport 'rxjs/add/operator/filter'\nimport 'rxjs/add/operator/map'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/operator/defaultIfEmpty'\nimport 'rxjs/add/operator/ignoreElements'\nimport 'rxjs/add/operator/merge'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/take'\n\nimport snakeCase from 'snake-case'\nimport deepEqual from 'deep-equal'\n\nimport checkArgs from './util/check-args'\nimport validIndexValue from './util/valid-index-value.js'\nimport { serialize } from './serialization.js'\n\nimport watchRewrites from './hacks/watch-rewrites'\n\n\n/**\n @this TermBase\n\n Validation check to throw an exception if a method is chained onto a\n query that already has it. It belongs to TermBase, but we don't want\n to pollute the objects with it (since it isn't useful to api users),\n so it's dynamically bound with .call inside methods that use it.\n*/\nfunction checkIfLegalToChain(key) {\n  if (this._legalMethods.indexOf(key) === -1) {\n    throw new Error(`${key} cannot be called on the current query`)\n  }\n  if (snakeCase(key) in this._query) {\n    throw new Error(`${key} has already been called on this query`)\n  }\n}\n\n// Abstract base class for terms\nexport class TermBase {\n  constructor(sendRequest, query, legalMethods) {\n    this._sendRequest = sendRequest\n    this._query = query\n    this._legalMethods = legalMethods\n  }\n\n  toString() {\n    let string = `Collection('${this._query.collection}')`\n    if (this._query.find) {\n      string += `.find(${JSON.stringify(this._query.find)})`\n    }\n    if (this._query.find_all) {\n      string += `.findAll(${JSON.stringify(this._query.find_all)})`\n    }\n    if (this._query.order) {\n      string += `.order(${JSON.stringify(this._query.order[0])}, ` +\n                       `${JSON.stringify(this._query.order[1])})`\n    }\n    if (this._query.above) {\n      string += `.above(${JSON.stringify(this.query.above[0])}, ` +\n                       `${JSON.stringify(this.query.above[1])})`\n    }\n    if (this._query.below) {\n      string += `.below(${JSON.stringify(this.query.below[0])}, ` +\n                       `${JSON.stringify(this.query.below[1])})`\n    }\n    if (this._query.limit) {\n      string += `.limit(${JSON.stringify(this._query.limit)})`\n    }\n    return string\n  }\n  // Returns a sequence of the result set. Every time it changes the\n  // updated sequence will be emitted. If raw change objects are\n  // needed, pass the option 'rawChanges: true'. An observable is\n  // returned which will lazily emit the query when it is subscribed\n  // to\n  watch({ rawChanges = false } = {}) {\n    const query = watchRewrites(this, this._query)\n    const raw = this._sendRequest('subscribe', query)\n    if (rawChanges) {\n      return raw\n    } else {\n      return makePresentable(raw, this._query)\n    }\n  }\n  // Grab a snapshot of the current query (non-changefeed). Emits an\n  // array with all results. An observable is returned which will\n  // lazily emit the query when subscribed to\n  fetch() {\n    const raw = this._sendRequest('query', this._query).map(val => {\n      delete val.$hz_v$\n      return val\n    })\n    if (this._query.find) {\n      return raw.defaultIfEmpty(null)\n    } else {\n      return raw.toArray()\n    }\n  }\n  findAll(...fieldValues) {\n    checkIfLegalToChain.call(this, 'findAll')\n    checkArgs('findAll', arguments, { maxArgs: 100 })\n    return new FindAll(this._sendRequest, this._query, fieldValues)\n  }\n  find(idOrObject) {\n    checkIfLegalToChain.call(this, 'find')\n    checkArgs('find', arguments)\n    return new Find(this._sendRequest, this._query, idOrObject)\n  }\n  order(fields, direction = 'ascending') {\n    checkIfLegalToChain.call(this, 'order')\n    checkArgs('order', arguments, { minArgs: 1, maxArgs: 2 })\n    return new Order(this._sendRequest, this._query, fields, direction)\n  }\n  above(aboveSpec, bound = 'closed') {\n    checkIfLegalToChain.call(this, 'above')\n    checkArgs('above', arguments, { minArgs: 1, maxArgs: 2 })\n    return new Above(this._sendRequest, this._query, aboveSpec, bound)\n  }\n  below(belowSpec, bound = 'open') {\n    checkIfLegalToChain.call(this, 'below')\n    checkArgs('below', arguments, { minArgs: 1, maxArgs: 2 })\n    return new Below(this._sendRequest, this._query, belowSpec, bound)\n  }\n  limit(size) {\n    checkIfLegalToChain.call(this, 'limit')\n    checkArgs('limit', arguments)\n    return new Limit(this._sendRequest, this._query, size)\n  }\n}\n\n// Turn a raw observable of server responses into user-presentable events\n//\n// `observable` is the base observable with full responses coming from\n//              the HorizonSocket\n// `query` is the value of `options` in the request\nfunction makePresentable(observable, query) {\n  // Whether the entire data structure is in each change\n  const pointQuery = Boolean(query.find)\n\n  if (pointQuery) {\n    let hasEmitted = false\n    const seedVal = null\n    // Simplest case: just pass through new_val\n    return observable\n      .filter(change => !hasEmitted || change.type !== 'state')\n      .scan((previous, change) => {\n        hasEmitted = true\n        if (change.new_val != null) {\n          delete change.new_val.$hz_v$\n        }\n        if (change.old_val != null) {\n          delete change.old_val.$hz_v$\n        }\n        if (change.state === 'synced') {\n          return previous\n        } else {\n          return change.new_val\n        }\n      }, seedVal)\n  } else {\n    const seedVal = { emitted: false, val: [] }\n    return observable\n      .scan((state, change) => {\n        if (change.new_val != null) {\n          delete change.new_val.$hz_v$\n        }\n        if (change.old_val != null) {\n          delete change.old_val.$hz_v$\n        }\n        if (change.state === 'synced') {\n          state.emitted = true\n        }\n        state.val = applyChange(state.val.slice(), change)\n        return state\n      }, seedVal)\n      .filter(state => state.emitted)\n      .map(x => x.val)\n  }\n}\n\nexport function applyChange(arr, change) {\n  switch (change.type) {\n  case 'remove':\n  case 'uninitial': {\n    // Remove old values from the array\n    if (change.old_offset != null) {\n      arr.splice(change.old_offset, 1)\n    } else {\n      const index = arr.findIndex(x => deepEqual(x.id, change.old_val.id))\n      if (index === -1) {\n        // Programming error. This should not happen\n        throw new Error(\n          `change couldn't be applied: ${JSON.stringify(change)}`)\n      }\n      arr.splice(index, 1)\n    }\n    break\n  }\n  case 'add':\n  case 'initial': {\n    // Add new values to the array\n    if (change.new_offset != null) {\n      // If we have an offset, put it in the correct location\n      arr.splice(change.new_offset, 0, change.new_val)\n    } else {\n      // otherwise for unordered results, push it on the end\n      arr.push(change.new_val)\n    }\n    break\n  }\n  case 'change': {\n    // Modify in place if a change is happening\n    if (change.old_offset != null) {\n      // Remove the old document from the results\n      arr.splice(change.old_offset, 1)\n    }\n    if (change.new_offset != null) {\n      // Splice in the new val if we have an offset\n      arr.splice(change.new_offset, 0, change.new_val)\n    } else {\n      // If we don't have an offset, find the old val and\n      // replace it with the new val\n      const index = arr.findIndex(x => deepEqual(x.id, change.old_val.id))\n      if (index === -1) {\n        // indicates a programming bug. The server gives us the\n        // ordering, so if we don't find the id it means something is\n        // buggy.\n        throw new Error(\n          `change couldn't be applied: ${JSON.stringify(change)}`)\n      }\n      arr[index] = change.new_val\n    }\n    break\n  }\n  case 'state': {\n    // This gets hit if we have not emitted yet, and should\n    // result in an empty array being output.\n    break\n  }\n  default:\n    throw new Error(\n      `unrecognized 'type' field from server ${JSON.stringify(change)}`)\n  }\n  return arr\n}\n\n/** @this Collection\n Implements writeOps for the Collection class\n*/\nfunction writeOp(name, args, documents) {\n  checkArgs(name, args)\n  let isBatch = true\n  let wrappedDocs = documents\n  if (!Array.isArray(documents)) {\n    // Wrap in an array if we need to\n    wrappedDocs = [ documents ]\n    isBatch = false\n  } else if (documents.length === 0) {\n    // Don't bother sending no-ops to the server\n    return Observable.empty()\n  }\n  const options = Object.assign(\n    {}, this._query, { data: serialize(wrappedDocs) })\n  let observable = this._sendRequest(name, options)\n  if (isBatch) {\n    // If this is a batch writeOp, each document may succeed or fail\n    // individually.\n    observable = observable.map(\n      resp => resp.error ? new Error(resp.error) : resp)\n  } else {\n    // If this is a single writeOp, the entire operation should fail\n    // if any fails.\n    const _prevOb = observable\n    observable = Observable.create(subscriber => {\n      _prevOb.subscribe({\n        next(resp) {\n          if (resp.error) {\n            // TODO: handle error ids when we get them\n            subscriber.error(new Error(resp.error))\n          } else {\n            subscriber.next(resp)\n          }\n        },\n        error(err) { subscriber.error(err) },\n        complete() { subscriber.complete() },\n      })\n    })\n  }\n  if (!this._lazyWrites) {\n    // Need to buffer response since this becomes a hot observable and\n    // when we subscribe matters\n    observable = observable.publishReplay().refCount()\n    observable.subscribe()\n  }\n  return observable\n}\n\nexport class Collection extends TermBase {\n  constructor(sendRequest, collectionName, lazyWrites) {\n    const query = { collection: collectionName }\n    const legalMethods = [\n      'find', 'findAll', 'order', 'above', 'below', 'limit' ]\n    super(sendRequest, query, legalMethods)\n    this._lazyWrites = lazyWrites\n  }\n  store(documents) {\n    return writeOp.call(this, 'store', arguments, documents)\n  }\n  upsert(documents) {\n    return writeOp.call(this, 'upsert', arguments, documents)\n  }\n  insert(documents) {\n    return writeOp.call(this, 'insert', arguments, documents)\n  }\n  replace(documents) {\n    return writeOp.call(this, 'replace', arguments, documents)\n  }\n  update(documents) {\n    return writeOp.call(this, 'update', arguments, documents)\n  }\n  remove(documentOrId) {\n    const wrapped = validIndexValue(documentOrId) ?\n          { id: documentOrId } : documentOrId\n    return writeOp.call(this, 'remove', arguments, wrapped)\n  }\n  removeAll(documentsOrIds) {\n    if (!Array.isArray(documentsOrIds)) {\n      throw new Error('removeAll takes an array as an argument')\n    }\n    const wrapped = documentsOrIds.map(item => {\n      if (validIndexValue(item)) {\n        return { id: item }\n      } else {\n        return item\n      }\n    })\n    return writeOp.call(this, 'removeAll', arguments, wrapped)\n  }\n}\n\nexport class Find extends TermBase {\n  constructor(sendRequest, previousQuery, idOrObject) {\n    const findObject = validIndexValue(idOrObject) ?\n          { id: idOrObject } : idOrObject\n    const query = Object.assign({}, previousQuery, { find: findObject })\n    super(sendRequest, query, [])\n  }\n}\n\nexport class FindAll extends TermBase {\n  constructor(sendRequest, previousQuery, fieldValues) {\n    const wrappedFields = fieldValues\n          .map(item => validIndexValue(item) ? { id: item } : item)\n    const options = { find_all: wrappedFields }\n    const findAllQuery = Object.assign({}, previousQuery, options)\n    let legalMethods\n    if (wrappedFields.length === 1) {\n      legalMethods = [ 'order', 'above', 'below', 'limit' ]\n    } else {\n      // The vararg version of findAll cannot have anything chained to it\n      legalMethods = []\n    }\n    super(sendRequest, findAllQuery, legalMethods)\n  }\n}\n\nexport class Above extends TermBase {\n  constructor(sendRequest, previousQuery, aboveSpec, bound) {\n    const option = { above: [ aboveSpec, bound ] }\n    const query = Object.assign({}, previousQuery, option)\n    const legalMethods = [ 'findAll', 'order', 'below', 'limit' ]\n    super(sendRequest, query, legalMethods)\n  }\n}\n\nexport class Below extends TermBase {\n  constructor(sendRequest, previousQuery, belowSpec, bound) {\n    const options = { below: [ belowSpec, bound ] }\n    const query = Object.assign({}, previousQuery, options)\n    const legalMethods = [ 'findAll', 'order', 'above', 'limit' ]\n    super(sendRequest, query, legalMethods)\n  }\n}\n\nexport class Order extends TermBase {\n  constructor(sendRequest, previousQuery, fields, direction) {\n    const wrappedFields = Array.isArray(fields) ? fields : [ fields ]\n    const options = { order: [ wrappedFields, direction ] }\n    const query = Object.assign({}, previousQuery, options)\n    const legalMethods = [ 'findAll', 'above', 'below', 'limit' ]\n    super(sendRequest, query, legalMethods)\n  }\n}\n\nexport class Limit extends TermBase {\n  constructor(sendRequest, previousQuery, size) {\n    const query = Object.assign({}, previousQuery, { limit: size })\n    // Nothing is legal to chain after .limit\n    super(sendRequest, query, [])\n  }\n}\n\n\nexport class UserDataTerm {\n  constructor(hz, handshake, socket) {\n    this._hz = hz\n    this._before = socket.ignoreElements().merge(handshake)\n  }\n\n  _query(userId) {\n    return this._hz('users').find(userId)\n  }\n\n  fetch() {\n    return this._before.mergeMap(handshake => {\n        if (handshake.id == null) {\n          throw new Error('Unauthenticated users have no user document')\n        } else {\n          return this._query(handshake.id).fetch()\n        }\n      }).take(1) // necessary so that we complete, since _before is\n                 // infinite\n  }\n\n  watch(...args) {\n    return this._before.mergeMap(handshake => {\n      if (handshake.id === null) {\n        throw new Error('Unauthenticated users have no user document')\n      } else {\n        return this._query(handshake.id).watch(...args)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "client/src/auth.js",
    "content": "import queryParse from './util/query-parse'\nimport { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/map'\nimport 'rxjs/add/observable/dom/ajax'\n\nconst HORIZON_JWT = 'horizon-jwt'\n\n/** @this Horizon **/\nexport function authEndpoint(name) {\n  const endpointForName = methods => {\n    if (methods.hasOwnProperty(name)) {\n      return this._root + methods[name]\n    } else {\n      throw new Error(`Unconfigured auth type: ${name}`)\n    }\n  }\n  if (!this._authMethods) {\n    return Observable.ajax(`${this._horizonPath}/auth_methods`)\n      .map(ajax => ajax.response)\n      .do(authMethods => {\n        this._authMethods = authMethods\n      }).map(endpointForName)\n  } else {\n    return Observable.of(this._authMethods).map(endpointForName)\n  }\n}\n\n// Simple shim to make a Map look like local/session storage\nexport class FakeStorage {\n  constructor() { this._storage = new Map() }\n  setItem(a, b) { return this._storage.set(a, b) }\n  getItem(a) { return this._storage.get(a) }\n  removeItem(a) { return this._storage.delete(a) }\n}\n\nfunction getStorage(storeLocally = true) {\n  let storage\n  try {\n    if (!storeLocally ||\n        typeof window !== 'object' ||\n        window.localStorage === undefined) {\n      storage = new FakeStorage()\n    } else {\n      // Mobile safari in private browsing has a localStorage, but it\n      // has a size limit of 0\n      window.localStorage.setItem('$$fake', 1)\n      window.localStorage.removeItem('$$fake')\n      storage = window.localStorage\n    }\n  } catch (error) {\n    if (window.sessionStorage === undefined) {\n      storage = new FakeStorage()\n    } else {\n      storage = window.sessionStorage\n    }\n  }\n  return storage\n}\n\nexport class TokenStorage {\n  constructor({ authType = 'token',\n                storage = getStorage(authType.storeLocally),\n                path = 'horizon' } = {}) {\n    this._storage = storage\n    this._path = path\n    if (typeof authType === 'string') {\n      this._authType = authType\n    } else {\n      this._authType = 'token'\n      this.set(authType.token)\n    }\n  }\n\n  _getHash() {\n    const val = this._storage.getItem(HORIZON_JWT)\n    if (val == null) {\n      return {}\n    } else {\n      return JSON.parse(val)\n    }\n  }\n\n  _setHash(hash) {\n    this._storage.setItem(HORIZON_JWT, JSON.stringify(hash))\n  }\n\n  set(jwt) {\n    const current = this._getHash()\n    current[this._path] = jwt\n    this._setHash(current)\n  }\n\n  get() {\n    return this._getHash()[this._path]\n  }\n\n  remove() {\n    const current = this._getHash()\n    delete current[this._path]\n    this._setHash(current)\n  }\n\n  setAuthFromQueryParams() {\n    const parsed = typeof window !== 'undefined' &&\n      typeof window.location !== 'undefined' ?\n            queryParse(window.location.search) : {}\n\n    if (parsed.horizon_token != null) {\n      this.set(parsed.horizon_token)\n    }\n  }\n\n  // Handshake types are implemented here\n  handshake() {\n    // If we have a token, we should send it rather than requesting a\n    // new one\n    const token = this.get()\n    if (token != null) {\n      return { method: 'token', token }\n    } else if (this._authType === 'token') {\n      throw new Error(\n        'Attempting to authenticate with a token, but no token is present')\n    } else {\n      return { method: this._authType }\n    }\n  }\n\n  // Whether there is an auth token for the provided authType\n  hasAuthToken() {\n    const token = this.get()\n    if (!token) {\n      return false\n    }\n    try {\n      const meta = JSON.parse(atob(token.split('.')[1]))\n      const exp = meta.exp\n      const now = new Date().getTime() / 1000\n      return (now < exp)\n    } catch (e) {\n      return false\n    }\n  }\n}\n\nexport function clearAuthTokens() {\n  return getStorage().removeItem(HORIZON_JWT)\n}\n"
  },
  {
    "path": "client/src/hacks/watch-rewrites.js",
    "content": "/*\n Some common queries run on an entire collection or on a collection of\n indeterminate size. RethinkDB doesn't actually keep track of the\n ordering of these queries when sending changes. The initial changes\n will be ordered, but subsequent changes come in arbitrary order and\n don't respect the ordering of the query. So, for convenience, we add\n a very high limit so that the server will keep track of the order for\n us.\n\n Note: queries like collection.order(field).watch are not reasonable\n in production systems. You should add an explicit limit.\n*/\n\nexport default function watchRewrites(self, query) {\n  // The only query type at the moment that doesn't get these rewrites\n  // is find, since it returns a single document\n  if (query.find === undefined &&\n      query.order !== undefined &&\n      query.limit === undefined) {\n    const limit = self.constructor.IMPLICIT_LIMIT || 100000\n    // Need to copy the object, since it could be reused\n    return Object.assign({ limit }, query)\n  } else {\n    return query\n  }\n}\n"
  },
  {
    "path": "client/src/index-polyfill.js",
    "content": "// Ensures these features are present or polyfilled\n// See http://kangax.github.io/compat-table/es6/\nrequire('core-js/fn/array/from')\nrequire('core-js/fn/array/find-index')\nrequire('core-js/fn/array/keys')\nrequire('core-js/fn/object/assign')\n\n// Export rxjs globally and add all operators to Observable\nif (typeof window !== 'undefined') {\n  window.Rx = require('rxjs')\n} else if (typeof global !== 'undefined') {\n  global.Rx = require('rxjs')\n}\n\nmodule.exports = require('./index')\n"
  },
  {
    "path": "client/src/index.js",
    "content": "import 'rxjs/add/observable/of'\nimport 'rxjs/add/observable/from'\nimport 'rxjs/add/operator/catch'\nimport 'rxjs/add/operator/concatMap'\nimport 'rxjs/add/operator/map'\nimport 'rxjs/add/operator/filter'\n\nimport { Collection, UserDataTerm } from './ast'\nimport { HorizonSocket } from './socket'\nimport { authEndpoint, TokenStorage, clearAuthTokens } from './auth'\nimport { aggregate, model } from './model'\n\nconst defaultHost = typeof window !== 'undefined' && window.location &&\n        `${window.location.host}` || 'localhost:8181'\nconst defaultSecure = typeof window !== 'undefined' && window.location &&\n        window.location.protocol === 'https:' || false\n\nfunction Horizon({\n  host = defaultHost,\n  secure = defaultSecure,\n  path = 'horizon',\n  lazyWrites = false,\n  authType = 'unauthenticated',\n  keepalive = 60,\n  WebSocketCtor = WebSocket,\n} = {}) {\n  // If we're in a redirection from OAuth, store the auth token for\n  // this user in localStorage.\n\n  const tokenStorage = new TokenStorage({ authType, path })\n  tokenStorage.setAuthFromQueryParams()\n\n  const url = `ws${secure ? 's' : ''}:\\/\\/${host}\\/${path}`\n  const socket = new HorizonSocket({\n    url,\n    handshakeMaker: tokenStorage.handshake.bind(tokenStorage),\n    keepalive,\n    WebSocketCtor,\n  })\n\n  // Store whatever token we get back from the server when we get a\n  // handshake response\n  socket.handshake.subscribe({\n    next(handshake) {\n      if (authType !== 'unauthenticated') {\n        tokenStorage.set(handshake.token)\n      }\n    },\n    error(err) {\n      if (/JsonWebTokenError|TokenExpiredError/.test(err.message)) {\n        console.error('Horizon: clearing token storage since auth failed')\n        tokenStorage.remove()\n      }\n    },\n  })\n\n  // This is the object returned by the Horizon function. It's a\n  // function so we can construct a collection simply by calling it\n  // like horizon('my_collection')\n  function horizon(name) {\n    return new Collection(sendRequest, name, lazyWrites)\n  }\n\n  horizon.currentUser = () =>\n    new UserDataTerm(horizon, socket.handshake, socket)\n\n  horizon.disconnect = () => {\n    socket.complete()\n  }\n\n  // Dummy subscription to force it to connect to the\n  // server. Optionally provide an error handling function if the\n  // socket experiences an error.\n  // Note: Users of the Observable interface shouldn't need this\n  horizon.connect = (\n    onError = err => { console.error(`Received an error: ${err}`) }\n  ) => {\n    socket.subscribe(\n      () => {},\n      onError\n    )\n  }\n\n  // Either subscribe to status updates, or return an observable with\n  // the current status and all subsequent status changes.\n  horizon.status = subscribeOrObservable(socket.status)\n\n  // Convenience method for finding out when disconnected\n  horizon.onDisconnected = subscribeOrObservable(\n    socket.status.filter(x => x.type === 'disconnected'))\n\n  // Convenience method for finding out when ready\n  horizon.onReady = subscribeOrObservable(\n    socket.status.filter(x => x.type === 'ready'))\n\n  // Convenience method for finding out when an error occurs\n  horizon.onSocketError = subscribeOrObservable(\n    socket.status.filter(x => x.type === 'error'))\n\n  horizon.utensils = {\n    sendRequest,\n    tokenStorage,\n    handshake: socket.handshake,\n  }\n  Object.freeze(horizon.utensils)\n\n  horizon._authMethods = null\n  horizon._root = `http${(secure) ? 's' : ''}://${host}`\n  horizon._horizonPath = `${horizon._root}/${path}`\n  horizon.authEndpoint = authEndpoint\n  horizon.hasAuthToken = tokenStorage.hasAuthToken.bind(tokenStorage)\n  horizon.aggregate = aggregate\n  horizon.model = model\n\n  return horizon\n\n  // Sends a horizon protocol request to the server, and pulls the data\n  // portion of the response out.\n  function sendRequest(type, options) {\n    // Both remove and removeAll use the type 'remove' in the protocol\n    const normalizedType = type === 'removeAll' ? 'remove' : type\n    return socket\n      .hzRequest({ type: normalizedType, options }) // send the raw request\n      .takeWhile(resp => resp.state !== 'complete')\n  }\n}\n\nfunction subscribeOrObservable(observable) {\n  return (...args) => {\n    if (args.length > 0) {\n      return observable.subscribe(...args)\n    } else {\n      return observable\n    }\n  }\n}\n\nHorizon.Socket = HorizonSocket\nHorizon.clearAuthTokens = clearAuthTokens\n\nmodule.exports = Horizon\n"
  },
  {
    "path": "client/src/model.js",
    "content": "import { Observable } from 'rxjs/Observable'\n\nimport 'rxjs/add/observable/of'\nimport 'rxjs/add/observable/forkJoin'\nimport 'rxjs/add/observable/combineLatest'\nimport 'rxjs/add/operator/map'\n\n// Other imports\nimport isPlainObject from 'is-plain-object'\n\n// Unlike normal queries' .watch(), we don't support rawChanges: true\n// for aggregates\nfunction checkWatchArgs(args) {\n  if (args.length > 0) {\n    throw new Error(\".watch() on aggregates doesn't support arguments!\")\n  }\n}\n\nfunction isTerm(term) {\n  return typeof term.fetch === 'function' &&\n         typeof term.watch === 'function'\n}\n\nfunction isPromise(term) {\n  return typeof term.then === 'function'\n}\n\nfunction isObservable(term) {\n  return typeof term.subscribe === 'function' &&\n         typeof term.lift === 'function'\n}\n\n// Whether an object is primitive. We consider functions\n// non-primitives, lump Dates and ArrayBuffers into primitives.\nfunction isPrimitive(value) {\n  if (value === null) {\n    return true\n  }\n  if (value === undefined) {\n    return false\n  }\n  if (typeof value === 'function') {\n    return false\n  }\n  if ([ 'boolean', 'number', 'string' ].indexOf(typeof value) !== -1) {\n    return true\n  }\n  if (value instanceof Date || value instanceof ArrayBuffer) {\n    return true\n  }\n  return false\n}\n\n// Simple wrapper for primitives. Just emits the primitive\nclass PrimitiveTerm {\n  constructor(value) {\n    this._value = value\n  }\n\n  toString() {\n    return this._value.toString()\n  }\n\n  fetch() {\n    return Observable.of(this._value)\n  }\n\n  watch(...watchArgs) {\n    checkWatchArgs(watchArgs)\n    return Observable.of(this._value)\n  }\n}\n\n// Simple wrapper for observables to normalize the\n// interface. Everything in an aggregate tree should be one of these\n// term-likes\nclass ObservableTerm {\n  constructor(value) {\n    this._value = value\n  }\n\n  toString() {\n    return this._value.toString()\n  }\n\n  fetch() {\n    return Observable.from(this._value)\n  }\n\n  watch(...watchArgs) {\n    checkWatchArgs(watchArgs)\n    return Observable.from(this._value)\n  }\n}\n\n// Handles aggregate syntax like [ query1, query2 ]\nclass ArrayTerm {\n  constructor(value) {\n    // Ensure this._value is an array of Term\n    this._value = value.map(x => aggregate(x))\n  }\n\n  _reducer(...args) {\n    return args\n  }\n\n  _query(operation) {\n    return this._value.map(x => x[operation]())\n  }\n\n  toString() {\n    return `[ ${this._query('toString').join(', ')} ]`\n  }\n\n  fetch() {\n    if (this._value.length === 0) {\n      return Observable.empty()\n    }\n\n    const qs = this._query('fetch')\n    return Observable.forkJoin(...qs, this._reducer)\n  }\n\n  watch(...watchArgs) {\n    checkWatchArgs(watchArgs)\n\n    if (this._value.length === 0) {\n      return Observable.empty()\n    }\n\n    const qs = this._query('watch')\n    return Observable.combineLatest(...qs, this._reducer)\n  }\n}\n\nclass AggregateTerm {\n  constructor(value) {\n    // Ensure this._value is an array of [ key, Term ] pairs\n    this._value = Object.keys(value).map(k => [ k, aggregate(value[k]) ])\n  }\n\n  _reducer(...pairs) {\n    return pairs.reduce((prev, [ k, x ]) => {\n      prev[k] = x\n      return prev\n    }, {})\n  }\n\n  _query(operation) {\n    return this._value.map(\n      ([ k, term ]) => term[operation]().map(x => [ k, x ]))\n  }\n\n  toString() {\n    const s = this._value.map(([ k, term ]) => `'${k}': ${term}`)\n    return `{ ${s.join(', ')} }`\n  }\n\n  fetch() {\n    if (this._value.length === 0) {\n      return Observable.of({})\n    }\n\n    const qs = this._query('fetch')\n    return Observable.forkJoin(...qs, this._reducer)\n  }\n\n  watch(...watchArgs) {\n    checkWatchArgs(watchArgs)\n\n    if (this._value.length === 0) {\n      return Observable.of({})\n    }\n\n    const qs = this._query('watch')\n    return Observable.combineLatest(...qs, this._reducer)\n  }\n}\n\nexport function aggregate(spec) {\n  if (isTerm(spec)) {\n    return spec\n  }\n  if (isObservable(spec) || isPromise(spec)) {\n    return new ObservableTerm(spec)\n  }\n  if (isPrimitive(spec)) {\n    return new PrimitiveTerm(spec)\n  }\n  if (Array.isArray(spec)) {\n    return new ArrayTerm(spec)\n  }\n  if (isPlainObject(spec)) {\n    return new AggregateTerm(spec)\n  }\n\n  throw new Error(`Can't make an aggregate with ${spec} in it`)\n}\n\nexport function model(constructor) {\n  return (...args) => aggregate(constructor(...args))\n}\n"
  },
  {
    "path": "client/src/serialization.js",
    "content": "const PRIMITIVES = [\n  'string', 'number', 'boolean', 'function', 'symbol' ]\n\nfunction modifyObject(doc) {\n  Object.keys(doc).forEach(key => {\n    doc[key] = deserialize(doc[key])\n  })\n  return doc\n}\n\nexport function deserialize(value) {\n  if (value == null) {\n    return value\n  } else if (PRIMITIVES.indexOf(typeof value) !== -1) {\n    return value\n  } else if (Array.isArray(value)) {\n    return value.map(deserialize)\n  } else if (value.$reql_type$ === 'TIME') {\n    const date = new Date()\n    date.setTime(value.epoch_time * 1000)\n    return date\n  } else {\n    return modifyObject(value)\n  }\n}\n\nfunction jsonifyObject(doc) {\n  Object.keys(doc).forEach(key => {\n    doc[key] = serialize(doc[key])\n  })\n  return doc\n}\n\nexport function serialize(value) {\n  if (value == null) {\n    return value\n  } else if (PRIMITIVES.indexOf(typeof value) !== -1) {\n    return value\n  } else if (Array.isArray(value)) {\n    return value.map(serialize)\n  } else if (value instanceof Date) {\n    return {\n      $reql_type$: 'TIME',\n      epoch_time: value.getTime() / 1000,\n      // Rethink will serialize this as \"+00:00\", but accepts Z\n      timezone: 'Z',\n    }\n  } else {\n    return jsonifyObject(value)\n  }\n}\n"
  },
  {
    "path": "client/src/shim.js",
    "content": "/* global WebSocket */\n\n// Check for websocket\nif (typeof WebSocket !== 'undefined') {\n  module.exports.WebSocket = WebSocket\n} else {\n  module.exports.WebSocket = () => {\n    console.error(\"Tried to use WebSocket but it isn't defined or polyfilled\")\n  }\n}\n"
  },
  {
    "path": "client/src/socket.js",
    "content": "import { AsyncSubject } from 'rxjs/AsyncSubject'\nimport { BehaviorSubject } from 'rxjs/BehaviorSubject'\nimport { WebSocketSubject } from 'rxjs/observable/dom/WebSocketSubject'\nimport { Observable } from 'rxjs/Observable'\nimport { Subscription } from 'rxjs/Subscription'\nimport 'rxjs/add/observable/merge'\nimport 'rxjs/add/observable/timer'\nimport 'rxjs/add/operator/filter'\nimport 'rxjs/add/operator/share'\nimport 'rxjs/add/operator/ignoreElements'\nimport 'rxjs/add/operator/concat'\nimport 'rxjs/add/operator/takeWhile'\nimport 'rxjs/add/operator/publish'\n\nimport { serialize, deserialize } from './serialization.js'\n\nconst PROTOCOL_VERSION = 'rethinkdb-horizon-v0'\n\n// Before connecting the first time\nconst STATUS_UNCONNECTED = { type: 'unconnected' }\n// After the websocket is opened and handshake is completed\nconst STATUS_READY = { type: 'ready' }\n// After unconnected, maybe before or after connected. Any socket level error\nconst STATUS_ERROR = { type: 'error' }\n// Occurs when the socket closes\nconst STATUS_DISCONNECTED = { type: 'disconnected' }\n\nclass ProtocolError extends Error {\n  constructor(msg, errorCode) {\n    super(msg)\n    this.errorCode = errorCode\n  }\n  toString() {\n    return `${this.message} (Code: ${this.errorCode})`\n  }\n}\n\n\n// Wraps native websockets with a Subject, which is both an Subscriber\n// and an Observable (it is bi-directional after all!). This version\n// is based on the rxjs.observable.dom.WebSocketSubject implementation.\nexport class HorizonSocket extends WebSocketSubject {\n  // Deserializes a message from a string. Overrides the version\n  // implemented in WebSocketSubject\n  resultSelector(e) {\n    return deserialize(JSON.parse(e.data))\n  }\n\n  // We're overriding the next defined in AnonymousSubject so we\n  // always serialize the value. When this is called a message will be\n  // sent over the socket to the server.\n  next(value) {\n    const request = JSON.stringify(serialize(value))\n    super.next(request)\n  }\n\n  constructor({\n    url,              // Full url to connect to\n    handshakeMaker, // function that returns handshake to emit\n    keepalive = 60,   // seconds between keepalive messages\n    WebSocketCtor = WebSocket,    // optionally provide a WebSocket constructor\n  } = {}) {\n    super({\n      url,\n      protocol: PROTOCOL_VERSION,\n      WebSocketCtor,\n      openObserver: {\n        next: () => this.sendHandshake(),\n      },\n      closeObserver: {\n        next: () => {\n          if (this._handshakeSub) {\n            this._handshakeSub.unsubscribe()\n            this._handshakeSub = null\n          }\n          this.status.next(STATUS_DISCONNECTED)\n        },\n      },\n    })\n    // Completes or errors based on handshake success. Buffers\n    // handshake response for later subscribers (like a Promise)\n    this.handshake = new AsyncSubject()\n    this._handshakeMaker = handshakeMaker\n    this._handshakeSub = null\n\n    this.keepalive = Observable\n      .timer(keepalive * 1000, keepalive * 1000)\n      .map(n => this.makeRequest({ type: 'keepalive' }).subscribe())\n      .publish()\n\n    // This is used to emit status changes that others can hook into.\n    this.status = new BehaviorSubject(STATUS_UNCONNECTED)\n    // Keep track of subscribers so we's can decide when to\n    // unsubscribe.\n    this.requestCounter = 0\n    // A map from request_ids to an object with metadata about the\n    // request. Eventually, this should allow re-sending requests when\n    // reconnecting.\n    this.activeRequests = new Map()\n    this._output.subscribe({\n      // This emits if the entire socket errors (usually due to\n      // failure to connect)\n      error: () => this.status.next(STATUS_ERROR),\n    })\n  }\n\n  deactivateRequest(req) {\n    return () => {\n      this.activeRequests.delete(req.request_id)\n      return { request_id: req.request_id, type: 'end_subscription' }\n    }\n  }\n\n  activateRequest(req) {\n    return () => {\n      this.activeRequests.set(req.request_id, req)\n      return req\n    }\n  }\n\n  filterRequest(req) {\n    return resp => resp.request_id === req.request_id\n  }\n\n  getRequest(request) {\n    return Object.assign({ request_id: this.requestCounter++ }, request)\n  }\n\n  // This is a trimmed-down version of multiplex that only listens for\n  // the handshake requestId. It also starts the keepalive observable\n  // and cleans up after it when the handshake is cleaned up.\n  sendHandshake() {\n    if (!this._handshakeSub) {\n      this._handshakeSub = this.makeRequest(this._handshakeMaker())\n        .subscribe({\n          next: n => {\n            if (n.error) {\n              this.status.next(STATUS_ERROR)\n              this.handshake.error(new ProtocolError(n.error, n.error_code))\n            } else {\n              this.status.next(STATUS_READY)\n              this.handshake.next(n)\n              this.handshake.complete()\n            }\n          },\n          error: e => {\n            this.status.next(STATUS_ERROR)\n            this.handshake.error(e)\n          },\n        })\n\n      // Start the keepalive and make sure it's\n      // killed when the handshake is cleaned up\n      this._handshakeSub.add(this.keepalive.connect())\n    }\n    return this.handshake\n  }\n\n  // Incorporates shared logic between the inital handshake request and\n  // all subsequent requests.\n  // * Generates a request id and filters by it\n  // * Send `end_subscription` when observable is unsubscribed\n  makeRequest(rawRequest) {\n    const request = this.getRequest(rawRequest)\n\n    return super.multiplex(\n      this.activateRequest(request),\n      this.deactivateRequest(request),\n      this.filterRequest(request)\n    )\n  }\n\n  // Wrapper around the makeRequest with the following additional\n  // features we need for horizon's protocol:\n  // * Sends handshake on subscription if it hasn't happened already\n  // * Wait for the handshake to complete before sending the request\n  // * Errors when a document with an `error` field is received\n  // * Completes when `state: complete` is received\n  // * Emits `state: synced` as a separate document for easy filtering\n  // * Reference counts subscriptions\n  hzRequest(rawRequest) {\n    return this.sendHandshake().ignoreElements()\n      .concat(this.makeRequest(rawRequest))\n      .concatMap(resp => {\n        if (resp.error !== undefined) {\n          throw new ProtocolError(resp.error, resp.error_code)\n        }\n        const data = resp.data || []\n\n        if (resp.state !== undefined) {\n          // Create a little dummy object for sync notifications\n          data.push({\n            type: 'state',\n            state: resp.state,\n          })\n        }\n\n        return data\n      })\n      .share()\n  }\n}\n"
  },
  {
    "path": "client/src/util/check-args.js",
    "content": "import ordinal from './ordinal.js'\n\n// Validation helper\nexport default function checkArgs(name, args, {\n                    nullable: nullable = false,\n                    minArgs: minArgs = 1,\n                    maxArgs: maxArgs = 1 } = {}) {\n  if (minArgs === maxArgs && args.length !== minArgs) {\n    const plural = minArgs === 1 ? '' : 's'\n    throw new Error(`${name} must receive exactly ${minArgs} argument${plural}`)\n  }\n  if (args.length < minArgs) {\n    const plural1 = minArgs === 1 ? '' : 's'\n    throw new Error(\n      `${name} must receive at least ${minArgs} argument${plural1}.`)\n  }\n  if (args.length > maxArgs) {\n    const plural2 = maxArgs === 1 ? '' : 's'\n    throw new Error(\n      `${name} accepts at most ${maxArgs} argument${plural2}.`)\n  }\n  for (let i = 0; i < args.length; i++) {\n    if (!nullable && args[i] === null) {\n      const ordinality = maxArgs !== 1 ? ` ${ordinal(i + 1)}` : ''\n      throw new Error(`The${ordinality} argument to ${name} must be non-null`)\n    }\n    if (args[i] === undefined) {\n      throw new Error(\n        `The ${ordinal(i + 1)} argument to ${name} must be defined`)\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/util/glob.js",
    "content": "module.exports = function glob() {\n  return typeof self !== 'undefined' ?\n  self :\n  typeof window !== 'undefined' ?\n  window :\n  typeof global !== 'undefined' ?\n  global :\n  {}\n}\n"
  },
  {
    "path": "client/src/util/ordinal.js",
    "content": "export default function ordinal(x) {\n  if ([ 11, 12, 13 ].indexOf(x) !== -1) {\n    return `${x}th`\n  } else if (x % 10 === 1) {\n    return `${x}st`\n  } else if (x % 10 === 2) {\n    return `${x}nd`\n  } else if (x % 10 === 3) {\n    return `${x}rd`\n  }\n  return `${x}th`\n}\n"
  },
  {
    "path": "client/src/util/query-parse.js",
    "content": "/* Pulled from @sindresorhus query-string module and reformatted.\nThis is simply to avoid requiring the other methods in the module.\n\nMIT License © Sindre Sorhus\n*/\nexport default function(str) {\n  if (typeof str !== 'string') {\n    return {}\n  }\n\n  const str2 = str.trim().replace(/^(\\?|#|&)/, '')\n\n  if (!str2) {\n    return {}\n  }\n\n  return str2.split('&').reduce((ret, param) => {\n    const parts = param.replace(/\\+/g, ' ').split('=')\n    // Firefox (pre 40) decodes `%3D` to `=`\n    // https://github.com/sindresorhus/query-string/pull/37\n    const key = parts.shift()\n    const val = parts.length > 0 ? parts.join('=') : undefined\n\n    const key2 = decodeURIComponent(key)\n\n    // missing `=` should be `null`:\n    // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters\n    const val2 = val === undefined ? null : decodeURIComponent(val)\n\n    if (!ret.hasOwnProperty(key2)) {\n      ret[key2] = val2\n    } else if (Array.isArray(ret[key2])) {\n      ret[key2].push(val2)\n    } else {\n      ret[key2] = [ ret[key2], val2 ]\n    }\n\n    return ret\n  }, {})\n}\n"
  },
  {
    "path": "client/src/util/valid-index-value.js",
    "content": "// Checks whether the return value is a valid primary or secondary\n// index value in RethinkDB.\nexport default function validIndexValue(val) {\n  if (val === null) {\n    return false\n  }\n  if ([ 'boolean', 'number', 'string' ].indexOf(typeof val) !== -1) {\n    return true\n  }\n  if (val instanceof ArrayBuffer) {\n    return true\n  }\n  if (val instanceof Date) {\n    return true\n  }\n  if (Array.isArray(val)) {\n    let isValid = true\n    val.forEach(v => {\n      isValid = isValid && validIndexValue(v)\n    })\n    return isValid\n  }\n  return false\n}\n"
  },
  {
    "path": "client/test/above.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion } from './utils'\n\nexport default function aboveSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // By default `above` is closed\n  it('is a closed bound by default', assertCompletes(() =>\n    data.order('id').above({ id: 5 }).fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 5, a: 60 },\n        { id: 6, a: 50 },\n      ]))\n  ))\n\n  // We can also pass that explicitly\n  it('allows \"closed\" to be passed explicitly', assertCompletes(() =>\n    data.order('id').above({ id: 5 }, 'closed').fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 5, a: 60 },\n        { id: 6, a: 50 },\n      ]))\n  ))\n\n  // But we can make it open\n  it('can return an open bounded result', assertCompletes(() =>\n    data.order('id').above({ id: 5 }, 'open').fetch()\n      .do(([ res ]) => compareWithoutVersion(res, { id: 6, a: 50 }))\n  ))\n\n  // Let's try something that returns no values\n  it('returns no results if bound eliminates all documents',\n     assertCompletes(() =>\n    data.order('id').above({ id: 7 }).fetch()\n      .do(res => compareWithoutVersion(res, []))\n  ))\n\n  // We can chain `above` off a collection\n  it('can be chained from a collection directly', assertCompletes(() =>\n    data.above({ id: 5 }).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // Or off other things\n  it('can be chained from a findAll', assertCompletes(() =>\n    data.findAll({ a: 20 }).above({ id: 3 }).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // `above` can't include any keys that are in `findAll`\n  it('errors when it contains any keys from the findAll term', assertErrors(() =>\n    data.findAll({ a: 20 }).above({ a: 3 }).fetch(),\n    /\"a\" cannot be used in \"order\", \"above\", or \"below\" when finding by that field/\n  ))\n\n  // Let's try it on a non-primary key\n  it('can be used on a non-primary key', assertCompletes(() =>\n    data.order([ 'a', 'id' ]).above({ a: 20 }).fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n        { id: 4, a: 20, b: 3 },\n        { id: 6, a: 50 },\n        { id: 5, a: 60 },\n      ]))\n  ))\n\n  // Let's try it on a non-primary key, but open\n  it('can be used on non-primary key with open bound', assertCompletes(() =>\n    data.order([ 'a', 'id' ]).above({ a: 20 }, 'open').fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 6, a: 50 },\n        { id: 5, a: 60 },\n      ]))\n  ))\n  // The key in `above` must be the first key in `order`\n  it('must receive as an argument the first key in the order term', assertErrors(() =>\n    data.order([ 'a', 'id' ]).above({ id: 20 }).fetch(),\n    /\"above\" must be on the same field as the first in \"order\"/\n  ))\n\n  // Passing multiple keys to `above` isn't legal\n  it('errors if multiple keys are passed', assertErrors(() =>\n    data.order([ 'a', 'id' ]).above({ a: 20, id: 20 }).fetch(),\n    /\"find\" is required/\n  ))\n\n  // Nor is passing a field that isn't specified in `order`\n  it(`errors if the field passed isn't in the order term`, assertErrors(() =>\n    data.order([ 'a', 'id' ]).above({ b: 20 }).fetch(),\n    /\"above\" must be on the same field as the first in \"order\"/\n  ))\n\n  // If chaining `above/below`, they must be passed the same key\n  it(`errors if it doesn't receive the same key as the below term`,\n     assertErrors(() =>\n    data.above({ b: 0 }).below({ a: 100 }).fetch(),\n    /\"below\" must be on the same field as the first in \"order\"/\n  ))\n\n  // Starting with `null` is not ok\n  it('throws if it is passed null', assertThrows(\n    'The 1st argument to above must be non-null',\n    () => data.above(null).fetch()\n  ))\n\n  // Empty value is not ok\n  it('throws if it does not receive an argument', assertThrows(\n    'above must receive at least 1 argument.',\n    () => data.above().fetch()\n  ))\n\n  // Bad arguments are not ok\n  it('errors if it receives a non-string argument', assertErrors(() =>\n    data.above(1).fetch(),\n    /\"find\" is required/\n  ))\n  it('errors if it receives more than one argument', assertErrors(() =>\n    data.above({ id: 1 }, 1).fetch(),\n    /\"find\" is required/\n  ))\n}}\n"
  },
  {
    "path": "client/test/aboveSub.js",
    "content": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default function aboveSubscriptionSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's grab a specific document using 'above'\n  it('can get a specific document', assertCompletes(() =>\n    observableInterleave({\n      query: data.above({ id: 1 }).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's grab a specific document using 'above' and also test the\n  // 'changed' event.\n  it('can get a document and reflect changes to it', assertCompletes(() =>\n    observableInterleave({\n      query: data.above({id: 1}).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Secondary index, open\n  it('can get a document by secondary index with open bound', assertCompletes(() =>\n    observableInterleave({\n      query: data.above({a: 0}, 'open').watch(),\n      operations: [\n        data.store({ id: 1, a: 0 })\n          .concat(data.store({ id: 1, a: 1 })),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure we don't see events that aren't ours\n  it(\"doesn't see updates to documents outside its bound\", assertCompletes(() =>\n    observableInterleave({\n      query: data.above({ id: 3 }).watch(),\n      operations: [\n        data.store({ id: 2, a: 1 })\n          .concat(data.store({ id: 2, a: 2 }))\n          .concat(data.store({ id: 3, val: 'foo' }))\n          .concat(data.remove(2)),\n        data.remove(3),\n      ],\n      expected: [\n        [],\n        [ { id: 3, val: 'foo' } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's try subscribing to multiple IDs\n  it('can subscribe to multiple ids', assertCompletes(() =>\n    observableInterleave({\n      query: data.above({ id: 1 }).below({ id: 3 }, 'open').watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 2, a: 1 })\n          .concat(data.store({ id: 3, a: 1 })),\n        data.store({ id: 1, a: 2 }),\n        data.store({ id: 2, a: 2 })\n          .concat(data.store({ id: 3, a: 2 })),\n        data.remove(1),\n        data.remove(2)\n          .concat(data.remove(3)),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 1 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 2 } ],\n        [ { id: 2, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure initial vals works correctly\n  it('handles initial values correctly', assertCompletes(() =>\n    data.store({ id: 1, a: 1 }).concat(\n      observableInterleave({\n        query: data.above({ id: 1 }).watch(),\n        operations: [\n          data.store({ id: 1, a: 2 }),\n          data.remove(1),\n        ],\n        expected: [\n          [ { id: 1, a: 1 } ],\n          [ { id: 1, a: 2 } ],\n          [],\n        ],\n      })\n    )\n  ))\n}}\n"
  },
  {
    "path": "client/test/aggregate.js",
    "content": "import Rx from 'rxjs/Rx'\n\nimport { assertCompletes,\n         removeAllDataObs,\n         observableInterleave } from './utils'\n\n// Raises an exception if corresponding elements in an array don't\n// have the same elements (in any order)\nfunction arrayHasSameElements(a, b) {\n  if (a.length !== b.length) {\n    return false\n  }\n  for (let i = 0; i < a.length; i++) {\n    assert.sameDeepMembers(a[i], b[i])\n  }\n}\n\nexport default function aggregateSuite(getData, getHorizon) {\n  return () => {\n  let data, horizon, hzA, hzB\n  before(() => {\n    data = getData()\n    horizon = getHorizon()\n    hzA = horizon('testA')\n    hzB = horizon('testB')\n  })\n  afterEach(done => {\n    removeAllDataObs(data)\n    .concat(removeAllDataObs(hzA))\n    .concat(removeAllDataObs(hzB))\n      .subscribe({\n        next() { },\n        error(err) { done(err) },\n        complete() { done() },\n      })\n  })\n\n  it('is equivalent to a subquery if it is not passed an object',\n     assertCompletes(() => {\n       const underlyingQuery = data.order('id').limit(3)\n       return data.insert([\n         { id: 1 },\n         { id: 2 },\n         { id: 3 },\n         { id: 4 },\n       ]).concat(observableInterleave({\n         query: horizon.aggregate(underlyingQuery).fetch(),\n         operations: [],\n         expected: [\n           [ { id: 1 }, { id: 2 }, { id: 3 } ],\n         ],\n       }))\n     })\n    )\n\n  it('combines multiple queries in an array into one',\n     assertCompletes(() => {\n       const query = horizon.aggregate([ hzA, hzB ]).fetch()\n       const expected = [\n         [ { id: 1 }, { id: 3 } ],\n         [ { id: 2 }, { id: 4 } ],\n       ]\n       return hzA.insert([\n         { id: 1 },\n         { id: 3 },\n       ]).concat(hzB.insert([\n         { id: 2 },\n         { id: 4 },\n       ])).concat(observableInterleave({\n         query,\n         operations: [],\n         equality: arrayHasSameElements,\n         expected: [ expected ],\n       }))\n     })\n    )\n\n  it('allows constants in an array spec', assertCompletes(() => {\n    const query = horizon.aggregate([ 1, hzA ]).fetch()\n    const expected = [ 1, [ { id: 1 }, { id: 2 } ] ]\n    return hzA.insert([\n      { id: 1 },\n      { id: 2 },\n    ]).concat(observableInterleave({\n      query,\n      operations: [],\n      equality: arrayHasSameElements,\n      expected: [ expected ],\n    }))\n  }))\n\n  it('allows a fully constant aggregate of primitives', assertCompletes(() => {\n    const agg = {\n      a: 'Some string',\n      b: [ true ],\n      c: new Date(),\n      d: {\n        e: new ArrayBuffer(),\n        f: 1.2,\n        g: [ 1.3, true, new Date(), {} ],\n      },\n    }\n\n    return observableInterleave({\n      query: horizon.aggregate(agg).fetch(),\n      operations: [],\n      equality: assert.deepEqual,\n      expected: [ agg ],\n    })\n  }))\n\n  it('aggregates data from objects', assertCompletes(() => {\n    const hzAContents = [\n      { id: 1, a: true },\n      { id: 2, b: false },\n      { id: 3, c: true },\n      { id: 4, d: true },\n    ]\n    const hzBContents = [\n      { id: 5, e: 'E' },\n      { id: 6, f: 'F' },\n      { id: 7, g: 'G' },\n      { id: 8, h: 'H' },\n    ]\n    const query = horizon.aggregate({\n      item1: hzA.find(1),\n      item2: hzB.above({ id: 5 }).below({ id: 8 }),\n    }).fetch()\n    const expectedResult = {\n      item1: { id: 1, a: true },\n      item2: [\n        { id: 5, e: 'E' },\n        { id: 6, f: 'F' },\n        { id: 7, g: 'G' },\n      ],\n    }\n    return hzA.insert(hzAContents).concat(hzB.insert(hzBContents))\n    .concat(observableInterleave({\n      query,\n      operations: [],\n      equality: assert.deepEqual,\n      expected: [ expectedResult ],\n    }))\n  }))\n\n  it('allows observables in aggregates', assertCompletes(() => {\n    const hzAContents = [\n      { id: 1, foo: true },\n    ]\n    const constantObservable = Rx.Observable.of({ id: 2, foo: false })\n    assert.instanceOf(constantObservable, Rx.Observable)\n    const regularConstant = { id: 3, foo: true }\n    const expectedResult = {\n      a: { id: 1, foo: true },\n      b: { id: 2, foo: false },\n      c: { id: 3, foo: true },\n      d: { id: 4, foo: false },\n    }\n    return hzA.insert(hzAContents)\n      .concat(observableInterleave({\n        query: horizon.aggregate({\n          a: hzA.find(1),\n          b: constantObservable,\n          c: regularConstant,\n          d: Promise.resolve({ id: 4, foo: false }),\n        }).fetch(),\n        operations: [],\n        equality: assert.deepEqual,\n        expected: [ expectedResult ],\n      }))\n  }))\n\n  it('allows nested aggregates with queries at different levels',\n     assertCompletes(() => {\n       const hzAContents = [\n         { id: 1, contents: 'a' },\n         { id: 2, contents: 'b' },\n         { id: 3, contents: 'c' },\n       ]\n       const hzBContents = [\n         { id: 4, contents: 'd' },\n         { id: 5, contents: 'e' },\n         { id: 6, contents: 'f' },\n       ]\n       const query = horizon.aggregate({\n         a: hzA.find(1),\n         b: {\n           c: hzB.find(4),\n           d: hzB.find(5),\n           e: {\n             f: [ hzA.find(2), hzA.find(3) ],\n           },\n         },\n       }).fetch()\n       const expectedResult = {\n         a: { id: 1, contents: 'a' },\n         b: {\n           c: { id: 4, contents: 'd' },\n           d: { id: 5, contents: 'e' },\n           e: {\n             f: [ { id: 2, contents: 'b' }, { id: 3, contents: 'c' } ],\n           },\n         },\n       }\n       return hzA.insert(hzAContents)\n         .concat(hzB.insert(hzBContents))\n         .concat(observableInterleave({\n           query,\n           operations: [],\n           equality: assert.deepEqual,\n           expected: [ expectedResult ],\n         }))\n     }\n  ))\n\n  it('can be parameterized with .model',\n     assertCompletes(() => {\n       const hzAContents = [\n         { id: 1, contents: 'a' },\n         { id: 2, contents: 'b' },\n         { id: 3, contents: 'c' },\n       ]\n       const hzBContents = [\n         { id: 1, contents: 'd' },\n         { id: 2, contents: 'e' },\n         { id: 3, contents: 'f' },\n       ]\n       const Model = horizon.model((foo, bar, baz) => ({\n         a: hzA.find(foo),\n         b: {\n           c: hzB.find(foo),\n           d: hzB.find(bar),\n           e: {\n             f: [ hzA.find(bar), hzA.find(baz) ],\n           },\n         },\n       }))\n       const expectedResult = {\n         a: { id: 1, contents: 'a' },\n         b: {\n           c: { id: 1, contents: 'd' },\n           d: { id: 2, contents: 'e' },\n           e: {\n             f: [ { id: 2, contents: 'b' },\n                  { id: 3, contents: 'c' } ],\n           },\n         },\n       }\n       return hzA.insert(hzAContents)\n         .concat(hzB.insert(hzBContents))\n         .concat(observableInterleave({\n           query: Model(1, 2, 3).fetch(),\n           operations: [],\n           equality: assert.deepEqual,\n           expected: [ expectedResult ],\n         }))\n  }))\n}}\n"
  },
  {
    "path": "client/test/aggregateSub.js",
    "content": "import { Observable } from 'rxjs/Observable'\n\nimport { assertCompletes,\n         removeAllDataObs,\n         observableInterleave } from './utils'\n\nexport default function aggregateSubSuite(getData, getHorizon) {\n  return () => {\n  let data, horizon, hzA, hzB\n  before(() => {\n    data = getData()\n    horizon = getHorizon()\n    hzA = horizon('testA')\n    hzB = horizon('testB')\n  })\n  afterEach(done => {\n    Observable.merge(\n      removeAllDataObs(hzA),\n      removeAllDataObs(hzB),\n      removeAllDataObs(data)).subscribe({\n        next() { },\n        error(err) { done(err) },\n        complete() { done() },\n      })\n  })\n\n  it('is equivalent to a subquery if it is not passed an object',\n     assertCompletes(() => {\n       const underlyingQuery = data.order('id').limit(3)\n       return data.insert([\n         { id: 1 },\n         { id: 2 },\n         { id: 3 },\n         { id: 4 },\n       ]).concat(observableInterleave({\n         query: horizon.aggregate(underlyingQuery).watch(),\n         operations: [],\n         expected: [\n           [ { id: 1 }, { id: 2 }, { id: 3 } ],\n         ],\n       }))\n     })\n    )\n\n  it('combines multiple queries in an array into one', done => {\n    const query = horizon.aggregate([ hzA, hzB ])\n    const expected = [\n      [ { id: 1 }, { id: 3 } ],\n      [ { id: 2 }, { id: 4 } ],\n    ]\n    return hzA.insert([\n      { id: 1 },\n      { id: 3 },\n    ]).ignoreElements().concat(hzB.insert([\n      { id: 2 },\n      { id: 4 },\n    ])).ignoreElements().concat(query.watch())\n      .take(1)\n      .subscribe({\n        next(x) {\n          for (let i = 0; i < x.length; i++) {\n            assert.sameDeepMembers(expected[i], x[i])\n          }\n        },\n        error(err) {\n          done(new Error(err))\n        },\n        complete() { done() },\n      })\n  })\n\n  it('allows constants in an array spec', assertCompletes(() => {\n    const query = horizon.aggregate([ 1, hzA ])\n    const expected = [ 1, [ { id: 1 }, { id: 2 } ] ]\n    return hzA.insert([\n      { id: 1 },\n      { id: 2 },\n    ]).ignoreElements()\n      .concat(query.watch())\n      .take(1)\n      .do(x => {\n        assert.equal(x[0], 1)\n        assert.sameDeepMembers(x[1], expected[1])\n      })\n  }))\n\n  it('allows a fully constant aggregate of primitives', assertCompletes(() => {\n    const aggregate = {\n      a: 'Some string',\n      b: [ true ],\n      c: new Date(),\n      d: {\n        e: new ArrayBuffer(),\n        f: 1.2,\n        g: [ 1.3, true, new Date(), { } ],\n      },\n    }\n    const query = horizon.aggregate(aggregate).watch()\n    return observableInterleave({\n      query,\n      operations: [],\n      equality: assert.deepEqual,\n      expected: [ aggregate ],\n    })\n  }))\n\n  it('aggregates data from objects', assertCompletes(() => {\n    const hzAContents = [\n      { id: 1, a: true },\n      { id: 2, b: false },\n      { id: 3, c: true },\n      { id: 4, d: true },\n    ]\n    const hzBContents = [\n      { id: 5, e: 'E' },\n      { id: 6, f: 'F' },\n      { id: 7, g: 'G' },\n      { id: 8, h: 'H' },\n    ]\n    const query = horizon.aggregate({\n      item1: hzA.find(1),\n      item2: hzB.above({ id: 5 }).below({ id: 8 }),\n    }).watch()\n    const expectedResult = {\n      item1: { id: 1, a: true },\n      item2: [\n        { id: 5, e: 'E' },\n        { id: 6, f: 'F' },\n        { id: 7, g: 'G' },\n      ],\n    }\n    return hzA.insert(hzAContents).concat(hzB.insert(hzBContents))\n    .concat(observableInterleave({\n      query,\n      operations: [],\n      equality: assert.deepEqual,\n      expected: [ expectedResult ],\n    }))\n  }))\n\n  it('allows observables in aggregates', assertCompletes(() => {\n    const hzAContents = [\n      { id: 1, foo: true },\n    ]\n    const constantObservable = Observable.of({ id: 2, foo: false })\n    assert.instanceOf(constantObservable, Observable)\n    const regularConstant = { id: 3, foo: true }\n    const expectedResult = {\n      a: { id: 1, foo: true },\n      b: { id: 2, foo: false },\n      c: { id: 3, foo: true },\n      d: { id: 4, foo: false },\n    }\n    return hzA.insert(hzAContents)\n      .concat(observableInterleave({\n        query: horizon.aggregate({\n          a: hzA.find(1),\n          b: constantObservable,\n          c: regularConstant,\n          d: Promise.resolve({ id: 4, foo: false }),\n        }).watch(),\n        operations: [],\n        equality: assert.deepEqual,\n        expected: [ expectedResult ],\n      }))\n  }))\n\n  it('allows nested aggregates with queries at different levels',\n     assertCompletes(() => {\n       const hzAContents = [\n         { id: 1, contents: 'a' },\n         { id: 2, contents: 'b' },\n         { id: 3, contents: 'c' },\n       ]\n       const hzBContents = [\n         { id: 4, contents: 'd' },\n         { id: 5, contents: 'e' },\n         { id: 6, contents: 'f' },\n       ]\n       const query = horizon.aggregate({\n         a: hzA.find(1),\n         b: {\n           c: hzB.find(4),\n           d: hzB.find(5),\n           e: {\n             f: [ hzA.find(2), hzA.find(3) ],\n           },\n         },\n       }).watch()\n       const expectedResult = {\n         a: { id: 1, contents: 'a' },\n         b: {\n           c: { id: 4, contents: 'd' },\n           d: { id: 5, contents: 'e' },\n           e: {\n             f: [ { id: 2, contents: 'b' }, { id: 3, contents: 'c' } ],\n           },\n         },\n       }\n       return hzA.insert(hzAContents)\n         .concat(hzB.insert(hzBContents))\n         .concat(observableInterleave({\n           query,\n           operations: [],\n           equality: assert.deepEqual,\n           expected: [ expectedResult ],\n         }))\n  }))\n\n  it('can be parameterized with .model',\n     assertCompletes(() => {\n       const hzAContents = [\n         { id: 1, contents: 'a' },\n         { id: 2, contents: 'b' },\n         { id: 3, contents: 'c' },\n       ]\n       const hzBContents = [\n         { id: 1, contents: 'd' },\n         { id: 2, contents: 'e' },\n         { id: 3, contents: 'f' },\n       ]\n       const Model = horizon.model((foo, bar, baz) => ({\n         a: hzA.find(foo),\n         b: {\n           c: hzB.find(foo),\n           d: hzB.find(bar),\n           e: {\n             f: [ hzA.find(bar), hzA.find(baz) ],\n           },\n         },\n       }))\n       const expectedResultA = {\n         a: { id: 1, contents: 'a' },\n         b: {\n           c: { id: 1, contents: 'd' },\n           d: { id: 2, contents: 'e' },\n           e: {\n             f: [ { id: 2, contents: 'b' },\n                  { id: 3, contents: 'c' } ],\n           },\n         },\n       }\n       const expectedResultB = {\n         a: { id: 1, contents: 'a' },\n         b: {\n           c: { id: 1, contents: 'something' },\n           d: { id: 2, contents: 'e' },\n           e: {\n             f: [ { id: 2, contents: 'b' },\n                  { id: 3, contents: 'c' } ],\n           },\n         },\n       }\n       return hzA.insert(hzAContents)\n         .concat(hzB.insert(hzBContents))\n         .concat(observableInterleave({\n           query: Model(1, 2, 3).watch(),\n           operations: [\n             hzB.upsert({ id: 1, contents: 'something' }),\n           ],\n           equality: assert.deepEqual,\n           expected: [ expectedResultA,\n                       expectedResultB ],\n         }))\n  }))\n}}\n"
  },
  {
    "path": "client/test/api.js",
    "content": "import 'rxjs/add/operator/ignoreElements'\nimport 'rxjs/add/operator/concat'\nimport 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         removeAllData,\n         compareSetsWithoutVersion } from './utils'\n\nimport horizonObjectSuite from './horizonObject'\n\nimport storeSuite from './store'\nimport insertSuite from './insert'\nimport updateSuite from './update'\nimport upsertSuite from './upsert'\nimport replaceSuite from './replace'\n\nimport removeSuite from './remove'\nimport removeAllSuite from './removeAll'\n\nimport timesSuite from './times'\nimport authSuite from './auth'\nimport collectionSuite from './collection'\nimport findSuite from './find'\nimport findAllSuite from './findAll'\nimport orderSuite from './order'\nimport limitSuite from './limit'\nimport aboveSuite from './above'\nimport belowSuite from './below'\nimport chainingSuite from './chaining'\n\nimport findSubscriptionSuite from './findSub'\nimport findAllSubscriptionSuite from './findAllSub'\nimport aboveSubscriptionSuite from './aboveSub'\nimport belowSubscriptionSuite from './belowSub'\nimport orderLimitSubSuite from './orderLimitSub'\n\nimport aggregateSuite from './aggregate'\nimport aggregateSubSuite from './aggregateSub'\n\nimport unitUtilsSuite from './unit/utilsTest'\nimport unitAuthSuite from './unit/auth'\nimport unitAstSuite from './unit/ast'\n\n// This test suite covers various edge cases in the Horizon client library API.\n// It does not cover correctness of the full system in various circumstances.\n// The purpose of the API test suite is to act as a runnable, checkable spec for\n// API of the client library. This also doesn't cover subscriptions, there is a\n// separate test suite for that.\n\n// Test the methods and event callbacks on the Horizon object.\ndescribe('Horizon Object API', horizonObjectSuite)\n\n// Test the core client library API\ndescribe('Core API tests', () => {\n  // The connection for our tests\n  let horizon, data\n\n  const getHorizon = () => horizon\n  const getData = () => data\n\n  // Set up the horizon connection before running these tests.\n  before(done => {\n    Horizon.clearAuthTokens()\n    horizon = Horizon({ lazyWrites: true })\n    horizon.connect(err => done(err))\n    horizon.onReady(() => {\n      data = horizon('test_data')\n      done()\n    })\n  })\n\n  // Kill the horizon connection after running these tests.\n  after(done => {\n    let alreadyDone = false\n    function wrappedDone(...args) {\n      if (!alreadyDone) {\n        alreadyDone = true\n        return done(...args)\n      }\n    }\n    horizon.disconnect()\n    horizon.onDisconnected(() => wrappedDone())\n  })\n\n  // Test the mutation commands\n  describe('Write API', () => {\n    // Drop all data after each test\n    afterEach(done => removeAllData(data, done))\n\n    describe('Testing `store`', storeSuite(getData))\n    describe('Testing `insert`', insertSuite(getData))\n    describe('Testing `upsert`', upsertSuite(getData))\n    describe('Testing `update`', updateSuite(getData))\n    describe('Testing `replace`', replaceSuite(getData))\n  }) // Storage API\n  describe('Remove API', () => {\n    describe('Testing `remove`', removeSuite(getData))\n    describe('Testing `removeAll`', removeAllSuite(getData))\n  })\n\n  describe('Testing `times`', timesSuite(getData))\n  describe('Testing authentication', authSuite(getHorizon))\n  // Test the lookup API\n  describe('Fetch API', () => {\n    const testData = [\n      { id: 1, a: 10 },\n      { id: 2, a: 20, b: 1 },\n      { id: 3, a: 20, b: 2 },\n      { id: 4, a: 20, b: 3 },\n      { id: 5, a: 60 },\n      { id: 6, a: 50 },\n    ]\n\n    const getTestData = () => {\n      return testData\n    }\n\n    // Drop all the existing data\n    before(done => {\n      removeAllData(data, done)\n    })\n\n    // Insert the test data and make sure it's in\n    before(assertCompletes(() =>\n      data.store(testData)\n       .ignoreElements()\n       .concat(data.fetch())\n       .do(res => compareSetsWithoutVersion(res, testData))\n    ))\n\n    describe('Testing full collection read',\n             collectionSuite(getHorizon, getData, getTestData))\n    describe('Testing `find`', findSuite(getData))\n    describe('Testing `findAll`', findAllSuite(getData))\n    describe('Testing `order`', orderSuite(getData, getTestData))\n    describe('Testing `limit`', limitSuite(getData))\n    describe('Testing `above`', aboveSuite(getData))\n    describe('Testing `below`', belowSuite(getData))\n    describe('Test `above/below/limit` chaining variations',\n             chainingSuite(getData))\n  }) // Test the lookup API\n\n  // Test the subscriptions API\n  describe('Watch API', () => {\n\n    // Drop all the existing data\n    beforeEach(done => {\n      removeAllData(data, done)\n    })\n\n    describe('Testing `find` subscriptions', findSubscriptionSuite(getData))\n    describe('Testing `findAll` subscriptions', findAllSubscriptionSuite(getData))\n    describe('Testing `above` subscriptions', aboveSubscriptionSuite(getData))\n    describe('Testing `below` subscriptions', belowSubscriptionSuite(getData))\n    describe('Testing `order.limit` subscriptions', orderLimitSubSuite(getData))\n  }) // Test the subscriptions API\n  describe('Serialization', () => {\n    describe('Testing `times`', timesSuite(getData))\n  })\n\n  describe('Aggregate API', () => {\n    describe('fetch', aggregateSuite(getData, getHorizon))\n    describe('watch', aggregateSubSuite(getData, getHorizon))\n  })\n\n  describe('Unit tests', () => {\n    describe('Auth', unitAuthSuite)\n    describe('Utils', unitUtilsSuite)\n    describe('AST', unitAstSuite)\n  })\n}) // Core API tests\n"
  },
  {
    "path": "client/test/auth.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/mergeMapTo'\n\nexport default function authSuite(getHorizon) {\n  return () => {\n  let horizon\n  before(() => {\n    horizon = getHorizon()\n  })\n  it('gets an error when unauthenticated', done => {\n    const unauthHorizon = Horizon({\n      secure: false,\n      lazyWrites: true,\n      authType: 'unauthenticated',\n    })\n    unauthHorizon.currentUser().fetch().subscribe({\n      next(user) {\n        throw new Error('Expected an error, got a document')\n      },\n      error(err) {\n        assert.equal(err.message, 'Unauthenticated users have no user document')\n        done()\n      },\n      complete() {\n        throw new Error('Expected an error, completed successfully instead')\n      },\n    })\n  })\n  it('gets a normal user object when anonymous', done => {\n    Horizon.clearAuthTokens()\n    const myHorizon = Horizon({\n      secure: false,\n      lazyWrites: true,\n      authType: 'anonymous',\n    })\n    let asserted = 0\n    myHorizon.currentUser().fetch().subscribe({\n      next(user) {\n        assert.isObject(user)\n        assert.isString(user.id)\n        assert.sameDeepMembers(user.groups, [ 'default', 'authenticated' ])\n        asserted += 1\n      },\n      error(e) { done(e) },\n      complete() {\n        if (asserted < 1) {\n          done(new Error('Completed before receiving a document'))\n        } else if (asserted > 1) {\n          done(new Error('Received too many documents before completing'))\n        } else {\n          done()\n        }\n      },\n    })\n  })\n  it('write to the user object', done => {\n    Horizon.clearAuthTokens()\n    const myHorizon = Horizon({ secure: false, lazyWrites: true, authType: 'anonymous' })\n    const new_groups = [ 'admin', 'superuser', 'default' ];\n    let asserted = 0\n    myHorizon.currentUser().fetch()\n      .mergeMap(user => myHorizon('users')\n                .update({ id: user.id, groups: [ 'admin', 'superuser', 'default' ] }))\n      .mergeMapTo(myHorizon.currentUser().fetch()).subscribe({\n        next(user) {\n          assert.isObject(user)\n          assert.isString(user.id)\n          assert.sameDeepMembers(user.groups, new_groups)\n          asserted += 1\n        },\n        error(e) { done(e) },\n        complete() {\n          if (asserted < 1) {\n            done(new Error('Completed before receiving a document'))\n          } else if (asserted > 1) {\n            done(new Error('Received too many documents before completing'))\n          } else {\n            done()\n          }\n        },\n      })\n  })\n}}\n"
  },
  {
    "path": "client/test/below.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion } from './utils'\n\nexport default function belowSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // By default `below` is open\n  it('defaults to open', assertCompletes(() =>\n    data.order('id').below({ id: 3 }).fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 }\n      ]))\n  ))\n\n  // We can also pass that explicitly\n  it('can be explicitly set to be an open bound', assertCompletes(() =>\n    data.order('id').below({ id: 3 }, 'open').fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n      ]))\n  ))\n\n  // But we can make it closed\n  it('can be explicitly set to be a closed bound', assertCompletes(() =>\n    data.order('id').below({ id: 3 }, 'closed').fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n      ]))\n  ))\n\n  // Let's try something that returns no values\n  it('can return no values', assertCompletes(() =>\n    data.order('id').below({ id: 0 }).fetch()\n      .do(res => compareWithoutVersion(res, []))\n  ))\n\n  // We can chain `below` off a collection\n  it('can be chained off of a collection', assertCompletes(() =>\n    data.below({ id: 3 }).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // Or off other things\n  it('can be chained off of a findAll term', assertCompletes(() =>\n    data.findAll({ a: 20 }).below({ id: 4 }).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // `below` can't include any keys that are in `findAll`\n  it('cannot include any keys that are passed to findAll', assertErrors(() =>\n    data.findAll({ a: 20 }).below({ a: 3 }).fetch(),\n    /\"a\" cannot be used in \"order\", \"above\", or \"below\" when finding by that field/\n  ))\n\n  // Let's try it on a non-primary index\n  it('can bound a non-primary index', assertCompletes(() =>\n    data.order([ 'a', 'id' ]).below({ a: 20 }).fetch()\n      .do(([ res ]) => compareWithoutVersion(res, { id: 1, a: 10 }))\n  ))\n\n  // Let's try it on a non-primary key, but closed\n  it('can closed bound a non-primary key', assertCompletes(() =>\n    data.order([ 'a', 'id' ]).below({ a: 20 }, 'closed').fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n        { id: 4, a: 20, b: 3 },\n      ]))\n  ))\n\n  // The key in `below` must be the first key in `order`\n  it('must receive as an argument the first key in the order term', assertErrors(() =>\n    data.order(['a', 'id']).below({ id: 20 }).fetch(),\n    /\"below\" must be on the same field as the first in \"order\"/\n  ))\n\n  // Passing multiple keys to `below` isn't legal\n  it('errors if it receives multiple keys', assertErrors(() =>\n    data.order(['a', 'id']).below({ a: 20, id: 20 }).fetch(),\n    /\"find\" is required/\n  ))\n\n  // Nor is passing a field that isn't specified in `order`\n  it(`errors if it receives a field that wasn't passed to the order term`,\n     assertErrors(() =>\n    data.order(['a', 'id']).below({ b: 20 }).fetch(),\n    /\"below\" must be on the same field as the first in \"order\"/\n  ))\n\n  // If chaining `below/above`, they must be passed the same key\n  it('must be passed the same key as the above term', assertErrors(() =>\n    data.below({ a: 100 }).above({ b: 0 }).fetch(),\n    /\"below\" must be on the same field as the first in \"order\"/\n  ))\n\n  // Starting with `null` is not ok\n  it('throws if passed null', assertThrows(\n    'The 1st argument to below must be non-null',\n    () => data.below(null).fetch()\n  ))\n\n  // Empty value is not ok\n  it('throws if not given an argument', assertThrows(\n    'below must receive at least 1 argument.',\n    () => data.below().fetch()\n  ))\n\n  // Bad arguments are not ok\n  it('errors if passed a non-string', assertErrors(() =>\n    data.below(1).fetch(),\n    /\"find\" is required/\n  ))\n  it('errors if it receives a bound other than open or closed', assertErrors(() =>\n    data.below({ id: 1 }, 1).fetch(),\n    /\"find\" is required/\n  ))\n}}\n"
  },
  {
    "path": "client/test/belowSub.js",
    "content": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default function belowSubscriptionSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's grab a specific document using 'below'\n  it('can grab a specific document', assertCompletes(() =>\n    observableInterleave({\n      query: data.below({ id: 2 }).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's grab a specific document using 'below' and also test the 'changed'\n  // event.\n  it('properly handles changes to documents in its range', assertCompletes(() =>\n    observableInterleave({\n      query: data.below({ id: 2 }).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 2 } ],\n        [],\n      ]\n    })\n  ))\n\n  // Secondary index, closed\n  it('can find documents by secondary index with closed bound', assertCompletes(() =>\n    observableInterleave({\n      query: data.below({ a: 2 }, 'closed').watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 2 } ],\n        [],\n      ]\n    })\n  ))\n\n  // Let's make sure we don't see events that aren't ours\n  it(\"doesn't see updates to documents outside its range\", assertCompletes(() =>\n    observableInterleave({\n      query: data.below({ id: 1 }).watch(),\n      operations: [\n        data.store({ id: 2, a: 1 })\n          .concat(data.store({ id: 2, a: 2 }))\n          .concat(data.store({ id: 0, val: 'foo' })),\n        data.remove(2)\n          .concat(data.remove(0)),\n      ],\n      expected: [\n        [],\n        [ { id: 0, val: 'foo' } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's try subscribing to multiple IDs\n  it('can subscribe to multiple ids', assertCompletes(() =>\n    observableInterleave({\n      query: data.below({ id: 3 }).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 2, a: 1 })\n        .concat(data.store({ id: 3, a: 1 })),\n        data.store({ id: 1, a: 2 }),\n        data.store({ id: 2, a: 2 })\n          .concat(data.store({ id: 3, a: 2 })),\n        data.remove(1),\n        data.remove(2)\n          .concat(data.remove(3)),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 1 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 2 } ],\n        [ { id: 2, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure initial vals works correctly\n  it('handles initial values correctly', assertCompletes(() =>\n    data.store({ id: 1, a: 1 }).concat(\n      observableInterleave({\n        query: data.below({ id: 2 }).watch(),\n        operations: [\n          data.store({ id: 1, a: 2 }),\n          data.remove(1),\n        ],\n        expected: [\n          [ { id: 1, a: 1 } ],\n          [ { id: 1, a: 2 } ],\n          [],\n        ],\n      })\n    )\n  ))\n}}\n"
  },
  {
    "path": "client/test/chaining.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes, assertThrows, compareWithoutVersion } from './utils'\n\nexport default function chainingSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's do a biiig chain\n  it('findAll.order.above.below', assertCompletes(() =>\n    data.findAll({ a: 20 })\n      .order('id')\n      .above({ id: 2 })\n      .below({ id: 4 })\n      .fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n      ]))\n  ))\n\n  // Let's flip it the other way and change the order\n  it('findAll.below.above.order(desc)', assertCompletes(() =>\n    data.findAll({ a: 20 })\n      .below({ id: 4 })\n      .above({ id: 2 })\n      .order('id', 'descending')\n      .fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 3, a: 20, b: 2 },\n        { id: 2, a: 20, b: 1 },\n      ]))\n  ))\n\n  // Let's throw limit into the mix\n  it('findAll.order.above.below.limit', assertCompletes(() =>\n    data.findAll({ a: 20 })\n      .above({ id: 2 })\n      .order('id').below({ id: 4 }).limit(1)\n      .fetch()\n      .do(res => compareWithoutVersion(res, [ { id: 2, a: 20, b: 1 } ]))\n  ))\n\n  // Let's do it on the collection\n  it('order.above.below.limit', assertCompletes(() =>\n    data.below({ id: 4 })\n      .order('id')\n      .above({ id: 2 })\n      .limit(1)\n      .fetch()\n      .do(res => compareWithoutVersion(res, [ { id: 2, a: 20, b: 1 } ]))\n  ))\n\n  // Let's try a big compound example\n  it('findAll.order.above.below.limit', assertCompletes(() =>\n    data.findAll({ a: 20 })\n      .order('id')\n      .above({ id: 2 })\n      .below({ id: 4 }, 'closed')\n      .limit(2)\n      .fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n      ]))\n  ))\n\n  // Can't chain off vararg `findAll`\n  it('throws if order is chained off a multi-arg findAll', assertThrows(\n    'order cannot be called on the current query',\n    () => data.findAll({ a: 20 }, { a: 50 }).order('id').fetch()\n  ))\n}}\n"
  },
  {
    "path": "client/test/collection.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes, removeAllData, compareSetsWithoutVersion } from './utils'\n\nexport default function collectionSuite(getHorizon, getData, getTestData) {\n  return () => {\n  let horizon, data, testData, empty_collection\n\n  before(() => {\n    horizon = getHorizon()\n    data = getData()\n    testData = getTestData()\n  })\n\n  // We'll need a separate empty collection\n  before(done => {\n    empty_collection = horizon('empty_test_collection')\n    removeAllData(empty_collection, done)\n  })\n\n  // Grab everything from the collection.\n  it('allows getting all values from the collection', assertCompletes(() =>\n    data.fetch()\n      .do(res => compareSetsWithoutVersion(testData, res))\n  ))\n\n  // Reading from an empty collection should result in an empty array\n  it('returns an empty array from an empty collection', assertCompletes(() =>\n    empty_collection.fetch()\n      .do(res => compareSetsWithoutVersion(res, []))\n  ))\n\n  // Test forEach for promise behavior\n  it('Allows iterating over the entire collection', done => {\n    let didSomething = false\n    data.fetch().forEach(results => {\n      didSomething = true\n    }).then(() => {\n      if (didSomething) {\n        done()\n      } else {\n        done(new Error(\"Didn't do anything\"))\n      }\n    }).catch(err => done(err))\n  })\n}}\n"
  },
  {
    "path": "client/test/find.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion } from './utils'\n\nexport default function findSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's grab a specific document using `find`\n  it('locates a single document when passed an id', assertCompletes(() =>\n    data.find(1).fetch()\n      .do(res => compareWithoutVersion(res, { id: 1, a: 10 }))\n  ))\n\n  // This is equivalent to searching by field `id`\n  it('locates a single document when passed an object with an id field',\n     assertCompletes(() =>\n       data.find({ id: 1 }).fetch()\n         .do(res => compareWithoutVersion(res, { id: 1, a: 10 }))\n  ))\n\n  // `find` returns `null` if a document doesn't exist.\n  it(`returns null if an object doesn't exist`, assertCompletes(() =>\n    data.find('abracadabra').fetch()\n      .do(res => assert.equal(res, null))\n  ))\n\n  // Looking for `null` is an error. RethinkDB doesn't allow secondary\n  // index values to be `null`.\n  it('throws an error if called with null', assertThrows(\n    'The argument to find must be non-null',\n    () => data.find(null).fetch()\n  ))\n\n  // Looking for `undefined` is also an error.\n  it('throws an error if called with undefined', assertThrows(\n    'The 1st argument to find must be defined',\n    () => data.find(undefined).fetch()\n  ))\n\n  it('throws an error if no arguments are passed', assertThrows(\n    'find must receive exactly 1 argument',\n    () => data.find().fetch()\n  ))\n\n  // The document passed to `find` can't be empty\n  it('errors if the document passed is empty', assertErrors(() =>\n    data.find({}).fetch(),\n    /must have at least 1 children/\n  ))\n\n  // We can also `find` by a different (indexed!) field. In that case,\n  // `find` will return the first match.\n  it('locates documents by other fields if passed an object',\n     assertCompletes(() =>\n       data.find({ a: 10 }).fetch()\n         .do(res => compareWithoutVersion(res, { id: 1, a: 10 }))\n  ))\n\n  // Let's try this again for a value that doesn't exist.\n  it('returns null if a document with the given value doesnt exist',\n     assertCompletes(() => data.find({ a: 100 }).fetch()\n                     .do(res => assert.equal(res, null))\n  ))\n\n  // Let's try this again for a field that doesn't exist.\n  it('returns null if no object with the given field exists',\n     assertCompletes(() => data.find({ field: 'a' }).fetch()\n                     .do(res => assert.equal(res, null))\n  ))\n\n  // Let's try this again, now with multiple results.\n  it('returns one result even if several documents match', assertCompletes(() =>\n    data.find({ a: 20 }).fetch()\n      // The id should be one of 2, 3, or 4\n      .do(res => {\n        assert.include([ 2, 3, 4 ], res.id)\n      })\n  ))\n\n  // Users can pass multiple fields to look for\n  it('can find documents when constrained by multiple field values', assertCompletes(() =>\n    data.find({ a: 20, b: 1 }).fetch()\n      .do(res => compareWithoutVersion(res, { id: 2, a: 20, b: 1 }))\n  ))\n\n  // In this case there is no matching document\n  it(`wont return anything if documents dont match`, assertCompletes(() =>\n    data.find({ a: 20, c: 100 }).fetch()\n      .do(res => assert.equal(res, null))\n  ))\n\n  // Passing multiple arguments to find should return a nice error\n  it('throws an error if multiple arguments are passed', assertThrows(\n    'find must receive exactly 1 argument',\n    () => data.find(1, { id: 1 }).fetch()\n  ))\n\n\n  it('emits null when the document id is not found', done => {\n    let gotResult = false\n    return data.find('does_not_exist').fetch().subscribe({\n      next(result) {\n        gotResult = true\n        assert.deepEqual(result, null)\n      },\n      error(err) {\n        done(err)\n      },\n      complete() {\n        if (!gotResult) {\n          done(new Error('never received result'))\n        } else {\n          done()\n        }\n      },\n    })\n  })\n}}\n"
  },
  {
    "path": "client/test/findAll.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function findAllSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's grab a specific document using `findAll`\n  it('looks up documents by id when given a non-object', assertCompletes(() =>\n    data.findAll(1).fetch()\n      .do(res => compareSetsWithoutVersion(res, [ { id: 1, a: 10 } ]))\n  ))\n\n  // This is equivalent to searching by field `id`\n  it('looks up documents when the id field is given explicitly', assertCompletes(() =>\n    data.findAll({ id: 1 }).fetch()\n      .do(res => compareSetsWithoutVersion(res, [ { id: 1, a: 10 } ]))\n  ))\n\n  // `findAll` returns `[]` if a document doesn't exist.\n  it('returns nothing if no documents match', assertCompletes(() =>\n    data.findAll('abracadabra').fetch()\n      .do(res => compareSetsWithoutVersion(res, []))\n  ))\n\n  // We can also `findAll` by a different (indexed!) field.\n  it('returns objects matching non-primary fields', assertCompletes(() =>\n    data.findAll({ a: 10 }).fetch()\n      .do(res => compareSetsWithoutVersion(res, [{ id: 1, a: 10 } ]))\n  ))\n\n  // Let's try this again for a value that doesn't exist.\n  it('returns nothing if no documents match the criteria', assertCompletes(() =>\n    data.findAll({ a: 100 }).fetch()\n      .do(res => compareSetsWithoutVersion(res, []))\n  ))\n\n  // Let's try this again for a field that doesn't exist.\n  it(`returns nothing if the field provided doesn't exist`, assertCompletes(() =>\n    data.findAll({ field: 'a' }).fetch()\n      .do(res => compareSetsWithoutVersion(res, []))\n  ))\n\n  // Let's try this again, now with multiple results.\n  it('returns multiple values when several documents match', assertCompletes(() =>\n    data.findAll({ a: 20 }).fetch()\n      // There are three docs where `a == 20`\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n        { id: 4, a: 20, b: 3 },\n      ]))\n  ))\n\n  // Looking for `null` is an error since secondary index values cannot be\n  // `null` in RethinkDB.\n  it('throws an error when null is passed', assertThrows(\n    'The 1st argument to findAll must be non-null',\n    () => data.findAll(null).fetch()\n  ))\n\n  // No args is ok, because people will be using `apply`\n  it('throws an error when passed no arguments', assertThrows(\n    'findAll must receive at least 1 argument.',\n    () => data.findAll().fetch()\n  ))\n\n  // Looking for an empty object is also an error\n  it('errors when an empty object is passed', assertErrors(() =>\n    data.findAll({}).fetch(),\n    /\"find\" is required/\n  ))\n\n  // `findAll` lets us look for multiple documents. Let's try it on a primary\n  // key.\n  it('can be passed multiple documents to look for', assertCompletes(() =>\n    data.findAll(1, { id: 2 }, 20).fetch()\n      // There are two docs where `a == 20`\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n      ]))\n  ))\n\n  // Let's try a mix of primary and secondary keys, with some missing\n  it('can locate a mix of primary and secondary keys', assertCompletes(() =>\n    data.findAll({ a: 20 }, { id: 200 }, 1, { a: 200 }).fetch()\n      // There are three docs where `a == 20`\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n        { id: 4, a: 20, b: 3 },\n      ]))\n  ))\n\n  // Let's try when everything is missing\n  it('returns nothing when nothing matches', assertCompletes(() =>\n    data.findAll({ field: 1 }, 200, { a: 200 }).fetch()\n      .do(val => compareSetsWithoutVersion(val, []))\n  ))\n\n  // When one thing fails, everything fails.\n  it('throws an error if any argument is null', assertThrows(\n    'The 2nd argument to findAll must be non-null',\n    () => data.findAll(1, null, 2).fetch()\n  ))\n\n  // Let's try it again with an empty object.\n  it('errors if any argument passed is an empty object', assertErrors(() =>\n    data.findAll(1, {}, { a: 20 }).fetch(),\n    /\"find\" is required/\n  ))\n}}\n"
  },
  {
    "path": "client/test/findAllSub.js",
    "content": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default function findAllSubscriptionSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's grab a specific document using 'findAll'\n  it('can find a single document', assertCompletes(() =>\n    observableInterleave({\n      query: data.findAll(1).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's grab a specific document using 'findAll' and also test the 'changed'\n  // event.\n  it('can find a document and reflect changes', assertCompletes(() =>\n    observableInterleave({\n      query: data.findAll(1).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure we don't see events that aren't ours\n  it(\"doesn't see changes to documents outside its range\", assertCompletes(() =>\n    observableInterleave({\n      query: data.findAll(1).watch(),\n      operations: [\n        data.store({ id: 2, a: 1 })\n          .concat(data.store({ id: 2, a: 2 }))\n          .concat(data.remove(2)),\n      ],\n      expected: [\n        [],\n      ],\n    })\n  ))\n\n  // Let's try subscribing to multiple IDs\n  it('can subscribe to multiple ids', assertCompletes(() =>\n    observableInterleave({\n      query: data.findAll(1, 2).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 2, a: 1 })\n          .concat(data.store({ id: 3, a: 1 })),\n        data.store({ id: 1, a: 2 }),\n        data.store({ id: 2, a: 2 })\n          .concat(data.store({ id: 3, a: 2 })),\n        data.remove(1),\n        data.remove(2)\n          .concat(data.remove(3)),\n      ],\n      expected: [\n        [],\n        [ { id: 1, a: 1 } ],\n        [ { id: 1, a: 1 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 1 } ],\n        [ { id: 1, a: 2 }, { id: 2, a: 2 } ],\n        [ { id: 2, a: 2 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure initial vals works correctly\n  it('properly handles initial values', assertCompletes(() =>\n    data.store([ { id: 1, a: 1 }, { id: 2, b: 1 } ]).concat(\n      observableInterleave({\n        query: data.findAll(1, 2).watch(),\n        operations: [\n          data.store({ id: 1, a: 2 }),\n          data.remove(2),\n          data.remove(1),\n        ],\n        expected: [\n          [ { id: 1, a: 1 }, { id: 2, b: 1 } ],\n          [ { id: 1, a: 2 }, { id: 2, b: 1 } ],\n          [ { id: 1, a: 2 } ],\n          [],\n        ],\n      })\n    )\n  ))\n}}\n"
  },
  {
    "path": "client/test/findSub.js",
    "content": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default function findSubscriptionSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n  it('returns an updating document', assertCompletes(() =>\n    observableInterleave({\n      query: data.find(1).watch(),\n      operations: [\n        data.insert({ id: 1, val: 'foo' }),\n        data.replace({ id: 1, val: 'bar' }),\n        data.remove({ id: 1 }),\n      ],\n      expected: [\n        null,\n        { id: 1, val: 'foo' },\n        { id: 1, val: 'bar' },\n        null,\n      ],\n    })\n  ))\n\n  // Let's grab a specific document using `find`\n  it('receives results from store operations', assertCompletes(() =>\n    observableInterleave({\n      query: data.find(1).watch(),\n      operations: [\n        data.store({ id: 1, a: 1 }),\n        data.store({ id: 1, a: 2 }),\n        data.remove(1),\n      ],\n      expected: [\n        null,\n        { id: 1, a: 1 },\n        { id: 1, a: 2 },\n        null,\n      ],\n    })\n  ))\n\n  // Let's make sure we don't see events that aren't ours\n  it(\"doesn't see events that don't belong to it\", assertCompletes(() =>\n    observableInterleave({\n      query: data.find(1).watch(),\n      operations: [ // only one operation\n        data.store({ id: 2, a: 1 }) // irrelevant\n          .concat(data.store({ id: 2, a: 2 })) // irrelevant\n          .concat(data.insert({ id: 1, data: 'blep' })) // relevant\n          .concat(data.remove(2)), // removing irrelevant\n        data.remove(1), // triggered after relevant only\n      ],\n      expected: [\n        null,\n        { id: 1, data: 'blep' },\n        null,\n      ],\n    })\n  ))\n\n  // Let's make sure initial vals works correctly\n  it('properly handles initial values', assertCompletes(() =>\n    // before starting the feed, insert initial document\n    data.store({ id: 1, a: 1 })\n      .concat(observableInterleave({\n        query: data.find(1).watch(),\n        operations: [\n          data.store({ id: 1, a: 2 }),\n          data.remove(1),\n        ],\n        expected: [\n          { id: 1, a: 1 },\n          { id: 1, a: 2 },\n          null,\n        ],\n      })\n    )\n  ))\n\n  it('emits null when the document id is not found', assertCompletes(() => {\n    return observableInterleave({\n      query: data.find('does_not_exist').watch(),\n      operations: [],\n      expected: [ null ],\n    })\n  }))\n}}\n"
  },
  {
    "path": "client/test/horizonObject.js",
    "content": "'use strict'\n\n// Test object creation, the `disconnect` method, and `connected/disconnected`\n// events.\n\nfunction doneWrap(done) {\n  let alreadyDone = false\n  return (...args) => {\n    if (!alreadyDone) {\n      alreadyDone = true\n      return done(...args)\n    }\n  }\n}\n\nexport default function horizonObjectSuite() {\n  describe('Horizon', () => {\n    it('connects and can track its status', done => {\n      let oneDone = doneWrap(done)\n      Horizon.clearAuthTokens()\n      const horizon = Horizon({ secure: false })\n      assert.isDefined(horizon)\n      horizon.status(\n        stat => {\n          switch (stat.type) {\n          case 'unconnected':\n            break\n          case 'ready':\n            horizon.disconnect()\n            break\n          case 'error':\n            oneDone(new Error('Got an error in socket status'))\n            break\n          case 'disconnected':\n            oneDone()\n            break\n          default:\n            oneDone(new Error(`Received unknown status type ${stat.type}`))\n          }\n        },\n        () => oneDone(new Error('Got an error in status'))\n      )\n      horizon.connect(err => oneDone(err))\n    })\n\n    it('errors when it gets the wrong host', done => {\n      // Note -- the connection string specifies a bad host.\n      const horizon = Horizon({\n        host: 'wrong_host',\n        secure: false,\n      })\n      assert.isDefined(horizon)\n      horizon.status().take(3).toArray().subscribe(statuses => {\n        const expected = [\n          { type: 'unconnected' },\n          { type: 'error' }, // socket\n          { type: 'disconnected' },\n        ]\n        assert.deepEqual(expected, statuses)\n        done()\n      })\n      horizon.connect() // no-op error handler, already covered\n    })\n  })\n}\n"
  },
  {
    "path": "client/test/insert.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function insertSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // The `insert` command stores documents in the database, and errors if\n  // the documents already exist.\n  it('stores documents in db, errors if documents already exist', assertErrors(() =>\n    data.insert({ id: 1, a: 1, b: 1 }).toArray()\n      // Should return an array with an ID of the inserted\n      // document.\n      .do(res => compareWithoutVersion([ { id: 1 } ], res))\n      // Let's make sure we get back the document that we put in.\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion({ id: 1, a: 1, b: 1 }, res))\n      // Let's attempt to overwrite the document now. This should error.\n      .mergeMapTo(data.insert({ id: 1, c: 1 })),\n      /The document already exists/\n  ))\n\n  // If we insert a document without an ID, the ID is generated for us.\n  // Let's run the same test as above (insert the document and then\n  // attempt to overwrite it), but have the ID be generated for us.\n  it(`generates ids if documents don't already have one`, assertErrors(() => {\n    let new_id\n\n    return data.insert({ a: 1, b: 1 }).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 1)\n        assert.isString(res[0].id)\n        new_id = res[0].id\n      })\n      // Let's make sure we get back the document that we put in.\n      .mergeMap(() => data.find(new_id).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion({ id: new_id, a: 1, b: 1 }, res))\n      // Let's attempt to overwrite the document now\n      .mergeMap(() => data.insert({ id: new_id, c: 1 }))\n    }, /The document already exists/\n  ))\n\n  it('fails if null is passed', assertThrows(\n    'The argument to insert must be non-null',\n    () => data.insert(null)\n  ))\n\n  it('fails if undefined is passed', assertThrows(\n    'The 1st argument to insert must be defined',\n    () => data.insert(undefined)\n  ))\n\n  it('fails if given no argument', assertThrows(\n    'insert must receive exactly 1 argument',\n    () => data.insert()\n  ))\n\n  // The `insert` command allows storing multiple documents in one call.\n  // Let's insert a few kinds of documents and make sure we get them back.\n  it('can store multiple documents in one call', assertCompletes(() => {\n    let new_id_0, new_id_1\n\n    return data.insert([\n        {},\n        { a: 1 },\n        { id: 1, a: 1 },\n    ]).toArray()\n      .do(res => {\n        // should return an array with the IDs of the documents in\n        // order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 3)\n        assert.isString(res[0].id)\n        assert.isString(res[1].id)\n        assert.equal(1, res[2].id)\n\n        new_id_0 = res[0].id\n        new_id_1 = res[1].id\n      })\n      // Make sure we get what we put in.\n      .mergeMap(() =>\n               data.findAll(new_id_0, new_id_1, 1).fetch())\n      // We're supposed to get an array of documents we put in\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: new_id_0 },\n        { id: new_id_1, a: 1 },\n        { id: 1, a: 1 },\n      ]))\n  }))\n\n  // If any operation in a batch insert fails, everything is reported as a\n  // failure.\n  it('gets an Error object if an operation in a batch fails', assertCompletes(() =>\n    // Lets insert a document that will trigger a duplicate error when we\n    // attempt to reinsert it\n    data.insert({ id: 2, a: 2 })\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion(res, { id: 2 }))\n      // Let's make sure we get back the document that we put in.\n      .mergeMap(() => data.find(2).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 2, a: 2 }))\n      // One of the documents in the batch already exists\n      .mergeMap(() => data.insert([\n        { id: 1, a: 1 },\n        { id: 2, a: 2 },\n        { id: 3, a: 3 },\n      ]))\n      .toArray()\n      .do(results => {\n        assert.equal(results[0].id, 1)\n        assert.instanceOf(results[1], Error)\n        assert.equal(results[2].id, 3)\n      })\n  ))\n\n  // Let's trigger a failure in an insert batch again, this time by making\n  // one of the documents `null`.\n  it('fails if any member of batch is null', assertErrors(() =>\n    data.insert([ { a: 1 }, null, { id: 1, a: 1 } ]),\n    /must be an object/\n  ))\n\n  // Inserting an empty batch of documents is ok, and returns an empty\n  // array.\n  it('can store empty batches', assertCompletes(() =>\n    data.insert([])\n      .do(res => {\n        // should return an array with the IDs of the documents\n        // in order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 0)\n      })\n  ))\n}}\n"
  },
  {
    "path": "client/test/limit.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion } from './utils'\n\nexport default function limitSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Limit returns an array of documents\n  it('can return an array of documents', assertCompletes(() =>\n    data.order('id').limit(2).fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 1, a: 10 },\n        { id: 2, a: 20, b: 1 },\n      ]))\n  ))\n\n  // We can chain `limit` off a collection\n  it('can be called on a collection directly', assertCompletes(() =>\n    data.limit(2).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // Or off other things\n  it('can be called on findAll', assertCompletes(() =>\n    data.findAll({ a: 20 }).limit(2).fetch()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 2)\n      })\n  ))\n\n  // `limit(0)` is ok\n  it('can accept an argument of 0', assertCompletes(() =>\n    data.limit(0).fetch()\n      .do(res => compareWithoutVersion(res, []))\n  ))\n\n  // `limit(null)` is an error\n  it('throws if it receives null', assertThrows(\n    'The argument to limit must be non-null',\n    () => data.limit(null).fetch()\n  ))\n\n  // `limit(-1)` is an error\n  it('errors if it receives a negative argument', assertErrors(() =>\n    data.limit(-1).fetch(),\n    /\"find\" is required/\n  ))\n\n  // `limit(non_int)` is an error\n  it(`errors if the argument to limit isn't a number`, assertErrors(() =>\n    data.limit('k').fetch(),\n    /\"find\" is required/\n  ))\n\n  // Chaining off of limit is illegal\n  it('throws if findAll is called on it', assertThrows(\n    'findAll cannot be called on the current query',\n    () => data.limit(1).findAll({ id: 1 }).fetch()\n  ))\n  it('throws if below is called on it', assertThrows(\n    'below cannot be called on the current query',\n    () => data.limit(1).below({ id: 1 }).fetch()\n  ))\n  it('throws if above is called on it', assertThrows(\n    'above cannot be called on the current query',\n    () => data.limit(1).above({ id: 1 }).fetch()\n  ))\n  it('throws if order is called on it', assertThrows(\n    'order cannot be called on the current query',\n    () => data.limit(1).order('id').fetch()\n  ))\n}}\n"
  },
  {
    "path": "client/test/order.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion } from './utils'\n\nimport cloneDeep from 'lodash.clonedeep'\nimport sortBy from 'lodash.sortby'\n\nexport default function orderSuite(getData, getTestData) {\n  return () => {\n  let data, testData\n\n  before(() => {\n    data = getData()\n    testData = getTestData()\n  })\n\n  // We can order by a field (default order is ascending)\n  it('orders results by a field', assertCompletes(() =>\n    data.order('id').fetch()\n      .do(res => compareWithoutVersion(res, testData))\n  ))\n\n  // That's the same as passing `ascending` explicitly\n  it('orders results ascending implicitly', assertCompletes(() =>\n    data.order('id', 'ascending').fetch()\n      .do(res => compareWithoutVersion(res, testData))\n  ))\n\n  // We can also sort in descending order\n  it('can order results in descending order', assertCompletes(() =>\n    data.order('id', 'descending').fetch()\n      .do(res => compareWithoutVersion(res, cloneDeep(testData).reverse()))\n  ))\n\n  // Let's try ordering by a different field.\n  it('can order results by a field other than id', assertCompletes(() =>\n    data.order('b').fetch()\n      .do(res => compareWithoutVersion(res.slice(0, 3), [\n        { id: 2, a: 20, b: 1 },\n        { id: 3, a: 20, b: 2 },\n        { id: 4, a: 20, b: 3 },\n      ]))\n  ))\n\n  // Let's try ordering by a different field descneding.\n  it('can order results by another field in descending order', assertCompletes(() =>\n    data.order('b', 'descending').fetch()\n      .do(res => compareWithoutVersion(res.slice(0, 3), [\n        { id: 4, a: 20, b: 3 },\n        { id: 3, a: 20, b: 2 },\n        { id: 2, a: 20, b: 1 },\n      ]))\n  ))\n\n  // Let's try to order by a missing field\n  it('returns no documents if a bad field is given', assertCompletes(() =>\n    data.order('abracadabra').fetch()\n      .do(res => compareWithoutVersion(res, []))\n  ))\n\n  // We can pass multiple fields to `order` to disambiguate.\n  it('can order by multiple fields', assertCompletes(() =>\n    data.order([ 'a', 'id' ]).fetch()\n      .do(res => compareWithoutVersion(res, sortBy(testData, [ 'a', 'id' ])))\n  ))\n\n  // We can pass multiple fields to `order` to disambiguate. Let's do it in\n  // descending order.\n  it('can order by multiple fields descending', assertCompletes(() =>\n    data.order([ 'a', 'id' ], 'descending').fetch()\n      .do(res => compareWithoutVersion(res, sortBy(testData, ['a', 'id']).reverse()))\n  ))\n\n  // `order` cannot accept any keys that are present in `findAll`\n  it('cannot accept any keys that are present in findAll', assertErrors(() =>\n    data.findAll({id: 1}).order('id').fetch(),\n    /\"id\" cannot be used in \"order\", \"above\", or \"below\" when finding by that field/\n  ))\n\n  it(`errors if the 2nd argument isn't 'ascending' or 'descending'`,\n     assertErrors(() => data.order('id', 'foo').fetch(),\n     /\"find\" is required/\n  ))\n\n  // Passing no arguments, null, bad arguments, or too many arguments is\n  // an error.\n  it('throws if it receives no arguments', assertThrows(\n    'order must receive at least 1 argument.',\n    () => data.order().fetch()\n  ))\n  it('throws if it receives a null argument', assertThrows(\n    'The 1st argument to order must be non-null',\n    () => data.order(null).fetch()\n  ))\n  it('throws if its first argument is null', assertThrows(\n    'The 1st argument to order must be non-null',\n    () => data.order(null, 'foo').fetch()\n  ))\n  it('throws if it receives more than 2 arguments', assertThrows(\n    'order accepts at most 2 arguments.',\n    () => data.order('id', 'ascending', 1).fetch()\n  ))\n}}\n"
  },
  {
    "path": "client/test/orderLimitSub.js",
    "content": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default function orderLimitSubSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  it(`can find a single document`, assertCompletes(() =>\n    observableInterleave({\n      query: data.order('score').limit(1).watch(),\n      operations: [\n        data.store({ id: 1, score: 1 }),\n        data.remove(1),\n      ],\n      expected: [\n        [],\n        [ { id: 1, score: 1 } ],\n        [],\n      ],\n    })\n  ))\n\n  it('will swap out a document that goes out of range', assertCompletes(() =>\n    data.store({ id: 1, score: 200 }).concat(\n      observableInterleave({\n        query: data.order('score').limit(1).watch(),\n        operations: [\n          data.store({ id: 2, score: 100 }),\n          data.remove(2),\n          data.remove(1),\n        ],\n        expected: [\n          [ { id: 1, score: 200 } ],\n          [ { id: 2, score: 100 } ],\n          [ { id: 1, score: 200 } ],\n          [],\n        ],\n      })\n    )\n  ))\n\n  it('reflects changes that change sort order', assertCompletes(() =>\n    observableInterleave({\n      query: data.order('score').limit(2).watch(),\n      operations: [\n        data.store({ id: 1, score: 200 }),\n        data.store({ id: 2, score: 100 }),\n        data.store({ id: 1, score: 50 }),\n        data.remove(1),\n        data.remove(2),\n      ],\n      expected: [\n        [],\n        [ { id: 1, score: 200 } ],\n        [ { id: 2, score: 100 }, { id: 1, score: 200 } ],\n        [ { id: 1, score: 50 }, { id: 2, score: 100 } ],\n        [ { id: 2, score: 100 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's make sure we don't see events that aren't ours\n  it(\"doesn't see changes to documents outside its range\", assertCompletes(() =>\n    observableInterleave({\n      query: data.order('score').limit(1).watch(),\n      operations: [\n        data.store({ id: 1, score: 100 })\n          .concat(data.store({ id: 2, score: 200 }))\n          .concat(data.remove(2))\n          .concat(data.remove(1)),\n      ],\n      expected: [\n        [],\n        [ { id: 1, score: 100 } ],\n        [],\n      ],\n    })\n  ))\n\n  // Let's try subscribing to multiple IDs\n  it('respects descending order', assertCompletes(() =>\n    observableInterleave({\n      query: data.order('score', 'descending').limit(3).watch(),\n      operations: [\n        data.store({ id: 1, score: 10 }),\n        data.store({ id: 2, score: 20 }),\n        data.store({ id: 3, score: 15 }),\n        data.remove(2),\n        data.remove(1),\n        data.remove(3),\n      ],\n      expected: [\n        [],\n        [ { id: 1, score: 10 } ],\n        [ { id: 2, score: 20 }, { id: 1, score: 10 } ],\n        [ { id: 2, score: 20 }, { id: 3, score: 15 }, { id: 1, score: 10 } ],\n        [ { id: 3, score: 15 }, { id: 1, score: 10 } ],\n        [ { id: 3, score: 15 } ],\n        [],\n      ],\n    })\n  ))\n\n  it('properly handles documents coming in and out of range', assertCompletes(() =>\n    observableInterleave({\n      query: data.order('score').limit(2).watch(),\n      operations: [\n        data.store({ id: 1, score: 100 }), // after 1, results in 2\n        data.store({ id: 2, score: 200 }), // after 2, results in 3\n        data.store({ id: 3, score: 300 })\n          .concat(data.store({ id: 3, score: 50 })),  // after 3, results in 4\n        data.remove(1), // after 4, results in 5\n        data.store({ id: 2, score: 20 }), // after 5, results in 6\n        data.remove(2), // after 6, results in 7\n        data.remove(3), // after 7, results in 8\n      ],\n      expected: [\n        [], // 1\n        [ { id: 1, score: 100 } ], // 2\n        [ { id: 1, score: 100 }, { id: 2, score: 200 } ], // 3\n        [ { id: 3, score: 50 }, { id: 1, score: 100 } ], // 4\n        [ { id: 3, score: 50 }, { id: 2, score: 200 } ], // 5\n        [ { id: 2, score: 20 }, { id: 3, score: 50 } ], // 6\n        [ { id: 3, score: 50 } ], // 7\n        [], // 8\n      ],\n    })\n  ))\n}}\n"
  },
  {
    "path": "client/test/remove.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/operator/concat'\nimport 'rxjs/add/operator/map'\nimport 'rxjs/add/operator/ignoreElements'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         removeAllData,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function removeSuite(getData) {\n  return () => {\n  let data\n  const testData = [\n    { id: 1, a: 1 },\n    { id: 2, a: 2 },\n    { id: 3, a: 3 },\n    { id: 'do_not_remove_1' },\n    { id: 'do_not_remove_2' },\n  ]\n\n  before(() => {\n    data = getData()\n  })\n\n  // Drop all the existing data\n  before(done => {\n    removeAllData(data, done)\n  })\n\n  // Insert the test data and make sure it's in\n  before(assertCompletes(() =>\n    data.store(testData).ignoreElements()\n      .concat(data.fetch())\n      // Make sure it's there\n      .do(res => compareSetsWithoutVersion(res, testData))\n  ))\n\n  it('removes a document when passed an id', assertCompletes(() =>\n    data.remove(1)\n      .do(res => compareWithoutVersion(res, { id: 1 }))\n      // Let's make sure the removed document isn't there\n      .mergeMapTo(data.find(1).fetch())\n      // Let's make sure the removed document isn't there\n      .do(res => assert.isNull(res))\n  ))\n\n  it('removes a document with an id field', assertCompletes(() =>\n    data.remove({ id: 2 })\n      .do(res => compareWithoutVersion(res, { id: 2 }))\n      // Let's make sure the removed document isn't there\n      .mergeMapTo(data.find(2).fetch())\n      // Let's make sure the removed document isn't there\n      .do(res => assert.isNull(res))\n  ))\n\n  it(`removing a document that doesn't exist doesn't error`, assertCompletes(() =>\n    data.remove('abracadabra').do(res => assert.deepEqual(res, { id: 'abracadabra' }))\n  ))\n\n  it('fails when called with no arguments', assertThrows(\n    'remove must receive exactly 1 argument',\n    () => data.remove()\n  ))\n\n  it('fails when called with null', assertThrows(\n    'The argument to remove must be non-null',\n    () => data.remove(null)\n  ))\n\n  // Give an error if the user tries to use varargs (to help avoid\n  // confusion)\n  it('fails when called with more than one argument', assertThrows(\n    'remove must receive exactly 1 argument',\n    () => data.remove(1, 2)\n  ))\n\n  // Check that the remaining documents are there\n  it(`doesn't remove documents we didn't ask it to`, assertCompletes(() =>\n    data.fetch()\n      .map(docs => docs.map(x => x.id))\n      .do(res => assert.includeMembers(\n        res, [ 'do_not_remove_1', 'do_not_remove_2' ]))\n  ))\n}}\n"
  },
  {
    "path": "client/test/removeAll.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/operator/concat'\nimport 'rxjs/add/operator/map'\nimport 'rxjs/add/operator/ignoreElements'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         removeAllData,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function removeAllSuite(getData) {\n  return () => {\n  let data\n  const testData = [\n    { id: 1, a: 1 },\n    { id: 2, a: 2 },\n    { id: 3, a: 3 },\n    { id: 4, a: 4 },\n    { id: 'do_not_remove_1' },\n    { id: 'do_not_remove_2' },\n  ]\n\n  before(() => {\n    data = getData()\n  })\n\n  // Drop all the existing data\n  before(done => {\n    removeAllData(data, done)\n  })\n\n  // Insert the test data and make sure it's in\n  before(assertCompletes(() =>\n    data.store(testData).ignoreElements()\n     .concat(data.fetch())\n     // Make sure it's there\n     .do(res => compareSetsWithoutVersion(res, testData))\n  ))\n\n  // All right, let's remove a document. The promise resolves with no\n  // arguments.\n  it('removes documents when an array of ids is passed', assertCompletes(() =>\n    data.removeAll([ 1 ])\n      .do(res => compareWithoutVersion(res, { id: 1 }))\n      // Let's make sure the removed document isn't there\n      .mergeMapTo(data.find(1).fetch())\n      // Let's make sure the removed document isn't there\n      .do(res => assert.isNull(res))\n  ))\n\n  // Passing an array of objects to `removeAll` is also ok.\n  it('removes documents when array elements are objects', assertCompletes(() =>\n    data.removeAll([ { id: 2 } ])\n      .do(res => compareWithoutVersion(res, { id: 2 }))\n      // Let's make sure the removed document isn't there\n      .mergeMapTo(data.find(2).fetch())\n      // Let's make sure the removed document isn't there\n      .do(res => assert.isNull(res))\n  ))\n\n  // We can also remove multiple documents\n  it('removes multiple documents by id or as objects', assertCompletes(() =>\n    data.removeAll([ 3, 50, { id: 4 } ]).toArray()\n      .do(res => compareWithoutVersion(res, [ { id: 3 }, { id: 50 }, { id: 4 } ]))\n      // Let's make sure the removed document isn't there\n      .mergeMapTo(data.findAll(3, 50, 4).fetch())\n      // Let's make sure the removed document isn't there\n      .do(res => assert.deepEqual(res, []))\n  ))\n\n  // Removing a missing document shouldn't generate an error.\n  it('removes a non-existent document without error', assertCompletes(() =>\n    data.removeAll([ 'abracadabra' ])\n      .do(res => assert.deepEqual(res, { id: 'abracadabra' })),\n    /document was missing/\n  ))\n\n  // Calling `removeAll` with an empty array is also ok.\n  it(`doesn't error when an empty array is passed`, assertCompletes(() =>\n    data.removeAll([])\n      .do(res => assert.fail())\n  ))\n\n  // But an array with a `null` is an error.\n  it('errors when a null in an array is passed', assertErrors(() =>\n    data.removeAll([ null ]),\n    /must be an object/\n  ))\n\n  // Calling `removeAll` with anything but a single array is an error.\n  it('throws when no arguments are passed', assertThrows(\n    'removeAll takes an array as an argument',\n    () => data.removeAll()\n  ))\n  it('throws when more than one argument is passed', assertThrows(\n    'removeAll must receive exactly 1 argument',\n    () => data.removeAll([ 1 ], 2)\n  ))\n  it('throws when null is passed', assertThrows(\n    'removeAll takes an array as an argument',\n    () => data.removeAll(null)\n  ))\n  it('throws when passed a number', assertThrows(\n    'removeAll takes an array as an argument',\n    () => data.removeAll(1)\n  ))\n  it('throws when passed a string', assertThrows(\n    'removeAll takes an array as an argument',\n    () => data.removeAll('1')\n  ))\n  it('throws when passed an object', assertThrows(\n    'removeAll takes an array as an argument',\n    () => data.removeAll({ id: 1 })\n  ))\n\n  // Check that the remaining documents are there\n  it(`doesn't remove documents not specified`, assertCompletes(() =>\n    data.fetch()\n      .map(docs => docs.map(x => x.id))\n      .do(res => assert.includeMembers(\n        res, [ 'do_not_remove_1', 'do_not_remove_2' ]))\n  ))\n}}\n"
  },
  {
    "path": "client/test/replace.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function replaceSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's store a document first, then replace it.\n  it('replaces an existing document completely', assertCompletes(() =>\n    data.store({ id: 1, a: { b: 1, c: 1 }, d: 1 }).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion(res, [ { id: 1 } ]))\n      // Let's make sure we get back the document that we put in.\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 1, a: { b: 1, c: 1 }, d: 1 }))\n      // Let's replace the document now\n      .mergeMapTo(data.replace({ id: 1, a: { c: 2 } })).toArray()\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion(res, [ { id: 1 } ]))\n      // Make sure `replace` replaced the original document\n      .mergeMapTo(data.find(1).fetch())\n      // Check that the document was updated correctly\n      .do(res => compareWithoutVersion(res, { id: 1, a: { c: 2 } }))\n  ))\n\n  // The `replace` command replaces documents already in the database. It\n  // errors if the document doesn't exist.\n  it('fails if the document does not already exist', assertErrors(() =>\n    data.replace({ id: 1, a: 1, b: 1 }),\n    /document was missing/\n  ))\n\n  // It means you can't replace a document without providing an id.\n  it('fails if document does not have an id', assertErrors(() =>\n    data.replace({ a: 1, b: 1 }),\n    /\"id\" is required/\n  ))\n\n  // Calling `replace` with `null` is an error.\n  it('fails if null is passed', assertThrows(\n    'The argument to replace must be non-null',\n    () => data.replace(null)\n  ))\n\n  // Calling `replace` with `undefined` is also an error.\n  it('fails if undefined is passed', assertThrows(\n    'The 1st argument to replace must be defined',\n    () => data.replace(undefined)\n  ))\n\n  it('fails if passed no arguments', assertThrows(\n    'replace must receive exactly 1 argument',\n    () => data.replace()\n  ))\n\n  // The `replace` command allows storing multiple documents in one call.\n  // Let's replace a few documents and make sure we get them back.\n  it('allows replacing multiple documents with one call', assertCompletes(() =>\n    data.store([\n      { id: 1, a: { b: 1, c: 1 }, d: 1 },\n      { id: 2, a: { b: 2, c: 2 }, d: 2 },\n    ]).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion(res, [ { id: 1 }, { id: 2 } ]))\n      // Let's make sure we get back the documents that we put in.\n      .mergeMapTo(data.findAll(1, 2).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: { b: 1, c: 1 }, d: 1 },\n        { id: 2, a: { b: 2, c: 2 }, d: 2 },\n      ]))\n      // All right. Let's update the documents now\n      .mergeMapTo(data.replace([\n        { id: 1, a: { c: 2 } },\n        { id: 2, d: 3 },\n      ]))\n      .toArray()\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion(res, [ { id: 1 }, { id: 2 } ]))\n      // Make sure `update` updated the documents properly\n      .mergeMapTo(data.findAll(1, 2).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: { c: 2 } },\n        { id: 2, d: 3 },\n      ]))\n  ))\n\n  it('fails if any document in a batch is null', assertErrors(() =>\n    data.replace([ { id: 1, a: 1 }, null ]),\n    /must be an object/\n  ))\n\n  // Replacing an empty batch of documents is ok, and returns an empty\n  // array.\n  it('allows an empty batch of documents', assertCompletes(() =>\n    data.replace([])\n      .do(res => {\n        // should return an array with the IDs of the documents in\n        // order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 0)\n      })\n  ))\n}}\n"
  },
  {
    "path": "client/test/store.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function storeSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // The `store` command stores documents in the database, and overwrites\n  // them if they already exist.\n  it('creates or replaces a document', assertCompletes(() =>\n      data.store({ id: 1, a: 1, b: 1 })\n      // The promise should return an array with an ID of the inserted\n      // document.\n      .do(res => compareWithoutVersion(res, { id: 1 }))\n      // Let's make sure we get back the document that we put in.\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 1, a: 1, b: 1 }))\n      // Let's overwrite the document now\n      .mergeMapTo(data.store({ id: 1, c: 1 }))\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion(res, { id: 1 }))\n      // Make sure `store` overwrote the original document\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 1, c: 1 }))\n  ))\n\n  // If we store a document without an ID, the ID is generated for us.\n  // Let's run the same test as above (store the document and then\n  // overwrite it), but have the ID be generated for us.\n  it('generates ids for documents without them', assertCompletes(() => {\n    let new_id\n\n    return data.store({ a: 1, b: 1 }).toArray()\n      .do(res => {\n        // The promise should return an array with an ID of the\n        // inserted document.\n        assert.lengthOf(res, 1)\n        assert.isObject(res[0])\n        assert.isString(res[0].id)\n        new_id = res[0].id\n      })\n      // Let's make sure we get back the document that we put in.\n      .mergeMap(() => data.find(new_id).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion({ id: new_id, a: 1, b: 1 }, res))\n      // Let's overwrite the document now\n      .mergeMap(() => data.store({ id: new_id, c: 1 }))\n      // We should have gotten the ID back again\n      .do(res => assert.deepEqual(new_id, res.id))\n      // Make sure `store` overwrote the original document\n      .mergeMap(() => data.find(new_id).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion({ id: new_id, c: 1 }, res))\n  }))\n\n  // Storing `null` is an error.\n  it('fails if null is passed', assertThrows(\n    'The argument to store must be non-null',\n    () => data.store(null))\n  )\n\n  // Storing `undefined` is also an error.\n  it('fails if undefined is passed', assertThrows(\n    'The 1st argument to store must be defined',\n    () => data.store(undefined)\n  ))\n\n  // Storing nothing is an error\n  it('fails if no arguments are passed', assertThrows(\n    'store must receive exactly 1 argument',\n    () => data.store()\n  ))\n\n  // The `store` command allows storing multiple documents in one call.\n  // Let's store a few kinds of documents and make sure we get them back.\n  it('allows storing multiple documents in one call', assertCompletes(() => {\n    let new_id_0, new_id_1\n\n    return data.store([ {}, { a: 1 }, { id: 1, a: 1 } ])\n      .toArray()\n      .do(res => {\n        // The promise should return an array with the IDs of the documents\n        // in order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 3)\n        assert.isString(res[0].id)\n        assert.isString(res[1].id)\n        assert.equal(1, res[2].id)\n\n        new_id_0 = res[0].id\n        new_id_1 = res[1].id\n      })\n      // Make sure we get what we put in.\n      .mergeMap(() => data.findAll(new_id_0, new_id_1, 1)\n               .fetch())\n      // We're supposed to get an array of documents we put in\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: new_id_0 },\n        { id: new_id_1, a: 1 },\n        { id: 1, a: 1 },\n      ]))\n  }))\n\n  // If any operation in a batch store fails, everything is reported as a\n  // failure. Note that we're storing `null` below, which is a failure.\n  it('fails if any operation in a batch fails', assertErrors(() =>\n    data.store([ { a: 1 }, null, { id: 1, a: 1 } ]),\n    /must be an object/\n  ))\n\n  // Storing an empty batch of documents is ok, and returns an empty\n  // array.\n  it('allows storing empty batches', assertCompletes(() =>\n    data.store([]).toArray()\n      .do(res => {\n        // The promise should return an array with the IDs of the documents\n        // in order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 0)\n      })\n  ))\n\n  it('stores date objects and retrieves them again', assertCompletes(() => {\n    const originalDate = new Date()\n    return data.store({ date: originalDate }).toArray()\n      .mergeMap(id => data.find(id[0]).fetch())\n      .do(result => assert.deepEqual(originalDate, result.date))\n  }))\n}}\n"
  },
  {
    "path": "client/test/test.html",
    "content": "<!doctype html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <link href=\"mocha.css\" rel=\"stylesheet\"/>\n  <script src=\"horizon.js\"></script>\n</head>\n<body>\n\n  <!-- this page should be loaded from dist/test.html -->\n\n  <div id=\"mocha\"></div>\n\n  <script src=\"test.js\"></script>\n\n</body>\n</html>\n"
  },
  {
    "path": "client/test/test.js",
    "content": "const BROWSER = (typeof window !== 'undefined')\nconst path = require('path')\nconst glob = require('../src/util/glob')\n\nconst global = glob()\n\nif (BROWSER) {\n  // Use source maps in mocha errors (ordinarily source maps\n  // only work inside Developer Tools)\n  require('source-map-support/browser-source-map-support.js')\n  global.sourceMapSupport.install()\n} else {\n  // In node, require source-map-support directly. It is listed\n  // as an external dependency in webpack config, so that it is\n  // not bundled here.\n  require('source-map-support').install()\n}\n\nif (BROWSER) {\n  // Expose global.mocha and global.Mocha\n  require('mocha/mocha.js')\n  // Expose globals such as describe()\n  global.mocha.setup('bdd')\n  global.mocha.timeout(10000)\n} else {\n  global.WebSocket = require('ws')\n\n  if (__dirname.split(path.sep).pop(-1) === 'test') {\n    if (process.env.NODE_ENV === 'test') {\n      global.Horizon = require('../src/index.js')\n    } else {\n      global.Horizon = require('../lib/index.js')\n    }\n  } else {\n    global.Horizon = require('./horizon.js')\n  }\n}\n\nglobal.chai = require('chai/chai.js')\nglobal.chai.config.showDiff = true\nglobal.chai.config.truncateThreshold = 0\nglobal.expect = global.chai.expect\nglobal.assert = global.chai.assert\n\n// Wait until server is ready before proceeding to tests\ndescribe('Waiting until server ready...', function() {\n  this.timeout(60000)\n  it('connected', done => {\n    const tryConnecting = () => {\n      const horizon = Horizon()\n      horizon.onReady(() => {\n        clearInterval(connectInterval)\n        horizon.disconnect()\n        done()\n      })\n      horizon.connect(() => {\n        // Clients disconnect by themselves on failure\n      })\n    }\n    const connectInterval = setInterval(tryConnecting, 5000)\n    tryConnecting()\n  })\n})\n\n// Load the suite runner\nrequire('./api.js')\n\nif (BROWSER) {\n  mocha.run()\n} else {\n  // Run by mocha command\n}\n"
  },
  {
    "path": "client/test/times.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         compareWithoutVersion,\n         removeAllData } from './utils'\n\nexport default function timesSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  after(done => {\n    removeAllData(data, done)\n  })\n\n  let range = count => Array.from(Array(count).keys())\n\n  beforeEach(assertCompletes(() => {\n    const rows = range(16).map(i => (\n      {\n        id: i,\n        value: i % 4,\n        time: new Date(Math.floor(i / 4)),\n      }\n    ))\n    return data.store(rows).toArray()\n      .do(res => {\n        assert.isArray(res)\n        assert.lengthOf(res, 16)\n      })\n  }))\n\n  it('finds a document by a field with a time value', assertCompletes(() =>\n    data.find({ time: new Date(0) }).fetch()\n      .do(res => compareWithoutVersion(res, {\n        id: 0,\n        time: new Date(0),\n        value: 0,\n      }))\n  ))\n\n  it('finds a document by a time field and another field', assertCompletes(() =>\n    data.find({ value: 1, time: new Date(3) }).fetch()\n      .do(res => compareWithoutVersion(res, {\n        id: 13,\n        value: 1,\n        time: new Date(3),\n      }))\n  ))\n\n  it('finds all documents by a field with a time value', assertCompletes(() =>\n    data.findAll({ time: new Date(2) }).fetch()\n      .do(res => compareWithoutVersion(res, range(4).map(i => ({\n        id: i + 8,\n        value: i,\n        time: new Date(2),\n      }))))\n  ))\n\n  it('finds all documents by a time field and another field', assertCompletes(() =>\n    data.findAll({ value: 2, time: new Date(3) }).fetch()\n      .do(res => compareWithoutVersion(res, [ {\n        id: 14,\n        value: 2,\n        time: new Date(3),\n      } ]))\n  ))\n\n  it('finds all documents bounded above by a time', assertCompletes(() =>\n    data.findAll({ value: 3 })\n      .above({ time: new Date(1) })\n      .fetch()\n      .do(res => compareWithoutVersion(res, range(3).map(i => ({\n        id: 3 + (i + 1) * 4,\n        value: 3,\n        time: new Date(i + 1),\n      }))))\n  ))\n\n  it('finds all documents between two times', assertCompletes(() =>\n    data.findAll({ value: 2 })\n      .above({ time: new Date(1) })\n      .below({ time: new Date(3) })\n      .fetch()\n      .do(res => compareWithoutVersion(res, [\n        { id: 6, value: 2, time: new Date(1) },\n        { id: 10, value: 2, time: new Date(2) },\n      ]))\n  ))\n}}\n"
  },
  {
    "path": "client/test/unit/ast.js",
    "content": "import { applyChange } from '../../src/ast'\n\nexport default function unitAstSuite() {\n  describe('applyChanges', () => {\n    it('correctly changes an item with an array id in place', done => {\n      const existingArray = [\n        { id: [ 'A', 'B' ], val: 3 },\n        { id: [ 'B', 'C' ], val: 4 },\n      ]\n      const change = {\n        type: 'change',\n        new_offset: null,\n        old_offset: null,\n        old_val: {\n          id: [ 'B', 'C' ],\n          val: 4,\n        },\n        new_val: {\n          id: [ 'B', 'C' ],\n          val: 5,\n        },\n      }\n      const expected = [\n        { id: [ 'A', 'B' ], val: 3 },\n        { id: [ 'B', 'C' ], val: 5 },\n      ]\n      const obtained = applyChange(existingArray, change)\n      assert.deepEqual(obtained, expected)\n      done()\n    })\n\n    it('correctly deletes an uninitial item with an array id', done => {\n      const existingArray = [\n        { id: [ 'A', 'B' ], val: 3 },\n        { id: [ 'B', 'C' ], val: 4 },\n      ]\n      const change = {\n        type: 'uninitial',\n        old_val: {\n          id: [ 'B', 'C' ],\n          val: 4,\n        },\n      }\n      const expected = [\n        { id: [ 'A', 'B' ], val: 3 },\n      ]\n      const obtained = applyChange(existingArray, change)\n      assert.deepEqual(obtained, expected)\n      done()\n    })\n  })\n}\n"
  },
  {
    "path": "client/test/unit/auth.js",
    "content": "import { TokenStorage, FakeStorage } from '../../src/auth'\n\nexport default function unitAuthSuite() {\n  describe('TokenStorage', () => {\n    let fakeStorage\n    let tokenStore\n    beforeEach(() => {\n      fakeStorage = new FakeStorage()\n      tokenStore = new TokenStorage({\n        authType: 'token',\n        storage: fakeStorage,\n        path: 'testHorizon',\n      })\n    })\n    it('sets a token and retrieves it back', done => {\n      const fakeData = 'some kinda long テスト string'\n      tokenStore.set(fakeData)\n      const obtained = tokenStore.get()\n      assert.equal(obtained, fakeData)\n      done()\n    })\n    it('overwrites a token for the same path', done => {\n      const string1 = 'Test string 1'\n      const string2 = 'Test string 2'\n      tokenStore.set(string1)\n      tokenStore.set(string2)\n      const obtained = tokenStore.get()\n      assert.equal(obtained, string2)\n      done()\n    })\n    it('keeps storage from different paths separate', done => {\n      const otherTokens = new TokenStorage({\n        authType: 'token',\n        path: 'secondHorizon',\n        storage: fakeStorage,\n      })\n      tokenStore.set('A')\n      otherTokens.set('B')\n      const obtainedA = tokenStore.get()\n      assert.equal(obtainedA, 'A')\n      const obtainedB = otherTokens.get()\n      assert.equal(obtainedB, 'B')\n      done()\n    })\n    it('removes tokens', done => {\n      tokenStore.set('A')\n      tokenStore.remove()\n      assert.isUndefined(tokenStore.get())\n      done()\n    })\n    it('removes tokens independently by path', done => {\n      const otherToken = new TokenStorage({\n        authType: 'token',\n        path: 'anotherPath',\n        storage: fakeStorage,\n      })\n      tokenStore.set('A')\n      otherToken.set('B')\n      tokenStore.remove()\n      assert.equal(otherToken.get(), 'B')\n      assert.isUndefined(tokenStore.get())\n      done()\n    })\n  })\n}\n"
  },
  {
    "path": "client/test/unit/utilsTest.js",
    "content": "import validIndexValue from '../../src/util/valid-index-value'\n\nexport default function unitUtilsSuite() {\n  describe('validIndexValue', () => {\n    function assertValid(value) {\n      assert.isTrue(validIndexValue(value),\n                    `${JSON.stringify(value)} should be valid`)\n    }\n    function assertInvalid(value) {\n      assert.isFalse(validIndexValue(value),\n                    `${JSON.stringify(value)} should be invalid`)\n    }\n    it('disallows nulls', done => {\n      const value = null\n      assertInvalid(value)\n      done()\n    })\n    it('disallows undefined', done => {\n      const value = undefined\n      assertInvalid(value)\n      done()\n    })\n    it('allows booleans', done => {\n      const value = true\n      assertValid(value)\n      done()\n    })\n    it('allows strings', done => {\n      const value = 'some kinda string test'\n      assertValid(value)\n      done()\n    })\n    it('allows numbers', done => {\n      const value = 12.33\n      assertValid(value)\n      done()\n    })\n    it('allows Dates', done => {\n      const value = new Date()\n      assertValid(value)\n      done()\n    })\n    it('allows ArrayBuffers', done => {\n      const value = new ArrayBuffer(1)\n      assertValid(value)\n      done()\n    })\n    it('disallows bare objects', done => {\n      const value = { a: 1 }\n      assertInvalid(value)\n      done()\n    })\n    it('allows arrays of primitives', done => {\n      const value = [ true, false, 1, \"foo\", new Date(), new ArrayBuffer(1) ]\n      assertValid(value)\n      done()\n    })\n    it('allows empty arrays', done => {\n      const value = [ ]\n      assertValid(value)\n      done()\n    })\n    it('allows deeply nested arrays', done => {\n      const value = [ [ ], [ [ [ [ 0, 1, [ ] ], [ 1 ] ] ] ] ]\n      assertValid(value)\n      done()\n    })\n    it('disallows arrays containing objects', done => {\n      const value = [ 1, { a: 1 } ]\n      assertInvalid(value)\n      done()\n    })\n    it('disallows arrays with deeply nested objects', done => {\n      const value = [ [ ], [ [ [ [ ], [ {} ] ] ] ] ]\n      assertInvalid(value)\n      done()\n    })\n  })\n}\n"
  },
  {
    "path": "client/test/update.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function updateSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // Let's store a document first, then update it.\n  it('allows updating an existing document', assertCompletes(() =>\n    data.store({ id: 1, a: { b: 1, c: 1 }, d: 1 }).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion([ { id: 1 } ], res))\n      // Let's make sure we get back the document that we put in.\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 1, a: { b: 1, c: 1 }, d: 1 }))\n      // Let's update the document now\n      .mergeMapTo(data.update({ id: 1, a: { c: 2 } })).toArray()\n      // We should have gotten the ID back again\n      .do((res) => compareWithoutVersion([ { id: 1 } ], res))\n      // Make sure `upsert` updated the original document\n      .mergeMapTo(data.find(1).fetch())\n      // Check that the document was updated correctly\n      .do(res => compareWithoutVersion(res, { id: 1, a: { b: 1, c: 2 }, d: 1 }))\n  ))\n\n  // The `update` command updates documents already in the database. It\n  // errors if the document doesn't exist.\n  it(`fails if document doesn't exist`, assertErrors(() =>\n    data.update({ id: 1, a: 1, b: 1 }),\n    /The document was missing/\n  ))\n\n  // It means you can't update a document without providing an id.\n  it('fails if document has no id provided', assertErrors(() =>\n    data.update({ a: 1, b: 1 }),\n    /\"id\" is required/\n  ))\n\n  // Calling `update` with `null` is an error.\n  it('fails if null is passed', assertThrows(\n    'The argument to update must be non-null',\n    () => data.update(null)\n  ))\n\n  // Calling `update` with `undefined` is also an error.\n  it('fails if undefined is passed', assertThrows(\n    'The 1st argument to update must be defined',\n    () => data.update(undefined)\n  ))\n\n  it('fails if no arguments are passed', assertThrows(\n    'update must receive exactly 1 argument',\n    () => data.update()\n  ))\n\n  // The `update` command allows storing multiple documents in one call.\n  // Let's update a few documents and make sure we get them back.\n  it('allows updating multiple documents in one call', assertCompletes(() =>\n    data.store([\n      { id: 1, a: { b: 1, c: 1 }, d: 1 },\n      { id: 2, a: { b: 2, c: 2 }, d: 2 },\n    ]).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion([ { id: 1 }, { id: 2 } ], res))\n      // Let's make sure we get back the documents that we put in.\n      .mergeMapTo(data.findAll(1, 2).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: { b: 1, c: 1 }, d: 1 },\n        { id: 2, a: { b: 2, c: 2 }, d: 2 },\n      ]))\n      // All right. Let's update the documents now\n      .mergeMapTo(data.update([ { id: 1, a: { c: 2 } }, { id: 2, d: 3 } ]))\n      .toArray()\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion(res, [ { id: 1 }, { id: 2 } ]))\n      // Make sure `update` updated the documents properly\n      .mergeMapTo(data.findAll(1, 2).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: 1, a: { b: 1, c: 2 }, d: 1 },\n        { id: 2, a: { b: 2, c: 2 }, d: 3 },\n      ]))\n  ))\n\n  it('fails if any document is null', assertErrors(() =>\n    data.update([ { id: 1, a: 1 }, null ]),\n    /must be an object/\n  ))\n\n  // Updating an empty batch of documents is ok, and returns an empty\n  // array.\n  it('allows updating an empty batch', assertCompletes(() =>\n    data.update([])\n      .do(res => {\n        // should return an array with the IDs of the documents in\n        // order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 0)\n      })\n  ))\n}}\n"
  },
  {
    "path": "client/test/upsert.js",
    "content": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n         assertThrows,\n         assertErrors,\n         compareWithoutVersion,\n         compareSetsWithoutVersion } from './utils'\n\nexport default function upsertSuite(getData) {\n  return () => {\n  let data\n\n  before(() => {\n    data = getData()\n  })\n\n  // The `upsert` command stores documents in the database, and updates\n  // them if they already exist.\n  it(`updates existing documents or creates them if they don't exist`,\n     assertCompletes(() =>\n    data.upsert({ id: 1, a: { b: 1, c: 1 }, d: 1 }).toArray()\n      // should return an array with an ID of the inserted document.\n      .do(res => compareWithoutVersion(res, [ { id: 1 } ]))\n      // Let's make sure we get back the document that we put in.\n      .mergeMapTo(data.find(1).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: 1, a: { b: 1, c: 1 }, d: 1 }))\n      // Let's update the document now\n      .mergeMapTo(data.upsert({ id: 1, a: { c: 2 } })).toArray()\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion([ { id: 1 } ], res))\n      // Make sure `upsert` updated the original document\n      .mergeMapTo(data.find(1).fetch())\n      // Check that the document was updated correctly\n      .do(res => compareWithoutVersion(res, { id: 1, a: { b: 1, c: 2 }, d: 1 }))\n  ))\n\n  // If we upsert a document without an ID, the ID is generated for us.\n  // Let's run the same test as above (store the document and then update\n  // it), but have the ID be generated for us.\n  it('generates ids for documents without them', assertCompletes(() => {\n    let new_id\n\n    return data.upsert({ a: { b: 1, c: 1 }, d: 1 }).toArray()\n      .do(res => {\n        // should return an array with an ID of the inserted document.\n        assert.isArray(res)\n        assert.lengthOf(res, 1)\n        assert.isString(res[0].id)\n        new_id = res[0].id\n      })\n      // Let's make sure we get back the document that we put in.\n      .mergeMap(() => data.find(new_id).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: new_id, a: { b: 1, c: 1 }, d: 1 }))\n      // Let's update the document now\n      .mergeMap(() => data.upsert({ id: new_id, a: { c: 2 } })).toArray()\n      // We should have gotten the ID back again\n      .do(res => compareWithoutVersion(res, [ { id: new_id } ]))\n      // Make sure `upsert` updated the original document\n      .mergeMap(() => data.find(new_id).fetch())\n      // Check that we get back what we put in.\n      .do(res => compareWithoutVersion(res, { id: new_id, a: { b: 1, c: 2 }, d: 1 }))\n  }))\n\n  // Upserting `null` is an error.\n  it('fails if null is passed', assertThrows(\n    'The argument to upsert must be non-null',\n    () => data.upsert(null)\n  ))\n\n  // Upserting `undefined` is also an error.\n  it('fails if undefined is passed', assertThrows(\n    'The 1st argument to upsert must be defined',\n    () => data.upsert(undefined)\n  ))\n\n  it('fails if no arguments are passed', assertThrows(\n    'upsert must receive exactly 1 argument',\n    () => data.upsert()\n  ))\n\n  // The `upsert` command allows storing multiple documents in one call.\n  // Let's upsert a few kinds of documents and make sure we get them back.\n  it('allows upserting multiple documents in one call', assertCompletes(() => {\n    let new_id_0, new_id_1\n\n    return data.upsert([ {}, { a: 1 }, { id: 1, a: 1 } ]).toArray()\n      .do(res => {\n        // should return an array with the IDs of the documents in\n        // order, including the generated IDS.\n        assert.isArray(res)\n        assert.lengthOf(res, 3)\n        assert.isString(res[0].id)\n        assert.isString(res[1].id)\n        assert.equal(1, res[2].id)\n\n        new_id_0 = res[0].id\n        new_id_1 = res[1].id\n      })\n      // Make sure we get what we put in.\n      .mergeMap(() => data.findAll(new_id_0, new_id_1, 1).fetch())\n      // We're supposed to get an array of documents we put in\n      .do(res => compareSetsWithoutVersion(res, [\n        { id: new_id_0 },\n        { id: new_id_1, a: 1 },\n        { id: 1, a: 1 },\n      ]))\n  }))\n\n  // If any operation in a batch upsert fails, everything is reported\n  // as a failure.\n  it('errors if given a null document', assertErrors(() =>\n    data.upsert([ { a: 1 }, null ]),\n    /must be an object/\n  ))\n\n  // Upserting an empty batch of documents is ok, and returns an empty\n  // array.\n  it('allows upserting an empty batch', assertCompletes(() =>\n    data.upsert([]).toArray()\n      .do(res => {\n        // should return an array with the IDs of the documents in\n        // order, including the generated IDS.\n        assert.lengthOf(res, 0)\n      })\n  ))\n}}\n"
  },
  {
    "path": "client/test/utils.js",
    "content": "import { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/observable/empty'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/take'\nimport 'rxjs/add/operator/ignoreElements'\n\nexport function removeAllDataObs(collection) {\n  // Read all elements from the collection\n  return collection.fetch() // all documents in the collection\n    .do()\n    .mergeMap(docs => collection.removeAll(docs))\n    .mergeMapTo(collection.fetch())\n    .do(remaining => assert.deepEqual([], remaining))\n}\n\nexport function removeAllData(collection, done) {\n  removeAllDataObs(collection).subscribe(doneObserver(done))\n}\n\n// Used to subscribe to observables and call done appropriately\nfunction doneObserver(done) {\n  return {\n    next() {},\n    error(err = new Error()) { done(err) },\n    complete() { done() },\n  }\n}\n\n// Used to subscribe to observables when an error is expected\nfunction doneErrorObserver(done, regex) {\n  return {\n    next() {},\n    error(err) {\n      this.finished = true\n      if (regex && regex.test(err.message)) {\n        done()\n      } else {\n        done(err)\n      }\n    },\n    complete() {\n      if (!this.finished) {\n        done(new Error('Unexpectedly completed'))\n      }\n    },\n  }\n}\n\n// Used to check for stuff that should throw an exception, rather than\n// erroring the observable stream\nexport function assertThrows(message, callback) {\n  const f = done => {\n    try {\n      callback()\n      done(new Error(\"Didn't throw an exception\"))\n    } catch (err) {\n      if (err.message === message) {\n        done()\n      } else {\n        done(new Error('Threw the wrong exception. ' +\n                       `Expected \"${message}\", got \"${err.message}\"`))\n      }\n    }\n  }\n  f.toString = () => `assertThrows(\\n'${message}',\\n  ${callback}\\n)`\n  return f\n}\n\nexport function assertCompletes(observable) {\n  const f = done => observable().subscribe(doneObserver(done))\n  f.toString = () => `assertCompletes(\\n(${observable}\\n)`\n  return f\n}\n\nexport function assertErrors(observable, regex) {\n  const f = done => observable().subscribe(doneErrorObserver(done, regex))\n  f.toString = () => observable.toString()\n  return f\n}\n\n// Useful for asynchronously interleaving server actions with a\n// changefeed and testing the changes are as expected.\n//\n// Takes a sequence of actions and a changefeed query. Executes the\n// next action every time a value comes in over the feed. Asserts that\n// the expected values are returned from the final observable. The\n// changefeed is automatically limited to the length of the expected\n// array. Accepts a `debug` argument that receives every element in\n// the changefeed\nexport function observableInterleave(options) {\n  const query = options.query\n  const operations = options.operations\n  const expected = options.expected\n  const equality = options.equality || compareWithoutVersion\n  const debug = options.debug || (() => {})\n  const values = []\n  return query\n    .take(expected.length)\n    .do(debug)\n    .mergeMap((val, i) => {\n      values.push(val)\n      if (i < operations.length) {\n        return operations[i].ignoreElements()\n      } else {\n        return Observable.empty()\n      }\n    })\n    .do({ complete() { equality(expected, values) } })\n}\n\nconst withoutVersion = function withoutVersion(value) {\n  if (Array.isArray(value)) {\n    const modified = [ ]\n    for (const item of value) {\n      modified.push(withoutVersion(item))\n    }\n    return modified\n  } else if (typeof value === 'object') {\n    const modified = Object.assign({ }, value)\n    delete modified['$hz_v$']\n    return modified\n  } else {\n    return value\n  }\n}\n\n// Compare write results - ignoring the new version field ($hz_v$)\nexport function compareWithoutVersion(actual, expected, message) {\n  return assert.deepEqual(withoutVersion(actual),\n                          withoutVersion(expected),\n                          message)\n}\n\nexport function compareSetsWithoutVersion(actual, expected, message) {\n  return assert.sameDeepMembers(withoutVersion(actual),\n                                withoutVersion(expected),\n                                message)\n}\n"
  },
  {
    "path": "client/webpack.config.js",
    "content": "const BUILD_ALL = (process.env.NODE_ENV === 'production')\n\nconst build = require('./webpack.horizon.config.js')\nconst test = require('./webpack.test.config.js')\n\nif (BUILD_ALL) {\n  module.exports = [\n    build({\n      FILENAME: 'horizon-dev.js',\n      DEV_BUILD: true,\n      POLYFILL: true,\n    }),\n    build({\n      FILENAME: 'horizon.js',\n      DEV_BUILD: false,\n      POLYFILL: true,\n    }),\n    build({\n      FILENAME: 'horizon-core-dev.js',\n      DEV_BUILD: true,\n      POLYFILL: false,\n    }),\n    build({\n      FILENAME: 'horizon-core.js',\n      DEV_BUILD: false,\n      POLYFILL: false,\n    }),\n    test,\n  ]\n} else {\n  module.exports = [\n    build({\n      // same filename as prod build to simplify switching\n      FILENAME: 'horizon.js',\n      DEV_BUILD: true,\n      POLYFILL: true,\n    }),\n    test,\n  ]\n}\n"
  },
  {
    "path": "client/webpack.horizon.config.js",
    "content": "const path = require('path')\n\nconst BannerPlugin = require('webpack/lib/BannerPlugin')\nconst DedupePlugin = require('webpack/lib/optimize/DedupePlugin')\nconst DefinePlugin = require('webpack/lib/DefinePlugin')\nconst OccurrenceOrderPlugin = require(\n  'webpack/lib/optimize/OccurrenceOrderPlugin')\nconst UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin')\nconst ProvidePlugin = require('webpack/lib/ProvidePlugin')\n\nmodule.exports = function(buildTarget) {\n  const FILENAME = buildTarget.FILENAME\n  const DEV_BUILD = buildTarget.DEV_BUILD\n  const POLYFILL = buildTarget.POLYFILL\n  const SOURCEMAPS = !process.env.NO_SOURCEMAPS\n\n  return {\n    entry: {\n      horizon: POLYFILL ?\n        './src/index-polyfill.js' :\n        './src/index.js',\n    },\n    target: 'web',\n    output: {\n      path: path.resolve(__dirname, 'dist'),\n      filename: FILENAME,\n      library: 'Horizon', // window.Horizon if loaded by a script tag\n      libraryTarget: 'umd',\n      pathinfo: DEV_BUILD, // Add module filenames as comments in the bundle\n      devtoolModuleFilenameTemplate: DEV_BUILD ?\n        function(file) {\n          if (file.resourcePath.indexOf('webpack') >= 0) {\n            return `webpack:///${file.resourcePath}`\n          } else {\n            // Show correct paths in stack traces\n            return path.join('..', file.resourcePath)\n              .replace(/~/g, 'node_modules')\n          }\n        } :\n      null,\n    },\n    externals: [\n      function(context, request, callback) {\n        // Selected modules are not packaged into horizon.js. Webpack\n        // allows them to be required natively at runtime, either from\n        // filesystem (node) or window global.\n        if (!POLYFILL && /^rxjs\\/?/.test(request)) {\n          callback(null, {\n          // If loaded via script tag, has to be at window.Rx when\n          // library loads\n          root: 'Rx',\n          // Otherwise imported via `require('rx')`\n          commonjs: 'rxjs',\n          commonjs2: 'rxjs',\n          amd: 'rxjs'\n        })\n        } else {\n          callback()\n        }\n      },\n      { ws: 'commonjs ws' }\n    ],\n    debug: DEV_BUILD,\n    devtool: SOURCEMAPS ? (DEV_BUILD ? 'source-map' : 'source-map') : false,\n    module: {\n      noParse: [\n      ],\n      preLoaders: [],\n      loaders: [\n        {\n          test: /\\.js$/,\n          exclude: /node_modules/,\n          loader: 'babel-loader',\n          query: {\n            cacheDirectory: true,\n            extends: path.resolve(__dirname, 'package.json'),\n          },\n        },\n      ],\n    },\n    plugins: [\n      new BannerPlugin('__LICENSE__'),\n      // Possibility to replace constants such as `if (__DEV__)`\n      // and thus strip helpful warnings from production build:\n      new DefinePlugin({\n        'process.env.NODE_ENV': (DEV_BUILD ? 'development' : 'production'),\n      }),\n      new ProvidePlugin({\n        Promise: 'es6-promise',\n      }),\n    ].concat(DEV_BUILD ? [] : [\n      new DedupePlugin(),\n      new OccurrenceOrderPlugin(),\n      new UglifyJsPlugin({\n        compress: {\n          screw_ie8: false,\n          warnings: false,\n        },\n        mangle: {\n          except: [],\n        },\n      }),\n    ]),\n    node: {\n      // Don't include unneeded node libs in package\n      process: false,\n      fs: false,\n      __dirname: false,\n      __filename: false,\n    },\n  }\n}\n"
  },
  {
    "path": "client/webpack.test.config.js",
    "content": "const path = require('path')\n\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\nconst DEV_BUILD = (process.env.NODE_ENV !== 'production')\nconst SOURCEMAPS = !process.env.NO_SOURCEMAPS\n\nmodule.exports = {\n  entry: {\n    test: './test/test.js',\n  },\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: '[name].js',\n    pathinfo: DEV_BUILD, // Add module filenames as comments in the bundle\n    devtoolModuleFilenameTemplate: function(file) {\n      if (file.resourcePath.indexOf('webpack') >= 0) {\n        return `webpack:///${file.resourcePath}`\n      } else {\n        // Show correct paths in stack traces\n        return path.join('..', file.resourcePath).replace(/~/g, 'node_modules')\n      }\n    },\n  },\n  target: 'web',\n  debug: DEV_BUILD,\n  devtool: SOURCEMAPS ? 'source-map' : false,\n  externals: {\n    // These modules are not packaged into test.js. Webpack allows\n    // them to be required natively at runtime when the tests are run\n    // in node\n    './horizon.js': 'commonjs ./horizon.js',\n    ws: 'commonjs ws',\n    'source-map-support': 'commonjs source-map-support',\n  },\n  module: {\n    noParse: [\n      // Pre-built files don't need parsing\n      /mocha\\/mocha\\.js/,\n      /chai\\/chai\\.js/,\n      /lodash\\/lodash\\.js/,\n      /source-map-support/,\n      /rxjs\\/bundles\\/Rx\\.umd\\.js/,\n    ],\n    loaders: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        loader: 'babel-loader',\n        query: {\n          cacheDirectory: true,\n          presets: [\n            'babel-preset-es2015-loose',\n            { plugins: [\n              [ 'babel-plugin-transform-runtime', { polyfill: false } ],\n            ] },\n          ],\n        },\n      },\n    ],\n  },\n  node: {\n    // Don't include unneeded node libs in package\n    process: false,\n    fs: false,\n    __dirname: false,\n    __filename: false,\n  },\n  plugins: [\n    new CopyWebpackPlugin([\n      { from: './test/test.html' },\n      { from: './node_modules/mocha/mocha.css' },\n    ]),\n  ],\n}\n"
  },
  {
    "path": "docker-compose.dev.yml",
    "content": "rethinkdb:\n  image: rethinkdb\n  ports:\n    - \"28015:28015\"\n    - \"8080:8080\"\nhorizon:\n  image: rethinkdb/horizon\n  command: su -s /bin/sh horizon -c \"hz serve --dev --connect rethinkdb://rethinkdb:28015 --bind all /usr/app\"\n  volumes:\n    - ./:/usr/app\n  links:\n    - rethinkdb\n  ports:\n    - \"8181:8181\"\n"
  },
  {
    "path": "docker-compose.prod.yml",
    "content": "rethinkdb:\n  image: rethinkdb\n  ports:\n    - \"28015:28015\"\n    - \"8080:8080\"\nhorizon:\n  image: rethinkdb/horizon\n  command: su -s /bin/sh horizon -c \"hz serve --connect rethinkdb://rethinkdb:28015 --bind all /usr/app\"\n  volumes:\n    - ./:/usr/app\n  links:\n    - rethinkdb\n  ports:\n    - \"8181:8181\"\n"
  },
  {
    "path": "examples/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"array-bracket-spacing\": [ 2, \"always\" ],\n    \"arrow-parens\": [ 2, \"as-needed\" ],\n    \"arrow-spacing\": [ 2 ],\n    \"block-spacing\": [ 2, \"always\" ],\n    \"brace-style\": [ 2, \"1tbs\", { \"allowSingleLine\": true } ],\n    \"comma-dangle\": [ 2, \"always-multiline\" ],\n    \"comma-spacing\": [ 2 ],\n    \"comma-style\": [ 2, \"last\" ],\n    \"constructor-super\": [ 2 ],\n    \"curly\": [ 2, \"all\" ],\n    \"dot-notation\": [ 2 ],\n    \"eqeqeq\": [ 2, \"allow-null\" ],\n    \"func-style\": [ 2, declaration, { \"allowArrowFunctions\": true} ],\n    \"indent\": [ 2, 2 ],\n    \"key-spacing\": [ 2 ],\n    \"linebreak-style\": [ 2, \"unix\" ],\n    \"new-parens\": [ 2 ],\n    \"no-array-constructor\": [ 2 ],\n    \"no-case-declarations\": [ 2 ],\n    \"no-class-assign\": [ 2 ],\n    \"no-console\": [ 0 ],\n    \"no-const-assign\": [ 2 ],\n    \"no-dupe-class-members\": [ 2 ],\n    \"no-eval\": [ 2 ],\n    \"no-extend-native\": [ 2 ],\n    \"no-extra-semi\": [ 2 ],\n    \"no-floating-decimal\": [ 2 ],\n    \"no-implicit-coercion\": [ 2 ],\n    \"no-implied-eval\": [ 2 ],\n    \"no-invalid-this\": [ 2 ],\n    \"no-labels\": [ 2 ],\n    \"no-lonely-if\": [ 2 ],\n    \"no-mixed-requires\": [ 2 ],\n    \"no-multi-spaces\": [ 2 ],\n    \"no-multi-str\": [ 2 ],\n    \"no-multiple-empty-lines\": [ 2, { \"max\": 2, \"maxEOF\": 1 } ],\n    \"no-native-reassign\": [ 2 ],\n    \"no-new-func\": [ 2 ],\n    \"no-new-object\": [ 2 ],\n    \"no-new-require\": [ 2 ],\n    \"no-new-wrappers\": [ 2 ],\n    \"no-param-reassign\": [ 2 ],\n    \"no-proto\": [ 2 ],\n    \"no-return-assign\": [ 2 ],\n    \"no-self-compare\": [ 2 ],\n    \"no-sequences\": [ 2 ],\n    \"no-shadow\": [ 2 ],\n    \"no-shadow-restricted-names\": [ 2 ],\n    \"no-this-before-super\": [ 2 ],\n    \"no-throw-literal\": [ 2 ],\n    \"no-trailing-spaces\": [ 2 ],\n    \"no-unneeded-ternary\": [ 2 ],\n    \"no-use-before-define\": [ 2, \"nofunc\" ],\n    \"no-var\": [ 2 ],\n    \"no-void\": [ 2 ],\n    \"no-with\": [ 2 ],\n    \"object-curly-spacing\": [ 2, \"always\" ],\n    \"one-var\": [ 2, { \"uninitialized\": \"always\", \"initialized\": \"never\" } ],\n    \"operator-assignment\": [ 2, \"always\" ],\n    \"operator-linebreak\": [ 2, \"after\" ],\n    \"padded-blocks\": [ 2, \"never\" ],\n    \"quote-props\": [ 2, \"as-needed\" ],\n    \"quotes\": [ 2, \"single\" ],\n    \"semi-spacing\": [ 2 ],\n    \"space-after-keywords\": [ 2, \"always\" ],\n    \"space-before-blocks\": [ 2, \"always\" ],\n    \"space-before-function-paren\": [ 2, \"never\" ],\n    \"space-before-keywords\": [ 2, \"always\" ],\n    \"space-in-parens\": [ 2, \"never\" ],\n    \"space-infix-ops\": [ 2 ],\n    \"space-return-throw-case\": [ 2 ],\n    \"space-unary-ops\": [ 2 ],\n    \"spaced-comment\": [ 2, \"always\" ],\n    \"strict\": [ 2, \"global\" ],\n    \"wrap-iife\": [ 2, \"inside\" ],\n    \"yoda\": [ 2, \"never\" ],\n  },\n  \"env\": {\n    \"es6\": true,\n    \"node\": true,\n    \"mocha\": true\n  },\n  \"extends\": \"eslint:recommended\"\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Community Examples\n\nComing soon!\n\n# Horizon Extension Examples\n\nWe decided it was important to leverage Horizon alongside all your normal app functions. Here are some basic examples of how one would extend your JS web framework of choice with Horizon:  [express](https://github.com/strongloop/express), [koa](https://github.com/koajs/koa), or [hapi](https://github.com/hapijs) frameworks with Horizon.\n\n* [Horizon & Express](/examples/express-server)\n* [Horizon & Koa](/examples/koa-server)\n* [Horizon & Hapi](/examples/hapi-server)\n\n# Horizon App Examples\nAnd finally, we've created a few example applications to help you get started with Horizon. In each directory\nyou will find a `dist` folder which will have all the files needed to serve the example application. In some of the\nTodoMVC examples, you will find a `package.json` inside the `dist` directory, and you will need to run `npm install` for\nsome of their dependencies.\n\nIf you have any questions just ask on [Slack](http://slack.rethinkdb.com) or [Twitter](https://twitter.com/rethinkdb)!\n\n## AngularJS\n* [AngularJS Todo App](/examples/angularjs-todo-app)\n\n## CycleJS\n* [CycleJS Chat App](/examples/cyclejs-chat-app)\n\n## React\n* [React Chat App](/examples/react-chat-app/)\n* [React Todo App](/examples/react-todo-app/)\n\n## RiotJS\n* [RiotJS Chat App](/examples/riotjs-chat-app)\n\n## Vuejs\n* [Vue.js Chat App](/examples/vue-chat-app/)\n* [Vue Todo App](/examples/vue-todo-app/)\n"
  },
  {
    "path": "examples/angularjs-todo-app/.gitignore",
    "content": "node_modules/\nrethinkdb_data\n**/*.log\n.hz/\n"
  },
  {
    "path": "examples/angularjs-todo-app/README.md",
    "content": "#TodoMVC\n\nA basic example of using [AngularJS](http://angularjs.org/) and Horizon to create real-time TodoMVC app.\n\n## Prerequisites\n\n- [RethinkDB](https://www.rethinkdb.com/docs/install/) (The open-source database for the realtime web)\n- [Horizon](https://github.com/rethinkdb/horizon/) (A realtime, open-source backend for JavaScript apps)\n\n## Installing\n\n```\n$ git clone git@github.com:rethinkdb/horizon.git\n$ cd horizon/examples/angularjs-todo-app\n$ hz init\n$ cd dist && npm install\n$ cd .. && hz serve --dev\n```\n\n## Credit\n\nThis TodoMVC application is built based on the [todomvc-angularjs-horizon](https://github.com/endetti/todomvc-angularjs-horizon).\n"
  },
  {
    "path": "examples/angularjs-todo-app/dist/css/style.css",
    "content": "[ng\\:cloak],\n[ng-cloak],\n[data-ng-cloak],\n[x-ng-cloak],\n.ng-cloak,\n.x-ng-cloak {\n    display: none !important;\n}\n"
  },
  {
    "path": "examples/angularjs-todo-app/dist/index.html",
    "content": "<!doctype html>\n<html>\n\n    <head>\n        <meta charset=\"UTF-8\">\n        <script src=\"/horizon/horizon.js\"></script>\n        <link rel=\"stylesheet\" href=\"node_modules/todomvc-app-css/index.css\">\n        <link rel=\"stylesheet\" href=\"node_modules/todomvc-common/base.css\">\n        <link rel=\"stylesheet\" href=\"css/style.css\">\n    </head>\n\n    <body ng-app=\"todomvc\" ng-cloak>\n        <section class=\"todoapp\" ng-controller=\"TodoCtrl as Todo\">\n            <header class=\"header\">\n                <h1>todos</h1>\n                <form ng-submit=\"Todo.addTask()\">\n                    <input class=\"new-todo\" placeholder=\"What needs to be done?\" ng-model=\"Todo.taskTitle\" autofocus>\n                </form>\n            </header>\n            <section class=\"main\" ng-show=\"Todo.tasks.length\">\n                <input class=\"toggle-all\" type=\"checkbox\" ng-checked=\"Todo.allCompleted\" ng-click=\"Todo.toggleAll()\">\n                <label for=\"toggle-all\">Mark all as complete</label>\n                <ul class=\"todo-list\">\n                    <li ng-repeat=\"task in Todo.tasks track by task.id\" ng-class=\"{completed: task.completed, editing: task.id == Todo.editedTask.id}\">\n                        <div class=\"view\">\n                            <input class=\"toggle\" type=\"checkbox\" ng-model=\"task.completed\" ng-change=\"Todo.toggleCompleted(task)\">\n                            <label ng-dblclick=\"Todo.editTask(task)\">{{task.title}}</label>\n                            <button class=\"destroy\" ng-click=\"Todo.removeTask(task)\"></button>\n                        </div>\n                        <form ng-submit=\"Todo.editTaskSave(task)\">\n                            <input type=\"text\" id=\"{{task.id}}\" class=\"edit\" ng-model=\"task.title\" ng-blur=\"Todo.editTaskCancel(task)\">\n                        </form>\n                    </li>\n                </ul>\n            </section>\n            <footer class=\"footer\" ng-show=\"Todo.tasks.length\">\n                <span class=\"todo-count\"><strong>{{Todo.remainingCount}}</strong>\n                    <ng-pluralize count=\"Todo.remainingCount\" when=\"{ one: 'item left', other: 'items left' }\"></ng-pluralize>\n                </span>\n                <button class=\"clear-completed\" ng-click=\"Todo.removeCompletedTasks()\" ng-show=\"Todo.completedCount\">Clear completed</button>\n            </footer>\n        </section>\n        <footer class=\"info\">\n            <p>Double-click to edit a todo</p>\n            <p>Created by <a href=\"https://github.com/endetti\">endetti</a></p>\n            <p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p>\n        </footer>\n        <script src=\"node_modules/todomvc-common/base.js\"></script>\n        <script src=\"node_modules/angular/angular.js\"></script>\n        <script src=\"js/app.js\"></script>\n        <script src=\"js/controllers/TodoController.js\"></script>\n    </body>\n\n</html>\n"
  },
  {
    "path": "examples/angularjs-todo-app/dist/js/app.js",
    "content": "angular.module('todomvc', []);\n"
  },
  {
    "path": "examples/angularjs-todo-app/dist/js/controllers/TodoController.js",
    "content": "(function () {\n    'use strict';\n\n    angular\n        .module('todomvc')\n        .controller('TodoCtrl', TodoCtrl);\n\n    TodoCtrl.$inject = ['$filter', '$q'];\n\n    function TodoCtrl($filter, $q) {\n        const hz = new Horizon();\n        const Tasks = hz(\"todomvc_tasks\");\n\n        self = this;\n        self.allCompleted = false;\n\n        self.addTask = addTask;\n        self.removeTask = removeTask;\n        self.editTask = editTask;\n        self.editTaskSave = editTaskSave;\n        self.editTaskCancel = editTaskCancel;\n        self.toggleCompleted = toggleCompleted;\n        self.toggleAll = toggleAll;\n        self.removeCompletedTasks = removeCompletedTasks;\n\n        init();\n\n        function init() {\n            Tasks.order(\"date\", \"ascending\").watch().subscribe(function (tasks) {\n                var defer = $q.defer();\n\n                defer.resolve(tasks);\n\n                defer.promise.then(function (tasks) {\n                    self.remainingCount = $filter('filter')(tasks, { completed: false }).length;\n                    self.completedCount = tasks.length - self.remainingCount;\n                    self.allCompleted = !self.remainingCount;\n                    self.tasks = tasks;\n                });\n            });\n        }\n\n        function addTask() {\n            if (!self.taskTitle) return;\n\n            Tasks.store({\n                title: self.taskTitle.trim(),\n                completed: false,\n                date: new Date()\n            });\n\n            self.taskTitle = '';\n        }\n\n        function removeTask(task) {\n            Tasks.remove(task);\n        }\n\n        function editTask(task) {\n            self.editedTask = task;\n\n            self.taskCopy = angular.copy(task);\n        }\n\n        function editTaskSave(task) {\n            self.editedTask = null;\n\n            Tasks.update(task);\n        }\n\n        function editTaskCancel(task) {\n            task.title = self.taskCopy.title;\n            self.editedTask = null;\n        }\n\n        function toggleCompleted(task) {\n            Tasks.update(task);\n        }\n\n        function toggleAll() {\n            self.tasks.forEach(function (task) {\n                task.completed = !self.allCompleted;\n            });\n\n            toggleCompleted(self.tasks);\n        }\n\n        function removeCompletedTasks() {\n            var completed = self.tasks.filter(function (task) {\n                return task.completed === true;\n            });\n\n            Tasks.remove(completed);\n        }\n    };\n})();\n"
  },
  {
    "path": "examples/angularjs-todo-app/dist/package.json",
    "content": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"angular\": \"^1.5.8\",\n    \"todomvc-app-css\": \"^2.0.0\",\n    \"todomvc-common\": \"^1.0.1\"\n  }\n}\n"
  },
  {
    "path": "examples/auth-app/.gitignore",
    "content": "node_modules\ndist/node_modules\n"
  },
  {
    "path": "examples/auth-app/README.md",
    "content": ""
  },
  {
    "path": "examples/auth-app/dist/app.css",
    "content": "#steps {\n  margin-top:25%;\n}\n\n#success > div > div                                                     {\n  margin-top:10%;\n  text-align:center;\n  color:green;\n}\n\n#login #login-button {\n  height: 100px;\n}\n"
  },
  {
    "path": "examples/auth-app/dist/app.js",
    "content": "'use strict'\nvar horizon = Horizon({\n    authType: 'anonymous'\n});\n\nconst crossOut = (element) => {\n    element.setAttribute(\"style\", \"text-decoration: line-through; color: grey;\");\n}\n\n// #1 - Connect to Horizon server\n\nhorizon.onReady(() => {\n    crossOut(document.querySelector('#connection-success'));\n})\n\nhorizon.connect();\n\n// #2 - Ensure Github OAuth client_id & client_secret added\n\nfetch(\"/horizon/auth_methods\").then((response) => {\n    return response.text();\n}).then((json) => {\n    const strategies = JSON.parse(json);\n    if (strategies.hasOwnProperty('github')) {\n        console.log(strategies)\n        crossOut(document.querySelector('#github-configured'));\n        const login = document.querySelector('#login-button');\n        login.disabled = false;\n        login.className = login.className.replace('btn-error-outline', 'btn-primary-outline');\n    }\n})\n\n// #3 - Test OAuth pathing with Github\n\nvar horizon = Horizon({\n    authType: 'token'\n});\nhorizon.connect()\n\nif (!horizon.hasAuthToken()) {\n    console.log(\"no auth token\")\n    document.querySelector('#login-button').addEventListener('click', () => {\n        console.log('clicked');\n        horizon.authEndpoint('github').subscribe((endpoint) => {\n            window.location.pathname = endpoint;\n        });\n    });\n} else {\n    const user = horizon.currentUser().fetch().forEach((user) => {\n        document.querySelector('#success').innerHTML = `\n    <div class=\"col-sm-12\">\n    <div class=\"card card-inverse card-success\">\n        <div class=\"card-block\">\n            <h1 class=\"card-title\">Authentication successful <br> Your user ID is ${user.id}</h1>\n        </div>\n    </div>\n    </div> `;\n    });\n}\n"
  },
  {
    "path": "examples/auth-app/dist/index.html",
    "content": "<html>\n\n<head>\n    <link href=\"app.css\" rel=\"stylesheet\">\n    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-y3tfxAZXuh4HwSYylfB+J125MxIs6mR5FOHamPBG064zB+AFeWH94NdvaCBm8qnd\" crossorigin=\"anonymous\">\n    <script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js\"></script>\n</head>\n\n<body>\n    <div class=\"container\">\n        <div id=\"steps\" class=\"row\">\n            <div class=\"col-sm-5 col-sm-offset-1\">\n                <p>This small example will help you get started with setting up OAuth on Horizon. Here's what you need to get OAuth going:\n                    <ol>\n                        <li><a href=\"https://github.com/settings/developers\">Create an application at Github.</a></li>\n                        <li>Add the client id and client secret to the configuration file</li>\n                        <li><a href=\"https://localhost:8181/horizon/auth_methods\">Ensure that you can find Github in the registered providers</a></li>\n                        <li>Click the authenticate with Github button</li>\n                    </ol>\n                </p>\n            </div>\n            <div class=\"col-sm-5\">\n                <ol>\n                    <li id=\"connection-success\">Connect to Horizon</li>\n                    <li id=\"github-configured\">Configure Github OAuth</li>\n                </ol>\n                <div id=\"login\">\n                    <button type=\"button\" id=\"login-button\" class=\"btn btn-error-outline\" disabled>Authenticate with Github</button>\n                </div>\n            </div>\n        </div>\n        <div id=\"success\" class=\"row\">\n        </div>\n    </div>\n    <script src=\"/horizon/horizon.js\"></script>\n    <script src=\"app.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/cyclejs-chat-app/README.md",
    "content": "# Horizon Chat example using Cycle.js\n\n<center>![](https://i.imgur.com/XFostB8.gif)</center>\n\n\n## Ideas for future\n\n- [ ] implement `presence`\n- [ ] implement `geo` to display users differently that are geographically near you\n- [ ] implement `auth` to allow users to log in via Github, Twitter, etc and have their handle appear.\n"
  },
  {
    "path": "examples/cyclejs-chat-app/dist/app.css",
    "content": "/* always present */\n.expand-transition {\n  transition: all .4s ease;\n  height: 30px;\n  padding: 10px;\n  background-color: #eee;\n  overflow: hidden;\n}\n\n/* .expand-enter defines the starting state for entering */\n/* .expand-leave defines the ending state for leaving */\n.expand-enter, .expand-leave {\n  height: 0;\n  padding: 0 10px;\n  opacity: 0;\n}\n\n#app ul {\n  list-style-type: none;\n}\n\n.message {\n  height: 50px;\n}\n\n.message img {\n  vertical-align:middle;\n}\n\n.message .text {\n  vertical-align:middle;\n  margin-left:5px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n  font-size:20px;\n}\n\n.message .datetime {\n  color:darkgrey;\n}\n\n#input {\n  margin-bottom:10%;\n}\n\n#input input {\n  height:100px;\n  font-size:5rem;\n  padding:10px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n}\n"
  },
  {
    "path": "examples/cyclejs-chat-app/dist/app.js",
    "content": "'use strict'\n\n;(function() {\n  const makeDOMDriver = CycleDOM.makeDOMDriver\n  const div = CycleDOM.div\n  const input = CycleDOM.input\n  const ul = CycleDOM.ul\n  const li = CycleDOM.li\n  const img = CycleDOM.img\n  const span = CycleDOM.span\n\n  // Intent grabs things from our sources and pulls out what events\n  // we're interested in, as well as mapping to useful data (so\n  // streams of strings, instead of observables of text input events)\n  function intent(sources) {\n    const horizonCollection = sources.horizon(sources.config.collectionName)\n    // Every time the enter key is hit in the text box\n    const enterHit$ = sources.DOM\n            .select('#input')\n            .events('keydown')\n            .filter(ev => ev.keyCode === 13)\n            .map(ev => ev.target.value || null)\n            .share()\n    // Every time the text in the input box changes\n    const inputChange$ = sources.DOM\n            .select('#input')\n            .events('input')\n            .map(ev => ev.target.value)\n    // all our chats from the horizon server\n    const messages$ = horizonCollection\n            .order('datetime', 'descending')\n            .limit(sources.config.chatLength)\n            .watch()\n\n    // Every time the user hits enter, store the message to the server\n    // Note: this is an observable of observables\n    const writeOps$$ = enterHit$.map(text =>\n      horizonCollection.store({\n        authorId: sources.config.authorId,\n        datetime: new Date(),\n        text,\n      })\n    )\n\n    // This merges the stream of the input values with a stream that\n    // returns null whenever enter is hit. This will clear the text\n    // box after submitting.\n    const inputValue$ = inputChange$.merge(enterHit$.map(() => null))\n    return {\n      inputValue$,\n      writeOps$$,\n      messages$,\n    }\n  }\n\n  // Model takes our action streams and turns them into the stream of\n  // app states\n  function model(inputValue$, messages$) {\n    return Rx.Observable.combineLatest(\n      inputValue$.startWith(null),\n      messages$.startWith([]),\n      (inputValue, messages) => ({ messages, inputValue })\n    )\n  }\n\n  // View takes the state and create a stream of virtual-dom trees for\n  // the app.\n  function view(state$) {\n    // Displayed for each chat message.\n    function chatMessage(msg) {\n      return li('.message', { key: msg.id }, [\n        img({\n          height: '50', width: '50',\n          src: `http://api.adorable.io/avatars/50/${msg.authorId}.png`,\n        }),\n        span('.text', msg.text),\n      ])\n    }\n\n    return state$.map(\n      state =>\n        div([\n          div('.row',\n              ul(state.messages.map(chatMessage))),\n          div('#input.row',\n              input('.u-full-width', { value: state.inputValue, autoFocus: true })),\n        ])\n    )\n  }\n\n  // In main we just wire everything together\n  function main(sources) {\n    const intents = intent(sources)\n    const state$ = model(intents.inputValue$, intents.messages$)\n    return {\n      // Send the virtual tree to the real DOM\n      DOM: view(state$),\n      // Send our messages to the horizon server\n      horizon: intents.writeOps$$,\n    }\n  }\n\n  // All the effects the app uses\n  const drivers = {\n    // Link the DOM driver to our app container\n    DOM: makeDOMDriver('#app'),\n    // Create a connection to the horizon server\n    horizon: makeHorizonDriver(),\n    // App-level configuration options\n    config: () => ({\n      // How many chats to show\n      chatLength: 8,\n      // RethinkDB table\n      collectionName: 'cyclejs_messages',\n      // unique-ish id created once when opening the page\n      authorId: new Date().getMilliseconds(),\n    }),\n  }\n\n  // Run the application\n  Cycle.run(main, drivers)\n\n  // Little CycleJS driver for horizon. This will probably be a small\n  // standalone library at some point\n  function makeHorizonDriver() {\n    return function horizonDriver(writeOps$$) {\n      // Send outgoing messages\n      writeOps$$.switch().subscribe()\n      // Return chat observable\n      return Horizon({ lazyWrites: true })\n    }\n  }\n})()\n"
  },
  {
    "path": "examples/cyclejs-chat-app/dist/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" data-framework=\"react\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <link rel=\"stylesheet\" href=\"app.css\">\n        <link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>\n        <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css\">\n    </head>\n    <body>\n        <div id=\"app\" class=\"container\">\n        </div>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.7/rx.all.js\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/cyclejs-dom/9.0.2/cycle-dom.js\"></script>\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/cyclejs-core/6.0.2/cycle.js\"></script>\n        <script src=\"/horizon/horizon.js\"></script>\n        <script src=\"app.js\"></script>\n\n    </body>\n\n</html>\n"
  },
  {
    "path": "examples/express-server/main.js",
    "content": "#!/usr/bin/env node\n'use strict'\n\nconst express = require('express');\nconst horizon = require('@horizon/server');\n\nconst app = express();\nconst http_server = app.listen(8181);\nconst options = { auth: { token_secret: 'my_super_secret_secret' } };\nconst horizon_server = horizon(http_server, options);\n\nconsole.log('Listening on port 8181.');\n"
  },
  {
    "path": "examples/express-server/package.json",
    "content": "{\n  \"name\": \"express-horizon-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example of a horizon server within a express server.\",\n  \"bin\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@horizon/server\": \"^1.0.1\",\n    \"express\": \"^4.13.3\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rethinkdb/horizon.git\"\n  }\n}\n"
  },
  {
    "path": "examples/hapi-server/main.js",
    "content": "#!/usr/bin/env node\n'use strict'\n\nconst Hapi = require('hapi');\nconst horizon = require('horizon-server');\n\nconst server = new Hapi.Server();\nserver.connection({ port: 8181 });\n\nconst http_servers = server.connections.map((c) => c.listener);\nconst horizon_server = horizon(http_servers);\n\nserver.start(() => {\n  console.log(`Listening on port 8181.`);\n});\n"
  },
  {
    "path": "examples/hapi-server/package.json",
    "content": "{\n  \"name\": \"hapi-horizon-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example of a horizon server within a hapi server.\",\n  \"bin\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"horizon-server\": \"file:../../server\",\n    \"hapi\": \"^12.1.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rethinkdb/horizon.git\"\n  }\n}\n"
  },
  {
    "path": "examples/koa-server/main.js",
    "content": "#!/usr/bin/env node\n'use strict'\n\nconst koa = require('koa');\nconst horizon = require('horizon-server');\n\nconst app = koa();\nconst http_server = app.listen(8181);\nconst horizon_server = horizon(http_server);\n\nconsole.log('Listening on port 8181.');\n"
  },
  {
    "path": "examples/koa-server/package.json",
    "content": "{\n  \"name\": \"koa-horizon-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Example of a horizon server within a koa server.\",\n  \"bin\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"horizon-server\": \"file:../../server\",\n    \"koa\": \"^1.1.2\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rethinkdb/horizon.git\"\n  }\n}\n"
  },
  {
    "path": "examples/react-chat-app/README.md",
    "content": "# Horizon Chat example using React\n\n<center>![](https://i.imgur.com/XFostB8.gif)</center>\n\n\n## Ideas for future\n\n- [ ] implement `presence`\n- [ ] implement `geo` to display users differently that are geographically near you\n- [ ] implement `auth` to allow users to log in via Github, Twitter, etc and have their handle appear.\n"
  },
  {
    "path": "examples/react-chat-app/dist/app.css",
    "content": "/* always present */\n.expand-transition {\n  transition: all .4s ease;\n  height: 30px;\n  padding: 10px;\n  background-color: #eee;\n  overflow: hidden;\n}\n\n/* .expand-enter defines the starting state for entering */\n/* .expand-leave defines the ending state for leaving */\n.expand-enter, .expand-leave {\n  height: 0;\n  padding: 0 10px;\n  opacity: 0;\n}\n\n#app ul {\n  list-style-type: none;\n}\n\n.message {\n  height: 50px;\n}\n\n.message img {\n  vertical-align:middle;\n}\n\n.message .text {\n  vertical-align:middle;\n  margin-left:5px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n  font-size:20px;\n}\n\n.message .datetime {\n  color:darkgrey;\n}\n\n#input {\n  margin-bottom:10%;\n}\n\n#input input {\n  height:100px;\n  font-size:5rem;\n  padding:10px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n}\n"
  },
  {
    "path": "examples/react-chat-app/dist/app.jsx",
    "content": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n\nvar app = app || {};\n\n(function(){\n  'use strict';\n\n  //Setup Horizon connection\n  const horizon = Horizon();\n\n  app.ChatApp = React.createClass({\n\n    uuid: function () {\n      /*jshint bitwise:false */\n      var i, random;\n      var uuid = '';\n\n      for (i = 0; i < 32; i++) {\n        random = Math.random() * 16 | 0;\n        if (i === 8 || i === 12 || i === 16 || i === 20) {\n          uuid += '-';\n        }\n        uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))\n          .toString(16);\n      }\n      return uuid;\n    },\n\n    getDefaultProps: function(){\n\n      const time = new Date().getMilliseconds();\n\n      // Precache the avatar image so it immediately shows on input enter.\n      const image = new Image();\n      image.src = `http://api.adorable.io/avatars/50/${time}.png`;\n\n      return {\n        horizon: horizon(\"react_messages\"),\n        avatarUrl: image.src,\n        authorId: time\n      };\n    },\n\n    getInitialState: function(){\n      return {\n        disabled: true,\n        messages: []\n      };\n    },\n\n    componentDidMount: function(){\n      // As soon as this component is mounted, enable the input\n      this.setState({\n        disabled: false,\n      });\n\n      // Initiate the changefeeds\n      this.subscribe();\n    },\n\n    save: function(message){\n      //Save method for handling messages\n      this.props.horizon.store({\n        id: this.uuid(),\n        text: message,\n        authorId: this.props.authorId,\n        datetime: new Date()\n      }).subscribe();\n    },\n\n    subscribe: function(){\n      this.props.horizon\n        .order(\"datetime\", \"descending\")\n        .limit(8)\n        .watch()\n        .subscribe(messages => {\n            this.setState({ messages: messages })\n        })\n    },\n\n    render: function(){\n      return (\n        <div>\n        <app.ChatList messages={this.state.messages}/>\n        <app.ChatInput\n          disabled={this.props.disabled}\n          onSave={this.save}\n          />\n        </div>\n      );\n    },\n  });\n\n  app.ChatList = React.createClass({\n    render: function(){\n\n      // Construct list of ChatMessages\n      const messages = this.props.messages.map(function(message){\n        return <app.ChatMessage message={message} key={message.id}/>;\n      }, this);\n\n      // Return assembled ChatList of Messages\n      return (\n        <div className=\"row\">\n          <ul>\n          {messages}\n          </ul>\n        </div>\n      );\n    }\n  });\n\n  app.ChatMessage = React.createClass({\n    render: function(){\n      return (\n        <li className=\"message\">\n          <img height=\"50px\" width=\"50px\" src={`http://api.adorable.io/avatars/50/${this.props.message.authorId}.png`}/>\n          <span className=\"text\">\n            {this.props.message.text}\n          </span>\n        </li>\n      );\n    }\n  });\n\n  app.ChatInput = React.createClass({\n    getDefaultProps: function(){\n      // Set default props for enter key\n      return {\n        ENTER_KEY: 13\n      };\n    },\n\n    getInitialState: function(){\n      // Initial state of the inputText is blank \"\"\n      return {\n        inputText: \"\"\n      }\n    },\n\n    handleKeyDown: function(event){\n      // Checking if enter key has been pressed to handle contents of\n      //  input field value.\n      if(event.keyCode === this.props.ENTER_KEY){\n        const val = this.state.inputText.trim();\n        if (val){\n          // Save the value\n          this.props.onSave(val);\n          // Empty the input value\n          this.setState({inputText: \"\"});\n        }\n      }\n    },\n\n    handleChange: function(event){\n      // Every time the value of the input field changes we update the state\n      //  object to have the value of the input field.\n      this.setState({\n        inputText: event.target.value\n      });\n    },\n\n    render: function(){\n      return (\n        <div id=\"input\" className=\"row\">\n          <input\n            className=\"u-full-width\"\n            value={this.state.inputText}\n            disabled={this.props.disabled}\n            onChange={this.handleChange}\n            onKeyDown={this.handleKeyDown}\n            autoFocus={true}\n            />\n        </div>\n      );\n    }\n  });\n\n  // Render this monster.\n  ReactDOM.render(\n    <app.ChatApp/>,\n    document.getElementById('app')\n  );\n})();\n"
  },
  {
    "path": "examples/react-chat-app/dist/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" data-framework=\"react\">\n<head>\n  <link rel=\"stylesheet\" href=\"app.css\">\n  <link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css\">\n</head>\n<body>\n  <div id=\"app\" class=\"container\">\n\n  </div>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react-with-addons.js\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react-dom.js\"></script>\n  <script src=\"https://fb.me/JSXTransformer-0.13.3.js\"></script>\n  <script src=\"/horizon/horizon.js\"></script>\n  <script type=\"text/jsx\" src=\"app.jsx\"></script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/react-chat-app/dist/package.json",
    "content": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"react\": \"^0.13.3\"\n  }\n}\n"
  },
  {
    "path": "examples/react-todo-app/.gitignore",
    "content": "node_modules\ndist/node_modules\n"
  },
  {
    "path": "examples/react-todo-app/dist/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" data-framework=\"react\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>React • TodoMVC</title>\n\t\t<link rel=\"stylesheet\" href=\"node_modules/todomvc-common/base.css\">\n\t\t<link rel=\"stylesheet\" href=\"node_modules/todomvc-app-css/index.css\">\n\t</head>\n\t<body>\n\t\t<section class=\"todoapp\"></section>\n\t\t<footer class=\"info\">\n\t\t\t<p>Double-click to edit a todo</p>\n\t\t\t<p>Created by <a href=\"http://github.com/petehunt/\">petehunt</a></p>\n\t\t\t<p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p>\n\t\t</footer>\n\n\t\t<script src=\"node_modules/todomvc-common/base.js\"></script>\n\t\t<script src=\"node_modules/react/dist/react-with-addons.js\"></script>\n\t\t<script src=\"node_modules/classnames/index.js\"></script>\n\t\t<script src=\"node_modules/react/dist/JSXTransformer.js\"></script>\n\t\t<script src=\"node_modules/director/build/director.js\"></script>\n\n\t\t<script type=\"text/javascript\" src=\"js/utils.js\"></script>\n\t\t<script type=\"text/javascript\" src=\"http://localhost:8181/horizon/horizon.js\"></script>\n\t\t<script type=\"text/javascript\" src=\"https://localhost:8181/horizon/horizon.js\"></script>\n\t\t<script type=\"text/javascript\" src=\"/horizon/horizon.js\"></script>\n\t\t<script src=\"js/todoModel.js\"></script>\n\t\t<!-- jsx is an optional syntactic sugar that transforms methods in React's\n\t\t`render` into an HTML-looking format. Since the two models above are\n\t\tunrelated to React, we didn't need those transforms. -->\n\n\t\t<script type=\"text/jsx\" src=\"js/todoItem.jsx\"></script>\n\t\t<script type=\"text/jsx\" src=\"js/footer.jsx\"></script>\n\t\t<script type=\"text/jsx\" src=\"js/app.jsx\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "examples/react-todo-app/dist/js/app.jsx",
    "content": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n/*global React, Router*/\nvar app = app || {};\n\n(function () {\n\t'use strict';\n\n\tapp.ALL_TODOS = 'all';\n\tapp.ACTIVE_TODOS = 'active';\n\tapp.COMPLETED_TODOS = 'completed';\n\tvar TodoFooter = app.TodoFooter;\n\tvar TodoItem = app.TodoItem;\n\n\tvar ENTER_KEY = 13;\n\n\tvar TodoApp = React.createClass({\n\t\tgetInitialState: function () {\n\t\t\treturn {\n\t\t\t\tnowShowing: app.ALL_TODOS,\n\t\t\t\tediting: null,\n\t\t\t\tnewTodo: ''\n\t\t\t};\n\t\t},\n\n\t\tcomponentDidMount: function () {\n\t\t\tvar setState = this.setState;\n\t\t\tvar router = Router({\n\t\t\t\t'/': setState.bind(this, {nowShowing: app.ALL_TODOS}),\n\t\t\t\t'/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),\n\t\t\t\t'/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS})\n\t\t\t});\n\t\t\trouter.init('/');\n\t\t},\n\n\t\thandleChange: function (event) {\n\t\t\tthis.setState({newTodo: event.target.value});\n\t\t},\n\n\t\thandleNewTodoKeyDown: function (event) {\n\t\t\tif (event.keyCode !== ENTER_KEY) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\n\t\t\tvar val = this.state.newTodo.trim();\n\n\t\t\tif (val) {\n\t\t\t\tthis.props.model.addTodo(val);\n\t\t\t\tthis.setState({newTodo: ''});\n\t\t\t}\n\t\t},\n\n\t\ttoggleAll: function (event) {\n\t\t\tvar checked = event.target.checked;\n\t\t\tthis.props.model.toggleAll(checked);\n\t\t},\n\n\t\ttoggle: function (todoToToggle) {\n\t\t\tthis.props.model.toggle(todoToToggle);\n\t\t},\n\n\t\tdestroy: function (todo) {\n\t\t\tthis.props.model.destroy(todo);\n\t\t},\n\n\t\tedit: function (todo) {\n\t\t\tthis.setState({editing: todo.id});\n\t\t},\n\n\t\tsave: function (todoToSave, text) {\n\t\t\tthis.props.model.save(todoToSave, text);\n\t\t\tthis.setState({editing: null});\n\t\t},\n\n\t\tcancel: function () {\n\t\t\tthis.setState({editing: null});\n\t\t},\n\n\t\tclearCompleted: function () {\n\t\t\tthis.props.model.clearCompleted();\n\t\t},\n\n\t\trender: function () {\n\t\t\tvar footer;\n\t\t\tvar main;\n\t\t\tvar todos = this.props.model.todos;\n\n\t\t\tvar shownTodos = todos.filter(function (todo) {\n\t\t\t\tswitch (this.state.nowShowing) {\n\t\t\t\tcase app.ACTIVE_TODOS:\n\t\t\t\t\treturn !todo.completed;\n\t\t\t\tcase app.COMPLETED_TODOS:\n\t\t\t\t\treturn todo.completed;\n\t\t\t\tdefault:\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}, this);\n\n\t\t\tvar todoItems = shownTodos.map(function (todo) {\n\t\t\t\treturn (\n\t\t\t\t\t<TodoItem\n\t\t\t\t\t\tkey={todo.id}\n\t\t\t\t\t\ttodo={todo}\n\t\t\t\t\t\tonToggle={this.toggle.bind(this, todo)}\n\t\t\t\t\t\tonDestroy={this.destroy.bind(this, todo)}\n\t\t\t\t\t\tonEdit={this.edit.bind(this, todo)}\n\t\t\t\t\t\tediting={this.state.editing === todo.id}\n\t\t\t\t\t\tonSave={this.save.bind(this, todo)}\n\t\t\t\t\t\tonCancel={this.cancel}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\t}, this);\n\n\t\t\tvar activeTodoCount = todos.reduce(function (accum, todo) {\n\t\t\t\treturn todo.completed ? accum : accum + 1;\n\t\t\t}, 0);\n\n\t\t\tvar completedCount = todos.length - activeTodoCount;\n\n\t\t\tif (activeTodoCount || completedCount) {\n\t\t\t\tfooter =\n\t\t\t\t\t<TodoFooter\n\t\t\t\t\t\tcount={activeTodoCount}\n\t\t\t\t\t\tcompletedCount={completedCount}\n\t\t\t\t\t\tnowShowing={this.state.nowShowing}\n\t\t\t\t\t\tonClearCompleted={this.clearCompleted}\n\t\t\t\t\t/>;\n\t\t\t}\n\n\t\t\tif (todos.length) {\n\t\t\t\tmain = (\n\t\t\t\t\t<section className=\"main\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclassName=\"toggle-all\"\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tchecked={activeTodoCount === 0}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ul className=\"todo-list\">\n\t\t\t\t\t\t\t{todoItems}\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</section>\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn (\n\t\t\t\t<div>\n\t\t\t\t\t<header className=\"header\">\n\t\t\t\t\t\t<h1>todos</h1>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclassName=\"new-todo\"\n\t\t\t\t\t\t\tplaceholder=\"What needs to be done?\"\n\t\t\t\t\t\t\tvalue={this.state.newTodo}\n\t\t\t\t\t\t\tonKeyDown={this.handleNewTodoKeyDown}\n\t\t\t\t\t\t\tonChange={this.handleChange}\n\t\t\t\t\t\t\tautoFocus={true}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</header>\n\t\t\t\t\t{main}\n\t\t\t\t\t{footer}\n\t\t\t\t</div>\n\t\t\t);\n\t\t}\n\t});\n\n\tconst model = new app.TodoModel(\"todos_react\");\n\n\tfunction render() {\n\t\tReact.render(\n\t\t\t<TodoApp model={model}/>,\n\t\t\tdocument.getElementsByClassName('todoapp')[0]\n\t\t);\n\t}\n\n\tmodel.subscribe(render);\n\tmodel.subscribeChangefeeds();\n\trender();\n})();\n"
  },
  {
    "path": "examples/react-todo-app/dist/js/footer.jsx",
    "content": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n/*global React */\nvar app = app || {};\n\n(function () {\n\t'use strict';\n\n\tapp.TodoFooter = React.createClass({\n\t\trender: function () {\n\t\t\tvar activeTodoWord = app.Utils.pluralize(this.props.count, 'item');\n\t\t\tvar clearButton = null;\n\n\t\t\tif (this.props.completedCount > 0) {\n\t\t\t\tclearButton = (\n\t\t\t\t\t<button\n\t\t\t\t\t\tclassName=\"clear-completed\"\n\t\t\t\t\t\tonClick={this.props.onClearCompleted}>\n\t\t\t\t\t\tClear completed\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tvar nowShowing = this.props.nowShowing;\n\t\t\treturn (\n\t\t\t\t<footer className=\"footer\">\n\t\t\t\t\t<span className=\"todo-count\">\n\t\t\t\t\t\t<strong>{this.props.count}</strong> {activeTodoWord} left\n\t\t\t\t\t</span>\n\t\t\t\t\t<ul className=\"filters\">\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"#/\"\n\t\t\t\t\t\t\t\tclassName={classNames({selected: nowShowing === app.ALL_TODOS})}>\n\t\t\t\t\t\t\t\t\tAll\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t{' '}\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"#/active\"\n\t\t\t\t\t\t\t\tclassName={classNames({selected: nowShowing === app.ACTIVE_TODOS})}>\n\t\t\t\t\t\t\t\t\tActive\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t{' '}\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"#/completed\"\n\t\t\t\t\t\t\t\tclassName={classNames({selected: nowShowing === app.COMPLETED_TODOS})}>\n\t\t\t\t\t\t\t\t\tCompleted\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t\t{clearButton}\n\t\t\t\t</footer>\n\t\t\t);\n\t\t}\n\t});\n})();\n"
  },
  {
    "path": "examples/react-todo-app/dist/js/todoItem.jsx",
    "content": "/*jshint quotmark: false */\n/*jshint white: false */\n/*jshint trailing: false */\n/*jshint newcap: false */\n/*global React */\nvar app = app || {};\n\n(function () {\n\t'use strict';\n\n\tvar ESCAPE_KEY = 27;\n\tvar ENTER_KEY = 13;\n\n\tapp.TodoItem = React.createClass({\n\t\thandleSubmit: function (event) {\n\t\t\tvar val = this.state.editText.trim();\n\t\t\tif (val) {\n\t\t\t\tthis.props.onSave(val);\n\t\t\t\tthis.setState({editText: val});\n\t\t\t} else {\n\t\t\t\tthis.props.onDestroy();\n\t\t\t}\n\t\t},\n\n\t\thandleEdit: function () {\n\t\t\tthis.props.onEdit();\n\t\t\tthis.setState({editText: this.props.todo.title});\n\t\t},\n\n\t\thandleKeyDown: function (event) {\n\t\t\tif (event.which === ESCAPE_KEY) {\n\t\t\t\tthis.setState({editText: this.props.todo.title});\n\t\t\t\tthis.props.onCancel(event);\n\t\t\t} else if (event.which === ENTER_KEY) {\n\t\t\t\tthis.handleSubmit(event);\n\t\t\t}\n\t\t},\n\n\t\thandleChange: function (event) {\n\t\t\tif (this.props.editing) {\n\t\t\t\tthis.setState({editText: event.target.value});\n\t\t\t}\n\t\t},\n\n\t\tgetInitialState: function () {\n\t\t\treturn {editText: this.props.todo.title};\n\t\t},\n\n\t\t/**\n\t\t * This is a completely optional performance enhancement that you can\n\t\t * implement on any React component. If you were to delete this method\n\t\t * the app would still work correctly (and still be very performant!), we\n\t\t * just use it as an example of how little code it takes to get an order\n\t\t * of magnitude performance improvement.\n\t\t */\n\t\tshouldComponentUpdate: function (nextProps, nextState) {\n\t\t\treturn (\n\t\t\t\tnextProps.todo !== this.props.todo ||\n\t\t\t\tnextProps.editing !== this.props.editing ||\n\t\t\t\tnextState.editText !== this.state.editText\n\t\t\t);\n\t\t},\n\n\t\t/**\n\t\t * Safely manipulate the DOM after updating the state when invoking\n\t\t * `this.props.onEdit()` in the `handleEdit` method above.\n\t\t * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate\n\t\t * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate\n\t\t */\n\t\tcomponentDidUpdate: function (prevProps) {\n\t\t\tif (!prevProps.editing && this.props.editing) {\n\t\t\t\tvar node = React.findDOMNode(this.refs.editField);\n\t\t\t\tnode.focus();\n\t\t\t\tnode.setSelectionRange(node.value.length, node.value.length);\n\t\t\t}\n\t\t},\n\n\t\trender: function () {\n\t\t\treturn (\n\t\t\t\t<li className={classNames({\n\t\t\t\t\tcompleted: this.props.todo.completed,\n\t\t\t\t\tediting: this.props.editing\n\t\t\t\t})}>\n\t\t\t\t\t<div className=\"view\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tclassName=\"toggle\"\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tchecked={this.props.todo.completed}\n\t\t\t\t\t\t\tonChange={this.props.onToggle}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<label onDoubleClick={this.handleEdit}>\n\t\t\t\t\t\t\t{this.props.todo.title}\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<button className=\"destroy\" onClick={this.props.onDestroy} />\n\t\t\t\t\t</div>\n\t\t\t\t\t<input\n\t\t\t\t\t\tref=\"editField\"\n\t\t\t\t\t\tclassName=\"edit\"\n\t\t\t\t\t\tvalue={this.state.editText}\n\t\t\t\t\t\tonBlur={this.handleSubmit}\n\t\t\t\t\t\tonChange={this.handleChange}\n\t\t\t\t\t\tonKeyDown={this.handleKeyDown}\n\t\t\t\t\t/>\n\t\t\t\t</li>\n\t\t\t);\n\t\t}\n\t});\n})();\n"
  },
  {
    "path": "examples/react-todo-app/dist/js/todoModel.js",
    "content": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\nvar app = app || {};\n\n(function () {\n        'use strict';\n\n        const Utils = app.Utils;\n\n        //Setup RethinkDB\n        const horizon = Horizon();\n\n        // Generic \"model\" object. You can use whatever\n        // framework you want. For this application it\n        // may not even be worth separating this logic\n        // out, but we do this to demonstrate one way to\n        // separate out parts of your application.\n        app.TodoModel = function (table_key) {\n                this.todos = [];\n                this.onChanges = [];\n                this.todosDB = horizon(table_key);\n        };\n\n        app.TodoModel.prototype.subscribe = function (onChange) {\n                this.onChanges.push(onChange);\n        };\n\n        app.TodoModel.prototype.inform = function () {\n                this.onChanges.forEach(function (cb) { cb(); });\n        };\n\n        app.TodoModel.prototype.addTodo = function (title) {\n                const newTodo = {\n                        id: Utils.uuid(),\n                        title: title,\n                        completed: false\n                };\n\n          this.todosDB.store(newTodo);\n        };\n\n        app.TodoModel.prototype.toggleAll = function (checked) {\n                console.log(checked);\n                this.todosDB.replace(this.todos.map(function (todo) {\n                        return Utils.extend({}, todo, {completed: checked});\n                }));\n        };\n\n        app.TodoModel.prototype.toggle = function (todoToToggle) {\n                console.log(todoToToggle);\n                this.todosDB.replace(\n                        Utils.extend({}, todoToToggle, {completed: !todoToToggle.completed})\n                );\n        };\n\n        app.TodoModel.prototype.destroy = function (todo) {\n            this.todosDB.remove(todo);\n        };\n\n        app.TodoModel.prototype.save = function (todoToSave, text) {\n          this.todosDB.store(Utils.extend({}, todoToSave, {title: text}));\n        };\n\n        app.TodoModel.prototype.clearCompleted = function () {\n                const oldTodos = this.todos.slice();\n\n                this.todos = this.todos.filter((todo) => {\n                        return !todo.completed;\n                });\n\n                // Send batched deletion of completed todos\n                this.todosDB.removeAll(oldTodos.filter((todo) => {\n                        return !this.todos.includes(todo);\n                }));\n        };\n\n        app.TodoModel.prototype.subscribeChangefeeds = function(){\n                this.todosDB.watch().subscribe(todos => {\n                  this.todos = todos;\n                  this.inform()\n                })\n        };\n})();\n"
  },
  {
    "path": "examples/react-todo-app/dist/js/utils.js",
    "content": "var app = app || {};\n\n(function () {\n\t'use strict';\n\n\tapp.Utils = {\n\t\tuuid: function () {\n\t\t\t/*jshint bitwise:false */\n\t\t\tvar i, random;\n\t\t\tvar uuid = '';\n\n\t\t\tfor (i = 0; i < 32; i++) {\n\t\t\t\trandom = Math.random() * 16 | 0;\n\t\t\t\tif (i === 8 || i === 12 || i === 16 || i === 20) {\n\t\t\t\t\tuuid += '-';\n\t\t\t\t}\n\t\t\t\tuuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))\n\t\t\t\t\t.toString(16);\n\t\t\t}\n\n\t\t\treturn uuid;\n\t\t},\n\n\t\tpluralize: function (count, word) {\n\t\t\treturn count === 1 ? word : word + 's';\n\t\t},\n\n\t\t// store: function (namespace, data) {\n\t\t// \tif (data) {\n\t\t// \t\treturn localStorage.setItem(namespace, JSON.stringify(data));\n\t\t// \t}\n\t\t//\n\t\t// \tvar store = localStorage.getItem(namespace);\n\t\t// \treturn (store && JSON.parse(store)) || [];\n\t\t// },\n\n\t\textend: function () {\n\t\t\tvar newObj = {};\n\t\t\tfor (var i = 0; i < arguments.length; i++) {\n\t\t\t\tvar obj = arguments[i];\n\t\t\t\tfor (var key in obj) {\n\t\t\t\t\tif (obj.hasOwnProperty(key)) {\n\t\t\t\t\t\tnewObj[key] = obj[key];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn newObj;\n\t\t}\n\t};\n})();\n"
  },
  {
    "path": "examples/react-todo-app/dist/package.json",
    "content": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"classnames\": \"^2.1.5\",\n    \"director\": \"^1.2.0\",\n    \"react\": \"^0.13.3\",\n    \"todomvc-app-css\": \"^2.0.0\",\n    \"todomvc-common\": \"^1.0.1\"\n  }\n}\n"
  },
  {
    "path": "examples/react-todo-app/readme.md",
    "content": "# React TodoMVC example using React\n\n## Ideas for future\n\n- [ ] implement `presence` for other connected people ToDo-ing\n\n### Credit\nThanks to @todomvc for the original repo this was forked from.\n"
  },
  {
    "path": "examples/riotjs-chat-app/dist/app.css",
    "content": "/* always present */\n/*.expand-transition {\n  transition: all .2s ease;\n  height: 30px;\n  padding: 10px;\n  background-color: #eee;\n  overflow: hidden;\n}*/\n\n/* .expand-enter defines the starting state for entering */\n/* .expand-leave defines the ending state for leaving */\n/*.expand-enter, .expand-leave {\n  height: 0;\n  padding: 0 10px;\n  opacity: 0;\n}*/\n\n.messages ul {\n  list-style-type: none;\n}\n\n.message {\n  height: 50px;\n}\n\n.message img {\n  vertical-align:middle;\n}\n\n.message .text {\n  vertical-align:middle;\n  margin-left:5px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n  font-size:20px;\n}\n\n.message .datetime {\n  color:darkgrey;\n}\n\n.input {\n  margin-bottom:10%;\n}\n\n.input input {\n  top: 80%;\n  height:100px;\n  font-size:5rem;\n  padding:10px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n}\n"
  },
  {
    "path": "examples/riotjs-chat-app/dist/chat.tag",
    "content": "<chat>\n  <div class=\"container\">\n  <div class=\"row messages\">\n      <ul>\n        <li each={messages.slice().reverse().slice(-8)} class=\"message\">\n          <img height=\"50px\" width=\"50px\" src={ url }>\n          <span class=\"text\">\n            { text }\n          </span>\n\n          <span class=\"datetime u-pull-right\">\n            { datetime.toTimeString() }\n          </span>\n        </li>\n      </ul>\n    </div>\n\n    <div class=\"input row\">\n      <form onsubmit={ saveMessage }>\n        <input autofocus=true name=\"input\" class=\"u-full-width\"> </input>\n      </form>\n    </div>\n  </div>\n\n  <script>\n    horizon = Horizon({authType: \"unauthenticated\"});\n    this.messages = [];\n    this.db = horizon(\"riotjs_chat\");\n\n    this.avatar = new Image()\n    this.avatar.src = \"http://api.adorable.io/avatars/50/\" + new Date().getTime() + \".png\";\n\n    saveMessage(e){\n      this.db.store({\n          text: this.input.value,\n          datetime: new Date(),\n          url: this.avatar.src,\n      });\n      this.input.value = \"\";\n    }\n\n    // Setup changefeed\n    this.db.order(\"datetime\", \"descending\")\n        .limit(8)\n        .watch()\n        .subscribe(messages => {\n          this.messages = messages;\n          this.update();\n        })\n\n    </script>\n\n</chat>\n"
  },
  {
    "path": "examples/riotjs-chat-app/dist/index.html",
    "content": "<html>\n<head>\n  <link rel=\"stylesheet\" href=\"app.css\">\n  <link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css\">\n</head>\n<body>\n  <chat></chat>\n<script type=\"riot/tag\" src=\"chat.tag\"></script>\n<script type=\"text/javascript\" src=\"http://localhost:8181/horizon/horizon.js\"></script>\n<script type=\"text/javascript\" src=\"https://cdn.rawgit.com/riot/riot/master/riot+compiler.min.js\"></script>\n<script>\nriot.mount(\"chat\");\n</script>\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/vue-chat-app/.gitignore",
    "content": "node_modules\ndist/node_modules\n"
  },
  {
    "path": "examples/vue-chat-app/README.md",
    "content": "# RethinkDB Horizon Chat example using Vue.js\n\n<center>![](https://i.imgur.com/XFostB8.gif)</center>\n\n- [ ] implement `presence`\n- [ ] implement `geo` to display users differently that are geographically near you\n- [ ] implement `auth` to allow users to log in via Github, Twitter, etc and have their handle appear.\n"
  },
  {
    "path": "examples/vue-chat-app/dist/app.css",
    "content": "/* always present */\n.expand-transition {\n  transition: all .2s ease;\n  height: 30px;\n  padding: 10px;\n  background-color: #eee;\n  overflow: hidden;\n}\n\n/* .expand-enter defines the starting state for entering */\n/* .expand-leave defines the ending state for leaving */\n.expand-enter, .expand-leave {\n  height: 0;\n  padding: 0 10px;\n  opacity: 0;\n}\n\n#app ul {\n  list-style-type: none;\n}\n\n.message {\n  height: 50px;\n}\n\n.message img {\n  vertical-align:middle;\n}\n\n.message .text {\n  vertical-align:middle;\n  margin-left:5px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n  font-size:20px;\n}\n\n.message .datetime {\n  color:darkgrey;\n}\n\n#input {\n  margin-bottom:10%;\n}\n\n#input input {\n  position: fixed;\n  top: 80%;\n  width: 80%;\n  height:100px;\n  font-size:5rem;\n  padding:10px;\n  font-family: 'Source Code Pro', \"Raleway\", \"Helvetica Neue\";\n}\n"
  },
  {
    "path": "examples/vue-chat-app/dist/app.js",
    "content": "'use strict'\n\nconst horizon = Horizon();\nconst chat = horizon('chat')\nconst app = new Vue({\n\n  el: '#app',\n  data: {\n    newMessage: '',\n    avatar_url: `http://api.adorable.io/avatars/50/${new Date().getMilliseconds()}.png`,\n    messages: [],\n  },\n\n  methods: {\n    addMessage: function() {\n      var text = this.newMessage.trim();\n      if (text) {\n        chat.store({\n          text: text,\n          datetime: new Date(),\n          url: this.avatar_url,\n        }).subscribe();\n        this.newMessage = '';\n      }\n    },\n\n    messagesUpdate: function(newMessages) {\n      this.messages = newMessages;\n      console.log(this.messages);\n    },\n  },\n});\nchat.order('datetime', 'descending')\n  .limit(8)\n  .watch()\n  .subscribe(app.messagesUpdate);\n\n// Image preloading\nconst image = new Image();\nimage.src = app.avatar_url;\n"
  },
  {
    "path": "examples/vue-chat-app/dist/index.html",
    "content": "<html>\n<head>\n  <link rel=\"stylesheet\" href=\"app.css\">\n  <link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css\">\n</head>\n<body>\n  <div id=\"app\" class=\"container\">\n    <div class=\"row\">\n      <ul>\n        <li v-for=\"message in messages\" transition=\"expand\" class=\"message\">\n          <img height=\"50px\" width=\"50px\" :src=\"message.url\">\n          <span class=\"text\">\n            {{ message.text }}\n          </span>\n\n          <span class=\"datetime u-pull-right\">\n            {{message.datetime.toTimeString()}}\n          </span>\n        </li>\n      </ul>\n    </div>\n    <div id=\"input\" class=\"row\">\n      <input class=\"u-full-width\"\n             v-model=\"newMessage\"\n             v-on:keyup.enter=\"addMessage\"\n             autofocus=\"true\"></input>\n    </div>\n  </div>\n  <script src=\"http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.4/vue.min.js\"></script>\n  <script src=\"http://localhost:8181/horizon/horizon.js\"></script>\n  <script src=\"https://localhost:8181/horizon/horizon.js\"></script>\n  <script src=\"/horizon/horizon.js\"></script>\n  <script src=\"app.js\"></script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "examples/vue-todo-app/.gitignore",
    "content": "dist/node_modules\nnode_modules\n"
  },
  {
    "path": "examples/vue-todo-app/dist/index.html",
    "content": "<!doctype html>\n<html data-framework=\"vue\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Vue.js • TodoMVC</title>\n\t\t<link rel=\"stylesheet\" href=\"node_modules/todomvc-common/base.css\">\n\t\t<link rel=\"stylesheet\" href=\"node_modules/todomvc-app-css/index.css\">\n\t\t<style> [v-cloak] { display: none; } </style>\n\t</head>\n\t<body>\n\t\t<section class=\"todoapp\">\n\t\t\t<header class=\"header\">\n\t\t\t\t<h1>fused todos</h1>\n\t\t\t\t<input class=\"new-todo\" autofocus autocomplete=\"off\" placeholder=\"What needs to be done?\" v-model=\"newTodo\" @keyup.enter=\"addTodo\">\n\t\t\t</header>\n\t\t\t<section class=\"main\" v-show=\"todos.length\" v-cloak>\n\t\t\t\t<input class=\"toggle-all\" type=\"checkbox\" v-model=\"allDone\">\n\t\t\t\t<ul class=\"todo-list\">\n\t\t\t\t\t<li class=\"todo\" v-for=\"todo in filteredTodos | orderBy 'datetime' -1\" :class=\"{completed: todo.completed, editing: todo == editedTodo}\">\n\t\t\t\t\t\t<div class=\"view\">\n\t\t\t\t\t\t\t<input class=\"toggle\" type=\"checkbox\" v-model=\"todo.completed\" v-on:change=\"updateTodo(todo)\">\n\t\t\t\t\t\t\t<label @dblclick=\"editTodo(todo)\">{{todo.title}}</label>\n\t\t\t\t\t\t\t<button class=\"destroy\" @click=\"removeTodo(todo)\"></button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<input class=\"edit\" type=\"text\" v-model=\"todo.title\" v-todo-focus=\"todo == editedTodo\" @blur=\"doneEdit(todo)\" @keyup.enter=\"doneEdit(todo)\" @keyup.esc=\"cancelEdit(todo)\">\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t\t<footer class=\"footer\" v-show=\"todos.length\" v-cloak>\n\t\t\t\t<span class=\"todo-count\">\n\t\t\t\t\t<strong v-text=\"remaining\"></strong> {{remaining | pluralize 'item'}} left\n\t\t\t\t</span>\n\t\t\t\t<ul class=\"filters\">\n\t\t\t\t\t<li><a href=\"#/all\" :class=\"{selected: visibility == 'all'}\">All</a></li>\n\t\t\t\t\t<li><a href=\"#/active\" :class=\"{selected: visibility == 'active'}\">Active</a></li>\n\t\t\t\t\t<li><a href=\"#/completed\" :class=\"{selected: visibility == 'completed'}\">Completed</a></li>\n\t\t\t\t</ul>\n\t\t\t\t<button class=\"clear-completed\" @click=\"removeCompleted\" v-show=\"todos.length > remaining\">\n\t\t\t\t\tClear completed\n\t\t\t\t</button>\n\t\t\t</footer>\n\t\t</section>\n\t\t<footer class=\"info\">\n\t\t\t<p>Double-click to edit a todo</p>\n\t\t\t<p>Forked from <a href=\"http://evanyou.me\">Evan You</a></p>\n\t\t\t<p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p>\n\t\t</footer>\n\t\t<script src=\"node_modules/todomvc-common/base.js\"></script>\n\t\t<script src=\"node_modules/director/build/director.js\"></script>\n\t\t<script src=\"node_modules/vue/dist/vue.js\"></script>\n\t\t<script src=\"http://localhost:8181/horizon/horizon.js\"></script>\n\t\t<script src=\"https://localhost:8181/horizon/horizon.js\"></script>\n\t\t<script src=\"/horizon/horizon.js\"></script>\n\t\t<script src=\"js/store.js\"></script>\n\t\t<script src=\"js/app.js\"></script>\n\t\t<script src=\"js/routes.js\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "examples/vue-todo-app/dist/js/app.js",
    "content": "/*global Vue, todoStorage */\n\n(function (exports) {\n        'use strict';\n\n        var filters = {\n                all: function (todos) {\n                        return todos;\n                },\n                active: function (todos) {\n                        return todos.filter(function (todo) {\n                                return !todo.completed;\n                        });\n                },\n                completed: function (todos) {\n                        return todos.filter(function (todo) {\n                                return todo.completed;\n                        });\n                }\n        };\n\n        exports.app = new Vue({\n\n                // the root element that will be compiled\n                el: '.todoapp',\n                debug: true,\n\n                // app initial state\n                data: {\n                        todos: [],\n                        newTodo: '',\n                        editedTodo: null,\n                        visibility: 'all'\n                },\n\n                // watch todos change for localStorage persistence\n                // watch: {\n                //      // todos: {\n                //      //      deep: false,\n                //      //      handler: todoStorage.save,\n                //      // },\n                // },\n\n                // computed properties\n                //  http://vuejs.org/guide/computed.html\n                computed: {\n                        filteredTodos: function () {\n                                return filters[this.visibility](this.todos);\n                        },\n                        remaining: function () {\n                                return filters.active(this.todos).length;\n                        },\n                        allDone: {\n                                get: function () {\n                                        return this.remaining === 0;\n                                },\n                                set: function (value) {\n                                        this.todos.forEach(function (todo) {\n                                                todo.completed = value;\n                                                this.updateTodo(todo);\n                                        }.bind(this));\n                                }\n                        },\n                },\n\n                // methods that implement data logic.\n                //  note there's no DOM manipulation here at all.\n                methods: {\n\n                        addTodo: function () {\n                                const value = this.newTodo && this.newTodo.trim();\n                                if (!value) {\n                                        return;\n                                }\n                                const todo = {\n                                        title: value,\n                                        completed: false,\n                                        datetime: new Date(),\n                                };\n\n                                todoStorage.save(todo);\n                                this.newTodo = '';\n                        },\n\n                        removeTodo: function (todo){\n                                todoStorage.remove(todo);\n                        },\n\n                        editTodo: function (todo) {\n                                this.beforeEditCache = todo.title;\n                                this.editedTodo = todo;\n                                this.updateTodo(todo);\n                        },\n\n                        doneEdit: function (todo) {\n                                if (!this.editedTodo) {\n                                        return;\n                                }\n                                this.editedTodo = null;\n                                todo.title = todo.title.trim();\n                                if (!todo.title) {\n                                        this.removeTodo(todo);\n                                }\n                        },\n\n                        updateTodo: function(todo){\n                                todoStorage.update(todo);\n                        },\n\n                        cancelEdit: function (todo) {\n                                this.editedTodo = null;\n                                todo.title = this.beforeEditCache;\n                        },\n\n                        removeCompleted: function () {\n                                filters.completed(this.todos).forEach(this.removeTodo);\n                        },\n                },\n\n                // a custom directive to wait for the DOM to be updated\n                // before focusing on the input field.\n                // http://vuejs.org/guide/custom-directive.html\n                directives: {\n                        'todo-focus': function (value) {\n                                if (!value) {\n                                        return;\n                                }\n                                var el = this.el;\n                                Vue.nextTick(function () {\n                                        el.focus();\n                                });\n                        }\n                }\n        });\n\n        todoStorage.changes().subscribe(todos => exports.app.todos = todos)\n\n})(window);\n"
  },
  {
    "path": "examples/vue-todo-app/dist/js/routes.js",
    "content": "/*global app, Router */\n\n(function (app, Router) {\n\n\t'use strict';\n\n\tvar router = new Router();\n\n\t['all', 'active', 'completed'].forEach(function (visibility) {\n\t\trouter.on(visibility, function () {\n\t\t\tapp.visibility = visibility;\n\t\t});\n\t});\n\n\trouter.configure({\n\t\tnotfound: function () {\n\t\t\twindow.location.hash = '';\n\t\t\tapp.visibility = 'all';\n\t\t}\n\t});\n\n\trouter.init();\n\n})(app, Router);\n"
  },
  {
    "path": "examples/vue-todo-app/dist/js/store.js",
    "content": "/*jshint unused:false */\n(function(exports) {\n\n  'use strict';\n\n  const horizon = Horizon();\n  const todos = horizon(\"vuejs_todos\");\n\n  exports.todoStorage = {\n    todos: todos,\n\n    save: function(newVal) {\n      todos.store(newVal);\n    },\n\n    update: function(todo) {\n      todos.replace({\n        id: todo.id,\n        title: todo.title,\n        completed: todo.completed,\n        datetime: todo.datetime,\n      })\n    },\n\n    remove: function(todo) {\n      todos.remove(todo);\n    },\n\n    changes: function() {\n      return todos.watch()\n    }\n  };\n\n})(window);\n"
  },
  {
    "path": "examples/vue-todo-app/dist/package.json",
    "content": "{\n  \"private\": true,\n  \"dependencies\": {\n    \"director\": \"^1.2.0\",\n    \"vue\": \"^1.0.1\",\n    \"todomvc-common\": \"^1.0.1\",\n    \"todomvc-app-css\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/vue-todo-app/readme.md",
    "content": "# RethinkDB Horizon TodoMVC example using Vue.js\n\n## Ideas for future\n\n- [ ] implement `presence` for other connected people ToDo-ing\n\n\n## Credit\n\nThis TodoMVC application was originally created by [Evan You](http://evanyou.me).\n"
  },
  {
    "path": "protocol.md",
    "content": "### Handshake\nThe handshake is required before any requests can be served.  If the first message sent cannot be parsed as a handshake, the connection will be dropped.  The handshake will be used to associate the client with a specific user (and set of security rules) on the server.  This should be extensible in the same way as #12.\n\nFor now let's just leave this a placeholder, since we haven't gotten to authentication yet.\n\n#### Handshake Request\n```\n{\n  \"request_id\": <NUMBER>,\n  \"method\": \"unauthenticated\" | \"anonymous\" | \"token\",\n  \"token\": <STRING>,\n}\n```\n\n* `request_id` is a number uniquely identifying this request, it will be returned in the response.\n* `method` designates the type of authentication to be performed.\n  * `unauthenticated` performs no further steps and will not associate the connection with any user.\n  * `anonymous` will create a new account with no external authentication provider.\n  * `token` will associate the connection with the user in the horizon access token provided.\n* `token` is the horizon access token that the client must already possess.\n  * This field is required when `method` is `token`, and invalid otherwise.\n\n#### Handshake Response\n```\n{\n  \"request_id\": <NUMBER>,\n  \"token\": <STRING>\n}\n```\n* `token` is the horizon access token that is associated with this connection.\n  * This token may be used to establish new connections under the same user account until the token expires.\n\n#### Handshake Error Response\n```\n{\n  \"request_id\": <NUMBER>,\n  \"error\": <STRING>,\n  \"error_code\": <NUMBER>\n}\n```\n\n### Requests\n\nAll requests match the following pattern:\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": <STRING>,\n  \"options\": <OBJECT>\n}\n```\n* `request_id` is a number uniquely identifying this request, it will be returned in any responses\n* `type` is the endpoint for the query - one of `query`, `subscribe`, `store_error`, `store_replace`, `update`, or `remove`.\n* `options` is an object structured differently for each endpoint.\n\n\n#### query, subscribe\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"query\" | \"subscribe\",\n  \"options\": {\n    \"collection\": <STRING>,\n    \"order\": [ <ARRAY>, \"ascending\" | \"descending\"],\n    \"above\": [ <OBJECT>, \"open\" | \"closed\" ],\n    \"below\": [ <OBJECT>, \"open\" | \"closed\" ],\n    \"find\": <OBJECT>,\n    \"find_all\": [<OBJECT>, ...],\n    \"limit\": <NUMBER>,\n  }\n}\n```\n* `collection` describes which table to operate on in the horizon database.\n* `order` orders the results according to an array of fields - optional.\n  * The first argument is an array of field names, most-significant first.\n  * The second argument determines which direction the results are sorted in.\n* `above` and `below` are arrays describing the boundaries regarding `order` - optional.\n  * `above` and `below` can only be specified if `order` is provided.\n  * The first argument is an object whose key-value pairs correspond to fields in `order`.\n  * The second argument should be `closed` to include the boundary, and `open` otherwise.\n* `find` returns one object in `collection` that exactly matches the fields in the object given - optional.\n  * `find` cannot be used with `find_all`, `order`, `above`, or `below`.\n* `find_all` is an array of objects whose key-value pairs correspond to keys in `index` - optional.\n  * Returns any object in `collection` that exactly matches the fields in any of the objects given.\n  * `find_all` cannot be used with `find`.\n  * `find_all` with multiple objects cannot be used with `order`, `above`, or `below`.\n* `limit` limits the number of results to be selected - optional.\n\n#### insert, store, upsert, replace, update, remove\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"store\" | \"update\" | \"upsert\" | \"insert\" | \"replace\" | \"remove\",\n  \"options\": {\n    \"collection\": <STRING>,\n    \"data\": [<OBJECT>, ... ]\n  }\n}\n```\n* `collection` describes which table to operate on in the horizon database\n* `data` is the documents to be written (or removed)\n  * `data[i].id` is required for `remove` operations, all other fields are optional\n  * `data[i].id` may be omitted in an `insert`, `store`, or `upsert` operations: a new row will be inserted in the collection\n* `type` is the write operation to perform\n  * `insert` inserts new documents, erroring if any document already exists\n  * `update` updates existing documents. It errors if any document does not already exist\n  * `upsert` updates existing documents or inserts them if they do not exist\n  * `replace` replaces existing documents entirely. It errors if any document does not already exist\n  * `store` replaces existing documents entirely, or inserts them if they don't exist.\n  * `remove` removes documents. It will not error if a document does not exist\n\n#### end_subscription\nTells the horizon server to stop sending data for a given subscription.  Data may still be received until the server has processed this and sent a `\"state\": \"complete\"` response for the subscription.\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"end_subscription\"\n}\n```\n\n#### Keepalive\nThis is used by the client to perform an empty request to avoid connection interruption.\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"keepalive\"\n}\n```\n\n### Responses\n\n#### Error Response\nThis can be sent for any request at any time.  Once an error response is sent, no further responses shall be sent for the corresponding `request_id`.\n```\n{\n  \"request_id\": <NUMBER>,\n  \"error\": <STRING>,\n  \"error_code\": <INTEGER>\n}\n```\n* `request_id` is the same as the `request_id` in the corresponding request\n* `error` is a descriptive error string\n* `error_code` is a code that can be used to identify the type of error, values TBD\n\n#### query, subscribe\n`query` and `subscribe` requests will result in a stream of results from the horizon server to the client.  The stream will be an ordered set of messages from the server following the structure below.  If an error occurs, the above Error Response structure will be used, and the stream is considered \"complete\".  An Error Response may still be sent even after a successful data response, but not after `\"state\": \"complete\"`.\n```\n{\n  \"request_id\": <NUMBER>,\n  \"data\": <ARRAY>,\n  \"state\": \"synced\" | \"complete\"\n}\n```\n* `request_id` is the same as the `request_id` in the corresponding request\n* `data` is an array of results for the `query` or `subscribe`, and may be empty\n* `state` is optional, and indicates a change in the stream of data:\n  * `synced` means that following the consumption of `data`, the client has all the initial results of a `subscribe`\n  * `complete` means that following the consumption of `data`, no more results will be returned for the request\n\n#### Write responses\n`store`, `replace`, `insert`, `update`, `upsert`, and `remove` requests will be given a single response.  This may be an Error Response, or:\n```\n{\n  \"request_id\": <NUMBER>,\n  \"data\": [ { \"id\": <DOCUMENT_ID>, \"$hz_v$\": <DOCUMENT_VERSION> } | { \"error\": <STRING>, \"error_code\": <INTEGER> }, ...],\n  \"state\": \"complete\"\n}\n```\n* `data` is an array of objects corresponding to the documents specified in the write (whether or not a change occurred). For inserted documents it will be the id generated by the server as well as the latest version field for the affected document.  If an error occurred, there will instead be an error description string and an error code in the object .  The items in the array correspond directly to the changes in the request, in the same order.\n* `state` can only be \"complete\" for write responses\n\n#### Keepalive\n`keepalive` requests will be given a single response.  This will never be an error response unless there is a protocol error.\n```\n{\n  \"request_id\": <NUMBER>,\n  \"state\": \"complete\"\n}\n```\n"
  },
  {
    "path": "rfcs/identity_mgmt.md",
    "content": "# Identity Management\n\n### Related issue:\nrethinkdb/horizon#3\n\n### Problem\n\nHorizon needs a concept of a user, and clients of a Horizon server\nneed to be able to add, delete and list users. Users are a fundamental\nentity needed for both the authentication system and permission\nsystem. App developers may also want to add additional meaning to\nusers depending on their needs.\n\n### Proposed solution\n\nCreate a virtual collection called `users` which is really a view into the internal Horizon users table. All operations should transparently operate on a subdocument in the real table.\n\n#### Client changes\n\nNo client changes necessary. There will simply appear to be an\nordinary collection in new apps called `users`.\n\nExamples:\n\n```js\nhorizon('users').find(userId)\nhorizon('users').findAll({first_name: \"John\", last_name: \"Smith\"})\n```\n\n#### Protocol changes\n\nNo protocol changes.\n\n#### Server changes\n\nA `horizon_internal.users` table exists internally right now for\nauthentication. We should continue to use this to track users, but\nshould add a sub-document with the key `app_data`. When a query or\nwrite operation from the client refers to the `users` collection, it\ninstead should operate on the the `app_data` subdocument.\n\nExample:\n\nActual document in `horizon_internal.users`:\n\n```json\n{\n  \"id\": 23,\n  \"permissions\": [],\n  \"app_data\": {\n    \"foo\": \"bar\"\n  }\n}\n```\nDocument returned when client requests user 23:\n\n```json\n{\n  \"id\": 23,\n  \"foo\": \"bar\"\n}\n```\n\nResulting document after `horizon('users').update({id: 23, age: 65})`\n\n``` json\n{\n  \"id\": 23,\n  \"permissions\": [],\n  \"app_data\": {\n    \"foo\": \"bar\",\n    \"age\": 65\n  }\n}\n```\n\nAdditionally, queries for users will use secondary indexes on the\nnested `app_data` document, but queries for users by id will use the\nprimary key of the document.\n\nExample:\n\nA client query like:\n\nClient query:\n```js\nhorizon('users').findAll({age: 65}).fetch()\n```\n\nBackend:\n```js\n// index creation\nr.db('horizon_internal').table('users').indexCreate('app_data_age', x => x('app_data')('age'))\n// query\nr.db('horizon_internal').table('users').getAll(65, {index: 'app_data_age'})\n```\n"
  },
  {
    "path": "rfcs/permissions.md",
    "content": "# Permissions\n\n## Description\n\n[Related issue (#4)](https://github.com/rethinkdb/horizon/issues/4)\n\nAny real application needs to be able to restrict what operations\nusers are able to perform on the database. Horizon needs a way to\nspecify and enforce these kinds of permissions.\n\n## Proposed solution\n\nAt a high level, we are adding three new things:\n\n1. **Groups** which users can belong to and own. It's also possible\n   for a group to own another group. There will be new query types\n   specifically for interacting with groups.\n2. **Query template rules** which restrict the kinds of queries that can be\n   run by a user. These are very performant since we can reject bad\n   operations before a query is even executed.\n3. **Arbitrary js rules** which restrict the results that can be\n   returned from a query. They are specified with a pure javascript\n   function, and are more flexible than query templates (at the cost\n   of being potentially very slow). These are intended as an escape\n   hatch when query templates aren't able to enforce the desired\n   security rule.\n\nPermissions will be specified in the `hzapp.config` file (which is\nwhere app-level configuration like table names and secondary indexes\nare defined). You may also specify hardcoded users and groups in the\nconfig as well.\n\n## Groups\n\n* Each group has either an `owner` or an `owning_group`, as well as a\nlist of members.\n* An `owner` can delete the group and add members to the group.\n* If the group has an `owning_group` instead of an `owner`, any member\nof the `owning_group` may delete the owned group as well as add and remove\nmembers from the owned group.\n* If a user is the `owner` of a group, she is automatically a member\nof the group.\n* However, members of an `owning_group` are not considered members of\n  the owned group unless explicitly placed into the group.\n* The `owning_group` can be the group itself. This is useful for\n  creating administrator groups.\n* Group ownership is not transitive. That is, if group `A` is the\n  `owning_group` of group `B`, and group `B` is the `owning_group` for\n  group `C`, members of group `A` are **not** considered owners of\n  group `C`, even though members of group `B` are. This is to simplify\n  logic for checking ownership and circularity.\n* The `owner` of a group cannot be removed from the group without\n  replacing the owner, or changing ownership to another group. This is\n  intended to prevent groups from being impossible to administrate.\n* A group cannot be named `\"ANYONE\"`.\n\n### Config file changes for groups\n\nThe config file gains two new sections related to groups:\n\n`initial.users` contains one section for each user that should be\ncreated when the app is set up for the first time. These users are\nconsidered \"hard coded\" and can be assumed to be present in client\ncode. The keys in each user's section will be placed in the appData\ndocument for that user (see the\n[RFC on users](https://github.com/rethinkdb/horizon/pull/151)). If\nthere is no appData for that user, the section can be empty. The\npurpose of this section is mainly useful for hard-coding\nadministrative users.\n\n`initial.groups` contains one section for each new group. Each group\nmust have either an `owner` key, or an `owning_group` key. The `owner`\nand `owning_group` values may only refer to groups and users specified\nin the `initial.groups` and `initial.users` section. While groups can\nbe created dynamically (see the client section below), it's also\nperfectly possible that an app may create all of its groups up front\nand forbid new group creation outright.\n\nExample:\n\n```toml\n[initial.user.admin]\nfoo = 'bar'\n[initial.user.superadmin]\n[initial.user.dalanmiller]\n[initial.user.deontologician]\n[initial.user.tryneus]\n\n[initial.groups.administrators]\nowning_group = 'administrators'\nmembers = [ 'superadmin', 'admin' ]\n\n[initial.groups.users]\nowning_group = 'administrators'\nmembers = [\n 'dalanmiller',\n 'deontologician',\n 'tryneus',\n]\n```\n\n### Client changes for groups\n\nThe Horizon client will gain a new `group` method at the same level as\ncollections, and supporting all the same operations as `.get`, with\nrestrictions on how the document can be manipulated.\n\n`horizon.groups()` will refer to the \"collection\" of groups. It lives\nin a different namespace from normal collections because the server\nenforces some rules for operating on documents in the table.\n\nA group document has the keys:\n- `id`: contains the name of the group (referred to as `groupName` in\n  this rfc)\n- `owner`/`owning_group` with the `userId`/`groupName` that owns it\n\nAll of the write operations allowed on normal documents are allowed on\n`.groups()`:\n- `horizon.groups().[insert|store|upsert|replace|remove]`. These have\n  their normal semantics, but will fail if any keys other than `id`,\n  `owner` or `owning_group` are added, or if they are not valid (for\n  instance, if they don't refer to a real user or group).\n  - these operations are how groups are created, deleted, and\n    ownership is changed.\n  - if `insert`ing a group, and no `owner`/`owning_group` is set, the\n    insert will fail.\n- `horizon.groups().[find|findAll|above|below|order|limit]` have their\n  normal semantics, except on groups.\n- `horizon.groups().find(groupName).add(userId)` will add a user to a group.\n- `horizon.groups().find(groupName).remove(userId)` will remove a user from a\n  group. This is different from `horizon.groups().remove(groupName)`\n  which will delete the group itself.\n- `horizon.groups().find(groupName).members()` will list all members\n  of the group. Documents will look like:\n\n```\n{\n \"id\": <GROUPNAME>,\n \"owner\" | \"member\": <USERID>,\n}\n```\n\nSince members of an `owning_group` are not considered members of the\nowned group, they will not be returned in the results of a `members()`\nquery.\n\n### Protocol changes for groups\n\nThe write operations (`insert`, `update`, `upsert`, `replace`,\n`store`, `remove`), as well as the read operations (`query`,\n`subscribe`) will now allow the field `group_name` to be given\nanywhere `collection` was accepted before. `group_name` and\n`collection` are mutually exclusive.\n\nAdditionally, when the request type is `query` or `subscribe`, and the\n`options` object has the `find` key set, the key `members` may be set\nto `null`.\n\nThere is a new request type `group_members`, which enables the `add`\nand `remove` operations on groups. When the the type is\n`group_members`, the `options` key must contain the key `group_name`\nwith the name of the group, and either:\n  - `add` with an array of userIds to add to the group; or\n  - `remove` with an array of userIds to remove from the group\n\nExamples:\n\n`horizon.groups().find(groupName).members()` translates to:\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"query\",\n  \"options\": {\n    \"group_name\": <GROUPNAME>,\n    \"members\": null\n  }\n}\n```\n\n`horizon.groups().findAll({owner: userId}).limit(3)` translates to:\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"query\",\n  \"options\": {\n    \"group_name\": null,\n    \"findAll\": [{owner: <USERID>}],\n    \"limit\": 3\n  }\n}\n```\n\n`horizon.groups().find(groupName).add(userId)` translates to:\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"group_members\",\n  \"options\": {\n    \"group_name\": <GROUPNAME>,'\n    \"add\": [<USERID>, ...],\n  }\n}\n```\n\n`horizon.groups().remove(groupName)` translates to:\n\n```\n{\n  \"request_id\": <NUMBER>,\n  \"type\": \"remove\",\n  \"options\": {\n    \"group_name\": <GROUPNAME>,\n    \"data\": [<USERID>, ...]\n  }\n}\n```\n\n### Server changes for groups\n\nThe server will create two new internal tables: `groups` and\n`group_members`.\n\n`groups` will have the schema:\n\n```\n{\n  \"id\": <GROUPNAME>,\n  \"owner\" | \"owning_group\": <USERID> | <GROUPNAME>\n}\n```\n\nThe `group_members` table will have the schema:\n\n```\n{\n  \"id\": [<GROUPNAME>, <USERID>]\n}\n```\n\nThere should be a secondary index on the user id, so groups can be\nlooked up for a user.\n\nWhen creating a group, the document should first be inserted into the\n`groups` table with the intended `owner`, then a member entry should\nbe added to the `group_membership` table. This allows the groups table\nto become inconsistent if a crash in the server or client happens\nbetween the `groups` insert and the `group_members` insert. In the\nfuture, we should solve this with true two-phase commits, but for now\nit's a rare enough problem and is mainly an inconvenience (since the\ngroup name will just become unusable).\n\nAdditional things the server needs to do:\n* ensure that inserts into the `group_members` table are valid user\n  ids from the `users` table.\n* ensure that when setting `owning_group`, the value is either the\n  name of the current group, or is the name of an existing group.\n* ensure that when a request operates on a group, that the user is\n  either the owner of that group, or the user is a member of the\n  `owning_group`.\n* ensure that the current owner of a group cannot be removed as a\n  member of the group.\n* ensure no group can be created with the name `ANYONE`\n\n## Query template rules\n\nQuery templates are a white list of the shapes of queries that can be\nexecuted by users. Rules can be enabled for certain groups, or for all\ngroups.\n\n### Config changes for query template rules\n\nTemplates are specified in the `hzapp.config` file in the section\n`[query_whitelist]`\n\nThere are 2 special variables: `USER` and `ANYTHING`.\n- `USER` stands in for the current user. It has several properties:\n  - `id` the username/primary key of the user\n  - `groups` a set of groups the user belongs to\n  - `groups_owned` a set of groups the user owns (either directly, or\n    by being a member of an `owning_group`)\n  - `appData` an object with data created and maintained by the\n    app. If users have access to write to their own document, rules\n    based on this data may be insecure.\n- `ANYTHING` is a don't care value. This is to explicitly declare any\n  value is acceptable in a query.\n\nIn the section `[query_whitelist]` each key specifies the group that\nthe specified queries are whitelisted for. They can't refer to groups\ncreated dynamically by the application. There is a special group\n`ANYONE` that indicates the specified queries are executable by any\nuser, even ones that are anonymous or unauthenticated.\n\nAs for syntax:\n* The queries can start with `horizon` or `hz` before the parenthesis\n* The queries should be valid javascript syntax.\n\nExample:\n\n```toml\n[query_whitelist]\nANYONE = [\n  \"horizon('messages').findAll({to: USER.id})\",\n  \"horizon('messages').findAll({from: USER.id})\",\n  \"horizon('broadcasts').findAll({to_group: USER.groups, from: ANYTHING})\",\n]\nadmin = [\n  \"horizon('messages').findAll({})\",\n]\n```\n\nThis would allow a user to retrieve all messages they have either sent\nor received, as well as any broadcasts to any group the user is a\nmember of. (Note that `to_group: USER.groups` is not interpreted as\nequality, it's translated implicitly to 'the `to_group` field matches\nany elements of `USER.groups`).\n\nThis would also allow any members of the `admin` group to run queries\nfor all messages.\n\n#### Subsets of whitelisted queries are ok\n\nExtensions of whitelisted queries are allowed without explicitly\nstating them.\n\nN.B. This takes advantage of the fact that chaining more operations\nonto a Horizon query currently always returns fewer results. If this\nassumption is violated in the future, implicit whitelisting described\nin this section will not be safe.\n\nExample:\n\nIf there is a template:\n\n```toml\n[query_whitelist]\nANYONE = [\n  \"horizon('A').findAll({owner: USER.id})\",\n  \"horizon('B')\"\n]\n```\n\nThen all of these queries are legal:\n\n- `horizon('A').findAll({owner: userId})`\n- `horizon('A').findAll({owner: userId}).above({date: Date.now()})`\n- `horizon('A').findAll({owner: userId, type: 'car'})`\n- `horizon('B')`\n- `horizon('B').findAll({category: 'cars'})`\n- `horizon('B').findAll({category: 'cars'}).above({date: tomorrow})`\n\n#### Specifying write operations\n\nTemplates for write operations can restrict certain fields of\ndocuments being stored. Implicitly, any fields not mentioned in the\ntemplate are free to be whatever the user wants.\n\nExample:\n\n```toml\n[query_whitelist]\nANYONE = [\n  \"horizon('messages').insert({from: USER.id, to: USER.groups})\"\n]\n```\n\nThe above rule would allow this query for a user in the `players`\ngroup:\n\n- `horizon('messages').insert({from: userId, to: 'players', msg: 'Hey there!'})`\n\n### Server changes for query template rules\n\nThe server will need to be able to eval the whitelist rules and store\na representation of them that makes it easy to quickly validate an\nincoming query.\n\nThe server will need to create a few changefeeds per user:\n\n- A changefeed on the user's document itself. This is to keep an up-to-date view of\nthe user's `appData` object for evaluating rules. This may be skipped\nif none of the rules make use of a user's `appData` field.\n- A changefeed on the `group` internal table. This is to keep track of\nwhich groups a user is the owner of.\n- A changefeed on the `group_membership` table to keep track of which\n  groups a user is a member of.\n\nThe server should be able to infer from this data which groups the\nuser is a member of `owning_group` for.\n\nCrucially, it should reject queries that do not match one of the\nwhitelisted rules for the user's groups.\n\n## Arbitrary js rules\n\nArbitrary js rules are specified in the `[js_blacklist]`\nsection of `hzapp.config`. Each rule has a name as a key, which can be\nreturned in errors to the client indicating which rule was violated.\n\n### Config changes for arbitrary js rules\n\n#### Read rules\n\nUsers provide an arbitrary JS function that takes as an input the\ncurrent user (including attributes on the `USER` object above), and\nthe document they're trying to read from the database. It returns\nwhether or not they're allowed to read it as a boolean.\n\nThese rules go in the `[js_blacklist]` section. As\nwith query templates, the group or `ANYONE` should be specified to\nnarrow the scope of where the rule applies.\n\nExample:\n\n```toml\n[js_blacklist.no_old_docs]\napplies_to = 'ANYONE'\nfor = 'reads'\ncollection = 'aged_documents'\njs = \"\"\"\nfunction(user, document) {\n  if (document.age > user.appData.maxDocAge) {\n    return false;\n  } else {\n    return true;\n  }\n}\n\"\"\"\n```\n\n- `applies_to` specifies this rule applies to anyone, this can be any\ngroup name or 'ANYONE'\n- `for` can be either `reads` or `writes`\n- `collection` is an optional field specifying a collection the rule\n  should apply to. This can also be an array of collection names. If\n  not present, the rule applies globally.\n- `js` contains the actual function definition\n\nWhen an error is raised, it should mention that the rule `no_old_docs`\nwas violated for the query.\n\nN.B. Arbitrary document ages are a very inefficient way to enforce\ndocument schemas. They aren't intended to be used for that purpose.\n\n#### Write rules\n\nWrite rules are similar to read rules, except that they get both the\nprevious version of the document and the new version of the\ndocument. These rules go in the `[js_blacklist.writes]`\nsection.\n\nExample:\n\nSuppose for a group called `users` we have the following rule:\n\n```toml\n[js_blacklist.no_changing_ownership]\nfor = 'writes'\ngroup = 'ANYONE'\njs = \"\"\"\nfunction(user, oldDocument, newDocument) {\n  if (oldDocument.owner !== newDocument.owner) {\n    return false;\n  } else {\n    return true;\n  }\n}\n\"\"\"\n```\n\nThis rule would disallow changing the ownership of any document in any\ncollection.\n\n#### Errors in batch writes\n\nWhen an individual write in a batch write fails an arbitrary js check,\nthe entire batch won't fail. Instead that document will be skipped,\nand the server will attempt to continue on for the rest of the\ndocuments in the batch.\n\n### Server changes for arbitrary js rules\n\nThe server will need to read in the permissions from the\n`hzapp.config` file, and enforce them as described above.\n"
  },
  {
    "path": "server/.babelrc",
    "content": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "server/.eslintrc.js",
    "content": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n  extends: \"../.eslintrc.js\",\n  rules: {\n    \"max-len\": [ ERROR, 130 ],\n    \"prefer-template\": [ OFF ],\n  },\n  env: {\n    \"es6\": true,\n    \"node\": true,\n    \"mocha\": true,\n  },\n};\n"
  },
  {
    "path": "server/.gitignore",
    "content": "cert.pem\nkey.pem\nnode_modules\n*.log\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# RethinkDB stuff\nrethinkdb_data/\nrethinkdb_data_test/"
  },
  {
    "path": "server/README.md",
    "content": "# Horizon Server\n\nAn extensible middleware server built on top of [RethinkDB](https://github.com/rethinkdb/rethinkdb) which exposes a websocket API to front-end applications.\n\n## Documentation\n\nFollow our documentation at [samuelhughes.com/rethinkdb/horizon-docs/install.html](http://samuelhughes.com/rethinkdb/horizon-docs/install.html) for instructions on installing Horizon.\n\n## Requirements\nThe Horizon server requires some tools and libraries to be available before it\ncan run:\n\n * `node.js` - interpreter to run the Horizon server\n * `openssl` - generating ssl certificates\n * [`rethinkdb`](https://github.com/rethinkdb/rethinkdb) - for running a RethinkDB server\n\n### OpenSSL\n\nOpenSSL is required to generate the cert and key pair necessary to serve Horizon securely via HTTPS and WSS. Usually this is done on the production server where you are running Horizon, however to do this locally you'll need to have the OpenSSL installed.\n\n* **OSX**    - `brew install openssl`\n* **Ubuntu** -  [Follow this guide here](https://help.ubuntu.com/community/OpenSSL#Practical_OpenSSL_Usage).\n* **Windows** - [Unofficial list of Windows OpenSSL Binaries](https://wiki.openssl.org/index.php/Binaries)\n\n### RethinkDB\n\nCheck out [rethinkdb.com/install](https://rethinkdb.com/install) for the best method of installing RethinkDB on your platform.\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"@horizon/server\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Server for RethinkDB Horizon, an open-source developer platform for building realtime, scalable web apps.\",\n  \"main\": \"src/horizon.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src test\",\n    \"test\": \"mocha test/test.js test/schema.js --timeout 10000\",\n    \"coverage\": \"istanbul cover _mocha test/test.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rethinkdb/horizon.git\"\n  },\n  \"author\": \"RethinkDB\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/rethinkdb/horizon/issues\"\n  },\n  \"homepage\": \"https://github.com/rethinkdb/horizon#readme\",\n  \"engines\": {\n    \"node\": \">=4.0.0\"\n  },\n  \"dependencies\": {\n    \"@horizon/client\": \"2.0.0\",\n    \"bluebird\": \"^3.4.0\",\n    \"cookie\": \"^0.2.3\",\n    \"joi\": \"^8.0.4\",\n    \"jsonwebtoken\": \"^5.5.4\",\n    \"mime-types\": \"^2.0.4\",\n    \"oauth\": \"^0.9.14\",\n    \"pem\": \"^1.8.1\",\n    \"rethinkdb\": \"^2.1.1\",\n    \"winston\": \"^2.1.0\",\n    \"ws\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^7.3.1\",\n    \"istanbul\": \"^0.4.3\",\n    \"mocha\": \"^2.3.3\",\n    \"nodemon\": \"^1.8.1\"\n  }\n}\n"
  },
  {
    "path": "server/src/auth/auth0.js",
    "content": "'use strict';\n\nconst auth_utils = require('./utils');\nconst logger = require('../logger');\n\nconst https = require('https');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst Joi = require('joi');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n  host: Joi.string().required(),\n}).unknown(false);\n\nfunction auth0(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const host = options.host;\n\n  const self_url = (self_host, path) =>\n    url.format({ protocol: 'https', host: self_host, pathname: path });\n\n  const make_acquire_url = (state, redirect_uri) =>\n    url.format({ protocol: 'https',\n                 host: host,\n                 pathname: '/authorize',\n                 query: { response_type: 'code', client_id, redirect_uri, state } });\n\n  const make_token_request = (code, redirect_uri) => {\n    const req = https.request({ method: 'POST', host, path: '/oauth/token',\n                                headers: { 'Content-type': 'application/x-www-form-urlencoded' } });\n    req.write(querystring.stringify({\n        client_id, redirect_uri, client_secret, code,\n        grant_type: 'authorization_code'\n      }));\n    return req;\n  };\n\n  const make_inspect_request = (access_token) =>\n    https.request({ host, path: '/userinfo',\n                    headers: { Authorization: `Bearer ${access_token}` } });\n\n  const extract_id = (user_info) => user_info && user_info.user_id;\n\n\n  auth_utils.oauth2({\n    horizon,\n    provider: options.path,\n    make_acquire_url,\n    make_token_request,\n    make_inspect_request,\n    extract_id,\n  });\n}\n\nmodule.exports = auth0;\n"
  },
  {
    "path": "server/src/auth/facebook.js",
    "content": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n}).unknown(false);\n\nfunction facebook(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const provider = options.path;\n\n  // Facebook requires inspect requests to use a separate app access token\n  let app_token;\n\n  const make_app_token_request = () =>\n    https.request(\n      url.format({ protocol: 'https',\n                   host: 'graph.facebook.com',\n                   pathname: '/oauth/access_token',\n                   query: { client_id, client_secret, grant_type: 'client_credentials' } }));\n\n  auth_utils.run_request(make_app_token_request(), (err, body) => {\n    const parsed = body && querystring.parse(body);\n    app_token = parsed && parsed.access_token;\n\n    if (err) {\n      logger.error(`Failed to obtain \"${provider}\" app token: ${err}`);\n    } else if (!app_token) {\n      logger.error(`Could not parse access token from API response: ${body}`);\n    }\n  });\n\n  const oauth_options = { horizon, provider };\n\n  oauth_options.make_acquire_url = (state, redirect_uri) =>\n    url.format({ protocol: 'https',\n                 host: 'www.facebook.com',\n                 pathname: '/dialog/oauth',\n                 query: { client_id, state, redirect_uri, response_type: 'code' } });\n\n  oauth_options.make_token_request = (code, redirect_uri) => {\n    const req = https.request({ method: 'POST',\n                                host: 'graph.facebook.com',\n                                path: '/v2.3/oauth/access_token' });\n    req.write(querystring.stringify({ code, redirect_uri, client_id, client_secret }));\n    return req;\n  };\n\n  oauth_options.make_inspect_request = (input_token) =>\n    https.request(\n      url.format({ protocol: 'https',\n                   host: 'graph.facebook.com',\n                   pathname: '/debug_token',\n                   query: { access_token: app_token, input_token } }));\n\n  oauth_options.extract_id = (user_info) =>\n    user_info && user_info.data && user_info.data.user_id;\n\n  auth_utils.oauth2(oauth_options);\n}\n\nmodule.exports = facebook;\n"
  },
  {
    "path": "server/src/auth/github.js",
    "content": "'use strict';\n\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n}).unknown(false);\n\nfunction github(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const provider = options.path;\n\n  const oauth_options = { horizon, provider };\n\n  oauth_options.make_acquire_url = (state, redirect_uri) =>\n    url.format({ protocol: 'https',\n                 host: 'github.com',\n                 pathname: '/login/oauth/authorize',\n                 query: { client_id, redirect_uri, state } });\n\n  oauth_options.make_token_request = (code, redirect_uri) => {\n    const req = https.request({ method: 'POST',\n                                host: 'github.com',\n                                path: '/login/oauth/access_token',\n                                headers: { accept: 'application/json' } });\n\n    req.write(querystring.stringify({ code, client_id, client_secret, redirect_uri }));\n\n    return req;\n  };\n\n  oauth_options.make_inspect_request = (access_token) =>\n    https.request({ host: 'api.github.com',\n                    path: `/user?${querystring.stringify({ access_token })}`,\n                    headers: { 'user-agent': 'node.js' } });\n\n  oauth_options.extract_id = (user_info) => user_info && user_info.id;\n\n  auth_utils.oauth2(oauth_options);\n}\n\nmodule.exports = github;\n"
  },
  {
    "path": "server/src/auth/google.js",
    "content": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n}).unknown(false);\n\nfunction google(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const provider = options.path;\n\n  const oauth_options = { horizon, provider };\n\n  oauth_options.make_acquire_url = (state, redirect_uri) =>\n    url.format({ protocol: 'https',\n                 host: 'accounts.google.com',\n                 pathname: '/o/oauth2/v2/auth',\n                 query: { client_id, redirect_uri, state, response_type: 'code', scope: 'profile' } });\n\n  oauth_options.make_token_request = (code, redirect_uri) => {\n    const query_params = querystring.stringify({\n      code, client_id, client_secret, redirect_uri,\n      grant_type: 'authorization_code' });\n    const path = `/oauth2/v4/token?${query_params}`;\n    return https.request({ method: 'POST', host: 'www.googleapis.com', path });\n  };\n\n  oauth_options.make_inspect_request = (access_token) => {\n    logger.debug(`using access token: ${access_token}`);\n    const path = `/oauth2/v1/userinfo?${querystring.stringify({ access_token })}`;\n    return https.request({ host: 'www.googleapis.com', path });\n  };\n\n  oauth_options.extract_id = (user_info) => user_info && user_info.id;\n\n  auth_utils.oauth2(oauth_options);\n}\n\nmodule.exports = google;\n"
  },
  {
    "path": "server/src/auth/slack.js",
    "content": "'use strict';\n\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n}).unknown(false);\n\nfunction slack(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const provider = options.path;\n  const scope = options && options.scope || 'identify';\n  const team = options && options.team || '';\n\n  const oauth_options = {\n    horizon,\n    provider,\n  };\n\n  oauth_options.make_acquire_url = (state, redirect_uri) =>\n    url.format({\n      protocol: 'https',\n      host: 'slack.com',\n      pathname: '/oauth/authorize',\n      query: {\n        client_id,\n        redirect_uri,\n        state,\n        scope,\n        team,\n      },\n    });\n\n  oauth_options.make_token_request = (code, redirect_uri) =>\n    https.request({\n      method: 'POST',\n      host: 'slack.com',\n      path: `/api/oauth.access?${querystring.stringify({\n        code,\n        client_id,\n        client_secret,\n        redirect_uri,\n      })}`,\n      headers: {\n        'Content-Type': 'application/json',\n        accept: 'application/json',\n      },\n    });\n\n  oauth_options.make_inspect_request = (access_token) =>\n    https.request({\n      host: 'slack.com',\n      path: `/api/auth.test?${querystring.stringify({ token: access_token })}`,\n      headers: {\n        'Content-Type': 'application/json',\n        'user-agent': 'node.js',\n      },\n    });\n\n  oauth_options.extract_id = (user_info) => user_info && user_info.user_id;\n\n  auth_utils.oauth2(oauth_options);\n}\n\nmodule.exports = slack;\n"
  },
  {
    "path": "server/src/auth/twitch.js",
    "content": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst querystring = require('querystring');\nconst url = require('url');\n\nconst options_schema = Joi.object().keys({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n}).unknown(false);\n\nfunction twitch(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const client_id = options.id;\n  const client_secret = options.secret;\n  const provider = options.path;\n\n  const oauth_options = { horizon, provider };\n\n  oauth_options.make_acquire_url = (state, redirect_uri) =>\n    url.format({ protocol: 'https',\n                 host: 'api.twitch.tv',\n                 pathname: '/kraken/oauth2/authorize',\n                 query: { client_id, redirect_uri, state, response_type: 'code', scope: 'user_read' } });\n\n  oauth_options.make_token_request = (code, redirect_uri) => {\n    const req = https.request({ method: 'POST',\n                                host: 'api.twitch.tv',\n                                path: '/kraken/oauth2/token' });\n    req.write(querystring.stringify({\n      client_id, redirect_uri, client_secret, code,\n      grant_type: 'authorization_code' }));\n    return req;\n  };\n\n  oauth_options.make_inspect_request = (access_token) => {\n    logger.debug(`using access token: ${access_token}`);\n    return https.request({ host: 'api.twitch.tv',\n                           path: '/kraken/user',\n                           headers: { authorization: `OAuth ${access_token}` } });\n  };\n\n  oauth_options.extract_id = (user_info) => user_info && user_info._id;\n\n  auth_utils.oauth2(oauth_options);\n}\n\nmodule.exports = twitch;\n"
  },
  {
    "path": "server/src/auth/twitter.js",
    "content": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst Joi = require('joi');\nconst oauth = require('oauth');\nconst url = require('url');\n\nconst options_schema = Joi.object({\n  path: Joi.string().required(),\n  id: Joi.string().required(),\n  secret: Joi.string().required(),\n});\n\n// Cache for request token secrets\nconst nonce_cache = new Map();\nconst nonce_cache_ttl_ms = 60 * 60 * 1000;\n\nconst store_app_token = (nonce, token) => {\n  const time = Date.now();\n  const cutoff = time - nonce_cache_ttl_ms;\n  const iter = nonce_cache.entries();\n\n  let item = iter.next();\n  while (item.value && item.value[1].time < cutoff) {\n    nonce_cache.delete(item.value[0]);\n    item = iter.next();\n  }\n\n  nonce_cache.set(nonce, { time, token });\n};\n\nconst get_app_token = (nonce) => {\n  const res = nonce_cache.get(nonce);\n  nonce_cache.delete(nonce);\n  return res && res.token;\n};\n\nfunction twitter(horizon, raw_options) {\n  const options = Joi.attempt(raw_options, options_schema);\n  const provider = options.path;\n  const consumer_key = options.id;\n  const consumer_secret = options.secret;\n\n  const oa = new oauth.OAuth('https://twitter.com/oauth/request_token',\n                             'https://twitter.com/oauth/access_token',\n                             consumer_key,\n                             consumer_secret,\n                             '1.0a',\n                             '', // Callback URL, to be filled in per-user\n                             'HMAC-SHA1');\n\n  const user_info_url = 'https://api.twitter.com/1.1/account/verify_credentials.json';\n\n  const make_success_url = (horizon_token) =>\n    url.format(auth_utils.extend_url_query(horizon._auth._success_redirect, { horizon_token }));\n\n  const make_failure_url = (horizon_error) =>\n    url.format(auth_utils.extend_url_query(horizon._auth._failure_redirect, { horizon_error }));\n\n  horizon.add_http_handler(provider, (req, res) => {\n    const request_url = url.parse(req.url, true);\n    const user_token = request_url.query && request_url.query.oauth_token;\n    const verifier = request_url.query && request_url.query.oauth_verifier;\n\n    logger.debug(`oauth request: ${JSON.stringify(request_url)}`);\n    if (!user_token) {\n      // Auth has not been started yet, determine our callback URL and register an app token for it\n      // First generate a nonce to track this client session to prevent CSRF attacks\n      auth_utils.make_nonce((nonce_err, nonce) => {\n        if (nonce_err) {\n          logger.error(`Error creating nonce for oauth state: ${nonce_err}`);\n          auth_utils.do_redirect(res, make_failure_url('error generating nonce'));\n        } else {\n          oa._authorize_callback =\n            url.format({ protocol: 'https',\n                         host: req.headers.host,\n                         pathname: request_url.pathname,\n                         query: { state: auth_utils.nonce_to_state(nonce) } });\n\n          oa.getOAuthRequestToken((err, app_token, app_token_secret, body) => {\n            if (err || body.oauth_callback_confirmed !== 'true') {\n              logger.error(`Error acquiring app oauth token: ${JSON.stringify(err)}`);\n              auth_utils.do_redirect(res, make_failure_url('error acquiring app oauth token'));\n            } else {\n              store_app_token(nonce, app_token_secret);\n              auth_utils.set_nonce(res, horizon._name, nonce);\n              auth_utils.do_redirect(res, url.format({ protocol: 'https',\n                                                       host: 'api.twitter.com',\n                                                       pathname: '/oauth/authenticate',\n                                                       query: { oauth_token: app_token } }));\n            }\n          });\n        }\n      });\n    } else {\n      // Make sure this is the same client who obtained the code to prevent CSRF attacks\n      const nonce = auth_utils.get_nonce(req, horizon._name);\n      const state = request_url.query.state;\n      const app_token = get_app_token(nonce);\n\n      if (!nonce || !state || !app_token || state !== auth_utils.nonce_to_state(nonce)) {\n        auth_utils.do_redirect(res, make_failure_url('session expired'));\n      } else {\n        oa.getOAuthAccessToken(user_token, app_token, verifier, (err, access_token, secret) => {\n          if (err) {\n            logger.error(`Error contacting oauth API: ${err}`);\n            auth_utils.do_redirect(res, make_failure_url('oauth provider error'));\n          } else {\n            oa.get(user_info_url, access_token, secret, (err2, body) => {\n              const user_info = auth_utils.try_json_parse(body);\n              const user_id = user_info && user_info.id;\n\n              if (err2) {\n                logger.error(`Error contacting oauth API: ${err2}`);\n                auth_utils.do_redirect(res, make_failure_url('oauth provider error'));\n              } else if (!user_id) {\n                logger.error(`Bad JSON data from oauth API: ${body}`);\n                auth_utils.do_redirect(res, make_failure_url('unparseable inspect response'));\n              } else {\n                horizon._auth.generate(provider, user_id).nodeify((err3, jwt) => {\n                  auth_utils.clear_nonce(res, horizon._name);\n                  auth_utils.do_redirect(res, err3 ?\n                    make_failure_url('invalid user') :\n                    make_success_url(jwt.token));\n                });\n              }\n            });\n          }\n        });\n      }\n    }\n  });\n}\n\nmodule.exports = twitter;\n"
  },
  {
    "path": "server/src/auth/utils.js",
    "content": "'use strict';\n\nconst logger = require('../logger');\n\nconst cookie = require('cookie');\nconst crypto = require('crypto');\nconst Joi = require('joi');\nconst url = require('url');\n\nconst do_redirect = (res, redirect_url) => {\n  logger.debug(`Redirecting user to ${redirect_url}`);\n  res.writeHead(302, { Location: redirect_url });\n  res.end();\n};\n\nconst extend_url_query = (path, query) => {\n  const path_copy = Object.assign({}, path);\n  if (path_copy.query === null) {\n    path_copy.query = query;\n  } else {\n    path_copy.query = Object.assign({}, path_copy.query);\n    path_copy.query = Object.assign({}, path_copy.query, query);\n  }\n  return path_copy;\n};\n\nconst run_request = (req, cb) => {\n  logger.debug(`Initiating request to ${req._headers.host}${req.path}`);\n  req.once('response', (res) => {\n    const chunks = [];\n    res.on('data', (data) => {\n      chunks.push(data);\n    });\n    res.once('end', () => {\n      if (res.statusCode !== 200) {\n        cb(new Error(`Request returned status code: ${res.statusCode} ` +\n                     `(${res.statusMessage}): ${chunks.join('')}`));\n      } else {\n        cb(null, chunks.join(''));\n      }\n    });\n  });\n  req.once('error', (err) => {\n    cb(err);\n  });\n  req.end();\n};\n\nconst try_json_parse = (data) => {\n  try {\n    return JSON.parse(data);\n  } catch (err) {\n    // Do nothing - just return undefined\n  }\n};\n\nconst nonce_cookie = (name) => `${name}_horizon_nonce`;\n\nconst make_nonce = (cb) => crypto.randomBytes(64, (err, res) => {\n  if (!err) {\n    cb(err, res.toString('base64'));\n  } else {\n    cb(err, res);\n  }\n});\n\n// TODO: this base64 encoding isn't URL-friendly\nconst nonce_to_state = (nonce) =>\n  crypto.createHash('sha256').update(nonce, 'base64').digest('base64');\n\nconst set_nonce = (res, name, nonce) =>\n  res.setHeader('set-cookie',\n                cookie.serialize(nonce_cookie(name), nonce,\n                                 { maxAge: 3600, secure: true, httpOnly: true }));\n\nconst clear_nonce = (res, name) =>\n  res.setHeader('set-cookie',\n                cookie.serialize(nonce_cookie(name), 'invalid',\n                                 { maxAge: -1, secure: true, httpOnly: true }));\n\nconst get_nonce = (req, name) => {\n  const field = nonce_cookie(name);\n  if (req.headers.cookie) {\n    const value = cookie.parse(req.headers.cookie);\n    return value[field];\n  }\n};\n\nconst options_schema = Joi.object({\n  horizon: Joi.object().required(),\n  provider: Joi.string().required(),\n  make_acquire_url: Joi.func().arity(2).required(), // take `state` and `return_url`, return string\n  make_token_request: Joi.func().arity(2).required(), // take `code` and `return_url`, return request\n  make_inspect_request: Joi.func().arity(1).required(), // take `access_token`, return request\n  extract_id: Joi.func().arity(1).required(), // take `user_info`, return value\n}).unknown(false);\n\n// Attaches an endpoint to the horizon server, providing an oauth2 redirect flow\nconst oauth2 = (raw_options) => {\n  const options = Joi.attempt(raw_options, options_schema);\n\n  const horizon = options.horizon;\n  const provider = options.provider;\n  const make_acquire_url = options.make_acquire_url;\n  const make_token_request = options.make_token_request;\n  const make_inspect_request = options.make_inspect_request;\n  const extract_id = options.extract_id;\n\n  const self_url = (host, path) =>\n    url.format({ protocol: 'https', host: host, pathname: path });\n\n  const make_success_url = (horizon_token) =>\n    url.format(extend_url_query(horizon._auth._success_redirect, { horizon_token }));\n\n  const make_failure_url = (horizon_error) =>\n    url.format(extend_url_query(horizon._auth._failure_redirect, { horizon_error }));\n\n  horizon.add_http_handler(provider, (req, res) => {\n    const request_url = url.parse(req.url, true);\n    const return_url = self_url(req.headers.host, request_url.pathname);\n    const code = request_url.query && request_url.query.code;\n    const error = request_url.query && request_url.query.error;\n\n    logger.debug(`oauth request: ${JSON.stringify(request_url)}`);\n    if (error) {\n      const description = request_url.query.error_description || error;\n      do_redirect(res, make_failure_url(description));\n    } else if (!code) {\n      // We need to redirect to the API to acquire a token, then come back and try again\n      // Generate a nonce to track this client session to prevent CSRF attacks\n      make_nonce((nonce_err, nonce) => {\n        if (nonce_err) {\n          logger.error(`Error creating nonce for oauth state: ${nonce_err}`);\n          res.statusCode = 503;\n          res.end('error generating nonce');\n        } else {\n          set_nonce(res, horizon._name, nonce);\n          do_redirect(res, make_acquire_url(nonce_to_state(nonce), return_url));\n        }\n      });\n    } else {\n      // Make sure this is the same client who obtained the code to prevent CSRF attacks\n      const nonce = get_nonce(req, horizon._name);\n      const state = request_url.query.state;\n\n      if (!nonce || !state || state !== nonce_to_state(nonce)) {\n        do_redirect(res, make_failure_url('session expired'));\n      } else {\n        // We have the user code, turn it into an access token\n        run_request(make_token_request(code, return_url), (err1, body) => {\n          const info = try_json_parse(body);\n          const access_token = info && info.access_token;\n\n          if (err1) {\n            logger.error(`Error contacting oauth API: ${err1}`);\n            res.statusCode = 503;\n            res.end('oauth provider error');\n          } else if (!access_token) {\n            logger.error(`Bad JSON data from oauth API: ${body}`);\n            res.statusCode = 500;\n            res.end('unparseable token response');\n          } else {\n            // We have the user access token, get info on it so we can find the user\n            run_request(make_inspect_request(access_token), (err2, inner_body) => {\n              const user_info = try_json_parse(inner_body);\n              const user_id = user_info && extract_id(user_info);\n\n              if (err2) {\n                logger.error(`Error contacting oauth API: ${err2}`);\n                res.statusCode = 503;\n                res.end('oauth provider error');\n              } else if (!user_id) {\n                logger.error(`Bad JSON data from oauth API: ${inner_body}`);\n                res.statusCode = 500;\n                res.end('unparseable inspect response');\n              } else {\n                horizon._auth.generate(provider, user_id).nodeify((err3, jwt) => {\n                  // Clear the nonce just so we aren't polluting clients' cookies\n                  clear_nonce(res, horizon._name);\n                  do_redirect(res, err3 ?\n                    make_failure_url('invalid user') :\n                    make_success_url(jwt.token));\n                });\n              }\n            });\n          }\n        });\n      }\n    }\n  });\n};\n\nmodule.exports = {\n  oauth2,\n  do_redirect, run_request,\n  make_nonce, set_nonce, get_nonce, clear_nonce, nonce_to_state,\n  extend_url_query,\n  try_json_parse,\n};\n"
  },
  {
    "path": "server/src/auth.js",
    "content": "'use strict';\n\nconst logger = require('./logger');\nconst options_schema = require('./schema/server_options').auth;\nconst writes = require('./endpoint/writes');\n\nconst Joi = require('joi');\nconst Promise = require('bluebird');\nconst jwt = Promise.promisifyAll(require('jsonwebtoken'));\nconst r = require('rethinkdb');\nconst url = require('url');\n\n\nclass JWT {\n  constructor(options) {\n    this.duration = options.duration;\n    this.algorithm = 'HS512';\n\n    if (options.token_secret != null) {\n      this.secret = new Buffer(options.token_secret, 'base64');\n    } else {\n      throw new Error(\n        'No token_secret set! Try setting it in .hz/secrets.toml ' +\n        'or passing it to the Server constructor.');\n    }\n  }\n\n  // A generated token contains the data:\n  // { id: <uuid>, provider: <string> }\n  sign(payload) {\n    const token = jwt.sign(\n      payload,\n      this.secret,\n      { algorithm: this.algorithm, expiresIn: this.duration }\n    );\n\n    return { token, payload };\n  }\n\n  verify(token) {\n    return jwt.verifyAsync(token, this.secret, { algorithms: [ this.algorithm ] })\n    .then((payload) => ({ token, payload }));\n  }\n}\n\n\nclass Auth {\n  constructor(server, user_options) {\n    const options = Joi.attempt(user_options, options_schema);\n\n    this._jwt = new JWT(options);\n\n    this._success_redirect = url.parse(options.success_redirect);\n    this._failure_redirect = url.parse(options.failure_redirect);\n    this._create_new_users = options.create_new_users;\n    this._new_user_group = options.new_user_group;\n    this._allow_anonymous = options.allow_anonymous;\n    this._allow_unauthenticated = options.allow_unauthenticated;\n\n    this._parent = server;\n  }\n\n  handshake(request) {\n    switch (request.method) {\n    case 'token':\n      return this._jwt.verify(request.token);\n    case 'unauthenticated':\n      if (!this._allow_unauthenticated) {\n        throw new Error('Unauthenticated connections are not allowed.');\n      }\n      return this._jwt.verify(this._jwt.sign({ id: null, provider: request.method }).token);\n    case 'anonymous':\n      if (!this._allow_anonymous) {\n        throw new Error('Anonymous connections are not allowed.');\n      }\n      return this.generate(request.method, r.uuid());\n    default:\n      throw new Error(`Unknown handshake method \"${request.method}\"`);\n    }\n  }\n\n  // Can't use objects in primary keys, so convert those to JSON in the db (deterministically)\n  auth_key(provider, info) {\n    if (info === null || Array.isArray(info) || typeof info !== 'object') {\n      return [ provider, info ];\n    } else {\n      return [ provider, r.expr(info).toJSON() ];\n    }\n  }\n\n  new_user_row(id) {\n    return {\n      id,\n      groups: [ 'default', this._new_user_group ],\n      [writes.version_field]: 0,\n    };\n  }\n\n  // TODO: maybe we should write something into the user data to track open sessions/tokens\n  generate(provider, info) {\n    return Promise.resolve().then(() => {\n      const key = this.auth_key(provider, info);\n      const db = r.db(this._parent._name);\n\n      const insert = (table, row) =>\n        db.table(table)\n          .insert(row, { conflict: 'error', returnChanges: 'always' })\n          .bracket('changes')(0)('new_val');\n\n      let query = db.table('users')\n                    .get(db.table('hz_users_auth').get(key)('user_id'))\n                    .default(r.error('User not found and new user creation is disabled.'));\n\n      if (this._create_new_users) {\n        query = insert('hz_users_auth', { id: key, user_id: r.uuid() })\n          .do((auth_user) => insert('users', this.new_user_row(auth_user('user_id'))));\n      }\n\n      return query.run(this._parent._reql_conn.connection()).catch((err) => {\n        // TODO: if we got a `Duplicate primary key` error, it was likely a race condition\n        // and we should succeed if we try again.\n        logger.debug(`Failed user lookup or creation: ${err}`);\n        throw new Error('User lookup or creation in database failed.');\n      });\n    }).then((user) =>\n      this._jwt.sign({ id: user.id, provider })\n    );\n  }\n}\n\n\nmodule.exports = { Auth };\n"
  },
  {
    "path": "server/src/client.js",
    "content": "'use strict';\n\nconst logger = require('./logger');\nconst schemas = require('./schema/horizon_protocol');\nconst Request = require('./request').Request;\n\nconst Joi = require('joi');\nconst websocket = require('ws');\n\nclass Client {\n  constructor(socket, server, metadata) {\n    logger.debug('Client connection established.');\n    this._socket = socket;\n    this._server = server;\n    this._auth = this._server._auth;\n    this._permissions_enabled = this._server._permissions_enabled;\n    this._metadata = metadata;\n    this._requests = new Map();\n    this.user_info = { };\n\n    this._socket.on('close', (code, msg) =>\n      this.handle_websocket_close(code, msg));\n\n    this._socket.on('error', (error) =>\n      this.handle_websocket_error(error));\n\n    // The first message should always be the handshake\n    this._socket.once('message', (data) =>\n      this.error_wrap_socket(() => this.handle_handshake(data)));\n\n    if (server._max_connections !== null && server._reql_conn._clients.size >= server._max_connections) {\n      this.close({ request_id: null, error: 'Max connections limit reached.', error_code: 0 });\n    }\n  }\n\n  handle_websocket_close() {\n    logger.debug('Client connection terminated.');\n    if (this.user_feed) {\n      this.user_feed.close().catch(() => { });\n    }\n    this._requests.forEach((request) => {\n      request.close();\n    });\n    this._requests.clear();\n    this._server._reql_conn._clients.delete(this);\n  }\n\n  handle_websocket_error(code, msg) {\n    logger.error(`Received error from client: ${msg} (${code})`);\n  }\n\n  error_wrap_socket(cb) {\n    try {\n      cb();\n    } catch (err) {\n      logger.debug(`Unhandled error in request: ${err.stack}`);\n      this.close({ request_id: null,\n                   error: `Unhandled error: ${err}`,\n                   error_code: 0 });\n    }\n  }\n\n  parse_request(data, schema) {\n    let request;\n    try {\n      request = JSON.parse(data);\n    } catch (err) {\n      return this.close({ request_id: null,\n                          error: `Invalid JSON: ${err}`,\n                          error_code: 0 });\n    }\n\n    try {\n      return Joi.attempt(request, schema);\n    } catch (err) {\n      const detail = err.details[0];\n      const err_str = `Request validation error at \"${detail.path}\": ${detail.message}`;\n      const request_id = request.request_id === undefined ? null : request.request_id;\n\n      if (request.request_id === undefined) {\n        // This is pretty much an unrecoverable protocol error, so close the connection\n        this.close({ request_id, error: `Protocol error: ${err}`, error_code: 0 });\n      } else {\n        this.send_error({ request_id }, err_str);\n      }\n    }\n  }\n\n  group_changed(group_name) {\n    if (this.user_info.groups.indexOf(group_name) !== -1) {\n      this._requests.forEach((req) => req.evaluate_rules());\n    }\n  }\n\n  handle_handshake(data) {\n    const request = this.parse_request(data, schemas.handshake);\n    logger.debug(`Received handshake: ${JSON.stringify(request)}`);\n\n    if (request === undefined) {\n      return this.close({ error: 'Invalid handshake.', error_code: 0 });\n    }\n\n    let responded = false;\n    this._auth.handshake(request).then((res) => {\n      const finish_handshake = () => {\n        if (!responded) {\n          responded = true;\n          const info = { token: res.token, id: res.payload.id, provider: res.payload.provider };\n          this.send_response(request, info);\n          this._socket.on('message', (msg) =>\n            this.error_wrap_socket(() => this.handle_request(msg)));\n        }\n      };\n      this.user_info = res.payload;\n\n      if (this.user_info.id != null) {\n        return this._metadata.get_user_feed(this.user_info.id).then((feed) => {\n          this.user_feed = feed;\n          return feed.eachAsync((change) => {\n            if (!change.new_val) {\n              throw new Error('User account has been deleted.');\n            }\n            Object.assign(this.user_info, change.new_val);\n            this._requests.forEach((req) => req.evaluate_rules());\n            finish_handshake();\n          }).then(() => {\n            throw new Error('User account feed has been lost.');\n          });\n        });\n      } else {\n        this.user_info.groups = [ 'default' ];\n        finish_handshake();\n      }\n    }).catch((err) => {\n      if (!responded) {\n        responded = true;\n        this.close({ request_id: request.request_id, error: `${err}`, error_code: 0 });\n      }\n    });\n  }\n\n  handle_request(data) {\n    logger.debug(`Received request from client: ${data}`);\n    const raw_request = this.parse_request(data, schemas.request);\n\n    if (raw_request === undefined) {\n      return;\n    } else if (raw_request.type === 'end_subscription') {\n      return this.remove_request(raw_request); // there is no response for end_subscription\n    } else if (raw_request.type === 'keepalive') {\n      return this.send_response(raw_request, { state: 'complete' });\n    }\n\n    const endpoint = this._server.get_request_handler(raw_request);\n    if (endpoint === undefined) {\n      return this.send_error(raw_request,\n        `\"${raw_request.type}\" is not a registered request type.`);\n    } else if (this._requests.has(raw_request.request_id)) {\n      return this.send_error(raw_request,\n        `Request ${raw_request.request_id} already exists for this client.`);\n    }\n\n    const request = new Request(raw_request, endpoint, this);\n    this._requests.set(raw_request.request_id, request);\n    request.run();\n  }\n\n  remove_request(raw_request) {\n    const request = this._requests.get(raw_request.request_id);\n    this._requests.delete(raw_request.request_id);\n    if (request) {\n      request.close();\n    }\n  }\n\n  is_open() {\n    return this._socket.readyState === websocket.OPEN;\n  }\n\n  close(info) {\n    if (this.is_open()) {\n      const close_msg = (info.error && info.error.substr(0, 64)) || 'Unspecified reason.';\n      logger.debug('Closing client connection with message: ' +\n                   `${info.error || 'Unspecified reason.'}`);\n      logger.debug(`info: ${JSON.stringify(info)}`);\n      if (info.request_id !== undefined) {\n        this._socket.send(JSON.stringify(info));\n      }\n      this._socket.close(1002, close_msg);\n    }\n  }\n\n  send_response(request, data) {\n    // Ignore responses for disconnected clients\n    if (this.is_open()) {\n      data.request_id = request.request_id;\n      logger.debug(`Sending response: ${JSON.stringify(data)}`);\n      this._socket.send(JSON.stringify(data));\n    }\n  }\n\n  send_error(request, err, code) {\n    logger.debug(`Sending error result for request ${request.request_id}:\\n${err.stack}`);\n\n    const error = err instanceof Error ? err.message : err;\n    const error_code = code === undefined ? -1 : code;\n    this.send_response(request, { error, error_code });\n  }\n}\n\nconst make_client = (socket, server) => {\n  try {\n    const metadata = server._reql_conn.metadata();\n    const client = new Client(socket, server, metadata);\n    server._reql_conn._clients.add(client);\n  } catch (err) {\n    logger.debug(`Rejecting client connection because of error: ${err.message}`);\n    socket.close(1002, err.message.substr(0, 64));\n  }\n};\n\nmodule.exports = { make_client };\n"
  },
  {
    "path": "server/src/endpoint/common.js",
    "content": "'use strict';\n\nconst reql_options = {\n  timeFormat: 'raw',\n  binaryFormat: 'raw',\n};\n\nmodule.exports = {\n  reql_options,\n};\n"
  },
  {
    "path": "server/src/endpoint/insert.js",
    "content": "'use strict';\n\nconst insert = require('../schema/horizon_protocol').insert;\nconst writes = require('./writes');\nconst reql_options = require('./common').reql_options;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, insert);\n  if (parsed.error !== null) { done(new Error(parsed.error.details[0].message)); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      Array(rows.length).fill(null),\n    (row, info) => { // validation, each row\n      if (!ruleset.validate(context, info, row)) {\n        return new Error(writes.unauthorized_msg);\n      }\n    },\n    (rows) => // write to database, all valid rows\n      collection.table\n        .insert(rows.map((row) => writes.apply_version(r.expr(row), 0)),\n                { returnChanges: 'always' })\n        .run(conn, reql_options)\n  ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/query.js",
    "content": "'use strict';\n\nconst query = require('../schema/horizon_protocol').query;\nconst check = require('../error.js').check;\nconst reql_options = require('./common').reql_options;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst object_to_fields = (obj) =>\n  Object.keys(obj).map((key) => {\n    const value = obj[key];\n    if (value !== null && typeof value === 'object' && !value['$reql_type$']) {\n      return object_to_fields(value).map((subkeys) => [ key ].concat(subkeys));\n    } else {\n      return [ key ];\n    }\n  });\n\n// This is exposed to be reused by 'subscribe'\nconst make_reql = (raw_request, metadata) => {\n  const parsed = Joi.validate(raw_request.options, query);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n  const options = parsed.value;\n\n  const collection = metadata.collection(parsed.value.collection);\n  let reql = collection.table;\n\n  const ordered_between = (obj) => {\n    const fuzzy_fields = object_to_fields(obj);\n    const order_keys = (options.order && options.order[0]) ||\n                       (options.above && Object.keys(options.above[0])) ||\n                       (options.below && Object.keys(options.below[0])) || [ ];\n\n    if (order_keys.length >= 1) {\n      const k = order_keys[0];\n      check(!options.above || options.above[0][k] !== undefined,\n            '\"above\" must be on the same field as the first in \"order\".');\n      check(!options.below || options.below[0][k] !== undefined,\n            '\"below\" must be on the same field as the first in \"order\".');\n    }\n\n    order_keys.forEach((k) => {\n      check(obj[k] === undefined,\n            `\"${k}\" cannot be used in \"order\", \"above\", or \"below\" when finding by that field.`);\n    });\n\n    const index = collection.get_matching_index(fuzzy_fields, order_keys.map((k) => [ k ]));\n\n    const get_bound = (name) => {\n      const eval_key = (key) => {\n        if (obj[key] !== undefined) {\n          return obj[key];\n        } else if (options[name] && options[name][0][key] !== undefined) {\n          return options[name][0][key];\n        } else if (options[name] && options[name][1] === 'open') {\n          return name === 'above' ? r.maxval : r.minval;\n        } else {\n          return name === 'above' ? r.minval : r.maxval;\n        }\n      };\n\n      if (index.name === 'id') {\n        return eval_key('id');\n      }\n      return index.fields.map((k) => eval_key(k));\n    };\n\n    const above_value = get_bound('above');\n    const below_value = get_bound('below');\n\n    const optargs = {\n      index: index.name,\n      leftBound: options.above ? options.above[1] : 'closed',\n      rightBound: options.below ? options.below[1] : 'closed',\n    };\n\n    const order = (options.order && options.order[1] === 'descending') ?\n      r.desc(index.name) : index.name;\n    return reql.orderBy({ index: order }).between(above_value, below_value, optargs);\n  };\n\n  if (options.find) {\n    reql = ordered_between(options.find).limit(1);\n  } else if (options.find_all && options.find_all.length > 1) {\n    reql = r.union.apply(r, options.find_all.map((x) => ordered_between(x)));\n  } else {\n    reql = ordered_between((options.find_all && options.find_all[0]) || { });\n  }\n\n  if (options.limit !== undefined) {\n    reql = reql.limit(options.limit);\n  }\n\n  return reql;\n};\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  let cursor;\n  const reql = make_reql(raw_request, metadata);\n\n  reql.run(metadata.connection(), reql_options).then((res) => {\n    if (res !== null && res.constructor.name === 'Cursor') {\n      cursor = res;\n      return cursor.eachAsync((item) => {\n        if (!ruleset.validate(context, item)) {\n          done(new Error('Operation not permitted.'));\n          cursor.close().catch(() => { });\n        } else {\n          send({ data: [ item ] });\n        }\n      }).then(() => {\n        done({ data: [ ], state: 'complete' });\n      });\n    } else if (res !== null && res.constructor.name === 'Array') {\n      for (const item of res) {\n        if (!ruleset.validate(context, item)) {\n          return done(new Error('Operation not permitted.'));\n        }\n      }\n      done({ data: res, state: 'complete' });\n    } else if (!ruleset.validate(context, res)) {\n      done(new Error('Operation not permitted.'));\n    } else {\n      done({ data: [ res ], state: 'complete' });\n    }\n  }).catch(done);\n\n  return () => {\n    if (cursor) {\n      cursor.close().catch(() => { });\n    }\n  };\n};\n\nmodule.exports = { make_reql, run };\n"
  },
  {
    "path": "server/src/endpoint/remove.js",
    "content": "'use strict';\n\nconst remove = require('../schema/horizon_protocol').remove;\nconst reql_options = require('./common').reql_options;\nconst writes = require('./writes');\nconst hz_v = writes.version_field;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, remove);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      r.expr(rows.map((row) => row.id))\n        .map((id) => collection.table.get(id))\n        .run(conn, reql_options),\n    (row, info) => writes.validate_old_row_required(context, row, info, null, ruleset),\n    (rows) => // write to database, all valid rows\n      r.expr(rows).do((row_data) =>\n        row_data.forEach((info) =>\n          collection.table.get(info('id')).replace((row) =>\n              r.branch(// The row may have been deleted between the get and now\n                       row.eq(null),\n                       null,\n\n                       // The row may have been changed between the get and now\n                       r.and(info.hasFields(hz_v),\n                             row(hz_v).default(-1).ne(info(hz_v))),\n                       r.error(writes.invalidated_msg),\n\n                       // Otherwise, we can safely remove the row\n                       null),\n\n              { returnChanges: 'always' }))\n          // Pretend like we deleted rows that didn't exist\n          .do((res) =>\n            res.merge({ changes:\n              r.range(row_data.count()).map((index) =>\n                r.branch(res('changes')(index)('old_val').eq(null),\n                         res('changes')(index).merge({ old_val: { id: row_data(index)('id') } }),\n                         res('changes')(index))).coerceTo('array'),\n            })))\n        .run(conn, reql_options)\n  ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/replace.js",
    "content": "'use strict';\n\nconst replace = require('../schema/horizon_protocol').replace;\nconst reql_options = require('./common').reql_options;\nconst writes = require('./writes');\nconst hz_v = writes.version_field;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, replace);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      r.expr(rows.map((row) => row.id))\n        .map((id) => collection.table.get(id))\n        .run(conn, reql_options),\n    (row, info) => writes.validate_old_row_required(context, row, info, row, ruleset),\n    (rows) => // write to database, all valid rows\n      r.expr(rows)\n        .forEach((new_row) =>\n          collection.table.get(new_row('id')).replace((old_row) =>\n              r.branch(// The row may have been deleted between the get and now\n                       old_row.eq(null),\n                       r.error(writes.missing_msg),\n\n                       // The row may have been changed between the get and now\n                       r.and(new_row.hasFields(hz_v),\n                             old_row(hz_v).default(-1).ne(new_row(hz_v))),\n                       r.error(writes.invalidated_msg),\n\n                       // Otherwise, we can safely replace the row\n                       writes.apply_version(new_row, old_row(hz_v).default(-1).add(1))),\n              { returnChanges: 'always' }))\n      .run(conn, reql_options)\n  ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/store.js",
    "content": "'use strict';\n\nconst store = require('../schema/horizon_protocol').store;\nconst reql_options = require('./common').reql_options;\nconst writes = require('./writes');\nconst hz_v = writes.version_field;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, store);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      r.expr(rows.map((row) => (row.id === undefined ? null : row.id)))\n        .map((id) => r.branch(id.eq(null), null, collection.table.get(id)))\n        .run(conn, reql_options),\n    (row, info) => writes.validate_old_row_optional(context, row, info, row, ruleset),\n    (rows) => // write to database, all valid rows\n      r.expr(rows)\n        .forEach((new_row) =>\n          r.branch(new_row.hasFields('id'),\n                   collection.table.get(new_row('id')).replace((old_row) =>\n                       r.branch(\n                         old_row.eq(null),\n                         r.branch(\n                           // Error if we were expecting the row to exist\n                           new_row.hasFields(hz_v),\n                           r.error(writes.invalidated_msg),\n\n                           // Otherwise, insert the row\n                           writes.apply_version(new_row, 0)\n                         ),\n                         r.branch(\n                           // The row may have changed from the expected version\n                           r.and(new_row.hasFields(hz_v),\n                                 old_row(hz_v).default(-1).ne(new_row(hz_v))),\n                           r.error(writes.invalidated_msg),\n\n                           // Otherwise, we can safely overwrite the row\n                           writes.apply_version(new_row, old_row(hz_v).default(-1).add(1))\n                         )\n                       ), { returnChanges: 'always' }),\n\n                   // The new row does not have an id, so we insert it with an autogen id\n                   collection.table.insert(writes.apply_version(new_row, 0),\n                                           { returnChanges: 'always' })))\n        .run(conn, reql_options)\n  ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/subscribe.js",
    "content": "'use strict';\n\nconst make_reql = require('./query').make_reql;\nconst reql_options = require('./common').reql_options;\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  let feed;\n  const reql = make_reql(raw_request, metadata);\n\n  reql.changes({ include_initial: true,\n                 include_states: true,\n                 include_types: true,\n                 include_offsets: Boolean(raw_request.options.order) &&\n                                  Boolean(raw_request.options.limit) })\n    .run(metadata.connection(), reql_options)\n    .then((res) => {\n      feed = res;\n      feed.eachAsync((item) => {\n        if (item.state === 'initializing') {\n          // Do nothing - we don't care\n        } else if (item.state === 'ready') {\n          send({ state: 'synced' });\n        } else if ((item.old_val && !ruleset.validate(context, item.old_val)) ||\n                   (item.new_val && !ruleset.validate(context, item.new_val))) {\n          throw new Error('Operation not permitted.');\n        } else {\n          send({ data: [ item ] });\n        }\n      }).then(() => {\n        done({ state: 'complete' });\n      }).catch(done);\n    }).catch(done);\n\n  return () => {\n    if (feed) {\n      feed.close().catch(() => { });\n    }\n  };\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/update.js",
    "content": "'use strict';\n\nconst update = require('../schema/horizon_protocol').update;\nconst reql_options = require('./common').reql_options;\nconst writes = require('./writes');\nconst hz_v = writes.version_field;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, update);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      r.expr(rows)\n        .map((new_row) =>\n          collection.table.get(new_row('id')).do((old_row) =>\n            r.branch(old_row.eq(null),\n                     null,\n                     [ old_row, old_row.merge(new_row) ])))\n        .run(conn, reql_options),\n    (row, info) => writes.validate_old_row_required(context, row, info[0], info[1], ruleset),\n    (rows) => // write to database, all valid rows\n      r.expr(rows)\n        .forEach((new_row) =>\n          collection.table.get(new_row('id')).replace((old_row) =>\n              r.branch(// The row may have been deleted between the get and now\n                       old_row.eq(null),\n                       r.error(writes.missing_msg),\n\n                       // The row may have been changed between the get and now\n                       r.and(new_row.hasFields(hz_v),\n                             old_row(hz_v).default(-1).ne(new_row(hz_v))),\n                       r.error(writes.invalidated_msg),\n\n                       // Otherwise we can safely update the row and increment the version\n                       writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))),\n              { returnChanges: 'always' }))\n        .run(conn, reql_options)\n    ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/upsert.js",
    "content": "'use strict';\n\nconst upsert = require('../schema/horizon_protocol').upsert;\nconst reql_options = require('./common').reql_options;\nconst writes = require('./writes');\nconst hz_v = writes.version_field;\n\nconst Joi = require('joi');\nconst r = require('rethinkdb');\n\nconst run = (raw_request, context, ruleset, metadata, send, done) => {\n  const parsed = Joi.validate(raw_request.options, upsert);\n  if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); }\n\n  const collection = metadata.collection(parsed.value.collection);\n  const conn = metadata.connection();\n\n  writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout,\n    (rows) => // pre-validation, all rows\n      r.expr(rows)\n        .map((new_row) =>\n          r.branch(new_row.hasFields('id'),\n                   collection.table.get(new_row('id')).do((old_row) =>\n                     r.branch(old_row.eq(null),\n                              [ null, new_row ],\n                              [ old_row, old_row.merge(new_row) ])),\n                   [ null, new_row ]))\n        .run(conn, reql_options),\n    (row, info) => writes.validate_old_row_optional(context, row, info[0], info[1], ruleset),\n    (rows) => // write to database, all valid rows\n      r.expr(rows)\n        .forEach((new_row) =>\n          r.branch(new_row.hasFields('id'),\n                   collection.table.get(new_row('id')).replace((old_row) =>\n                       r.branch(\n                         old_row.eq(null),\n                         r.branch(\n                           // Error if we were expecting the row to exist\n                           new_row.hasFields(hz_v),\n                           r.error(writes.invalidated_msg),\n\n                           // Otherwise, insert the row\n                           writes.apply_version(new_row, 0)\n                         ),\n                         r.branch(\n                           // The row may have changed from the expected version\n                           r.and(new_row.hasFields(hz_v),\n                                 old_row(hz_v).default(-1).ne(new_row(hz_v))),\n                           r.error(writes.invalidated_msg),\n\n                           // Otherwise, we can safely update the row and increment the version\n                           writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))\n                         )\n                       ), { returnChanges: 'always' }),\n\n                   // The new row did not have an id, so we insert it with an autogen id\n                   collection.table.insert(writes.apply_version(new_row, 0),\n                                           { returnChanges: 'always' })))\n        .run(conn, reql_options)\n    ).then(done).catch(done);\n};\n\nmodule.exports = { run };\n"
  },
  {
    "path": "server/src/endpoint/writes.js",
    "content": "'use strict';\n\nconst check = require('../error').check;\n\nconst r = require('rethinkdb');\n\n// Common functionality used by write requests\n\nconst invalidated_msg = 'Write invalidated by another request, try again.';\nconst missing_msg = 'The document was missing.';\nconst timeout_msg = 'Operation timed out.';\nconst unauthorized_msg = 'Operation not permitted.';\n\nconst hz_v = '$hz_v$';\nconst apply_version = (row, new_version) => row.merge(r.object(hz_v, new_version));\n\nconst make_write_response = (data) => {\n  data.forEach((item, index) => {\n    if (item instanceof Error) {\n      data[index] = { error: item.message };\n    }\n  });\n  return { data, state: 'complete' };\n};\n\n// This function returns a Promise that resolves to an array of responses - one for each row in\n//  `original_rows`, or rejects with an appropriate error.\n// timeout -> integer\n//   minimum number of milliseconds before giving up on retrying writes\n//   null means no timeout\n// pre_validate -> function (rows):\n//   rows: all pending rows\n//   return: an array or the promise of an array of info for those rows\n//           (which will be passed to the validate callback)\n// validate_row -> function (row, info):\n//   row: The row from the original query\n//   info: The info returned by the pre_validate step for this row\n//   return: nothing if successful or an error to be put as the response for this row\n// do_write -> function (rows):\n//   rows: all pending rows\n//   return: a (promise of a) ReQL write result object\nconst retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, do_write) => {\n  const iterate = (row_data, response_data, deadline_optional) => {\n    let deadline = deadline_optional;\n    if (row_data.length === 0) {\n      return response_data;\n    } else if (timeout !== null) {\n      if (!deadline) {\n        deadline = Date.now() + timeout;\n      } else if (Date.now() > deadline) {\n        response_data.forEach((data, index) => {\n          if (data === null) {\n            response_data[index] = new Error(timeout_msg);\n          }\n        });\n        return response_data;\n      }\n    }\n\n\n    return Promise.resolve().then(() => {\n      // The validate callback may clobber the original version field in the row,\n      // so we have to restore it to the original value.\n      // This is done because validation only approves moving from one specific\n      // version of the row to another.  Even if the original request did not choose\n      // the version, we are locked in to the version fetched from the pre_validate\n      // callback until the next iteration.  If the version has changed in the meantime,\n      // it is an invalidated error which may be retried until we hit the deadline.\n      row_data.forEach((data) => {\n        if (data.version === undefined) {\n          delete data.row[hz_v];\n        } else {\n          data.row[hz_v] = data.version;\n        }\n      });\n\n      if (ruleset.validation_required()) {\n        // For the set of rows to write, gather info for the validation step\n        return Promise.resolve(pre_validate(row_data.map((data) => data.row))).then((infos) => {\n          check(infos.length === row_data.length);\n\n          // For each row to write (and info), validate it with permissions\n          const valid_rows = [ ];\n          row_data.forEach((data, i) => {\n            const res = validate_row(data.row, infos[i]);\n\n            if (res !== undefined) {\n              response_data[data.index] = res;\n            } else {\n              valid_rows.push(data);\n            }\n          });\n          row_data = valid_rows;\n        });\n      }\n    }).then(() => { // For the set of valid rows, call the write step\n      if (row_data.length === 0) {\n        return [ ];\n      }\n      return do_write(row_data.map((data) => data.row)).then((res) => res.changes);\n    }).then((changes) => {\n      check(changes.length === row_data.length);\n\n      // Remove successful writes and invalidated writes that had an initial version\n      const retry_rows = [ ];\n      row_data.forEach((data, index) => {\n        const res = changes[index];\n        if (res.error !== undefined) {\n          if (res.error.indexOf('Duplicate primary key') === 0) {\n            response_data[data.index] = { error: 'The document already exists.' };\n          } else if (res.error.indexOf(invalidated_msg) === 0 &&\n                     data.version === undefined) {\n            retry_rows.push(data);\n          } else {\n            response_data[data.index] = { error: res.error };\n          }\n        } else if (res.new_val === null) {\n          response_data[data.index] = { id: res.old_val.id, [hz_v]: res.old_val[hz_v] };\n        } else {\n          response_data[data.index] = { id: res.new_val.id, [hz_v]: res.new_val[hz_v] };\n        }\n      });\n\n      // Recurse, after which it will decide if there is more work to be done\n      return iterate(retry_rows, response_data, deadline);\n    });\n  };\n\n  return iterate(original_rows.map((row, index) => ({ row, index, version: row[hz_v] })),\n                 Array(original_rows.length).fill(null),\n                 null).then(make_write_response);\n};\n\nconst validate_old_row_optional = (context, original, old_row, new_row, ruleset) => {\n  const expected_version = original[hz_v];\n  if (expected_version !== undefined &&\n      (!old_row || expected_version !== old_row[hz_v])) {\n    return new Error(invalidated_msg);\n  } else if (!ruleset.validate(context, old_row, new_row)) {\n    return new Error(unauthorized_msg);\n  }\n\n  if (old_row) {\n    const old_version = old_row[hz_v];\n    if (expected_version === undefined) {\n      original[hz_v] = old_version === undefined ? -1 : old_version;\n    }\n  }\n};\n\nconst validate_old_row_required = (context, original, old_row, new_row, ruleset) => {\n  if (old_row === null) {\n    return new Error(missing_msg);\n  }\n\n  const old_version = old_row[hz_v];\n  const expected_version = original[hz_v];\n  if (expected_version !== undefined &&\n      expected_version !== old_version) {\n    return new Error(invalidated_msg);\n  } else if (!ruleset.validate(context, old_row, new_row)) {\n    return new Error(unauthorized_msg);\n  }\n\n  if (expected_version === undefined) {\n    original[hz_v] = old_version === undefined ? -1 : old_version;\n  }\n};\n\nmodule.exports = {\n  invalidated_msg,\n  missing_msg,\n  timeout_msg,\n  unauthorized_msg,\n  make_write_response,\n  version_field: hz_v,\n  apply_version,\n  retry_loop,\n  validate_old_row_required,\n  validate_old_row_optional,\n};\n"
  },
  {
    "path": "server/src/error.js",
    "content": "'use strict';\n\nconst check = (pred, message) => {\n  if (!pred) {\n    throw new Error(message);\n  }\n};\n\nconst fail = (message) => check(false, message);\n\nclass IndexMissing extends Error {\n  constructor(collection, fields) {\n    super(`Collection \"${collection.name}\" has no index matching ${JSON.stringify(fields)}.`);\n    this.collection = collection;\n    this.fields = fields;\n  }\n}\n\nclass CollectionMissing extends Error {\n  constructor(name) {\n    super(`Collection \"${name}\" does not exist.`);\n    this.name = name;\n  }\n}\n\nclass IndexNotReady extends Error {\n  constructor(collection, index) {\n    super(`Index on collection \"${collection.name}\" is not ready: ${JSON.stringify(index.fields)}.`);\n    this.collection = collection;\n    this.index = index;\n  }\n}\n\nclass CollectionNotReady extends Error {\n  constructor(collection) {\n    super(`Collection \"${collection.name}\" is not ready.`);\n    this.collection = collection;\n  }\n}\n\nmodule.exports = {\n  check,\n  fail,\n  IndexMissing,\n  IndexNotReady,\n  CollectionMissing,\n  CollectionNotReady,\n};\n"
  },
  {
    "path": "server/src/horizon.js",
    "content": "'use strict';\n\nconst joi = require('joi');\n\n// Issue a dummy joi validation to force joi to initialize its scripts.\n// This is used because tests will mock the filesystem, and the lazy\n// `require`s done by joi will no longer work at that point.\njoi.validate('', joi.any().when('', { is: '', then: joi.any() }));\n\nconst server = require('./server');\n\nconst create_server = (http_servers, options) =>\n  new server.Server(http_servers, options);\n\nmodule.exports = create_server;\nmodule.exports.Server = server.Server;\n\nmodule.exports.r = require('rethinkdb');\nmodule.exports.logger = require('./logger');\nmodule.exports.utils = require('./utils');\n\nmodule.exports.auth = {\n  auth0: require('./auth/auth0'),\n  facebook: require('./auth/facebook'),\n  github: require('./auth/github'),\n  google: require('./auth/google'),\n  slack: require('./auth/slack'),\n  twitch: require('./auth/twitch'),\n  twitter: require('./auth/twitter'),\n};\n"
  },
  {
    "path": "server/src/logger.js",
    "content": "'use strict';\n\nconst winston = require('winston');\n\nmodule.exports = winston;\n"
  },
  {
    "path": "server/src/metadata/collection.js",
    "content": "'use strict';\n\nconst error = require('../error');\nconst Table = require('./table').Table;\n\nconst r = require('rethinkdb');\n\nclass Collection {\n  constructor(db, name) {\n    this.name = name;\n    this.table = r.db(db).table(name); // This is the ReQL Table object\n    this._tables = new Map(); // A Map of Horizon Table objects\n    this._registered = false; // Whether the `hz_collections` table says this collection exists\n    this._waiters = [ ];\n  }\n\n  _close() {\n    this._tables.forEach((table) => {\n      table._waiters.forEach((w) => w(new Error('collection deleted')));\n      table._waiters = [ ];\n      table.close();\n    });\n    this._waiters.forEach((w) => w(new Error('collection deleted')));\n    this._waiters = [ ];\n  }\n\n  _update_table(table_id, indexes, conn) {\n    let table = this._tables.get(table_id);\n    if (indexes) {\n      if (!table) {\n        table = new Table(this.table, conn);\n        this._tables.set(table_id, table);\n      }\n      table.update_indexes(indexes, conn);\n      this._waiters.forEach((w) => table.on_ready(w));\n      this._waiters = [ ];\n    } else {\n      this._tables.delete(table_id);\n      if (table) {\n        table._waiters.forEach((w) => this.on_ready(w));\n        table._waiters = [ ];\n        table.close();\n      }\n    }\n  }\n\n  _register() {\n    this._registered = true;\n  }\n\n  _unregister() {\n    this._registered = false;\n  }\n\n  _is_safe_to_remove() {\n    return this._tables.size === 0 && !this._registered;\n  }\n\n  _on_ready(done) {\n    if (this._tables.size === 0) {\n      this._waiters.push(done);\n    } else {\n      this._get_table().on_ready(done);\n    }\n  }\n\n  _get_table() {\n    if (this._tables.size === 0) {\n      throw new error.CollectionNotReady(this);\n    }\n    return this._tables.values().next().value;\n  }\n\n  _create_index(fields, conn, done) {\n    return this._get_table().create_index(fields, conn, done);\n  }\n\n  get_matching_index(fuzzy_fields, ordered_fields) {\n    const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields);\n\n    if (match && !match.ready()) {\n      throw new error.IndexNotReady(this, match);\n    } else if (!match) {\n      throw new error.IndexMissing(this, fuzzy_fields.concat(ordered_fields));\n    }\n\n    return match;\n  }\n}\n\nmodule.exports = { Collection };\n"
  },
  {
    "path": "server/src/metadata/index.js",
    "content": "'use strict';\n\nconst check = require('../error').check;\nconst logger = require('../logger');\n\n// Index names are of the format \"hz_[<flags>_]<JSON>\" where <flags> may be\n// omitted or \"multi_<offset>\" or \"geo\" (at the moment).  <JSON> is a JSON array\n// specifying which fields are indexed in which order.  The value at each index\n// in the array is either a nested array (for indexing nested fields) or a string\n// for a root-level field name.\n//\n// Example:\n//  Fields indexed: foo.bar, baz\n//  Index name: hz_[[\"foo\",\"bar\"],\"baz\"]\nconst primary_index_name = 'id';\n\nconst name_to_info = (name) => {\n  if (name === primary_index_name) {\n    return { geo: false, multi: false, fields: [ [ 'id' ] ] };\n  }\n\n  const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\\[/;\n\n  const matches = name.match(re);\n  check(matches !== null, `Unexpected index name (invalid format): \"${name}\"`);\n\n  const json_offset = matches[0].length - 1;\n\n  const info = { name, geo: Boolean(matches[1]), multi: isNaN(matches[2]) ? false : Number(matches[2]) };\n\n  // Parse remainder as JSON\n  try {\n    info.fields = JSON.parse(name.slice(json_offset));\n  } catch (err) {\n    check(false, `Unexpected index name (invalid JSON): \"${name}\"`);\n  }\n\n  // Sanity check fields\n  const validate_field = (f) => {\n    check(Array.isArray(f), `Unexpected index name (invalid field): \"${name}\"`);\n    f.forEach((s) => check(typeof s === 'string',\n                           `Unexpected index name (invalid field): \"${name}\"`));\n  };\n\n  check(Array.isArray(info.fields),\n        `Unexpected index name (fields are not an array): \"${name}\"`);\n  check((info.multi === false) || (info.multi < info.fields.length),\n        `Unexpected index name (multi index out of bounds): \"${name}\"`);\n  info.fields.forEach(validate_field);\n  return info;\n};\n\nconst info_to_name = (info) => {\n  let res = 'hz_';\n  if (info.geo) {\n    res += 'geo_';\n  }\n  if (info.multi !== false) {\n    res += 'multi_' + info.multi + '_';\n  }\n  res += JSON.stringify(info.fields);\n  return res;\n};\n\nconst info_to_reql = (info) => {\n  if (info.geo && (info.multi !== false)) {\n    throw new Error('multi and geo cannot be specified on the same index');\n  }\n\n  if (info.multi !== false) {\n    const multi_field = info.fields[info.multi];\n    return (row) =>\n      row(multi_field).map((value) => info.fields.map((f, i) => {\n        if (i === info.multi) {\n          return value;\n        } else {\n          let res = row;\n          f.forEach((field_name) => { res = res(field_name); });\n          return res;\n        }\n      }));\n  } else {\n    return (row) =>\n      info.fields.map((f) => {\n        let res = row;\n        f.forEach((field_name) => { res = res(field_name); });\n        return res;\n      });\n  }\n};\n\nconst compare_fields = (a, b) => {\n  if (a.length !== b.length) {\n    return false;\n  }\n  for (let i = 0; i < a.length; ++i) {\n    if (a[i] !== b[i]) {\n      return false;\n    }\n  }\n  return true;\n};\n\nclass Index {\n  constructor(name, table, conn) {\n    logger.debug(`${table} index registered: ${name}`);\n    const info = name_to_info(name);\n    this.name = name;\n    this.geo = info.geo; // true or false\n    this.multi = info.multi; // false or the offset of the multi field\n    this.fields = info.fields; // array of fields or nested field paths\n\n    this._waiters = [ ];\n    this._result = null;\n\n    if (this.geo) {\n      logger.warn(`Unsupported index (geo): ${this.name}`);\n    } else if (this.multi !== false) {\n      logger.warn(`Unsupported index (multi): ${this.name}`);\n    }\n\n    if (name !== primary_index_name) {\n      table.indexWait(name).run(conn).then(() => {\n        logger.debug(`${table} index ready: ${name}`);\n        this._result = true;\n        this._waiters.forEach((w) => w());\n        this._waiters = [ ];\n      }).catch((err) => {\n        this._result = err;\n        this._waiters.forEach((w) => w(err));\n        this._waiters = [ ];\n      });\n    } else {\n      logger.debug(`${table} index ready: ${name}`);\n      this._result = true;\n    }\n  }\n\n  close() {\n    this._waiters.forEach((w) => w(new Error('index deleted')));\n    this._waiters = [ ];\n  }\n\n  ready() {\n    return this._result === true;\n  }\n\n  on_ready(done) {\n    if (this._result === true) {\n      done();\n    } else if (this._result) {\n      done(this._result);\n    } else {\n      this._waiters.push(done);\n    }\n  }\n\n  // `fuzzy_fields` may be in any order at the beginning of the index.\n  // These must be immediately followed by `ordered_fields` in the exact\n  // order given.  There may be no other fields present in the index\n  // (because the absence of a field would mean that row is not indexed).\n  // `fuzzy_fields` may overlap with `ordered_fields`.\n  is_match(fuzzy_fields, ordered_fields) {\n    // TODO: multi index matching\n    if (this.geo || this.multi !== false) {\n      return false;\n    }\n\n    if (this.fields.length > fuzzy_fields.length + ordered_fields.length ||\n        this.fields.length < fuzzy_fields.length ||\n        this.fields.length < ordered_fields.length) {\n      return false;\n    }\n\n    for (let i = 0; i < fuzzy_fields.length; ++i) {\n      let found = false;\n      for (let j = 0; j < fuzzy_fields.length && !found; ++j) {\n        found = compare_fields(fuzzy_fields[i], this.fields[j]);\n      }\n      if (!found) { return false; }\n    }\n\n    for (let i = 0; i < ordered_fields.length; ++i) {\n      const pos = this.fields.length - ordered_fields.length + i;\n      if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { return false; }\n    }\n\n    return true;\n  }\n}\n\nmodule.exports = { Index, primary_index_name, name_to_info, info_to_name, info_to_reql };\n"
  },
  {
    "path": "server/src/metadata/metadata.js",
    "content": "'use strict';\n\nconst error = require('../error');\nconst logger = require('../logger');\nconst Group = require('../permissions/group').Group;\nconst Collection = require('./collection').Collection;\nconst version_field = require('../endpoint/writes').version_field;\nconst utils = require('../utils');\n\nconst r = require('rethinkdb');\n\nconst metadata_version = [ 2, 0, 0 ];\n\nconst create_collection = (db, name, conn) =>\n  r.db(db).table('hz_collections').get(name).replace({ id: name }).do((res) =>\n    r.branch(\n      res('errors').ne(0),\n      r.error(res('first_error')),\n      res('inserted').eq(1),\n      r.db(db).tableCreate(name),\n      res\n    )\n  ).run(conn);\n\nconst initialize_metadata = (db, conn) =>\n  r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn)\n    .then(() =>\n      Promise.all([ 'hz_collections', 'hz_users_auth', 'hz_groups' ].map((table) =>\n        r.branch(r.db(db).tableList().contains(table),\n                 { },\n                 r.db(db).tableCreate(table))\n          .run(conn))))\n    .then(() =>\n      r.db(db).table('hz_collections').wait({ timeout: 30 }).run(conn))\n    .then(() =>\n      Promise.all([\n        r.db(db).tableList().contains('users').not().run(conn).then(() =>\n          create_collection(db, 'users', conn)),\n        r.db(db).table('hz_collections')\n          .insert({ id: 'hz_metadata', version: metadata_version })\n          .run(conn),\n      ])\n    );\n\nclass Metadata {\n  constructor(project_name,\n              conn,\n              clients,\n              auto_create_collection,\n              auto_create_index) {\n    this._db = project_name;\n    this._conn = conn;\n    this._clients = clients;\n    this._auto_create_collection = auto_create_collection;\n    this._auto_create_index = auto_create_index;\n    this._closed = false;\n    this._ready = false;\n    this._collections = new Map();\n    this._groups = new Map();\n    this._collection_feed = null;\n    this._group_feed = null;\n    this._index_feed = null;\n\n    this._ready_promise = Promise.resolve().then(() => {\n      logger.debug('checking rethinkdb version');\n      return r.db('rethinkdb').table('server_status').nth(0)('process')('version').run(this._conn)\n               .then((res) => utils.rethinkdb_version_check(res));\n    }).then(() => {\n      const old_metadata_db = `${this._db}_internal`;\n      return r.dbList().contains(old_metadata_db).run(this._conn).then((has_old_db) => {\n        if (has_old_db) {\n          throw new Error('The Horizon metadata appears to be from v1.x because ' +\n                          `the \"${old_metadata_db}\" database exists.  Please use ` +\n                          '`hz migrate` to convert your metadata to the new format.');\n        }\n      });\n    }).then(() => {\n      logger.debug('checking for internal tables');\n      if (this._auto_create_collection) {\n        return initialize_metadata(this._db, this._conn);\n      } else {\n        return r.dbList().contains(this._db).run(this._conn).then((has_db) => {\n          if (!has_db) {\n            throw new Error(`The database ${this._db} does not exist.  ` +\n                            'Run `hz schema apply` to initialize the database, ' +\n                            'then start the Horizon server.');\n          }\n        });\n      }\n    }).then(() => {\n      logger.debug('waiting for internal tables');\n      return r.expr([ 'hz_collections', 'hz_users_auth', 'hz_groups', 'users' ])\n        .forEach((table) => r.db(this._db).table(table).wait({ timeout: 30 })).run(this._conn);\n    }).then(() => {\n      logger.debug('syncing metadata changefeeds');\n\n      const group_changefeed =\n        r.db(this._db)\n          .table('hz_groups')\n          .changes({ squash: true,\n                     includeInitial: true,\n                     includeStates: true,\n                     includeTypes: true })\n          .run(this._conn).then((res) => {\n            if (this._closed) {\n              res.close().catch(() => { });\n              throw new Error('This metadata instance has been closed.');\n            }\n            return new Promise((resolve, reject) => {\n              this._group_feed = res;\n              this._group_feed.eachAsync((change) => {\n                if (change.type === 'state') {\n                  if (change.state === 'ready') {\n                    logger.info('Groups metadata synced.');\n                    resolve();\n                  }\n                } else if (change.type === 'initial' ||\n                           change.type === 'add' ||\n                           change.type === 'change') {\n                  const group = new Group(change.new_val);\n                  this._groups.set(group.name, group);\n                  this._clients.forEach((c) => c.group_changed(group.name));\n                } else if (change.type === 'uninitial' ||\n                           change.type === 'remove') {\n                  this._groups.delete(change.old_val.id);\n                  this._clients.forEach((c) => c.group_changed(change.old_val.id));\n                }\n              }).catch(reject);\n            });\n          });\n\n      const collection_changefeed =\n        r.db(this._db)\n          .table('hz_collections')\n          .filter((row) => row('id').match('^hz_').not())\n          .changes({ squash: false,\n                     includeInitial: true,\n                     includeStates: true,\n                     includeTypes: true })\n          .run(this._conn).then((res) => {\n            if (this._closed) {\n              res.close().catch(() => { });\n              throw new Error('This metadata instance has been closed.');\n            }\n            return new Promise((resolve, reject) => {\n              this._collection_feed = res;\n              this._collection_feed.eachAsync((change) => {\n                if (change.type === 'state') {\n                  if (change.state === 'ready') {\n                    logger.info('Collections metadata synced.');\n                    resolve();\n                  }\n                } else if (change.type === 'initial' ||\n                           change.type === 'add' ||\n                           change.type === 'change') {\n                  const collection_name = change.new_val.id;\n                  let collection = this._collections.get(collection_name);\n                  if (!collection) {\n                    collection = new Collection(this._db, collection_name);\n                    this._collections.set(collection_name, collection);\n                  }\n                  collection._register();\n                } else if (change.type === 'uninitial' ||\n                           change.type === 'remove') {\n                  const collection = this._collections.get(change.old_val.id);\n                  if (collection) {\n                    collection._unregister();\n                    if (collection._is_safe_to_remove()) {\n                      this._collections.delete(change.old_val.id);\n                      collection._close();\n                    }\n                  }\n                }\n              }).catch(reject);\n            });\n          });\n\n      const index_changefeed =\n        r.db('rethinkdb')\n          .table('table_config')\n          .filter((row) => r.and(row('db').eq(this._db),\n                                 row('name').match('^hz_').not()))\n          .map((row) => ({\n            id: row('id'),\n            name: row('name'),\n            indexes: row('indexes').filter((idx) => idx.match('^hz_')),\n          }))\n          .changes({ squash: true,\n                     includeInitial: true,\n                     includeStates: true,\n                     includeTypes: true })\n          .run(this._conn).then((res) => {\n            if (this._closed) {\n              res.close().catch(() => { });\n              throw new Error('This metadata instance has been closed.');\n            }\n            return new Promise((resolve, reject) => {\n              this._index_feed = res;\n              this._index_feed.eachAsync((change) => {\n                if (change.type === 'state') {\n                  if (change.state === 'ready') {\n                    logger.info('Index metadata synced.');\n                    resolve();\n                  }\n                } else if (change.type === 'initial' ||\n                           change.type === 'add' ||\n                           change.type === 'change') {\n                  const collection_name = change.new_val.name;\n                  const table_id = change.new_val.id;\n\n                  let collection = this._collections.get(collection_name);\n                  if (!collection) {\n                    collection = new Collection(this._db, collection_name);\n                    this._collections.set(collection_name, collection);\n                  }\n                  collection._update_table(table_id, change.new_val.indexes, this._conn);\n                } else if (change.type === 'uninitial' ||\n                           change.type === 'remove') {\n                  const collection = this._collections.get(change.old_val.name);\n                  if (collection) {\n                    collection._update_table(change.old_val.id, null, this._conn);\n                    if (collection._is_safe_to_remove()) {\n                      this._collections.delete(collection);\n                      collection._close();\n                    }\n                  }\n                }\n              }).catch(reject);\n            });\n          });\n\n      return Promise.all([ group_changefeed, collection_changefeed, index_changefeed ]);\n    }).then(() => {\n      logger.debug('adding admin user');\n      // Ensure that the admin user and group exists\n      return Promise.all([\n        r.db(this._db).table('users').get('admin')\n          .replace((old_row) =>\n            r.branch(old_row.eq(null),\n              {\n                id: 'admin',\n                groups: [ 'admin' ],\n                [version_field]: 0,\n              },\n              old_row),\n            { returnChanges: 'always' })('changes')(0)\n          .do((res) =>\n            r.branch(res('new_val').eq(null),\n                     r.error(res('error')),\n                     res('new_val'))).run(this._conn),\n        r.db(this._db).table('hz_groups').get('admin')\n          .replace((old_row) =>\n            r.branch(old_row.eq(null),\n              {\n                id: 'admin',\n                rules: { carte_blanche: { template: 'any()' } },\n                [version_field]: 0,\n              },\n              old_row),\n            { returnChanges: 'always' })('changes')(0)\n          .do((res) =>\n            r.branch(res('new_val').eq(null),\n                     r.error(res('error')),\n                     res('new_val'))).run(this._conn),\n      ]);\n    }).then(() => {\n      logger.debug('metadata sync complete');\n      this._ready = true;\n      return this;\n    });\n\n    this._ready_promise.catch(() => {\n      this.close();\n    });\n  }\n\n  close() {\n    this._closed = true;\n    this._ready = false;\n\n    if (this._group_feed) {\n      this._group_feed.close().catch(() => { });\n    }\n    if (this._collection_feed) {\n      this._collection_feed.close().catch(() => { });\n    }\n    if (this._index_feed) {\n      this._index_feed.close().catch(() => { });\n    }\n\n    this._collections.forEach((collection) => collection._close());\n    this._collections.clear();\n  }\n\n  is_ready() {\n    return this._ready;\n  }\n\n  ready() {\n    return this._ready_promise;\n  }\n\n  collection(name) {\n    if (name.indexOf('hz_') === 0) {\n      throw new Error(`Collection \"${name}\" is reserved for internal use ` +\n                      'and cannot be used in requests.');\n    }\n\n    const collection = this._collections.get(name);\n    if (collection === undefined) { throw new error.CollectionMissing(name); }\n    if (!collection._get_table().ready()) { throw new error.CollectionNotReady(collection); }\n    return collection;\n  }\n\n  handle_error(err, done) {\n    logger.debug(`Handling error: ${err.message}`);\n    try {\n      if (err instanceof error.CollectionNotReady) {\n        return err.collection._on_ready(done);\n      } else if (err instanceof error.IndexNotReady) {\n        return err.index.on_ready(done);\n      } else if (this._auto_create_collection && (err instanceof error.CollectionMissing)) {\n        logger.warn(`Auto-creating collection: ${err.name}`);\n        return this.create_collection(err.name, done);\n      } else if (this._auto_create_index && (err instanceof error.IndexMissing)) {\n        logger.warn(`Auto-creating index on collection \"${err.collection.name}\": ` +\n                    `${JSON.stringify(err.fields)}`);\n        return err.collection._create_index(err.fields, this._conn, done);\n      }\n      done(err);\n    } catch (new_err) {\n      logger.debug(`Error when handling error: ${new_err.message}`);\n      done(new_err);\n    }\n  }\n\n  create_collection(name, done) {\n    error.check(this._collections.get(name) === undefined,\n                `Collection \"${name}\" already exists.`);\n\n    const collection = new Collection(this._db, name);\n    this._collections.set(name, collection);\n\n    create_collection(this._db, name, this._conn).then((res) => {\n      error.check(!res.error, `Collection \"${name}\" creation failed: ${res.error}`);\n      logger.warn(`Collection created: \"${name}\"`);\n      collection._on_ready(done);\n    }).catch((err) => {\n      if (collection._is_safe_to_remove()) {\n        this._collections.delete(name);\n        collection._close();\n      }\n      done(err);\n    });\n  }\n\n  get_user_feed(id) {\n    return r.db(this._db).table('users').get(id)\n      .changes({ includeInitial: true, squash: true })\n      .run(this._conn);\n  }\n\n  get_group(group_name) {\n    return this._groups.get(group_name);\n  }\n\n  connection() {\n    return this._conn;\n  }\n}\n\nmodule.exports = { Metadata, create_collection, initialize_metadata };\n"
  },
  {
    "path": "server/src/metadata/table.js",
    "content": "'use strict';\n\nconst error = require('../error');\nconst index = require('./index');\nconst logger = require('../logger');\n\nconst r = require('rethinkdb');\n\nclass Table {\n  constructor(reql_table, conn) {\n    this.table = reql_table;\n    this.indexes = new Map();\n\n    this._waiters = [ ];\n    this._result = null;\n\n    this.table\n      .wait({ waitFor: 'all_replicas_ready' })\n      .run(conn)\n      .then(() => {\n        this._result = true;\n        this._waiters.forEach((w) => w());\n        this._waiters = [ ];\n      }).catch((err) => {\n        this._result = err;\n        this._waiters.forEach((w) => w(err));\n        this._waiters = [ ];\n      });\n  }\n\n  close() {\n    this._waiters.forEach((w) => w(new Error('collection deleted')));\n    this._waiters = [ ];\n\n    this.indexes.forEach((i) => i.close());\n    this.indexes.clear();\n  }\n\n  ready() {\n    return this._result === true;\n  }\n\n  on_ready(done) {\n    if (this._result === true) {\n      done();\n    } else if (this._result) {\n      done(this._result);\n    } else {\n      this._waiters.push(done);\n    }\n  }\n\n  update_indexes(indexes, conn) {\n    logger.debug(`${this.table} indexes changed, reevaluating`);\n\n    // Initialize the primary index, which won't show up in the changefeed\n    indexes.push(index.primary_index_name);\n\n    const new_index_map = new Map();\n    indexes.forEach((name) => {\n      try {\n        const old_index = this.indexes.get(name);\n        const new_index = new index.Index(name, this.table, conn);\n        if (old_index) {\n          // Steal any waiters from the old index\n          new_index._waiters = old_index._waiters;\n          old_index._waiters = [ ];\n        }\n        new_index_map.set(name, new_index);\n      } catch (err) {\n        logger.warn(`${err}`);\n      }\n    });\n\n    this.indexes.forEach((i) => i.close());\n    this.indexes = new_index_map;\n    logger.debug(`${this.table} indexes updated`);\n  }\n\n  // TODO: support geo and multi indexes\n  create_index(fields, conn, done) {\n    const info = { geo: false, multi: false, fields };\n    const index_name = index.info_to_name(info);\n    error.check(!this.indexes.get(index_name), 'index already exists');\n\n    const success = () => {\n      // Create the Index object now so we don't try to create it again before the\n      // feed notifies us of the index creation\n      const new_index = new index.Index(index_name, this.table, conn);\n      this.indexes.set(index_name, new_index); // TODO: shouldn't this be done before we go async?\n      return new_index.on_ready(done);\n    };\n\n    this.table.indexCreate(index_name, index.info_to_reql(info),\n                           { geo: info.geo, multi: (info.multi !== false) })\n      .run(conn)\n      .then(success)\n      .catch((err) => {\n        if (err instanceof r.Error.ReqlError &&\n            err.msg.indexOf('already exists') !== -1) {\n          success();\n        } else {\n          done(err);\n        }\n      });\n  }\n\n  // Returns a matching (possibly compound) index for the given fields\n  // fuzzy_fields and ordered_fields should both be arrays\n  get_matching_index(fuzzy_fields, ordered_fields) {\n    if (fuzzy_fields.length === 0 && ordered_fields.length === 0) {\n      return this.indexes.get(index.primary_index_name);\n    }\n\n    let match;\n    for (const i of this.indexes.values()) {\n      if (i.is_match(fuzzy_fields, ordered_fields)) {\n        if (i.ready()) {\n          return i;\n        } else if (!match) {\n          match = i;\n        }\n      }\n    }\n\n    return match;\n  }\n}\n\nmodule.exports = { Table };\n"
  },
  {
    "path": "server/src/permissions/group.js",
    "content": "'use strict';\nconst Rule = require('./rule').Rule;\n\nclass Group {\n  constructor(row_data) {\n    this.name = row_data.id;\n    this.rules = Object.keys(row_data.rules).map((name) => new Rule(name, row_data.rules[name]));\n  }\n}\n\nmodule.exports = { Group };\n"
  },
  {
    "path": "server/src/permissions/rule.js",
    "content": "'use strict';\nconst Template = require('./template').Template;\nconst Validator = require('./validator').Validator;\n\nclass Rule {\n  constructor(name, info) {\n    this._name = name;\n    this._template = new Template(info.template);\n    if (info.validator) {\n      this._validator = new Validator(info.validator);\n    }\n  }\n\n  is_match(query, context) {\n    return this._template.is_match(query, context);\n  }\n\n  is_valid() {\n    if (!this._validator) {\n      return true;\n    }\n    return this._validator.is_valid.apply(this._validator, arguments);\n  }\n}\n\nclass Ruleset {\n  constructor() {\n    this.clear();\n  }\n\n  clear() {\n    this._rules = [ ];\n  }\n\n  empty() {\n    return this._rules.length === 0;\n  }\n\n  update(rules) {\n    this._rules = rules;\n  }\n\n  validation_required() {\n    for (const rule of this._rules) {\n      if (!rule._validator) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  // Check that a query passes at least one rule in a set\n  // Returns the matching rule or undefined if no rules match\n  // Variadic - extra arguments are passed down to the validator\n  validate() {\n    for (const rule of this._rules) {\n      if (rule.is_valid.apply(rule, arguments)) {\n        return rule;\n      }\n    }\n  }\n}\n\n// The any_rule is used when permissions are disabled - it allows all queries\nconst any_rule = new Rule('permissions_disabled', { template: 'any()' });\n\nmodule.exports = { Rule, Ruleset, any_rule };\n"
  },
  {
    "path": "server/src/permissions/template.js",
    "content": "'use strict';\n\nconst check = require('../error').check;\nconst remake_error = require('../utils').remake_error;\n\nconst ast = require('@horizon/client/lib/ast');\nconst validIndexValue = require('@horizon/client/lib/util/valid-index-value').default;\nconst vm = require('vm');\n\nlet template_compare;\n\nclass Any {\n  constructor(values) {\n    this._values = values || [ ];\n  }\n\n  matches(value, context) {\n    if (value === undefined) {\n      return false;\n    } else if (this._values.length === 0) {\n      return true;\n    }\n\n    for (const item of this._values) {\n      if (template_compare(value, item, context)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n}\n\n// This works the same as specifying a literal object in a template, except that\n// unspecified key/value pairs are allowed.\nclass AnyObject {\n  constructor(obj) {\n    this._obj = obj || { };\n  }\n\n  matches(value, context) {\n    if (value === null || typeof value !== 'object') {\n      return false;\n    }\n\n    for (const key in this._obj) {\n      if (!template_compare(value[key], this._obj[key], context)) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n}\n\n// This matches an array where each item matches at least one of the values\n// specified at construction.\nclass AnyArray {\n  constructor(values) {\n    this._values = values || [ ];\n  }\n\n  matches(value, context) {\n    if (!Array.isArray(value)) {\n      return false;\n    }\n\n    for (const item of value) {\n      let match = false;\n      for (const template of this._values) {\n        if (template_compare(item, template, context)) {\n          match = true;\n          break;\n        }\n      }\n      if (!match) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n}\n\nclass UserId { }\n\nconst wrap_write = (query, docs) => {\n  if (docs instanceof AnyArray ||\n      Array.isArray(docs)) {\n    query.data = docs;\n  } else {\n    query.data = [ docs ];\n  }\n  return query;\n};\n\nconst wrap_remove = (doc) => {\n  if (validIndexValue(doc)) {\n    return { id: doc };\n  }\n  return doc;\n};\n\n// Add helper methods to match any subset of the current query for reads or writes\nast.TermBase.prototype.anyRead = function() {\n  return this._sendRequest(new Any([ 'query', 'subscribe' ]),\n                           new AnyObject(this._query));\n};\n\nast.Collection.prototype.anyWrite = function() {\n  let docs = arguments;\n  if (arguments.length === 0) {\n    docs = new AnyArray(new Any());\n  }\n  return this._sendRequest(new Any([ 'store', 'upsert', 'insert', 'replace', 'update', 'remove' ]),\n                           wrap_write(new AnyObject(this._query), docs));\n};\n\n// Monkey-patch the ast functions so we don't clobber certain things\nast.TermBase.prototype.watch = function() {\n  return this._sendRequest('subscribe', this._query);\n};\nast.TermBase.prototype.fetch = function() {\n  return this._sendRequest('query', this._query);\n};\nast.Collection.prototype.store = function(docs) {\n  return this._sendRequest('store', wrap_write(this._query, docs));\n};\nast.Collection.prototype.upsert = function(docs) {\n  return this._sendRequest('upsert', wrap_write(this._query, docs));\n};\nast.Collection.prototype.insert = function(docs) {\n  return this._sendRequest('insert', wrap_write(this._query, docs));\n};\nast.Collection.prototype.replace = function(docs) {\n  return this._sendRequest('replace', wrap_write(this._query, docs));\n};\nast.Collection.prototype.update = function(docs) {\n  return this._sendRequest('update', wrap_write(this._query, docs));\n};\nast.Collection.prototype.remove = function(doc) {\n  return this._sendRequest('remove', wrap_write(this._query, wrap_remove(doc)));\n};\nast.Collection.prototype.removeAll = function(docs) {\n  return this._sendRequest('remove', wrap_write(this._query,\n                                                docs.map((doc) => wrap_remove(doc))));\n};\n\nconst env = {\n  collection: (name) => new ast.Collection((type, options) =>\n    ({ request_id: new Any(),\n       type: Array.isArray(type) ? new Any(type) : type,\n       options }), name, false),\n  any: function() { return new Any(Array.from(arguments)); },\n  anyObject: function(obj) { return new AnyObject(obj); },\n  anyArray: function() { return new AnyArray(Array.from(arguments)); },\n  userId: function() { return new UserId(); },\n};\n\nconst make_template = (str) => {\n  try {\n    const sandbox = Object.assign({}, env);\n    return vm.runInNewContext(str, sandbox);\n  } catch (err) {\n    throw remake_error(err);\n  }\n};\n\n// eslint-disable-next-line prefer-const\ntemplate_compare = (query, template, context) => {\n  if (template === undefined) {\n    return false;\n  } else if (template instanceof Any ||\n             template instanceof AnyObject ||\n             template instanceof AnyArray) {\n    if (!template.matches(query, context)) {\n      return false;\n    }\n  } else if (template instanceof UserId) {\n    if (query !== context.id) {\n      return false;\n    }\n  } else if (template === null) {\n    if (query !== null) {\n      return false;\n    }\n  } else if (Array.isArray(template)) {\n    if (!Array.isArray(query) ||\n        template.length !== query.length) {\n      return false;\n    }\n    for (let i = 0; i < template.length; ++i) {\n      if (!template_compare(query[i], template[i], context)) {\n        return false;\n      }\n    }\n  } else if (typeof template === 'object') {\n    if (typeof query !== 'object') {\n      return false;\n    }\n\n    for (const key in query) {\n      if (!template_compare(query[key], template[key], context)) {\n        return false;\n      }\n    }\n\n    // Make sure all template keys were handled\n    for (const key in template) {\n      if (query[key] === undefined) {\n        return false;\n      }\n    }\n  } else if (template !== query) {\n    return false;\n  }\n\n  return true;\n};\n\nconst incomplete_template_message = (str) =>\n  `Incomplete template \"${str}\", ` +\n  'consider adding \".fetch()\", \".watch()\", \".anyRead()\", or \".anyWrite()\"';\n\nclass Template {\n  constructor(str) {\n    this._value = make_template(str);\n    check(this._value !== null, `Invalid template: ${str}`);\n    check(!Array.isArray(this._value), `Invalid template: ${str}`);\n    check(typeof this._value === 'object', `Invalid template: ${str}`);\n    if (!(this._value instanceof Any) && !(this._value instanceof AnyObject)) {\n      if (this._value.request_id === undefined &&\n          this._value.type === undefined &&\n          this._value.options === undefined &&\n          this._value.anyRead) {\n        this._value = this._value.anyRead();\n      }\n      check(this._value.request_id !== undefined, incomplete_template_message(str));\n      check(this._value.type !== undefined, incomplete_template_message(str));\n      check(this._value.options !== undefined, incomplete_template_message(str));\n    }\n  }\n\n  is_match(raw_query, context) {\n    return template_compare(raw_query, this._value, context);\n  }\n}\n\nmodule.exports = { Template };\n"
  },
  {
    "path": "server/src/permissions/validator.js",
    "content": "'use strict';\n\nconst check = require('../error').check;\nconst logger = require('../logger');\nconst remake_error = require('../utils').remake_error;\n\nconst vm = require('vm');\n\nclass Validator {\n  constructor(str) {\n    try {\n      this._fn = vm.runInNewContext(str, {});\n    } catch (err) {\n      throw remake_error(err);\n    }\n    check(typeof this._fn === 'function');\n  }\n\n  is_valid() {\n    try {\n      return this._fn.apply(this._fn, arguments);\n    } catch (err) {\n      // We don't want to pass the error message on to the user because it might leak\n      // information about the data.\n      logger.error(`Exception in validator function: ${err.stack}`);\n      throw new Error('Validation error');\n    }\n  }\n}\n\n\nmodule.exports = { Validator };\n"
  },
  {
    "path": "server/src/reql_connection.js",
    "content": "'use strict';\n\nconst check = require('./error').check;\nconst logger = require('./logger');\nconst Metadata = require('./metadata/metadata').Metadata;\nconst r = require('rethinkdb');\n\nconst default_user = 'admin';\nconst default_pass = '';\n\nclass ReqlConnection {\n  constructor(host, port, db,\n              auto_create_collection, auto_create_index,\n              user, pass, connect_timeout,\n              interruptor) {\n    this._rdb_options = {\n      host,\n      port,\n      db,\n      user: user || default_user,\n      password: pass || default_pass,\n      timeout: connect_timeout || null,\n    };\n\n    this._auto_create_collection = auto_create_collection;\n    this._auto_create_index = auto_create_index;\n    this._clients = new Set();\n    this._reconnect_delay = 0;\n    this._retry_timer = null;\n\n    interruptor.catch((err) => {\n      if (this._retry_timer) {\n        clearTimeout(this._retry_timer);\n      }\n\n      this._clients.forEach((client) =>\n        client.close({ error: err.message }));\n      this._clients.clear();\n\n      this._interrupted_err = err;\n      this._reconnect(); // This won't actually reconnect, but will do all the cleanup\n    });\n\n    logger.info('Connecting to RethinkDB: ' +\n      `${this._rdb_options.user} @ ${this._rdb_options.host}:${this._rdb_options.port}`);\n    this._ready_promise = this._reconnect();\n  }\n\n  _reconnect() {\n    if (this._conn) {\n      this._conn.removeAllListeners('close');\n      this._conn.close();\n    }\n    if (this._metadata) {\n      this._metadata.close();\n    }\n    this._conn = null;\n    this._metadata = null;\n\n    this._clients.forEach((client) =>\n      client.close({ error: 'Connection to the database was lost.' }));\n    this._clients.clear();\n\n    if (this._interrupted_err) {\n      return Promise.reject(this._interrupted_err);\n    } else if (!this._retry_timer) {\n      return new Promise((resolve) => {\n        this._retry_timer = setTimeout(() => resolve(this._init_connection()), this._reconnect_delay);\n        this._reconnect_delay = Math.min(this._reconnect_delay + 100, 1000);\n      });\n    }\n  }\n\n  _init_connection() {\n    this._retry_timer = null;\n\n    return r.connect(this._rdb_options).then((conn) => {\n      if (this._interrupted_err) {\n        return Promise.reject(this._interrupted_err);\n      }\n      this._conn = conn;\n      logger.debug('Connection to RethinkDB established.');\n      return new Metadata(this._rdb_options.db,\n                          conn,\n                          this._clients,\n                          this._auto_create_collection,\n                          this._auto_create_index).ready();\n    }).then((metadata) => {\n      logger.info('Connection to RethinkDB ready: ' +\n        `${this._rdb_options.user} @ ${this._rdb_options.host}:${this._rdb_options.port}`);\n\n      this._metadata = metadata;\n      this._reconnect_delay = 0;\n\n      this._conn.once('close', () => {\n        logger.error('Lost connection to RethinkDB.');\n        this._reconnect();\n      });\n\n      // This is to avoid EPIPE errors - handling is done by the 'close' listener\n      this._conn.on('error', () => { });\n\n      return this;\n    }).catch((err) => {\n      logger.error(`Connection to RethinkDB terminated: ${err}`);\n      logger.debug(`stack: ${err.stack}`);\n      return this._reconnect();\n    });\n  }\n\n  is_ready() {\n    return Boolean(this._conn);\n  }\n\n  ready() {\n    return this._ready_promise;\n  }\n\n  connection() {\n    check(this.is_ready(), 'Connection to the database is down.');\n    return this._conn;\n  }\n\n  metadata() {\n    check(this.is_ready(), 'Connection to the database is down.');\n    check(this._metadata, 'Connection to the database is initializing.');\n    return this._metadata;\n  }\n}\n\nmodule.exports = { ReqlConnection };\n"
  },
  {
    "path": "server/src/request.js",
    "content": "'use strict';\n\nconst logger = require('./logger');\nconst rule = require('./permissions/rule');\n\nclass Request {\n  constructor(raw_request, endpoint, client) {\n    this._raw_request = raw_request;\n    this._ruleset = new rule.Ruleset();\n    this._endpoint = endpoint;\n    this._client = client;\n    this.evaluate_rules();\n  }\n\n  evaluate_rules() {\n    if (this._client._permissions_enabled) {\n      const metadata = this._client._metadata;\n      const user_info = this._client.user_info;\n      const matching_rules = [ ];\n      for (const group_name of user_info.groups) {\n        const group = metadata.get_group(group_name);\n        if (group !== undefined) {\n          for (const r of group.rules) {\n            if (r.is_match(this._raw_request, user_info)) {\n              matching_rules.push(r);\n            }\n          }\n        }\n      }\n      this._ruleset.update(matching_rules);\n    } else {\n      this._ruleset.update([ rule.any_rule ]);\n    }\n  }\n\n  run() {\n    let complete = false;\n    try {\n      if (this._ruleset.empty()) {\n        throw new Error('Operation not permitted.');\n      }\n      this._cancel_cb = this._endpoint(this._raw_request,\n                                       this._client.user_info,\n                                       this._ruleset,\n                                       this._client._metadata,\n      (res) => {\n        this._client.send_response(this._raw_request, res);\n      },\n      (res) => {\n        // Only send something the first time 'done' is called\n        if (!complete) {\n          complete = true;\n          if (res instanceof Error) {\n            this.handle_error(res);\n          } else if (res) {\n            this._client.send_response(this._raw_request, res);\n          }\n          this._client.remove_request(this._raw_request);\n        }\n      });\n    } catch (err) {\n      this.handle_error(err);\n    }\n  }\n\n\n  close() {\n    this._ruleset.clear();\n    if (this._cancel_cb) {\n      this._cancel_cb();\n    }\n  }\n\n  handle_error(err) {\n    logger.debug(`Error on request ${this._raw_request.request_id}:\\n${err.stack}`);\n\n    // Ignore errors for disconnected clients\n    if (this._client.is_open()) {\n      this._client._metadata.handle_error(err, (inner_err) => {\n        if (inner_err) {\n          this._client.send_error(this._raw_request, inner_err);\n        } else {\n          setImmediate(() => this.run());\n        }\n      });\n    }\n  }\n}\n\nmodule.exports = { Request };\n"
  },
  {
    "path": "server/src/schema/horizon_protocol.js",
    "content": "'use strict';\n\nconst Joi = require('joi');\n\nconst handshake = Joi.object().keys({\n  request_id: Joi.number().required(),\n  method: Joi.only('token', 'anonymous', 'unauthenticated').required(),\n  token: Joi.string().required()\n    .when('method', { is: Joi.not('token').required(), then: Joi.forbidden() }),\n}).unknown(false);\n\nconst read = Joi.alternatives().try(\n  Joi.object().keys({\n    collection: Joi.string().token().required(),\n    find: Joi.object().min(1).unknown(true).required(),\n  }).unknown(false),\n  Joi.object().keys({\n    collection: Joi.string().token().required(),\n\n    limit: Joi.number().integer().greater(-1).optional()\n      .when('find', { is: Joi.any().required(), then: Joi.forbidden() }),\n\n    order: Joi.array().ordered(\n        Joi.array().items(Joi.string()).min(1).unique().label('fields').required(),\n        Joi.string().valid('ascending', 'descending').label('direction').required()).optional()\n      .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }),\n\n    above: Joi.array().ordered(\n        Joi.object().length(1).unknown(true).label('value').required(),\n        Joi.string().valid('open', 'closed').label('bound_type').required()).optional()\n      .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }),\n\n    below: Joi.array().ordered(\n        Joi.object().length(1).unknown(true).label('value').required(),\n        Joi.string().valid('open', 'closed').label('bound_type').required()).optional()\n      .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }),\n\n    find_all: Joi.array().items(Joi.object().min(1).label('item').unknown(true)).min(1).optional(),\n  }).unknown(false)\n);\n\nconst write_id_optional = Joi.object({\n  timeout: Joi.number().integer().greater(-1).optional().default(null),\n  collection: Joi.string().token().required(),\n  data: Joi.array().min(1).items(Joi.object({\n    id: Joi.any().optional(),\n  }).unknown(true)).required(),\n}).unknown(false);\n\nconst write_id_required = Joi.object({\n  timeout: Joi.number().integer().greater(-1).optional().default(null),\n  collection: Joi.string().token().required(),\n  data: Joi.array().min(1).items(Joi.object({\n    id: Joi.any().required(),\n  }).unknown(true)).required(),\n}).unknown(false);\n\nconst request = Joi.object({\n  request_id: Joi.number().required(),\n  type: Joi.string().required(),\n  options: Joi.object().required()\n    .when('type', { is: Joi.string().only('end_subscription'), then: Joi.forbidden() })\n    .when('type', { is: Joi.string().only('keepalive'), then: Joi.forbidden() }),\n}).unknown(false);\n\nmodule.exports = {\n  handshake,\n  request,\n  query: read,\n  subscribe: read,\n  insert: write_id_optional,\n  store: write_id_optional,\n  upsert: write_id_optional,\n  update: write_id_required,\n  replace: write_id_required,\n  remove: write_id_required,\n};\n"
  },
  {
    "path": "server/src/schema/server_options.js",
    "content": "'use strict';\n\nconst Joi = require('joi');\n\nconst server = Joi.object({\n  project_name: Joi.string().default('horizon'),\n  rdb_host: Joi.string().hostname().default('localhost'),\n  rdb_port: Joi.number().greater(0).less(65536).default(28015),\n\n  auto_create_collection: Joi.boolean().default(false),\n  auto_create_index: Joi.boolean().default(false),\n\n  permissions: Joi.boolean().default(true),\n\n  path: Joi.string().default('/horizon'),\n\n  auth: Joi.object().default({ }),\n  access_control_allow_origin: Joi.string().allow('').default(''),\n\n  rdb_user: Joi.string().allow(null),\n  rdb_password: Joi.string().allow(null),\n  rdb_timeout: Joi.number().allow(null),\n  max_connections: Joi.number().allow(null),\n}).unknown(false);\n\nconst auth = Joi.object({\n  success_redirect: Joi.string().default('/'),\n  failure_redirect: Joi.string().default('/'),\n\n  duration: Joi.alternatives(Joi.string(), Joi.number().positive()).default('1d'),\n\n  create_new_users: Joi.boolean().default(true),\n  new_user_group: Joi.string().default('authenticated'),\n\n  token_secret: Joi.string().allow(null),\n  allow_anonymous: Joi.boolean().default(false),\n  allow_unauthenticated: Joi.boolean().default(false),\n}).unknown(false);\n\nmodule.exports = { server, auth };\n"
  },
  {
    "path": "server/src/server.js",
    "content": "'use strict';\n\nconst Auth = require('./auth').Auth;\nconst make_client = require('./client').make_client;\nconst ReqlConnection = require('./reql_connection').ReqlConnection;\nconst logger = require('./logger');\nconst options_schema = require('./schema/server_options').server;\nconst getType = require('mime-types').contentType;\n\n// TODO: dynamically serve different versions of the horizon\n// library. Minified, Rx included etc.\nconst horizon_client_path = require.resolve('@horizon/client/dist/horizon');\n\nconst endpoints = {\n  insert: require('./endpoint/insert'),\n  query: require('./endpoint/query'),\n  remove: require('./endpoint/remove'),\n  replace: require('./endpoint/replace'),\n  store: require('./endpoint/store'),\n  subscribe: require('./endpoint/subscribe'),\n  update: require('./endpoint/update'),\n  upsert: require('./endpoint/upsert'),\n};\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst Joi = require('joi');\nconst path = require('path');\nconst url = require('url');\nconst websocket = require('ws');\n\nconst protocol_name = 'rethinkdb-horizon-v0';\n\nconst accept_protocol = (protocols, cb) => {\n  if (protocols.findIndex((x) => x === protocol_name) !== -1) {\n    cb(true, protocol_name);\n  } else {\n    logger.debug(`Rejecting client without \"${protocol_name}\" protocol (${protocols}).`);\n    cb(false, null);\n  }\n};\n\nconst serve_file = (file_path, res) => {\n  fs.access(file_path, fs.R_OK | fs.F_OK, (exists) => {\n    if (exists) {\n      res.writeHead(404, { 'Content-Type': 'text/plain' });\n      res.end(`Client library not found\\n`);\n    } else {\n      fs.readFile(file_path, 'binary', (err, file) => {\n        if (err) {\n          res.writeHead(500, { 'Content-Type': 'text/plain' });\n          res.end(`${err}\\n`);\n        } else {\n          const type = getType(path.extname(file_path)) || false;\n          if (type) {\n            res.writeHead(200, { 'Content-Type': type });\n          } else {\n            res.writeHead(200);\n          }\n          res.end(file, 'binary');\n        }\n      });\n    }\n  });\n};\n\nclass Server {\n  constructor(http_servers, user_opts) {\n    const opts = Joi.attempt(user_opts || { }, options_schema);\n    this._path = opts.path;\n    this._name = opts.project_name;\n    this._max_connections = opts.max_connections;\n    this._permissions_enabled = opts.permissions;\n    this._auth_methods = { };\n    this._request_handlers = new Map();\n    this._http_handlers = new Map();\n    this._ws_servers = [ ];\n    this._close_promise = null;\n    this._interruptor = new Promise((resolve, reject) => {\n      this._interrupt = reject;\n    });\n\n    try {\n      this._reql_conn = new ReqlConnection(opts.rdb_host,\n                                           opts.rdb_port,\n                                           opts.project_name,\n                                           opts.auto_create_collection,\n                                           opts.auto_create_index,\n                                           opts.rdb_user || null,\n                                           opts.rdb_password || null,\n                                           opts.rdb_timeout || null,\n                                           this._interruptor);\n      this._auth = new Auth(this, opts.auth);\n      for (const key in endpoints) {\n        this.add_request_handler(key, endpoints[key].run);\n      }\n\n      const verify_client = (info, cb) => {\n        // Reject connections if we aren't synced with the database\n        if (!this._reql_conn.is_ready()) {\n          cb(false, 503, 'Connection to the database is down.');\n        } else {\n          cb(true);\n        }\n      };\n\n      const ws_options = { handleProtocols: accept_protocol,\n                           allowRequest: verify_client,\n                           path: this._path };\n\n      const add_websocket = (server) => {\n        const ws_server = new websocket.Server(Object.assign({ server }, ws_options))\n        .on('error', (error) => logger.error(`Websocket server error: ${error}`))\n        .on('connection', (socket) => make_client(socket, this));\n\n        this._ws_servers.push(ws_server);\n      };\n\n      const path_replace = new RegExp('^' + this._path + '/');\n      const add_http_listener = (server) => {\n        // TODO: this doesn't play well with a user removing listeners (or maybe even `once`)\n        const extant_listeners = server.listeners('request').slice(0);\n        server.removeAllListeners('request');\n        server.on('request', (req, res) => {\n          const req_path = url.parse(req.url).pathname;\n          if (req_path.indexOf(`${this._path}/`) === 0) {\n            const sub_path = req_path.replace(path_replace, '');\n            const handler = this._http_handlers.get(sub_path);\n            if (handler !== undefined) {\n              logger.debug(`Handling HTTP request to horizon subpath: ${sub_path}`);\n              return handler(req, res);\n            }\n          }\n          if (extant_listeners.length === 0) {\n            res.statusCode = 404;\n            res.write('File not found.');\n            res.end();\n          } else {\n            extant_listeners.forEach((l) => l.call(server, req, res));\n          }\n        });\n      };\n\n      this.add_http_handler('horizon.js', (req, res) => {\n        serve_file(horizon_client_path, res);\n      });\n\n      this.add_http_handler('horizon.js.map', (req, res) => {\n        serve_file(`${horizon_client_path}.map`, res);\n      });\n\n      this.add_http_handler('auth_methods', (req, res) => {\n        res.writeHead(200, {\n          'Content-Type': 'application/json',\n          'Access-Control-Allow-Origin': opts.access_control_allow_origin,\n        });\n        res.end(JSON.stringify(this._auth_methods));\n      });\n\n      if (http_servers.forEach === undefined) {\n        add_websocket(http_servers);\n        add_http_listener(http_servers);\n      } else {\n        http_servers.forEach((s) => { add_websocket(s); add_http_listener(s); });\n      }\n    } catch (err) {\n      this._interrupt(err);\n      throw err;\n    }\n  }\n\n  add_request_handler(request_name, endpoint) {\n    assert(endpoint !== undefined);\n    assert(this._request_handlers.get(request_name) === undefined);\n    this._request_handlers.set(request_name, endpoint);\n  }\n\n  get_request_handler(request) {\n    return this._request_handlers.get(request.type);\n  }\n\n  remove_request_handler(request_name) {\n    return this._request_handlers.delete(request_name);\n  }\n\n  add_http_handler(sub_path, handler) {\n    logger.debug(`Added HTTP handler at ${this._path}/${sub_path}`);\n    assert.notStrictEqual(handler, undefined);\n    assert.strictEqual(this._http_handlers.get(sub_path), undefined);\n    this._http_handlers.set(sub_path, handler);\n  }\n\n  remove_http_handler(sub_path) {\n    return this._http_handlers.delete(sub_path);\n  }\n\n  add_auth_provider(provider, options) {\n    assert(provider.name);\n    assert(options.path);\n    assert.strictEqual(this._auth_methods[provider.name], undefined);\n    this._auth_methods[provider.name] = `${this._path}/${options.path}`;\n    provider(this, options);\n  }\n\n  ready() {\n    return this._reql_conn.ready().then(() => this);\n  }\n\n  close() {\n    if (!this._close_promise) {\n      this._interrupt(new Error('Horizon server is shutting down.'));\n      this._close_promise = Promise.all([\n        Promise.all(this._ws_servers.map((s) => new Promise((resolve) => {\n          s.close(resolve);\n        }))),\n        this._reql_conn.ready().catch(() => { }),\n      ]);\n    }\n    return this._close_promise;\n  }\n}\n\nmodule.exports = {\n  Server,\n  protocol: protocol_name,\n};\n"
  },
  {
    "path": "server/src/utils.js",
    "content": "'use strict';\n\nconst MIN_VERSION = [ 2, 3, 1 ];\n\n// Recursive version compare, could be flatter but opted for instant return if\n//  comparison is greater rather than continuing to compare to end.\nconst version_compare = (actual, minimum) => {\n  for (let i = 0; i < minimum.length; ++i) {\n    if (actual[i] > minimum[i]) {\n      return true;\n    } else if (actual[i] < minimum[i]) {\n      return false;\n    }\n  }\n  return true;\n};\n\n// Check that RethinkDB matches version requirements\nconst rethinkdb_version_check = (version_string) => {\n  const rethinkdb_version_regex = /^rethinkdb (\\d+)\\.(\\d+)\\.(\\d+)/i;\n  const matches = rethinkdb_version_regex.exec(version_string);\n\n  if (matches) {\n    // Convert strings to ints and remove first match\n    const versions = matches.slice(1).map((val) => parseInt(val));\n\n    if (!version_compare(versions, MIN_VERSION)) {\n      throw new Error(`RethinkDB (${versions.join('.')}) is below required version ` +\n                      `(${MIN_VERSION.join('.')}) for use with Horizon.`);\n    }\n  } else {\n    throw new Error('Unable to determine RethinkDB version, check ' +\n                    `RethinkDB is >= ${MIN_VERSION.join('.')}.`);\n  }\n};\n\n// Used when evaluating things in a different VM context - the errors\n// thrown from there will not evaluate as `instanceof Error`, so we recreate them.\nconst remake_error = (err) => {\n  const new_err = new Error(err.message || 'Unknown error when evaluating template.');\n  new_err.stack = err.stack || new_err.stack;\n  throw new_err;\n};\n\nmodule.exports = {\n  rethinkdb_version_check,\n  remake_error,\n};\n"
  },
  {
    "path": "server/test/http_tests.js",
    "content": "'use strict';\n\nconst horizon = require('../');\n\nconst assert = require('assert');\nconst child_process = require('child_process');\nconst fs = require('fs');\nconst http = require('http');\nconst https = require('https');\nconst path = require('path');\n\nconst all_tests = () => {\n  [ 'http', 'https' ].forEach((transport) => {\n    describe(transport, () => {\n      let http_server, key_file, cert_file;\n\n      before('Generate key and cert', (done) => {\n        if (transport === 'http') { done(); return; }\n\n        key_file = `key.${process.pid}.pem`;\n        cert_file = `cert.${process.pid}.pem`;\n\n        child_process.exec(\n          `openssl req -x509 -nodes -batch -newkey rsa:2048 -keyout ${key_file} -days 1`,\n          (err, stdout) => {\n            assert.ifError(err);\n            const cert_start = stdout.indexOf('-----BEGIN CERTIFICATE-----');\n            const cert_end = stdout.indexOf('-----END CERTIFICATE-----');\n            assert(cert_start !== -1 && cert_end !== -1);\n\n            const cert = `${stdout.slice(cert_start, cert_end)}-----END CERTIFICATE-----\\n`;\n            fs.writeFile(cert_file, cert, done);\n          });\n      });\n\n      after('Remove key and cert', () => {\n        [ key_file, cert_file ].forEach((f) => { if (f) { fs.unlinkSync(f); } });\n      });\n\n      before('Start horizon server', (done) => {\n        const four_o_four = (req, res) => {\n          res.writeHeader(404);\n          res.end();\n        };\n\n        if (transport === 'http') {\n          http_server = new http.createServer(four_o_four);\n        } else {\n          http_server = new https.createServer({ key: fs.readFileSync(key_file),\n                                                 cert: fs.readFileSync(cert_file) },\n                                               four_o_four);\n        }\n\n        horizon(http_server, { auth: { token_secret: 'hunter2' } });\n\n        http_server.listen(0, done);\n      });\n\n      after('Shutdown standalone horizon server', () => {\n        http_server.close();\n      });\n\n      it('localhost/horizon/horizon.js', (done) => {\n        require(transport).get({ host: 'localhost',\n                                 port: http_server.address().port,\n                                 path: '/horizon/horizon.js',\n                                 rejectUnauthorized: false }, (res) => {\n          const client_js = path.resolve(__dirname, '../node_modules/@horizon/client/dist/horizon.js');\n          const code = fs.readFileSync(client_js);\n          let buffer = '';\n          assert.strictEqual(res.statusCode, 200);\n          res.on('data', (delta) => { buffer += delta; });\n          res.on('end', () => (assert.equal(buffer, code), done()));\n        });\n      });\n    });\n  });\n};\n\nconst suite = (collection) => describe('Webserver', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/permissions.js",
    "content": "'use strict';\n\nconst hz_rule = require('../src/permissions/rule');\nconst hz_validator = require('../src/permissions/validator');\nconst query = require('../src/endpoint/query');\nconst subscribe = require('../src/endpoint/subscribe');\nconst insert = require('../src/endpoint/insert');\nconst store = require('../src/endpoint/store');\nconst update = require('../src/endpoint/update');\nconst upsert = require('../src/endpoint/upsert');\nconst replace = require('../src/endpoint/replace');\nconst remove = require('../src/endpoint/remove');\nconst utils = require('./utils');\n\nconst assert = require('assert');\n\nconst r = require('rethinkdb');\n\nconst Rule = hz_rule.Rule;\nconst Ruleset = hz_rule.Ruleset;\nconst Validator = hz_validator.Validator;\n\nconst make_request = (type, collection, options) => {\n  if (collection !== null) {\n    return { request_id: 5, type, options: Object.assign({ collection }, options) };\n  } else {\n    return { request_id: 5, type, options };\n  }\n};\n\nconst context = { id: 3, groups: [ 'admin', 'default', 'authenticated' ] };\n\n// Permit all rows\nconst permitted_validator = `\n(context) => {\n  if (!context) { throw new Error('no context'); }\n  return true;\n}\n`;\n\n// Forbid all rows\nconst forbidden_validator = `\n(context) => {\n  if (!context) { throw new Error('no context'); }\n  return false;\n}\n`;\n\n// Permit a row when the user's id is the last digit of the row's id\nconst user_permitted_validator = `\n(context, a, b) => {\n  if (!context) { throw new Error('no context'); }\n  const value = (a && a.id) || (b && b.id);\n  return context.id === (value % 10);\n}\n`;\n\nconst all_tests = (collection) => {\n  describe('Template', () => {\n    it('any', () => {\n      const rule = new Rule('foo', { template: 'any()' });\n\n      const tests = [ { },\n                      { type: 'query', options: { collection: 'test' } },\n                      { fake: 'bar' },\n                      { options: { } },\n                      { type: 'query', options: { fake: 'baz' } } ];\n\n      for (const t of tests) {\n        assert(rule.is_match(t, context));\n        assert(rule.is_valid());\n      }\n    });\n\n    it('any read', () => {\n      const rule = new Rule('foo', { template: 'collection(any()).anyRead()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('fake', 'test', { }), context));\n      assert(!rule.is_match(make_request('store', 'test', { }), context));\n      assert(!rule.is_match(make_request('query', null, { }), context));\n      assert(rule.is_match(make_request('query', 'fake', { }), context));\n      assert(rule.is_match(make_request('query', 'fake', { find: { } }), context));\n      assert(rule.is_match(make_request('query', 'test', { bar: 'baz' }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { }, { } ] }), context));\n      assert(!rule.is_match(make_request('subscribe', null, { }), context));\n      assert(rule.is_match(make_request('subscribe', 'fake', { }), context));\n      assert(rule.is_match(make_request('subscribe', 'fake', { find: { } }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { bar: 'baz' }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { find_all: [ { }, { } ] }), context));\n    });\n\n    it('any read with collection', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").anyRead()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'fake', { }), context));\n      assert(rule.is_match(make_request('query', 'test', { }), context));\n      assert(rule.is_match(make_request('query', 'test', { }), context));\n      assert(rule.is_match(make_request('query', 'test', { }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { }), context));\n    });\n\n    it('any read with order', () => {\n      // TODO: allow for any number of fields in order\n      const rule = new Rule('foo', { template: 'collection(\"test\").order(any(), any()).anyRead()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'fake', { order: [ 'foo', 'ascending' ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { }), context));\n      assert(!rule.is_match(make_request('query', 'test', { order: [ 'baz' ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { order: [ 'baz', 'fake' ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { order: [ [ 'fake' ] ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { order: [ [ 'foo' ], 'ascending' ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { order: [ [ 'bar' ], 'descending' ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { order: [ [ 'baz' ], 'fake' ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find: { }, order: [ [ 'baz' ], 'fake' ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ], order: [ [ 'baz' ], 'fake' ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { fake: 'baz', order: [ [ 'baz' ], 'fake' ] }), context));\n    });\n\n    it('any read with find', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").find(any()).anyRead()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'fake', { find: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { }), context));\n      assert(rule.is_match(make_request('query', 'test', { find: { } }), context));\n      assert(rule.is_match(make_request('query', 'test', { find: { }, fake: 'baz' }), context));\n    });\n\n    it('any read with findAll', () => {\n      // TODO: allow for any number of arguments in findAll\n      const rule = new Rule('foo', { template: 'collection(\"test\").findAll(any()).anyRead()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'fake', { find_all: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ], fake: 'baz' }), context));\n    });\n\n    it('single key in findAll', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").findAll({ owner: userId() }).fetch()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id }, { other: context.id } ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id } ] }), context));\n    });\n\n    it('multiple keys in findAll', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").findAll({ owner: userId(), key: any() }).fetch()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, key: 3 } ] }), context));\n    });\n\n    it('multiple items in findAll', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").findAll({ a: userId() }, { b: userId() })' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: (context.id + 1) }, { b: context.id } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id, bar: 'baz' } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id, b: context.id } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { b: context.id } ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id }, { b: context.id, bar: 'baz' } ] }), context));\n      assert(rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id }, { b: context.id } ] }), context));\n    });\n\n    it('collection fetch', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").fetch()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'fake', { }), context));\n      assert(!rule.is_match(make_request('query', 'test', { bar: 'baz' }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: { id: 5 } }), context));\n      assert(rule.is_match(make_request('query', 'test', { }), context));\n    });\n\n    it('collection watch', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").watch()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('subscribe', 'fake', { }), context));\n      assert(!rule.is_match(make_request('subscribe', 'test', { bar: 'baz' }), context));\n      assert(!rule.is_match(make_request('subscribe', 'test', { find: { id: 5 } }), context));\n      assert(rule.is_match(make_request('subscribe', 'test', { }), context));\n    });\n\n    for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) {\n      it(`collection ${type}`, () => {\n        const rule = new Rule('foo', { template: `collection(\"test\").${type}(any())` });\n        assert(rule.is_valid());\n        assert(!rule.is_match(make_request(type, 'test', { }), context));\n        assert(!rule.is_match(make_request(type, 'test', { data: { } }), context));\n        assert(!rule.is_match(make_request(type, 'test', { data: [ ] }), context));\n        assert(!rule.is_match(make_request(type, 'fake', { data: [ { } ] }), context));\n        assert(!rule.is_match(make_request(type, 'test', { data: [ { } ], fake: 6 }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context));\n      });\n      it(`collection ${type} batch`, () => {\n        const rule = new Rule('foo', { template: `collection(\"test\").${type}(anyArray(any()))` });\n        assert(rule.is_valid());\n        assert(!rule.is_match(make_request(type, 'test', { }), context));\n        assert(!rule.is_match(make_request(type, 'test', { data: { } }), context));\n        assert(!rule.is_match(make_request(type, 'test', { data: [ { } ], fake: 6 }), context));\n        assert(!rule.is_match(make_request(type, 'fake', { data: [ { } ] }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ ] }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ { }, { bar: 'baz' } ] }), context));\n      });\n    }\n\n    it('any write', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").anyWrite()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('fake', 'test', { }), context));\n      assert(!rule.is_match(make_request('query', 'test', { }), context));\n      assert(!rule.is_match(make_request('store', null, { }), context));\n\n      for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) {\n        assert(!rule.is_match(make_request(type, 'fake', { }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ ] }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context));\n        assert(rule.is_match(make_request(type, 'test', { data: [ ], bar: 'baz' }), context));\n      }\n    });\n\n    it('userId in find', () => {\n      const rule = new Rule('foo', { template: 'collection(\"test\").find({ owner: userId() }).fetch()' });\n      assert(rule.is_valid());\n      assert(!rule.is_match(make_request('query', 'test', { find: { } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: true }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: [ ] }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: { owner: (context.id + 1) } }), context));\n      assert(!rule.is_match(make_request('query', 'test', { find: { owner: context.id, bar: 'baz' } }), context));\n      assert(rule.is_match(make_request('query', 'test', { find: { owner: context.id } }), context));\n    });\n\n    it('adds readAny() implicitly', () => {\n      {\n        const rule = new Rule('foo', { template: 'collection(\"test\")' });\n        assert(rule.is_valid());\n        assert(rule.is_match(make_request('query', 'test', { find: { } }), context));\n        assert(rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context));\n      }\n      {\n        const rule = new Rule('foo', { template: 'collection(\"test\").find({bar: any()})' });\n        assert(rule.is_valid());\n        assert(!rule.is_match(make_request('query', 'test', { find: { } }), context));\n        assert(rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context));\n      }\n    });\n\n    it('error on incomplete template', () => {\n      assert.throws(() => new Rule('foo', { template: '({ })' }), /Incomplete template/);\n      assert.throws(() => new Rule('foo', { template: '[ ]' }), /Invalid template/);\n      assert.throws(() => new Rule('foo', { template: '5' }), /Invalid template/);\n      assert.throws(() => new Rule('foo', { template: 'null' }), /Invalid template/);\n    });\n  });\n\n  describe('Validator', () => {\n    it('unparseable', () => {\n      assert.throws(() => new Validator('() => ;'), /Unexpected token/);\n    });\n\n    it('broken', () => {\n      const validator = new Validator('() => foo');\n      assert.throws(() => validator.is_valid(), /Validation error/);\n    });\n\n    it('permitted', () => {\n      const validator = new Validator(permitted_validator);\n      assert(validator.is_valid({ id: 3 }));\n      assert(validator.is_valid({ id: 3 }, { id: 0 }));\n      assert(validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 }));\n    });\n\n    it('user permitted', () => {\n      const validator = new Validator(user_permitted_validator);\n      assert(validator.is_valid({ id: 3 }, { id: 3 }));\n      assert(validator.is_valid({ id: 3 }, { id: 13 }));\n      assert(!validator.is_valid({ id: 3 }, { id: 4 }));\n    });\n\n    it('forbidden', () => {\n      const validator = new Validator(forbidden_validator);\n      assert(!validator.is_valid({ id: 3 }));\n      assert(!validator.is_valid({ id: 3 }, { id: 3 }));\n      assert(!validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 }));\n    });\n  });\n\n  describe('Validation', () => {\n    const metadata = {\n      collection: () => ({\n        table: r.table(collection),\n        get_matching_index: () => ({ name: 'id', fields: [ 'id' ] }),\n      }),\n      connection: () => utils.rdb_conn(),\n    };\n\n    const table_data = [ ];\n    for (let i = 0; i < 10; ++i) {\n      table_data.push({ id: i });\n    }\n\n    beforeEach('Clear test table', () =>\n      r.table(collection).delete().run(utils.rdb_conn()));\n    beforeEach('Populate test table', () =>\n      r.table(collection).insert(table_data).run(utils.rdb_conn()));\n\n    const make_run = (run_fn) =>\n      (options, validator, limit) => new Promise((resolve, reject) => {\n        let cancel_fn;\n        const request = { options };\n        const results = [ ];\n        const ruleset = new Ruleset();\n        ruleset.update([ new Rule('test', { template: 'any()', validator }) ]);\n        options.collection = collection;\n\n        const add_response = (res) => {\n          res.data.forEach((item) => results.push(item));\n          if (limit && results.length >= limit) {\n            cancel_fn();\n            resolve(results);\n          }\n        };\n\n        cancel_fn = run_fn(\n          request, { id: 3 }, ruleset, metadata, add_response,\n          (res_or_error) => {\n            if (res_or_error instanceof Error) {\n              res_or_error.results = results;\n              reject(res_or_error);\n            } else {\n              if (res_or_error) {\n                add_response(res_or_error);\n              }\n              resolve(results);\n            }\n          });\n      });\n\n    describe('query', () => {\n      const run = make_run(query.run);\n      it('permitted', () =>\n        run({ order: [ [ 'id' ], 'ascending' ] }, permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, table_data);\n        }));\n\n      it('half-permitted', () =>\n        run({ order: [ [ 'id' ], 'ascending' ], above: [ { id: 3 }, 'closed' ] }, user_permitted_validator).then(() => {\n          assert(false, 'Read should not have been permitted.');\n        }).catch((err) => {\n          assert.strictEqual(err.message, 'Operation not permitted.');\n          // Check that we got the permitted row or nothing (race condition)\n          if (err.results.length !== 0) {\n            assert.deepStrictEqual(err.results, [ { id: 3 } ]);\n          }\n        }));\n\n      it('forbidden', () =>\n        run({ }, forbidden_validator).then(() => {\n          assert(false, 'Read should not have been permitted.');\n        }).catch((err) => {\n          assert.strictEqual(err.message, 'Operation not permitted.');\n          assert.strictEqual(err.results.length, 0);\n        }));\n    });\n\n    describe('subscribe', () => {\n      const run = make_run(subscribe.run);\n      it('permitted with subsequent permitted change', () => {\n        // TODO: can't use run, need to issue a write during the subscription\n      });\n\n      it('permitted with subsequent forbidden change', () => {\n        // TODO: can't use run, need to issue a write during the subscription\n      });\n\n      it('half-permitted', () =>\n        run({ order: [ [ 'id' ], 'ascending' ], above: [ { id: 3 }, 'closed' ] }, user_permitted_validator).then(() => {\n          assert(false, 'Read should not have been permitted.');\n        }).catch((err) => {\n          assert.strictEqual(err.message, 'Operation not permitted.');\n          // Check that we got the permitted row or nothing (race condition)\n          if (err.results.length !== 0) {\n            assert.deepStrictEqual(err.results, [ { id: 3 } ]);\n          }\n        }));\n\n      it('forbidden', () =>\n        run({ }, forbidden_validator).then(() => {\n          assert(false, 'Read should not have been permitted.');\n        }).catch((err) => {\n          assert.strictEqual(err.message, 'Operation not permitted.');\n          assert.strictEqual(err.results.length, 0);\n        }));\n    });\n\n    describe('insert', () => {\n      const run = make_run(insert.run);\n      it('permitted', () =>\n        run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 11);\n          return r.table(collection).get(11).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 13);\n          return r.table(collection).get(13).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n\n    describe('store', () => {\n      const run = make_run(store.run);\n      it('permitted', () =>\n        run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 11);\n          return r.table(collection).get(11).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 13);\n          return r.table(collection).get(13).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n\n    describe('upsert', () => {\n      const run = make_run(upsert.run);\n      it('permitted', () =>\n        run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 11);\n          return r.table(collection).get(11).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 13);\n          return r.table(collection).get(13).eq(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(11).ne(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n\n    describe('update', () => {\n      const run = make_run(update.run);\n      it('permitted', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 1);\n          return r.table(collection).get(1).hasFields('value').not()\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 3, value: 5 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 3);\n          return r.table(collection).get(3).hasFields('value').not()\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).hasFields('value')\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).hasFields('value')\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n\n    describe('replace', () => {\n      const run = make_run(replace.run);\n      it('permitted', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 1);\n          return r.table(collection).get(1).hasFields('value').not()\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 3, value: 5 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 3);\n          return r.table(collection).get(3).hasFields('value').not()\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).hasFields('value')\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 1, value: 5 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).hasFields('value')\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n\n    describe('remove', () => {\n      const run = make_run(remove.run);\n      it('permitted', () =>\n        run({ data: [ { id: 1 } ] }, permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 1);\n          return r.table(collection).get(1).ne(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('permitted based on context', () =>\n        run({ data: [ { id: 3 } ] }, user_permitted_validator).then((res) => {\n          assert.strictEqual(res.length, 1);\n          assert.strictEqual(res[0].id, 3);\n          return r.table(collection).get(3).ne(null)\n            .branch(r.error('write did not go through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden', () =>\n        run({ data: [ { id: 1 } ] }, forbidden_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).eq(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n\n      it('forbidden based on context', () =>\n        run({ data: [ { id: 1 } ] }, user_permitted_validator).then((res) => {\n          assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]);\n          return r.table(collection).get(1).eq(null)\n            .branch(r.error('write went through'), null).run(utils.rdb_conn());\n        }));\n    });\n  });\n};\n\nconst suite = (collection) => describe('Permissions', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/prereq_tests.js",
    "content": "'use strict';\n\nconst utils = require('./utils');\n\nconst assert = require('assert');\nconst crypto = require('crypto');\n\nconst all_tests = (collection) => {\n  beforeEach('clear collection', (done) => utils.clear_collection(collection, done));\n  beforeEach('authenticate', (done) => utils.horizon_admin_auth(done));\n\n  // Launch simultaneous queries that depend on a non-existent collection, then\n  // verify that only one table exists for that collection.\n  it('collection create race on read',\n     /** @this mocha */\n     function(done) {\n       const query_count = 5;\n       const rand_collection = crypto.randomBytes(8).toString('hex');\n\n       let finished = 0;\n       for (let i = 0; i < query_count; ++i) {\n         utils.stream_test(\n           { request_id: i, type: 'query', options: { collection: rand_collection } },\n           (err, res) => {\n             assert.ifError(err);\n             assert.strictEqual(res.length, 0);\n             if (++finished === query_count) {\n               utils.table(rand_collection).count().run(utils.rdb_conn())\n                .then((count) => (assert.strictEqual(count, 0), done()),\n                      (error) => done(error));\n             }\n           });\n       }\n     });\n\n  // Same as the previous test, but it exists because the ReQL error message\n  // is different for a read or a write when the table is unavailable.\n  it('collection create race on write',\n     /** @this mocha */\n     function(done) {\n       const query_count = 5;\n       const rand_collection = crypto.randomBytes(8).toString('hex');\n\n       let finished = 0;\n       for (let i = 0; i < query_count; ++i) {\n         utils.stream_test(\n           {\n             request_id: i,\n             type: 'insert',\n             options: {\n               collection: rand_collection,\n               data: [ { } ],\n             },\n           },\n           (err, res) => {\n             assert.ifError(err);\n             assert.strictEqual(res.length, 1);\n             if (++finished === query_count) {\n               utils.table(rand_collection).count().run(utils.rdb_conn())\n                .then((count) => (assert.strictEqual(count, query_count), done()),\n                      (error) => done(error));\n             }\n           });\n       }\n     });\n\n  // Launch two simultaneous queries that depend on a non-existent index, then\n  // verify that only one such index exists with that name.\n  it('index create race', (done) => {\n    const query_count = 5;\n    const field_name = crypto.randomBytes(8).toString('hex');\n    const conn = utils.rdb_conn();\n\n    utils.table(collection).indexStatus().count().run(conn).then((old_count) => {\n      let finished = 0;\n      for (let i = 0; i < query_count; ++i) {\n        utils.stream_test(\n          {\n            request_id: i,\n            type: 'query',\n            options: {\n              collection,\n              order: [ [ field_name ], 'ascending' ],\n            },\n          },\n          (err, res) => {\n            assert.ifError(err);\n            assert.strictEqual(res.length, 0);\n            if (++finished === query_count) {\n              utils.table(collection).indexStatus().count().run(conn).then((new_count) => {\n                assert.strictEqual(old_count + 1, new_count);\n                done();\n              }, (err2) => done(err2));\n            }\n          });\n      }\n    });\n  });\n};\n\nconst suite = (collection) => describe('Prereqs', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/protocol_tests.js",
    "content": "'use strict';\n\nconst utils = require('./utils');\n\nconst assert = require('assert');\n\nconst all_tests = (collection) => {\n  beforeEach('Authenticate client', utils.horizon_admin_auth);\n\n  it('unparseable', (done) => {\n    const conn = utils.horizon_conn();\n    conn.removeAllListeners('error');\n    conn.send('foobar');\n    conn.once('close', (code, reason) => {\n      assert.strictEqual(code, 1002);\n      assert(/^Invalid JSON/.test(reason));\n      done();\n    });\n  });\n\n  it('no request_id', (done) => {\n    const conn = utils.horizon_conn();\n    conn.removeAllListeners('error');\n    conn.send('{ }');\n    conn.once('close', (code, reason) => {\n      assert.strictEqual(code, 1002);\n      assert(/^Protocol error: ValidationError/.test(reason));\n      done();\n    });\n  });\n\n  it('no type', (done) => {\n    utils.stream_test({ request_id: 0 }, (err, res) => {\n      assert.deepStrictEqual(res, [ ]);\n      utils.check_error(err, '\"type\" is required');\n      done();\n    });\n  });\n\n  it('no options', (done) => {\n    utils.stream_test({ request_id: 1, type: 'fake' }, (err, res) => {\n      assert.deepStrictEqual(res, [ ]);\n      utils.check_error(err, '\"options\" is required');\n      done();\n    });\n  });\n\n  it('invalid endpoint', (done) => {\n    utils.stream_test({ request_id: 2, type: 'fake', options: { } }, (err, res) => {\n      assert.deepStrictEqual(res, [ ]);\n      assert.strictEqual(err.message, '\"fake\" is not a registered request type.');\n      done();\n    });\n  });\n\n  // Make sure the server properly cleans up a client connection when it\n  // disconnects. Open a changefeed, disconnect the client, then make sure the\n  // changefeed would have gotten an event.\n  // We don't check any results, we're just seeing if the server crashes.\n  it('client disconnect during changefeed', (done) => {\n    utils.horizon_conn().send(JSON.stringify(\n      {\n        request_id: 3,\n        type: 'subscribe',\n        options: { collection },\n      }));\n    utils.add_horizon_listener(3, (msg) => {\n      if (msg.error !== undefined) {\n        throw new Error(msg.error);\n      } else if (msg.state === 'synced') {\n        utils.close_horizon_conn();\n        utils.table(collection).insert({}).run(utils.rdb_conn())\n         .then(() => done());\n      }\n    });\n  });\n\n  // Make sure the server properly cleans up a client connection when it\n  // disconnects.  Close the connection immediately after sending the request.\n  // We don't check any results, we're just seeing if the server crashes.\n  it('client disconnect during query', (done) => {\n    utils.horizon_conn().send(JSON.stringify(\n      {\n        request_id: 4,\n        type: 'query',\n        options: {\n          collection,\n          field_name: 'id',\n        },\n      }), () => (utils.close_horizon_conn(), done()));\n  });\n};\n\nconst suite = (collection) => describe('Protocol', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/query_tests.js",
    "content": "'use strict';\n\nconst utils = require('./utils');\n\nconst assert = require('assert');\n\n// TODO: ensure each row is present in the results\nconst all_tests = (collection) => {\n  const num_rows = 10;\n\n  before('Clear collection', (done) => utils.clear_collection(collection, done));\n  before('Populate collection', (done) => utils.populate_collection(collection, num_rows, done));\n  beforeEach('Authenticate client', utils.horizon_admin_auth);\n\n  it('collection scan.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: { collection },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, num_rows);\n        done();\n      });\n  });\n\n  it('collection scan order.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          order: [ [ 'id' ], 'ascending' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, num_rows);\n        done();\n      });\n  });\n\n  it('collection scan limit.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          limit: 2,\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 2);\n        done();\n      });\n  });\n\n  it('collection scan order limit.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          order: [ [ 'id' ], 'descending' ],\n          limit: 4,\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 4);\n        done();\n      });\n  });\n\n  it('collection scan above.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          above: [ { id: 5 }, 'closed' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 5);\n        done();\n      });\n  });\n\n  it('collection scan below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          below: [ { id: 5 }, 'closed' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 6);\n        done();\n      });\n  });\n\n  it('collection scan above below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          above: [ { id: 5 }, 'open' ],\n          below: [ { id: 7 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find: { id: 4 },\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find missing.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find: { id: 14 },\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.deepStrictEqual(res, [ ]);\n        done();\n      });\n  });\n\n  it('find_all.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { id: 4 }, { id: 6 }, { id: 9 } ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 3);\n        done();\n      });\n  });\n\n  it('find_all order.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { id: 1 } ],\n          order: [ [ 'value' ], 'descending' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find_all limit.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { id: 4 }, { id: 8 }, { id: 2 }, { id: 1 } ],\n          limit: 3,\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 3);\n        done();\n      });\n  });\n\n  it('find_all order limit.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { id: 4 } ],\n          order: [ [ 'value' ], 'descending' ],\n          limit: 3,\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find_all above.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 1 } ],\n          above: [ { id: 3 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 2);\n        done();\n      });\n  });\n\n  it('find_all below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 1 } ],\n          below: [ { id: 5 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find_all above below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 1 } ],\n          above: [ { id: 1 }, 'closed' ],\n          below: [ { id: 9 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 2);\n        done();\n      });\n  });\n\n  it('find_all order above.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 1 } ],\n          order: [ [ 'id' ], 'ascending' ],\n          above: [ { id: 7 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 1);\n        done();\n      });\n  });\n\n  it('find_all order below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'id' ], 'descending' ],\n          below: [ { id: 8 }, 'open' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 2);\n        done();\n      });\n  });\n\n  it('find_all order above below.', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'id' ], 'descending' ],\n          above: [ { id: 3 }, 'closed' ],\n          below: [ { id: 9 }, 'closed' ],\n        },\n      },\n      (err, res) => {\n        assert.ifError(err);\n        assert.strictEqual(res.length, 2);\n        done();\n      });\n  });\n\n  // These tests are impossible to represent in the schema (as far as I can tell),\n  // so the test for this functionality must be at the integration level.\n  it('find_all \"above\" field not in \"order\".', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'value', 'a' ], 'descending' ],\n          above: [ { b: 4 }, 'closed' ],\n        },\n      },\n      (err) => {\n        utils.check_error(err, '\"above\" must be on the same field as the first in \"order\"');\n        done();\n      });\n  });\n\n  it('find_all \"above\" field not first in \"order\".', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'value', 'a' ], 'descending' ],\n          above: [ { a: 4 }, 'closed' ],\n        },\n      },\n      (err) => {\n        utils.check_error(err, '\"above\" must be on the same field as the first in \"order\"');\n        done();\n      });\n  });\n\n  it('find_all \"below\" field not in \"order\".', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'value', 'a' ], 'descending' ],\n          below: [ { b: 4 }, 'closed' ],\n        },\n      },\n      (err) => {\n        utils.check_error(err, '\"below\" must be on the same field as the first in \"order\"');\n        done();\n      });\n  });\n\n  it('find_all \"below\" field not first in \"order\".', (done) => {\n    utils.stream_test(\n      {\n        request_id: 0,\n        type: 'query',\n        options: {\n          collection,\n          find_all: [ { value: 0 } ],\n          order: [ [ 'value', 'a' ], 'descending' ],\n          below: [ { a: 4 }, 'closed' ],\n        },\n      },\n      (err) => {\n        utils.check_error(err, '\"below\" must be on the same field as the first in \"order\"');\n        done();\n      });\n  });\n};\n\nconst suite = (collection) => describe('Query', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/schema.js",
    "content": "'use strict';\n\nconst horizon_protocol = require('../src/schema/horizon_protocol');\nconst utils = require('./utils');\n\nconst assert = require('assert');\n\ndescribe('Schema', () => {\n  const test_required_fields = (schema, valid, fields) => {\n    fields.forEach((f) => {\n      const request = Object.assign({}, valid); // Create a shallow copy\n      request[f] = undefined;\n      const error = schema.validate(request).error;\n      utils.check_error(error, `\"${f}\" is required`);\n    });\n  };\n\n  const test_extra_field = (schema, valid) => {\n    const request = Object.assign({}, valid); // Create a shallow copy\n    request.fake_field = false;\n    const error = schema.validate(request).error;\n    utils.check_error(error, '\"fake_field\" is not allowed');\n  };\n\n  describe('Protocol', () => {\n    describe('Request', () => {\n      const valid = {\n        request_id: 1,\n        type: 'query',\n        options: { },\n      };\n\n      it('valid', () => {\n        const parsed = horizon_protocol.request.validate(valid);\n        assert.ifError(parsed.error);\n        assert.deepStrictEqual(parsed.value, valid);\n      });\n\n      it('required fields', () => {\n        test_required_fields(horizon_protocol.request, valid,\n                             [ 'request_id', 'type', 'options' ]);\n      });\n\n      it('wrong \"request_id\" type', () => {\n        const request = Object.assign({}, valid);\n        request.request_id = 'str';\n        const error = horizon_protocol.request.validate(request).error;\n        utils.check_error(error, '\"request_id\" must be a number');\n      });\n\n      it('wrong \"type\" type', () => {\n        const request = Object.assign({}, valid);\n        request.type = 5;\n        const error = horizon_protocol.request.validate(request).error;\n        utils.check_error(error, '\"type\" must be a string');\n      });\n\n      it('wrong \"options\" type', () => {\n        const request = Object.assign({}, valid);\n        request.options = [ 5, 6 ];\n        const error = horizon_protocol.request.validate(request).error;\n        utils.check_error(error, '\"options\" must be an object');\n      });\n\n      it('extra field', () => {\n        test_extra_field(horizon_protocol.request, valid);\n      });\n    });\n\n    describe('Write', () => {\n      const write_without_id = {\n        collection: 'horizon',\n        data: [ { field: 4 } ],\n      };\n\n      const write_with_id = {\n        collection: 'horizon',\n        data: [ { id: 5, field: 4 } ],\n      };\n\n      // In order to reduce the number of tests, these were written assuming\n      // that only two types of write schemas exist: id-required and id-optional.\n      // If this ever changes, this test will fail and more tests may need to\n      // be added.\n      it('common write schemas', () => {\n        // These schemas do not require an id in each \"data\" object\n        assert.equal(horizon_protocol.insert, horizon_protocol.upsert);\n        assert.equal(horizon_protocol.insert, horizon_protocol.store);\n\n        // These schemas require an id in each \"data\" object\n        assert.equal(horizon_protocol.replace, horizon_protocol.update);\n        assert.equal(horizon_protocol.replace, horizon_protocol.remove);\n      });\n\n      describe('Insert', () => {\n        it('with id', () => {\n          const error = horizon_protocol.insert.validate(write_with_id).error;\n          assert.ifError(error);\n        });\n\n        it('without id', () => {\n          const error = horizon_protocol.insert.validate(write_without_id).error;\n          assert.ifError(error);\n        });\n\n        it('required fields', () => {\n          test_required_fields(horizon_protocol.insert, write_with_id,\n                               [ 'collection', 'data' ]);\n        });\n\n        it('extra field', () => {\n          test_extra_field(horizon_protocol.insert, write_with_id);\n        });\n\n        it('wrong \"collection\" type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.collection = true;\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"collection\" must be a string');\n        });\n\n        it('wrong \"collection\" value', () => {\n          const request = Object.assign({}, write_with_id);\n          request.collection = '*.*';\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"collection\" must only contain alpha-numeric and underscore characters');\n        });\n\n        it('wrong \"data\" type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = 'abc';\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"data\" must be an array');\n        });\n\n        it('wrong \"data\" member type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = [ 7 ];\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"0\" must be an object');\n        });\n\n        it('empty \"data\" array', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = [ ];\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"data\" must contain at least 1 items');\n        });\n      });\n\n      describe('Replace', () => {\n        it('with id', () => {\n          const error = horizon_protocol.replace.validate(write_with_id).error;\n          assert.ifError(error);\n        });\n\n        it('without id', () => {\n          const error = horizon_protocol.replace.validate(write_without_id).error;\n          utils.check_error(error, '\"id\" is required');\n        });\n\n        it('required fields', () => {\n          test_required_fields(horizon_protocol.replace, write_with_id,\n                               [ 'collection', 'data' ]);\n        });\n\n        it('extra field', () => {\n          test_extra_field(horizon_protocol.replace, write_with_id);\n        });\n\n        it('wrong \"collection\" type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.collection = true;\n          const error = horizon_protocol.replace.validate(request).error;\n          utils.check_error(error, '\"collection\" must be a string');\n        });\n\n        it('wrong \"collection\" value', () => {\n          const request = Object.assign({}, write_with_id);\n          request.collection = '*.*';\n          const error = horizon_protocol.insert.validate(request).error;\n          utils.check_error(error, '\"collection\" must only contain alpha-numeric and underscore characters');\n        });\n\n        it('wrong \"data\" type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = 'abc';\n          const error = horizon_protocol.replace.validate(request).error;\n          utils.check_error(error, '\"data\" must be an array');\n        });\n\n        it('wrong \"data\" member type', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = [ 7 ];\n          const error = horizon_protocol.replace.validate(request).error;\n          utils.check_error(error, '\"0\" must be an object');\n        });\n\n        it('empty \"data\" array', () => {\n          const request = Object.assign({}, write_with_id);\n          request.data = [ ];\n          const error = horizon_protocol.replace.validate(request).error;\n          utils.check_error(error, '\"data\" must contain at least 1 items');\n        });\n      });\n    });\n\n    describe('Read', () => {\n      // The 'query' and 'subscribe' requests use the same schema\n      it('common read schemas', () => {\n        assert.equal(horizon_protocol.query, horizon_protocol.subscribe);\n      });\n\n      describe('no selection', () => {\n        const valid = {\n          collection: 'horizon',\n        };\n\n        it('valid', () => {\n          const parsed = horizon_protocol.query.validate(valid);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, valid);\n        });\n\n        it('required fields', () => {\n          test_required_fields(horizon_protocol.query, valid,\n                               [ 'collection' ]);\n        });\n\n        it('extra field', () => {\n          test_extra_field(horizon_protocol.query, valid);\n        });\n\n        it('order', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('above', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          request.above = [ { id: 10 }, 'open' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('below', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          request.below = [ { id: 5 }, 'open' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('limit', () => {\n          const request = Object.assign({}, valid);\n          request.limit = 2;\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('above and below and limit', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          request.below = [ { id: 0 }, 'closed' ];\n          request.below = [ { id: 5 }, 'closed' ];\n          request.limit = 4;\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('wrong \"collection\" type', () => {\n          const request = Object.assign({}, valid);\n          request.collection = null;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"collection\" must be a string');\n        });\n\n        it('wrong \"collection\" value', () => {\n          const request = Object.assign({}, valid);\n          request.collection = '*.*';\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"collection\" must only contain alpha-numeric and underscore characters');\n        });\n\n        it('wrong \"order\" type', () => {\n          const request = Object.assign({}, valid);\n          request.order = true;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"order\" must be an array');\n        });\n\n        it('wrong \"order\" value', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.order = [ ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"order\" does not contain [fields, direction]');\n          } {\n            request.order = [ [ 'id' ] ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"order\" does not contain [direction]');\n          } {\n            request.order = [ { }, 'ascending' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"fields\" must be an array');\n          } {\n            request.order = [ [ ], 'descending' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"fields\" must contain at least 1 item');\n          } {\n            request.order = [ [ 'field' ], 'baleeted' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"direction\" must be one of [ascending, descending]');\n          }\n        });\n\n        it('\"above\" without \"order\"', () => {\n          const request = Object.assign({}, valid);\n          request.above = [ { id: 5 }, 'open' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('wrong \"above\" type', () => {\n          const request = Object.assign({}, valid);\n          request.above = true;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"above\" must be an array');\n        });\n\n        it('wrong \"above\" value', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.above = [ ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"above\" does not contain [value, bound_type]');\n          } {\n            request.above = [ 1, 'closed' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"value\" must be an object');\n          } {\n            request.above = [ { }, 'open' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"value\" must have 1 child');\n          } {\n            request.above = [ { id: 4 }, 5 ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"bound_type\" must be a string');\n          } {\n            request.above = [ { id: 3 }, 'ajar' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"bound_type\" must be one of [open, closed]');\n          }\n        });\n\n        it('\"below\" without \"order\"', () => {\n          const request = Object.assign({}, valid);\n          request.below = [ { id: 1 }, 'open' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('wrong \"below\" type', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          request.below = true;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"below\" must be an array');\n        });\n\n        it('wrong \"below\" value', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          {\n            request.below = [ ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"below\" does not contain [value, bound_type]');\n          } {\n            request.below = [ 1, 'closed' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"value\" must be an object');\n          } {\n            request.below = [ { }, 'open' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"value\" must have 1 child');\n          } {\n            request.below = [ { id: 4 }, 5 ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"bound_type\" must be a string');\n          } {\n            request.below = [ { id: 3 }, 'ajar' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"bound_type\" must be one of [open, closed]');\n          }\n        });\n\n        it('wrong \"limit\" type', () => {\n          const request = Object.assign({}, valid);\n          request.limit = true;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"limit\" must be a number');\n        });\n\n        it('wrong \"limit\" value', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.limit = -1;\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"limit\" must be greater than -1');\n          } {\n            request.limit = 1.5;\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"limit\" must be an integer');\n          }\n        });\n      });\n\n      describe('find', () => {\n        const valid = {\n          collection: 'horizon',\n          find: { score: 4 },\n        };\n\n        it('valid', () => {\n          const parsed = horizon_protocol.query.validate(valid);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, valid);\n        });\n\n        it('order', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"order\" is not allowed');\n        });\n\n        it('above', () => {\n          const request = Object.assign({}, valid);\n          request.above = [ { id: 3 }, 'open' ];\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"above\" is not allowed');\n        });\n\n        it('below', () => {\n          const request = Object.assign({}, valid);\n          request.below = [ { id: 4 }, 'closed' ];\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"below\" is not allowed');\n        });\n\n        it('limit', () => {\n          const request = Object.assign({}, valid);\n          request.limit = 4;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"limit\" is not allowed');\n        });\n\n        it('wrong \"find\" type', () => {\n          const request = Object.assign({}, valid);\n          request.find = 'score';\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"find\" must be an object');\n        });\n      });\n\n      describe('find_all multiple', () => {\n        const valid = {\n          collection: 'horizon',\n          find_all: [ { score: 2 }, { score: 5, id: 0 } ],\n        };\n\n        it('valid', () => {\n          const parsed = horizon_protocol.query.validate(valid);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, valid);\n        });\n\n        it('order', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'descending' ];\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"order\" is not allowed');\n        });\n\n        it('limit', () => {\n          const request = Object.assign({}, valid);\n          request.limit = 2;\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('above', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.above = [ { id: 3 }, 'closed' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"above\" is not allowed');\n          } {\n            request.order = [ [ 'id' ], 'ascending' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"order\" is not allowed');\n          }\n        });\n\n        it('below', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.below = [ { id: 9 }, 'closed' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"below\" is not allowed');\n          } {\n            request.order = [ [ 'id' ], 'descending' ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"order\" is not allowed');\n          }\n        });\n      });\n\n      describe('find_all one', () => {\n        const valid = {\n          collection: 'horizon',\n          find_all: [ { score: 8, id: 5 } ],\n        };\n\n        it('valid', () => {\n          const parsed = horizon_protocol.query.validate(valid);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, valid);\n        });\n\n        it('order', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'descending' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('limit', () => {\n          const request = Object.assign({}, valid);\n          request.limit = 2;\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('above', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'ascending' ];\n          request.above = [ { id: 3 }, 'closed' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('below', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'descending' ];\n          request.below = [ { id: 9 }, 'closed' ];\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('above and below and limit', () => {\n          const request = Object.assign({}, valid);\n          request.order = [ [ 'id' ], 'descending' ];\n          request.above = [ { id: 'foo' }, 'open' ];\n          request.below = [ { id: 'bar' }, 'closed' ];\n          request.limit = 59;\n          const parsed = horizon_protocol.query.validate(request);\n          assert.ifError(parsed.error);\n          assert.deepStrictEqual(parsed.value, request);\n        });\n\n        it('wrong \"find_all\" type', () => {\n          const request = Object.assign({}, valid);\n          request.find_all = null;\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"find_all\" must be an array');\n        });\n\n        it('wrong \"find_all\" value', () => {\n          const request = Object.assign({}, valid);\n          {\n            request.find_all = [ ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"find_all\" must contain at least 1 items');\n          } {\n            request.find_all = [ { } ];\n            const error = horizon_protocol.query.validate(request).error;\n            utils.check_error(error, '\"item\" must have at least 1 child');\n          }\n        });\n\n        it('with \"find\"', () => {\n          const request = Object.assign({}, valid);\n          request.find = { id: 7 };\n          const error = horizon_protocol.query.validate(request).error;\n          utils.check_error(error, '\"find\" is not allowed'); // TODO: better message?\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "server/test/subscribe_tests.js",
    "content": "'use strict';\n\nconst utils = require('./utils');\n\nconst all_tests = (collection) => {\n  const num_rows = 10;\n\n  before('Clear collection', (done) => utils.clear_collection(collection, done));\n  before('Populate collection', (done) => utils.populate_collection(collection, num_rows, done));\n  beforeEach('Authenticate client', utils.horizon_admin_auth);\n};\n\nconst suite = (collection) => describe('Subscribe', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "server/test/test.js",
    "content": "'use strict';\n\nconst logger = require('../src/logger');\nconst utils = require('./utils');\n\nconst all_suites = [ 'http_tests',\n                     'prereq_tests',\n                     'protocol_tests',\n                     'query_tests',\n                     'subscribe_tests',\n                     'write_tests',\n                     'permissions' ];\nconst collection = 'test';\n\nbefore('Start RethinkDB Server', () => utils.start_rethinkdb());\nafter('Stop RethinkDB Server', () => utils.stop_rethinkdb());\n\nbeforeEach(\n  /** @this mocha */\n  function() { logger.info(`Start test '${this.currentTest.title}'`); });\n\nafterEach(\n  /** @this mocha */\n  function() { logger.info(`End test '${this.currentTest.title}'`); });\n\ndescribe('Horizon Server',\n  /** @this mocha */\n  function() {\n    before('Start Horizon Server', utils.start_horizon_server);\n    after('Close Horizon Server', utils.close_horizon_server);\n\n    before(`Creating general-purpose collection: '${collection}'`,\n           (done) => utils.create_collection(collection, done));\n\n    beforeEach('Connect Horizon Client', utils.open_horizon_conn);\n    afterEach('Close Horizon Client', utils.close_horizon_conn);\n    all_suites.forEach((s) => require(`./${s}`).suite(collection));\n  });\n"
  },
  {
    "path": "server/test/utils.js",
    "content": "'use strict';\n\nconst horizon = require('../src/server');\nconst logger = require('../src/logger');\n\nconst rm_sync_recursive = require('../../cli/src/utils/rm_sync_recursive');\nconst start_rdb_server = require('../../cli/src/utils/start_rdb_server');\nconst each_line_in_pipe = require('../../cli/src/utils/each_line_in_pipe');\n\nconst assert = require('assert');\nconst http = require('http');\nconst r = require('rethinkdb');\nconst websocket = require('ws');\n\nconst project_name = 'unittest';\nconst data_dir = './rethinkdb_data_test';\n\nconst log_file = `./horizon_test_${process.pid}.log`;\nlogger.level = 'debug';\nlogger.add(logger.transports.File, { filename: log_file });\nlogger.remove(logger.transports.Console);\n\n// Variables used by most tests\nlet rdb_server, rdb_http_port, rdb_port, rdb_conn, horizon_server, horizon_port, horizon_conn, horizon_listeners;\nlet horizon_authenticated = false;\n\nconst start_rethinkdb = () => {\n  logger.info('removing dir');\n  rm_sync_recursive(data_dir);\n\n  logger.info('creating server');\n  return start_rdb_server({ dataDir: data_dir }).then((server) => {\n    rdb_server = server;\n    rdb_port = server.driver_port;\n    rdb_http_port = server.http_port;\n    logger.info('server created, connecting');\n\n    return r.connect({ db: project_name, port: rdb_port });\n  }).then((conn) => {\n    logger.info('connected');\n    rdb_conn = conn;\n    return r.dbCreate(project_name).run(conn);\n  }).then((res) => {\n    assert.strictEqual(res.dbs_created, 1);\n  });\n};\n\nconst stop_rethinkdb = () => rdb_server.close();\n\n// Used to prefix reql queries with the underlying table of a given collection\nconst table = (collection) =>\n  r.table(\n    r.db(project_name)\n      .table('hz_collections')\n      .get(collection)\n      .do((row) =>\n        r.branch(row.eq(null),\n                 r.error('Collection does not exist.'),\n                 row('id'))));\n\nconst make_admin_token = () => {\n  const jwt = horizon_server && horizon_server._auth && horizon_server._auth._jwt;\n  assert(jwt);\n  return jwt.sign({ id: 'admin', provider: null }).token;\n};\n\n// Creates a collection, no-op if it already exists, uses horizon server prereqs\nconst create_collection = (collection, done) => {\n  assert.notStrictEqual(horizon_server, undefined);\n  assert.notStrictEqual(horizon_port, undefined);\n  const conn = new websocket(`ws://localhost:${horizon_port}/horizon`,\n                             horizon.protocol,\n                             { rejectUnauthorized: false })\n    .once('error', (err) => assert.ifError(err))\n    .on('open', () => {\n      conn.send(JSON.stringify({ request_id: 123, method: 'token', token: make_admin_token() }));\n      conn.once('message', (data) => {\n        const res = JSON.parse(data);\n        assert.strictEqual(res.request_id, 123);\n        assert.strictEqual(typeof res.token, 'string');\n        assert.strictEqual(res.id, 'admin');\n        assert.strictEqual(res.provider, null);\n\n        // This query should auto-create the collection if it's missing\n        conn.send(JSON.stringify({\n          request_id: 0,\n          type: 'query',\n          options: { collection, limit: 0 },\n        }));\n\n        conn.once('message', () => {\n          conn.close();\n          done();\n        });\n      });\n    });\n};\n\n// Removes all data from a collection - does not remove indexes\nconst clear_collection = (collection, done) => {\n  assert.notStrictEqual(rdb_conn, undefined);\n  table(collection).delete().run(rdb_conn).then(() => done());\n};\n\n// Populates a collection with the given rows\n// If `rows` is a number, fill in data using all keys in [0, rows)\nconst populate_collection = (collection, rows, done) => {\n  assert.notStrictEqual(rdb_conn, undefined);\n\n  if (rows.constructor.name !== 'Array') {\n    table(collection).insert(\n      r.range(rows).map(\n        (i) => ({ id: i, value: i.mod(4) })\n      )).run(rdb_conn).then(() => done());\n  } else {\n    table(collection).insert(rows).run(rdb_conn).then(() => done());\n  }\n};\n\nconst start_horizon_server = (done) => {\n  logger.info('creating http server');\n  assert.strictEqual(horizon_server, undefined);\n\n  const http_server = new http.Server();\n  http_server.listen(0, () => {\n    logger.info('creating horizon server');\n    horizon_port = http_server.address().port;\n    horizon_server = new horizon.Server(http_server,\n      { project_name,\n        rdb_port,\n        auto_create_collection: true,\n        auto_create_index: true,\n        permissions: true,\n        auth: {\n          token_secret: 'hunter2',\n          allow_unauthenticated: true,\n        },\n      });\n    horizon_server.ready().catch((err) => logger.info(`horizon server error: ${err}`));\n    horizon_server.ready().then(() => logger.info('horizon server ready'));\n    horizon_server.ready().then(() => done());\n  });\n  http_server.on('error', (err) => done(err));\n};\n\nconst close_horizon_server = () => {\n  if (horizon_server !== undefined) { horizon_server.close(); }\n  horizon_server = undefined;\n};\n\nconst add_horizon_listener = (request_id, cb) => {\n  assert(horizon_authenticated, 'horizon_conn was not authenticated before making requests');\n  assert.notStrictEqual(request_id, undefined);\n  assert.notStrictEqual(horizon_listeners, undefined);\n  assert.strictEqual(horizon_listeners.get(request_id), undefined);\n  horizon_listeners.set(request_id, cb);\n};\n\nconst remove_horizon_listener = (request_id) => {\n  assert.notStrictEqual(request_id, undefined);\n  assert.notStrictEqual(horizon_listeners, undefined);\n  horizon_listeners.delete(request_id);\n};\n\nconst dispatch_message = (raw) => {\n  const msg = JSON.parse(raw);\n  assert.notStrictEqual(msg.request_id, undefined);\n  assert.notStrictEqual(horizon_listeners, undefined);\n\n  if (msg.request_id !== null) {\n    const listener = horizon_listeners.get(msg.request_id);\n    assert.notStrictEqual(listener, undefined);\n    listener(msg);\n  }\n};\n\nconst open_horizon_conn = (done) => {\n  logger.info('opening horizon conn');\n  assert.notStrictEqual(horizon_server, undefined);\n  assert.strictEqual(horizon_conn, undefined);\n  horizon_authenticated = false;\n  horizon_listeners = new Map();\n  horizon_conn =\n    new websocket(`ws://localhost:${horizon_port}/horizon`,\n                  horizon.protocol,\n                  { rejectUnauthorized: false })\n      .once('error', (err) => assert.ifError(err))\n      .on('open', () => done());\n};\n\nconst close_horizon_conn = () => {\n  logger.info('closing horizon conn');\n  if (horizon_conn) { horizon_conn.close(); }\n  horizon_conn = undefined;\n  horizon_listeners = undefined;\n  horizon_authenticated = false;\n};\n\nconst horizon_auth = (req, cb) => {\n  assert(horizon_conn && horizon_conn.readyState === websocket.OPEN);\n  horizon_conn.send(JSON.stringify(req));\n  horizon_conn.once('message', (auth_msg) => {\n    horizon_authenticated = true;\n    const res = JSON.parse(auth_msg);\n    horizon_conn.on('message', (msg) => dispatch_message(msg));\n    cb(res);\n  });\n};\n\n// Create a token for the admin user and use that to authenticate\nconst horizon_admin_auth = (done) => {\n  horizon_auth({ request_id: -1, method: 'token', token: make_admin_token() }, (res) => {\n    assert.strictEqual(res.request_id, -1);\n    assert.strictEqual(typeof res.token, 'string');\n    assert.strictEqual(res.id, 'admin');\n    assert.strictEqual(res.provider, null);\n    done();\n  });\n};\n\nconst horizon_default_auth = (done) => {\n  horizon_auth({ request_id: -1, method: 'unauthenticated' }, (res) => {\n    assert.strictEqual(res.request_id, -1);\n    assert.strictEqual(typeof res.token, 'string');\n    assert.strictEqual(res.id, null);\n    assert.strictEqual(res.provider, 'unauthenticated');\n    done();\n  });\n};\n\n// `stream_test` will send a request (containing a request_id), and call the\n// callback with (err, res), where `err` is the error string if an error\n// occurred, or `null` otherwise.  `res` will be an array, being the concatenation\n// of all `data` items returned by the server for the given request_id.\n// TODO: this doesn't allow for dealing with multiple states (like 'synced').\nconst stream_test = (req, cb) => {\n  assert(horizon_conn && horizon_conn.readyState === websocket.OPEN);\n  const results = [];\n\n  add_horizon_listener(req.request_id, (msg) => {\n    if (msg.data !== undefined) {\n      results.push.apply(results, msg.data);\n    }\n    if (msg.error !== undefined) {\n      remove_horizon_listener(req.request_id);\n      cb(new Error(msg.error), results);\n    } else if (msg.state === 'complete') {\n      remove_horizon_listener(req.request_id);\n      cb(null, results);\n    }\n  });\n\n  horizon_conn.send(JSON.stringify(req));\n};\n\nconst check_error = (err, msg) => {\n  assert.notStrictEqual(err, null, 'Should have gotten an error.');\n  assert(err.message.indexOf(msg) !== -1, err.message);\n};\n\nconst set_group = (group, done) => {\n  assert(horizon_server && rdb_conn);\n  r.db(project_name)\n    .table('hz_groups')\n    .get(group.id)\n    .replace(group)\n    .run(rdb_conn)\n    .then((res, err) => {\n      assert.ifError(err);\n      assert(res && res.errors === 0);\n      done();\n    });\n};\n\nmodule.exports = {\n  rdb_conn: () => rdb_conn,\n  rdb_http_port: () => rdb_http_port,\n  rdb_port: () => rdb_port,\n  horizon_conn: () => horizon_conn,\n  horizon_port: () => horizon_port,\n  horizon_listeners: () => horizon_listeners,\n\n  start_rethinkdb, stop_rethinkdb,\n  create_collection,\n  populate_collection,\n  clear_collection,\n\n  start_horizon_server, close_horizon_server,\n  open_horizon_conn, close_horizon_conn,\n  horizon_auth, horizon_admin_auth, horizon_default_auth,\n  add_horizon_listener, remove_horizon_listener,\n\n  set_group,\n\n  stream_test,\n  check_error,\n  each_line_in_pipe,\n  table,\n};\n"
  },
  {
    "path": "server/test/write_tests.js",
    "content": "'use strict';\n\nconst utils = require('./utils');\nconst horizon_writes = require('../src/endpoint/writes');\n\nconst assert = require('assert');\nconst crypto = require('crypto');\n\nconst hz_v = horizon_writes.version_field;\nconst invalidated_msg = horizon_writes.invalidated_msg;\n\n// Before each test, ids [0, 4) will be present in the collection\nconst original_data = [\n  { id: 0, old_field: [ ], [hz_v]: 0 },\n  { id: 1, old_field: [ ], [hz_v]: 0 },\n  { id: 2, old_field: [ ], [hz_v]: 0 },\n  { id: 3, old_field: [ ], [hz_v]: 0 },\n];\n\nconst new_id = [ 4 ];\nconst conflict_id = [ 3 ];\nconst new_ids = [ 4, 5, 6 ];\nconst conflict_ids = [ 2, 3, 4 ];\n\nconst without_version = (item) => {\n  const res = Object.assign({ }, item);\n  delete res[hz_v];\n  return res;\n};\n\nconst compare_write_response = (actual, expected) => {\n  assert.deepStrictEqual(actual.map(without_version), expected);\n};\n\nconst check_collection_data = (actual, expected) => {\n  // TODO: make sure that versions increment properly\n  assert.deepStrictEqual(actual.map(without_version),\n                         expected.map(without_version));\n};\n\n// TODO: verify through reql that rows have been inserted/removed\nconst all_tests = (collection) => {\n  const new_row_from_id = (id) => ({ id, new_field: 'a' });\n  const merged_row_from_id = (id) => {\n    if (id >= 4) { return new_row_from_id(id); }\n    return { id, new_field: 'a', old_field: [ ] };\n  };\n\n  const make_request = (type, data, options) => ({\n    request_id: crypto.randomBytes(4).readUInt32BE(),\n    type,\n    options: Object.assign({}, options || {}, { collection, data }),\n  });\n\n  const check_collection = (expected, done) => {\n    utils.table(collection).orderBy({ index: 'id' }).coerceTo('array')\n      .run(utils.rdb_conn()).then((res) => {\n        check_collection_data(res, expected);\n        done();\n      }).catch((err) => done(err));\n  };\n\n  const combine_sort_data = (old_data, new_data, on_new, on_conflict) => {\n    const map = new Map();\n    old_data.forEach((row) => map.set(row.id, row));\n    new_data.forEach((row) => {\n      if (map.has(row.id)) {\n        on_conflict(row, map);\n      } else {\n        on_new(row, map);\n      }\n    });\n    return Array.from(map.values()).sort((a, b) => a.id - b.id);\n  };\n\n  const union_sort_data = (old_data, new_data) =>\n    combine_sort_data(old_data, new_data,\n      (row, map) => map.set(row.id, row), (row, map) => map.set(row.id, row));\n\n  const replace_sort_data = (old_data, new_data) =>\n    combine_sort_data(old_data, new_data,\n      () => null, (row, map) => map.set(row.id, row));\n\n  beforeEach('Clear collection', (done) => utils.clear_collection(collection, done));\n\n  describe('Basic writes', () => {\n    beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done));\n    beforeEach('Populate collection', (done) => utils.populate_collection(collection, original_data, done));\n\n    const request_from_ids = (type, ids) => make_request(type, ids.map(new_row_from_id));\n\n    describe('Store', () => {\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('store', ids), (err, res) => {\n          const expected = ids.map((id) => ({ id }));\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const new_data = ids.map(new_row_from_id);\n          check_collection(union_sort_data(original_data, new_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n\n    describe('Replace', () => {\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('replace', ids), (err, res) => {\n          const expected = ids.map((id) =>\n            (id < original_data.length ? { id } : { error: 'The document was missing.' })\n          );\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const new_data = ids.map(new_row_from_id);\n          check_collection(replace_sort_data(original_data, new_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n\n    describe('Upsert', () => {\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('upsert', ids), (err, res) => {\n          const expected = ids.map((id) => ({ id }));\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const new_data = ids.map(merged_row_from_id);\n          check_collection(union_sort_data(original_data, new_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n\n    describe('Update', () => {\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('update', ids), (err, res) => {\n          const expected = ids.map((id) =>\n            (id < original_data.length ? { id } : { error: 'The document was missing.' })\n          );\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const new_data = ids.map(merged_row_from_id);\n          check_collection(replace_sort_data(original_data, new_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n\n    describe('Insert', () => {\n      const add_sort_data = (old_data, new_data) =>\n        combine_sort_data(old_data, new_data,\n          (row, map) => map.set(row.id, row), () => null);\n\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('insert', ids), (err, res) => {\n          const expected = ids.map((id) =>\n            (id >= original_data.length ? { id } : { error: 'The document already exists.' })\n          );\n\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const new_data = ids.map(new_row_from_id);\n          check_collection(add_sort_data(original_data, new_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n\n    describe('Remove', () => {\n      // `old_data` and `new_data` may overlap, but each cannot contain duplicates\n      const remove_sort_data = (old_data, new_data) =>\n        combine_sort_data(old_data, new_data,\n          () => null,\n          (row, map) => map.delete(row.id));\n\n      const test_case = (ids, done) => {\n        utils.stream_test(request_from_ids('remove', ids), (err, res) => {\n          const expected = ids.map((id) => ({ id }));\n          assert.ifError(err);\n          compare_write_response(res, expected);\n          const deleted_data = ids.map(new_row_from_id);\n          check_collection(remove_sort_data(original_data, deleted_data), done);\n        });\n      };\n\n      it('new', (done) => test_case(new_id, done));\n      it('conflict', (done) => test_case(conflict_id, done));\n      it('batch new', (done) => test_case(new_ids, done));\n      it('batch conflict', (done) => test_case(conflict_ids, done));\n    });\n  });\n\n  describe('Versioned', () => {\n    beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done));\n\n    const test_data = [ { id: 'versioned', [hz_v]: 11, foo: 'bar' } ];\n    beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done));\n\n    describe('Store', () => {\n      const request = (row) => make_request('store', [ row ]);\n\n      it('correct version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versioned', [hz_v]: 12 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versioned', [hz_v]: 12, value: 1 } ], done);\n        });\n      });\n\n      it('incorrect version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Replace', () => {\n      const request = (row) => make_request('replace', [ row ]);\n\n      it('correct version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versioned', [hz_v]: 12 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versioned', [hz_v]: 12, value: 1 } ], done);\n        });\n      });\n\n      it('incorrect version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Upsert', () => {\n      const request = (row) => make_request('upsert', [ row ]);\n\n      it('correct version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versioned', [hz_v]: 12 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar' } ], done);\n        });\n      });\n\n      it('incorrect version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Update', () => {\n      const request = (row) => make_request('update', [ row ]);\n\n      it('correct version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versioned', [hz_v]: 12 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar' } ], done);\n        });\n      });\n\n      it('incorrect version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Remove', () => {\n      const request = (row) => make_request('remove', [ row ]);\n\n      it('correct version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versioned', [hz_v]: 11 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ ], done);\n        });\n      });\n\n      it('incorrect version', (done) => {\n        utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n  });\n\n  describe('Versionless', () => {\n    beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done));\n\n    const test_data = [ { id: 'versionless', foo: 'bar' } ];\n    beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done));\n\n    describe('Store', () => {\n      const request = (row) => make_request('store', [ row ]);\n\n      it('unspecified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versionless', [hz_v]: 0 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versionless', [hz_v]: 0, value: 3 } ], done);\n        });\n      });\n\n      it('specified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Replace', () => {\n      const request = (row) => make_request('replace', [ row ]);\n\n      it('unspecified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versionless', [hz_v]: 0 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versionless', [hz_v]: 0, value: 3 } ], done);\n        });\n      });\n\n      it('specified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Upsert', () => {\n      const request = (row) => make_request('upsert', [ row ]);\n\n      it('unspecified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versionless', [hz_v]: 0 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar' } ], done);\n        });\n      });\n\n      it('specified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Update', () => {\n      const request = (row) => make_request('update', [ row ]);\n\n      it('unspecified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versionless', [hz_v]: 0 } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ { id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar' } ], done);\n        });\n      });\n\n      it('specified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n\n    describe('Remove', () => {\n      const request = (row) => make_request('remove', [ row ]);\n\n      it('unspecified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { id: 'versionless' } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection([ ], done);\n        });\n      });\n\n      it('specified version', (done) => {\n        utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => {\n          assert.ifError(err);\n          const expected = [ { error: invalidated_msg } ];\n          assert.deepStrictEqual(res, expected);\n          check_collection(test_data, done);\n        });\n      });\n    });\n  });\n\n  // To guarantee multiple retries of a write, we combine a batch of writes\n  // for the same row (unspecified versions) with a validator.  This way, only one\n  // write will make it through each loop, although it is undefined in which order\n  // the writes occur.\n  describe('Retry', () => {\n    beforeEach('Authenticate', (done) => utils.horizon_default_auth(done));\n\n    // Set a catch-all rule for the 'default' group so we can have a validator\n    before('Set rules', (done) => utils.set_group({\n      id: 'default',\n      rules: {\n        dummy: {\n          template: 'any()',\n          validator: '() => true',\n        },\n      },\n    }, done));\n\n    const writes = [\n      { id: 0, a: 1 },\n      { id: 0, b: 2 },\n      { id: 0, c: 3 },\n    ];\n\n    const by_version = (a, b) => a[hz_v] - b[hz_v];\n    const check_and_get_latest_write = (res) => {\n      const latest_index = res.findIndex((x) => x[hz_v] === 2);\n      assert(latest_index !== -1);\n      res.sort(by_version);\n      assert.deepStrictEqual(res, [ { id: 0, [hz_v]: 0 },\n                                    { id: 0, [hz_v]: 1 },\n                                    { id: 0, [hz_v]: 2 } ]);\n      return writes[latest_index];\n    };\n\n    // For some tests, we expect exactly one write to succeed and the others\n    // to fail.  Which write succeeds is not guaranteed to be deterministic,\n    // so we return the successful write data.\n    const check_one_successful_write = (res, error) => {\n      const success_index = res.findIndex((x) => x.error === undefined);\n      assert(success_index !== -1);\n      for (let i = 0; i < res.length; ++i) {\n        if (i === success_index) {\n          assert.deepStrictEqual(res[i], { id: 0, [hz_v]: 0 });\n        } else {\n          assert.deepStrictEqual(res[i], { error });\n        }\n      }\n      return writes[success_index];\n    };\n\n    describe('Existing Row', () => {\n      const test_data = [ { id: 0, value: 0 } ];\n      beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done));\n\n      it('Store', (done) => {\n        utils.stream_test(make_request('store', writes), (err, res) => {\n          assert.ifError(err);\n          const latest_write = check_and_get_latest_write(res);\n          check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done);\n        });\n      });\n\n      it('Replace', (done) => {\n        utils.stream_test(make_request('replace', writes), (err, res) => {\n          assert.ifError(err);\n          const latest_write = check_and_get_latest_write(res);\n          check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done);\n        });\n      });\n\n      it('Upsert', (done) => {\n        utils.stream_test(make_request('upsert', writes), (err, res) => {\n          assert.ifError(err);\n          check_and_get_latest_write(res);\n          check_collection([ { id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done);\n        });\n      });\n\n      it('Update', (done) => {\n        utils.stream_test(make_request('update', writes), (err, res) => {\n          assert.ifError(err);\n          check_and_get_latest_write(res);\n          check_collection([ { id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done);\n        });\n      });\n\n      it('Remove', (done) => {\n        utils.stream_test(make_request('remove', writes), (err, res) => {\n          assert.ifError(err);\n          assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [ undefined, undefined, undefined ]);\n          assert.deepStrictEqual(res.map((x) => x.id), [ 0, 0, 0 ]);\n          check_collection([ ], done);\n        });\n      });\n    });\n\n    describe('New Row', () => {\n      it('Insert', (done) => {\n        utils.stream_test(make_request('insert', writes), (err, res) => {\n          assert.ifError(err);\n          const success_write = check_one_successful_write(res, 'The document already exists.');\n          check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done);\n        });\n      });\n\n      it('Store', (done) => {\n        utils.stream_test(make_request('store', writes), (err, res) => {\n          assert.ifError(err);\n          const latest_write = check_and_get_latest_write(res);\n          check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done);\n        });\n      });\n\n      it('Upsert', (done) => {\n        utils.stream_test(make_request('upsert', writes), (err, res) => {\n          assert.ifError(err);\n          assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [ 0, 1, 2 ]);\n          assert.deepStrictEqual(res.map((x) => x.id), [ 0, 0, 0 ]);\n          check_collection([ { id: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done);\n        });\n      });\n    });\n\n\n    // Because all the writes are to the same document, only one can succeed\n    // per iteration with the database.  In order to test timeouts, we use a\n    // timeout of zero, so the other rows should immediately error.\n    describe('Zero Timeout', () => {\n      const timeout = { timeout: 0 };\n      const test_data = [ { id: 0, value: 0 } ];\n      beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done));\n\n      it('Store', (done) => {\n        utils.stream_test(make_request('store', writes, timeout), (err, res) => {\n          assert.ifError(err);\n          const success_write = check_one_successful_write(res, 'Operation timed out.');\n          check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done);\n        });\n      });\n\n      it('Replace', (done) => {\n        utils.stream_test(make_request('replace', writes, timeout), (err, res) => {\n          assert.ifError(err);\n          const success_write = check_one_successful_write(res, 'Operation timed out.');\n          check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done);\n        });\n      });\n\n      it('Upsert', (done) => {\n        utils.stream_test(make_request('upsert', writes, timeout), (err, res) => {\n          assert.ifError(err);\n          const success_write = check_one_successful_write(res, 'Operation timed out.');\n          check_collection([ Object.assign({ [hz_v]: 0 }, test_data[0], success_write) ], done);\n        });\n      });\n\n      it('Update', (done) => {\n        utils.stream_test(make_request('update', writes, timeout), (err, res) => {\n          assert.ifError(err);\n          const success_write = check_one_successful_write(res, 'Operation timed out.');\n          check_collection([ Object.assign({ [hz_v]: 0 }, test_data[0], success_write) ], done);\n        });\n      });\n    });\n  });\n};\n\nconst suite = (collection) => describe('Write', () => all_tests(collection));\n\nmodule.exports = { suite };\n"
  },
  {
    "path": "test/serve.js",
    "content": "#!/usr/bin/env node\n'use strict'\n\nError.stackTraceLimit = Infinity;\n\nconst horizon = require('../server');\n\n// Utilities provided by the CLI library\nconst each_line_in_pipe = require('../cli/src/utils/each_line_in_pipe');\nconst start_rdb_server = require('../cli/src/utils/start_rdb_server');\nconst rm_sync_recursive = require('../cli/src/utils/rm_sync_recursive');\nconst parse_yes_no_option = require('../cli/src/utils/parse_yes_no_option');\n\n// We could make this a module, but we already require the server to be configured,\n// so reuse its argparse module\nconst argparse = require('../cli/node_modules/argparse');\n\nconst assert = require('assert');\nconst child_process = require('child_process');\nconst dns = require('dns');\nconst fs = require('fs');\nconst http = require('http');\nconst path = require('path');\nconst url = require('url');\nconst process = require('process');\nconst crypto = require('crypto');\n\nconst data_dir = path.resolve(__dirname, 'rethinkdb_data_test');\nconst test_dist_dir = path.resolve(__dirname, '../client/dist');\nconst examples_dir = path.resolve(__dirname, '../examples');\n\nconst parser = new argparse.ArgumentParser();\nparser.addArgument([ '--port', '-p' ],\n  { type: 'int', defaultValue: 8181, metavar: 'PORT',\n    help: 'Local port to serve HTTP assets and horizon on.' });\n\nparser.addArgument([ '--bind', '-b' ],\n  { type: 'string', defaultValue: [ 'localhost' ], action: 'append', metavar: 'HOST',\n    help: 'Local hostname(s) to serve HTTP and horizon on (repeatable).' });\n\nparser.addArgument([ '--keep', '-k' ],\n  { defaultValue: false, action: 'storeTrue',\n    help: 'Keep the existing \"rethinkdb_data_test\" directory.' });\n\nparser.addArgument([ '--permissions' ],\n  { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', defaultValue: 'no',\n    help: 'Enable or disable checking permissions on requests, defaults to disabled.' });\n\nconst options = parser.parseArgs();\n\nif (options.bind.indexOf('all') !== -1) { options.bind = [ '0.0.0.0' ]; }\n\nprocess.on('SIGINT', () => process.exit(0));\nprocess.on('SIGTERM', () => process.exit(0));\n\nconst serve_file = (file_path, res) => {\n  fs.access(file_path, fs.R_OK | fs.F_OK, (exists) => {\n    if (exists) {\n      res.writeHead(404, { 'Content-Type': 'text/plain' });\n      res.end(`File \"${file_path}\" not found\\n`);\n    } else {\n      fs.readFile(file_path, 'binary', (err, file) => {\n        if (err) {\n          res.writeHead(500, { 'Content-Type': 'text/plain' });\n          res.end(`${err}\\n`);\n        } else {\n          if (file_path.endsWith('.js')) {\n            res.writeHead(200, { 'Content-Type': 'application/javascript' });\n          } else if (file_path.endsWith('.html')) {\n            res.writeHead(200, { 'Content-Type': 'text/html' });\n          } else {\n              res.writeHead(200);\n          }\n          res.end(file, 'binary');\n        }\n      });\n    }\n  });\n};\n\n// On Windows, `npm` is actually `npm.cmd`\nconst npm_cmd = process.platform === \"win32\" ? \"npm.cmd\" : \"npm\";\n\n// Run the client build\nconst build_proc = child_process.spawn(npm_cmd, [ 'run', 'dev'],\n                                      { cwd: test_dist_dir });\n\nbuild_proc.on('exit', () => process.exit(1));\nprocess.on('exit', () => build_proc.kill('SIGTERM'));\n\nlet client_ready = false;\neach_line_in_pipe(build_proc.stdout, (line) => {\n  console.log(line);\n  if (/horizon.js[^.]/.test(line)) {\n    setImmediate(() => {\n      client_ready = true;\n      const date = new Date();\n      console.log(`${date.toLocaleTimeString()} - horizon.js rebuilt.`);\n    });\n  }\n});\neach_line_in_pipe(build_proc.stderr, (line) => {\n  console.error(line);\n});\n\nbuild_proc.stderr.on('data', (data) => {\n  const str = data.toString();\n  if (str.indexOf('% compile') >= 0) {\n    const date = new Date();\n    console.log(`${date.toLocaleTimeString()} - client assets compile.`);\n  }\n  if (str.indexOf('% emit') >= 0) {\n    const date = new Date();\n    console.log(`${date.toLocaleTimeString()} - client assets emit.`);\n  }\n});\n\n// Launch HTTP server with horizon that will serve the test files\nconst http_servers = options.bind.map((host) =>\n  new http.Server((req, res) => {\n    const req_path = url.parse(req.url).pathname;\n    if (req_path.indexOf('/examples/') === 0) {\n      serve_file(path.resolve(examples_dir, req_path.replace(/^[/]examples[/]/, '')), res);\n    } else {\n      if (!client_ready) {\n        res.writeHead(503, { 'Content-Type': 'text/plain' });\n        res.end('Initial client build is ongoing, try again in a few seconds.');\n      } else {\n        serve_file(path.resolve(test_dist_dir, req_path.replace(/^[/]/, '')), res);\n      }\n    }\n  }));\n\n// Determine the local IP addresses to tell `rethinkdb` to bind on\nnew Promise((resolve) => {\n  let outstanding = options.bind.length;\n  const res = new Set();\n  const add_results = (err, addrs) => {\n    assert.ifError(err);\n    addrs.forEach((addr) => {\n      // Filter out link-local addresses since node doesn't tell us the scope-id\n      if (addr.address.indexOf('fe80') !== 0) { res.add(addr.address); }\n    });\n    outstanding -= 1;\n    if (outstanding === 0) { resolve(res); }\n  };\n\n  options.bind.forEach((host) => {\n    dns.lookup(host, { all: true }, add_results);\n  });\n}).then((local_addresses) => {\n  // Launch rethinkdb - once we know the port we can attach horizon to the http server\n  if (!options.keep) {\n    rm_sync_recursive(data_dir);\n  }\n\n  console.log('starting rethinkdb');\n\n  return start_rdb_server({ bind: local_addresses, dataDir: data_dir });\n}).then((server) => {\n  assert.notStrictEqual(server.driver_port, undefined);\n  console.log(`RethinkDB server listening for clients on port ${server.driver_port}.`);\n  console.log(`RethinkDB server listening for HTTP on port ${server.http_port}.`);\n  console.log('starting horizon');\n\n  horizon.logger.level = 'debug';\n  const horizon_server = new horizon.Server(http_servers, {\n    auto_create_collection: true,\n    auto_create_index: true,\n    rdb_port: server.driver_port,\n    permissions: parse_yes_no_option(options.permissions),\n    project_name: 'test',\n    auth: {\n      allow_unauthenticated: true,\n      allow_anonymous: true,\n      token_secret: crypto.randomBytes(64).toString('base64'),\n    },\n  });\n  console.log('starting http servers');\n\n  // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server\n  http_servers.forEach((serv, i) => {\n    const extant_listeners = serv.listeners('request').slice(0);\n    serv.removeAllListeners('request');\n    serv.on('request', (req, res) => {\n      const req_path = url.parse(req.url).pathname;\n      if (req_path === '/horizon/horizon.js' || req_path === '/horizon/horizon.js.map') {\n        serve_file(path.resolve(test_dist_dir, req_path.replace('/horizon/', '')), res);\n      } else {\n        extant_listeners.forEach((l) => l.call(serv, req, res));\n      }\n    });\n\n    serv.listen(options.port, options.bind[i],\n      () => console.log(`HTTP server listening on ${options.bind[i]}:${options.port}.`));\n  });\n}).catch((err) => {\n  console.log(`Error when starting server:\\n${err.stack}`);\n  process.exit(1);\n});\n"
  },
  {
    "path": "test/setupDev.sh",
    "content": "#!/bin/bash\nset -e\n\ngreen () {\n    echo -e \"\\033[1;32m== $1 \\033[0m\"\n}\n\nif [ \"$1\" == '--clean' ]; then\n green 'Removing old node_modules dirs if present'\n if [ -d ../client/node_modules ]; then\n   echo Removing client/node_modules\n   rm -r ../client/node_modules\n fi\n if [ -d ../server/node_modules ]; then\n  echo Removing server/node_modules\n  rm -r ../server/node_modules\n fi\n if [ -d ../cli/node_modules ]; then\n  echo Removing cli/node_modules\n  rm -r ../cli/node_modules\n fi\nfi\n\npushd ../client\ngreen 'Unlinking existing client'\nnpm unlink\ngreen 'Linking client'\nnpm link --unsafe-perm --cache-min 9999999\npopd\n\npushd ../server\ngreen 'Unlinking existing server'\nnpm unlink\ngreen 'Linking server'\nnpm link @horizon/client\nnpm link --cache-min 9999999\npopd\n\npushd ../cli\ngreen 'Unlinking existing horizon cli'\nnpm unlink\ngreen 'Linking horizon cli'\nnpm link @horizon/server\nnpm link --cache-min 9999999\npopd\n\ngreen 'Dev environment set up'\n"
  },
  {
    "path": "update_versions.py",
    "content": "#!/usr/bin/env python2\n'''What? A Python script in a JavaScript library? Well I never...\nThis script is just for updating versions of Horizon, it doesn't get\npackaged or have any use for consumers of Horizon itself.\n'''\n\nimport json\nimport sys\nfrom contextlib import contextmanager\nfrom collections import OrderedDict\n\n@contextmanager\ndef rewrite(filename):\n    with open(filename, 'rb') as f:\n        package_json = json.load(f, object_pairs_hook=OrderedDict)\n\n    yield package_json\n\n    with open(filename, 'wb') as f:\n        json.dump(package_json, f, indent=2, separators=(',', ': '))\n        f.write('\\n') # json dump gives no trailing newline\n\n\ndef main(version):\n    with rewrite('./client/package.json') as client_pkg:\n        client_pkg['version'] = version\n\n    with rewrite('./server/package.json') as server_pkg:\n        server_pkg['version'] = version\n        server_pkg['dependencies']['@horizon/client'] = version\n\n    with rewrite('./cli/package.json') as cli_pkg:\n        cli_pkg['version'] = version\n        cli_pkg['dependencies']['@horizon/server'] = version\n\n\nif __name__ == '__main__':\n    try:\n        main(sys.argv[1])\n    except:\n        print 'Please provide a version'"
  }
]