Showing preview only (688K chars total). Download the full file or copy to clipboard to get everything.
Repository: rethinkdb/horizon
Branch: next
Commit: c1fa8bdd1999
Files: 211
Total size: 638.1 KB
Directory structure:
gitextract_ib87vw2t/
├── .dockerignore
├── .eslintrc.js
├── .gitignore
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.dev
├── GETTING-STARTED.md
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── circle.yml
├── cli/
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── create-cert.js
│ │ ├── init.js
│ │ ├── main.js
│ │ ├── make-token.js
│ │ ├── migrate.js
│ │ ├── schema.js
│ │ ├── serve.js
│ │ ├── utils/
│ │ │ ├── change_to_project_dir.js
│ │ │ ├── check-project-name.js
│ │ │ ├── config.js
│ │ │ ├── each_line_in_pipe.js
│ │ │ ├── initialize_joi.js
│ │ │ ├── interrupt.js
│ │ │ ├── is_directory.js
│ │ │ ├── nice_error.js
│ │ │ ├── parse_yes_no_option.js
│ │ │ ├── proc-promise.js
│ │ │ ├── rethrow.js
│ │ │ ├── rm_sync_recursive.js
│ │ │ └── start_rdb_server.js
│ │ └── version.js
│ └── test/
│ ├── config.js
│ ├── init.spec.js
│ ├── schema.spec.js
│ ├── serve.spec.js
│ ├── unit/
│ │ ├── check-project-name.js
│ │ └── nice_error.js
│ └── version.spec.js
├── client/
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── index.d.ts
│ ├── package.json
│ ├── scripts/
│ │ └── compile.js
│ ├── src/
│ │ ├── ast.js
│ │ ├── auth.js
│ │ ├── hacks/
│ │ │ └── watch-rewrites.js
│ │ ├── index-polyfill.js
│ │ ├── index.js
│ │ ├── model.js
│ │ ├── serialization.js
│ │ ├── shim.js
│ │ ├── socket.js
│ │ └── util/
│ │ ├── check-args.js
│ │ ├── glob.js
│ │ ├── ordinal.js
│ │ ├── query-parse.js
│ │ └── valid-index-value.js
│ ├── test/
│ │ ├── above.js
│ │ ├── aboveSub.js
│ │ ├── aggregate.js
│ │ ├── aggregateSub.js
│ │ ├── api.js
│ │ ├── auth.js
│ │ ├── below.js
│ │ ├── belowSub.js
│ │ ├── chaining.js
│ │ ├── collection.js
│ │ ├── find.js
│ │ ├── findAll.js
│ │ ├── findAllSub.js
│ │ ├── findSub.js
│ │ ├── horizonObject.js
│ │ ├── insert.js
│ │ ├── limit.js
│ │ ├── order.js
│ │ ├── orderLimitSub.js
│ │ ├── remove.js
│ │ ├── removeAll.js
│ │ ├── replace.js
│ │ ├── store.js
│ │ ├── test.html
│ │ ├── test.js
│ │ ├── times.js
│ │ ├── unit/
│ │ │ ├── ast.js
│ │ │ ├── auth.js
│ │ │ └── utilsTest.js
│ │ ├── update.js
│ │ ├── upsert.js
│ │ └── utils.js
│ ├── webpack.config.js
│ ├── webpack.horizon.config.js
│ └── webpack.test.config.js
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── examples/
│ ├── .eslintrc
│ ├── README.md
│ ├── angularjs-todo-app/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ └── dist/
│ │ ├── css/
│ │ │ └── style.css
│ │ ├── index.html
│ │ ├── js/
│ │ │ ├── app.js
│ │ │ └── controllers/
│ │ │ └── TodoController.js
│ │ └── package.json
│ ├── auth-app/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ └── dist/
│ │ ├── app.css
│ │ ├── app.js
│ │ └── index.html
│ ├── cyclejs-chat-app/
│ │ ├── README.md
│ │ └── dist/
│ │ ├── app.css
│ │ ├── app.js
│ │ └── index.html
│ ├── express-server/
│ │ ├── main.js
│ │ └── package.json
│ ├── hapi-server/
│ │ ├── main.js
│ │ └── package.json
│ ├── koa-server/
│ │ ├── main.js
│ │ └── package.json
│ ├── react-chat-app/
│ │ ├── README.md
│ │ └── dist/
│ │ ├── app.css
│ │ ├── app.jsx
│ │ ├── index.html
│ │ └── package.json
│ ├── react-todo-app/
│ │ ├── .gitignore
│ │ ├── dist/
│ │ │ ├── index.html
│ │ │ ├── js/
│ │ │ │ ├── app.jsx
│ │ │ │ ├── footer.jsx
│ │ │ │ ├── todoItem.jsx
│ │ │ │ ├── todoModel.js
│ │ │ │ └── utils.js
│ │ │ └── package.json
│ │ └── readme.md
│ ├── riotjs-chat-app/
│ │ └── dist/
│ │ ├── app.css
│ │ ├── chat.tag
│ │ └── index.html
│ ├── vue-chat-app/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ └── dist/
│ │ ├── app.css
│ │ ├── app.js
│ │ └── index.html
│ └── vue-todo-app/
│ ├── .gitignore
│ ├── dist/
│ │ ├── index.html
│ │ ├── js/
│ │ │ ├── app.js
│ │ │ ├── routes.js
│ │ │ └── store.js
│ │ └── package.json
│ └── readme.md
├── protocol.md
├── rfcs/
│ ├── identity_mgmt.md
│ └── permissions.md
├── server/
│ ├── .babelrc
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── auth/
│ │ │ ├── auth0.js
│ │ │ ├── facebook.js
│ │ │ ├── github.js
│ │ │ ├── google.js
│ │ │ ├── slack.js
│ │ │ ├── twitch.js
│ │ │ ├── twitter.js
│ │ │ └── utils.js
│ │ ├── auth.js
│ │ ├── client.js
│ │ ├── endpoint/
│ │ │ ├── common.js
│ │ │ ├── insert.js
│ │ │ ├── query.js
│ │ │ ├── remove.js
│ │ │ ├── replace.js
│ │ │ ├── store.js
│ │ │ ├── subscribe.js
│ │ │ ├── update.js
│ │ │ ├── upsert.js
│ │ │ └── writes.js
│ │ ├── error.js
│ │ ├── horizon.js
│ │ ├── logger.js
│ │ ├── metadata/
│ │ │ ├── collection.js
│ │ │ ├── index.js
│ │ │ ├── metadata.js
│ │ │ └── table.js
│ │ ├── permissions/
│ │ │ ├── group.js
│ │ │ ├── rule.js
│ │ │ ├── template.js
│ │ │ └── validator.js
│ │ ├── reql_connection.js
│ │ ├── request.js
│ │ ├── schema/
│ │ │ ├── horizon_protocol.js
│ │ │ └── server_options.js
│ │ ├── server.js
│ │ └── utils.js
│ └── test/
│ ├── http_tests.js
│ ├── permissions.js
│ ├── prereq_tests.js
│ ├── protocol_tests.js
│ ├── query_tests.js
│ ├── schema.js
│ ├── subscribe_tests.js
│ ├── test.js
│ ├── utils.js
│ └── write_tests.js
├── test/
│ ├── serve.js
│ └── setupDev.sh
└── update_versions.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
**/node_modules
docs
examples
server/test
client/test
client/lib/
client/build
client/dist
**/README.md
docker-compose.yml
**/rethinkdb_data_test
**/rethinkdb_data/
**/*.log
.#*
**/*-key.pem
**/*-cert.pem
**/.DS_Store
.hz/
config.toml
Dockerfile*
.git
================================================
FILE: .eslintrc.js
================================================
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
extends: "eslint:recommended",
rules: {
"arrow-body-style": [ERROR, "as-needed"],
"array-bracket-spacing": [ ERROR, "always" ],
"arrow-parens": [ ERROR, "always" ],
"arrow-spacing": [ ERROR ],
"block-spacing": [ ERROR, "always" ],
"brace-style": [ ERROR, "1tbs", { "allowSingleLine": true } ],
"comma-dangle": [ ERROR, "always-multiline" ],
"comma-spacing": [ ERROR ],
"comma-style": [ ERROR, "last" ],
"constructor-super": [ ERROR ],
"curly": [ ERROR, "all" ],
"dot-notation": [ ERROR ],
"eqeqeq": [ ERROR, "allow-null" ],
"func-style": [ ERROR, "declaration", { "allowArrowFunctions": true } ],
"indent": [ ERROR, 2 ],
"key-spacing": [ ERROR ],
"keyword-spacing": [ ERROR ],
"linebreak-style": [ ERROR, "unix" ],
"new-parens": [ ERROR ],
"max-len": [ ERROR, 80 ],
"no-array-constructor": [ ERROR ],
"no-case-declarations": [ ERROR ],
"no-class-assign": [ ERROR ],
"no-confusing-arrow": [ ERROR, { "allowParens": true } ],
"no-console": [ OFF ],
"no-const-assign": [ ERROR ],
"no-constant-condition": [ ERROR ],
"no-dupe-class-members": [ ERROR ],
"no-eval": [ ERROR ],
"no-extend-native": [ ERROR ],
"no-extra-semi": [ ERROR ],
"no-floating-decimal": [ ERROR ],
"no-implicit-coercion": [ ERROR ],
"no-implied-eval": [ ERROR ],
"no-invalid-this": [ ERROR ],
"no-labels": [ ERROR ],
"no-lonely-if": [ ERROR ],
"no-mixed-requires": [ ERROR ],
"no-multi-spaces": [ ERROR ],
"no-multi-str": [ ERROR ],
"no-multiple-empty-lines": [ ERROR, { "max": 2, "maxEOF": 1 } ],
"no-native-reassign": [ ERROR ],
"no-new-func": [ ERROR ],
"no-new-object": [ ERROR ],
"no-new-require": [ ERROR ],
"no-new-wrappers": [ ERROR ],
"no-param-reassign": [ ERROR ],
"no-proto": [ ERROR ],
"no-return-assign": [ ERROR ],
"no-self-compare": [ ERROR ],
"no-sequences": [ ERROR ],
"no-shadow": [ ERROR ],
"no-shadow-restricted-names": [ ERROR ],
"no-this-before-super": [ ERROR ],
"no-throw-literal": [ ERROR ],
"no-trailing-spaces": [ ERROR ],
"no-unexpected-multiline": [ ERROR ],
"no-unneeded-ternary": [ ERROR ],
"no-unreachable": [ ERROR ],
"no-use-before-define": [ ERROR, "nofunc" ],
"no-var": [ ERROR ],
"no-void": [ ERROR ],
"no-with": [ ERROR ],
"object-curly-spacing": [ ERROR, "always" ],
"one-var": [ ERROR, { "uninitialized": "always", "initialized": "never" } ],
"operator-assignment": [ ERROR, "always" ],
"operator-linebreak": [ ERROR, "after" ],
"padded-blocks": [ ERROR, "never" ],
"prefer-const": [ ERROR ],
"prefer-template": [ ERROR ],
"quote-props": [ ERROR, "as-needed" ],
"quotes": [ ERROR, "single", "avoid-escape" ],
"semi": [ ERROR, "always" ],
"semi-spacing": [ ERROR ],
"space-before-blocks": [ ERROR, "always" ],
"space-before-function-paren": [ ERROR, "never" ],
"space-in-parens": [ ERROR, "never" ],
"space-infix-ops": [ ERROR ],
"space-unary-ops": [ ERROR ],
"spaced-comment": [ ERROR, "always" ],
"strict": [ ERROR, "global" ],
"wrap-iife": [ ERROR, "inside" ],
"yoda": [ ERROR, "never" ],
},
env: {
"es6": true,
"node": true,
"mocha": true,
},
};
================================================
FILE: .gitignore
================================================
client/dist/
client/lib/
rethinkdb_data_test
rethinkdb_data/
**/*.log
.#*
**/*-key.pem
**/*-cert.pem
node_modules/
**/.DS_Store
.hz/
config.toml
**/.vscode
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
We're happy you want to contribute! You can help us in different ways:
- [Open an issue][1] with suggestions for improvements and errors you're facing
- Fork this repository and submit a pull request
- Improve the <a>documentation</a> (coming soon, see [Resources](#resources) below for now)
[1]: https://github.com/rethinkdb/horizon/issues
To submit a pull request, fork the [Horizon repository][3] and then clone your fork:
git clone git@github.com:<your-name>/horizon.git
[3]: https://github.com/rethinkdb/horizon
Make 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].
[4]: https://github.com/rethinkdb/horizon/compare/
[5]: http://rethinkdb.com/community/cla/
## Resources
Some useful resources to get started:
* [Getting started with Horizon][getting-started]
* [The Horizon client library API][client-api]
* [Configuring the `hz` command-line tool][cli-config]
[cli-config]: /cli/README.md
[client-api]: /client/README.md
[getting-started]: GETTING-STARTED.md
================================================
FILE: Dockerfile
================================================
# REQUIREMENTS
# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_URI=HOST:PORT
# * Your Horizon app needs to be mounted into /usr/app using -v /path/to/app:/usr/app
FROM node:5-slim
RUN yes '' | adduser --disabled-password horizon && \
mkdir -p /usr/horizon /usr/app /usr/certs
RUN apt update && apt install -y git
COPY . /usr/horizon/
WORKDIR /usr/horizon
RUN cd test; ./setupDev.sh
EXPOSE 8181
VOLUME /usr/app
CMD ["su", "-s", "/bin/sh", "horizon", "-c", "hz serve --bind all --connect $RETHINKDB_URI /usr/app"]
================================================
FILE: Dockerfile.dev
================================================
# REQUIREMENTS
# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_URI=HOST:PORT
# * Your Horizon app needs to be mounted into /usr/app using -v /path/to/app:/usr/app
FROM rethinkdb/horizon
ENV HZ_DEV = yes
================================================
FILE: GETTING-STARTED.md
================================================

# Getting Started with Horizon
**Getting Started**
* [Installation](#installation)
* [Creating your first app](#creating-your-first-app)
* [Starting Horizon Server](#starting-horizon-server)
* [Configuring Horizon Server](#configuring-horizon-server)
* [Adding OAuth authentication](#adding-oauth-authentication)
* [Intro to the Horizon Client Library](#the-horizon-client-library)
* [Storing documents](#storing-documents)
* [Retrieving documents](#retrieving-documents)
* [Removing documents](#removing-documents)
* [Watching for changes](#watching-for-changes)
* [Putting it all together](#putting-it-all-together)
* [Using an already existing application with Horizon](#bringing-your-app-to-horizon)
* [Do I need to move all my files into the `dist` folder?](#do-i-need-to-output-all-my-files-into-the-dist-folder)
* [How do I add Horizon to X?](#how-do-i-add-horizon-to-x)
**Examples**
* [Example Horizon Applications](#example-applications)
* [Extending Horizon Server examples](#extending-horizon-server)
---
## Installation
First, install horizon from npm:
```sh
$ npm install -g horizon
```
## Creating your first app
Now you can initialize a new horizon project:
```sh
$ hz init example-app
```
This will create a directory with the following files:
```sh
$ tree -aF example-app/
example-app/
├── dist/
│ └── index.html
├── .hz/
│ └── config.toml
└── src/
```
The `dist` directory is where you should output your static
files. Horizon doesn't have any opinions about what front-end build
system you use, just that the files to serve end up in `dist`. Your
source files would go into `src` but that's just a convention.
Horizon doesn't touch anything in `src`.
If you want, you can `npm init` or `bower init` in the `example-app`
directory to set up dependencies etc.
`.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).
By default, horizon creates a basic `index.html` to serve so you can verify everything is working:
```html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script src="/horizon/horizon.js"></script>
<script>
var horizon = Horizon();
horizon.onReady(function() {
document.querySelector('h1').innerHTML = 'It works!'
});
horizon.connect();
</script>
</head>
<body>
<marquee><h1></h1></marquee>
</body>
</html>
```
---
## Starting Horizon Server
We now need to start Horizon Server. Running `hz serve` does three main things:
1. Starts the Horizon Server node app which serves the Horizon Client API / WebSocket endpoint.
1. Serves the `horizon.js` client library.
1. Serves everything in the `dist` folder, _if it exists in the current working directory_.
*[RethinkDB](https://www.rethinkdb.com/docs/install/) needs to be installed first and accessible from the Path.*
Normally, running `hz serve` requires a running instance of RethinkDB as well as pre-created tables in your RethinkDB instance.
Luckily, running `hz serve --dev` has all that covered for you. Here's a comparison of what happens with and without `--dev`:
| | `hz serve`| `hz serve --dev` | Command-line Flag |
|----------------------------|:-----------:|:-----:|----------------------|
|Starts Horizon Server | ✅ | ✅ | |
|Starts RethinkDB Server | ❌ | ✅ | `--start-rethinkdb` |
|Insecure Mode (no HTTPS/WSS)| ❌ | ✅ | `--insecure` |
|Auto creates tables | ❌ | ✅ | `--auto-create-table`|
|Auto creates indexes | ❌ | ✅ | `--auto-create-index`|
So 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`
> 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.
Here you can find
<a href="https://github.com/rethinkdb/horizon/tree/next/cli#hz-serve">the complete list of command line flags</a> for `hz serve` ➡️.
On 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.
### Configuring Horizon Server
Horizon 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
in the current working directory. Here is [an example `.hz/config.toml` file from the Horizon CLI documentation](/cli/README.md#hzconfigtoml-file) ➡️.
> Be warned that there is a precedence to config file setting in the order of:
> environment variables > config file > command-line flags
### Adding OAuth authentication
With Horizon, we wanted to make it easy to allow your users to authenticate with the accounts
they already have with the most popular services.
You can find [a full list of OAuth implementations we support here](/server/src/auth).
The 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
to a the providers we currently support.
* 😵📖 - [Facebook](https://developers.facebook.com/apps/)
* 💻🏦 - [Github](https://github.com/settings/applications/new)
* 🔟<sup>100</sup> - [Google](https://console.developers.google.com/project)
* 🎮📹 - [Twitch](https://www.twitch.tv/kraken/oauth2/clients/new)
* 🐦💬 - [Twitter](https://apps.twitter.com/app/new)
From each of these providers you will eventually have a `client_id` and `client_secret`
(sometimes just `id` and `secret`) that you will need to put into the `.hz/config.toml`
configuration file.
Near the bottom of the automatically generated `.hz/config.toml` file you'll see commented out
sample 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:
```toml
# [auth.facebook]
# id = "000000000000000"
# secret = "00000000000000000000000000000000"
#
# [auth.google]
# id = "00000000000-00000000000000000000000000000000.apps.googleusercontent.com"
# secret = "000000000000000000000000"
#
# [auth.twitter]
# id = "0000000000000000000000000"
# secret = "00000000000000000000000000000000000000000000000000"
#
[auth.github]
id = "your_client_id"
secret = "your_client_secret"
```
Once you've added the lines in your `.hz/config.toml` you're basically all set. To verify that
Horizon Server picked them up, run `hz serve` then go to
`https://localhost:8181/horizon/auth_methods` (or where ever you are running Horizon Server) to
see a list of currently active authentication options.
> 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.
You 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:
```js
{
github: "/horizon/github"
}
```
Now the value of the property `github` is the path to replace on the current `window.location`
that will begin the authentication process. Or, just type in
`https://localhost:8181/horizon/github` in your browser to test it out.
As a result of a successful authentication, the browser will be redirected to the root of the
dev server (`https://localhost:8181/`) with the `?horizon_token=` in the query parameters and you
can now consider the user properly authenticated at this point. If an error occurs somewhere
during the authentication process, the browser will be redirected back to the root of the dev server with an error message in the query parameters.
A couple notes to mention:
* ***Where is the user data from authenticating with OAuth?***: At the moment we just
allow users to prove they have an account with the given provider. But obviously part of the
power 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.
* ***Why can't I configure the final redirect url?***: Customizing the final redirect_url on the
original domain will be possible in the future.
* ***Why doesn't Horizon use Passport?***: Passport was definitely considered for Horizon but
ultimately was too heavily tied with Express to achieve the amount of extensibility we wanted.
To ensure this extensibility we decided to implement our own handling of OAuth routes for
the different providers. If you're still convinced we should use Passport, feel free to
[open an issue](https://github.com/rethinkdb/horizon/issues/new) and direct your comments
to @Tryneus.
---
## The Horizon Client Library
In the boilerplate created by `hz init`, you can see that the Horizon client library is being
imported from the path `/horizon/horizon.js` served by Horizon Server. If you
```html
...
<head>
...
<script src="/horizon/horizon.js"></script>
</head>
...
```
After this script is loaded, you can connect to your running instance of Horizon Server.
```js
const horizon = Horizon();
```
From here you can start to interact with Horizon collections. Having `--dev` mode enabled on
the Horizon Server creates collections and indexes automatically so you can get your
application setup with as little hassle as possible.
> **Note:** With `--dev` mode enabled or `--auto-create-index`, indices will
be created automatically for queries that are run that don't already match
a pre-existing query.
```js
// This automatically creates
const chat = horizon("messages");
```
Now, `chat` is a Horizon collection of documents. You can perform a
variety of operations on this collection to filter them down to the ones
you need. This most basic operations are [`.store`][store] and [`.fetch`][fetch]:
### Storing documents
To store documents into the collection, we use [`.store`][store].
```js
// Object being stored
let message = {
text: "What a beautiful horizon 🌄!",
datetime: new Date(),
author: "@dalanmiller"
}
// Storing a document
chat.store(message);
```
If 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) ➡️.
### Retrieving documents
To retrieve messages from the collection we use [`.fetch`][fetch]. In this case, `.subscribe` takes a result and error handler function.
```js
chat.fetch().subscribe(
(items) => {
items.subscribe((item) => {
// Each result from the chat collection
// will pass through this function
console.log(item);
})
},
// If an error occurs, this function
// will execute with the `err` message
(err) => {
console.log(err);
})
```
### Removing documents
To remove documents from a collection, you can use either [`.remove`][remove] or [`.removeAll`][removeAll]:
```js
// These two queries are equivalent and will remove the document with id: 1.
chat.remove(1).subscribe((id) => { console.log(id) })
chat.remove({id: 1}).subscribe((id) => {console.log(id)})
```
Or, if you have a set of documents that you'd like to remove you can pass them in as an array to [`.removeAll`][removeAll].
```js
// Will remove documents with ids 1, 2, and 3 from the collection.
chat.removeAll([1, 2, 3])
```
As with the other functions, you can chain `.subscribe` onto the remove functions and provide response and error handlers.
### Watching for changes
We can also "listen" to an entire collection, query, or a single document by using [`.watch`][watch].
This is very convenient for building apps that want to update state immediately as data changes
in the database. Here are a few variations of how you can use [`.watch`][watch]:
```js
// Watch all documents, if any of them change, call the handler function.
chat.watch().subscribe((docs) => { console.log(docs) })
// Query all documents and sort them in ascending order by datetime,
// then if any of them change, the handler function is called.
chat.order("datetime").watch().subscribe((docs) => { console.log(docs) })
// Find a single document in the collection, if it changes, call the handler function
chat.find({author: "@dalanmiller"}).watch().subscribe((doc) => { console.log(doc) })
```
By default, the handler you pass to `.subscribe` chained on [`.watch`][watch] will receive
the entire collection of documents when one of them changes. This makes it easy when
using frameworks such as [Vue](https://vuejs.org/) or [React](https://facebook.github.io/react/)
allowing you to replace the current state with the new array given to you by Horizon.
```js
// Our current state of chat messages
let chats = [];
// Query chats with `.order` which by default
// is in ascending order.
chat.order("datetime").watch().subscribe(
// Returns the entire array
(newChats) => {
// Here we replace the old value of `chats` with the new
// array. Frameworks such as React will re-render based
// on the new values inserted into the array. Preventing you
// from having to do modifications on the original array.
//
// In short, it's this easy! :cool:
chats = newChats;
},
(err) => {
console.log(err);
})
```
To 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) ➡️.
## Putting it all together
Now that we have the basics covered, let's pretend we are building a
simple chat application where the messages are displayed
in ascending order. Here are some basic functions that would allow
you to build such an app.
```js
let chats = [];
// Retrieve all messages from the server
const retrieveMessages = () => {
chat.order('datetime')
// fetch all results as an array
.fetch()
// Retrieval successful, update our model
.subscribe((newChats) => {
chats = chats.concat(newChats);
},
// Error handler
error => console.log(error),
// onCompleted handler
() => console.log('All results received!')
)
};
// Retrieve an single item by id
const retrieveMessage = id => {
chat.find(id).fetch()
// Retrieval successful
.subscribe(result => {
chats.push(result);
},
// Error occurred
error => console.log(error))
};
// Store new item
const storeMessage = (message) => {
chat.store(message)
.subscribe(
// Returns id of saved objects
result => console.log(result),
// Returns server error message
error => console.log(error)
// called when store is complete
() => console.log('completed store')
)
};
// Replace item that has equal `id` field
// or insert if it doesn't exist.
const updateMessage = message => {
chat.replace(message);
};
// Remove item from collection
const deleteMessage = message => {
chat.remove(message);
};
```
And 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.
```js
chat.watch().subscribe(chats => {
// Each time through it will returns all results of your query
renderChats(allChats)
},
// When error occurs on server
error => console.log(error)
)
```
You can also get notifications when the client connects and disconnects from the server
``` js
// Triggers when client successfully connects to server
horizon.onReady().subscribe(() => console.log("Connected to Horizon Server"))
// Triggers when disconnected from server
horizon.onDisconnected().subscribe(() => console.log("Disconnected from Horizon Server"))
```
From here, you could take any framework and add these functions to create a realtime chat application
without writing a single line of backend code.
There's also plenty of other functions in the Horizon Client library to meet your needs, including:
[above][above], [below][below], [limit][limit], [replace][replace], and [upsert][upsert].
## Bringing your app to Horizon
We expect many people to already have an application in place but want to leverage
the power of Horizon for their realtime data. Here are a few scenarios that will
be relevant to you:
### Do I need to output all my files into the `dist` folder?
The short and long answer is, **_no_**.
If you are already using some other process to serve your static files, you absolutely
do 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:
1. Use `horizon.js` served by Horizon Server (simplest option)
1. Install `@horizon/client` as a dependency in your project
We recommend using the `horizon.js` library as served by Horizon Server for solely the
reason that there will be no mismatches between your client library version and your
current running version of Horizon Server.
This means somewhere in your application, you'll need to have:
```html
<script src="localhost:8181/horizon/horizon.js"></script>
```
And then when you init the Horizon connection you need to specify the `host` property:
```js
const horizon = Horizon({host: 'localhost:8181'});
```
However, 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.
Just remember that when you make connections to Horizon Server to specify the port number (which is by default `8181`) when connecting.
> **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).
### How do I add Horizon to X?
If 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.
---
## Example Applications
To show how Horizon fits with your framework of choice, we've put together a handful of
example applications to help you get started.
<img src="https://i.imgur.com/XFostB8.gif" align="right" width="450px">
* [Horizon Repo Examples Directory](https://github.com/rethinkdb/horizon/tree/next/examples)
* [CycleJS Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/cyclejs-chat-app)
* [RiotJS Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/riotjs-chat-app)
* [React Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/react-chat-app)
* [React TodoMVC App](https://github.com/rethinkdb/horizon/tree/next/examples/react-todo-app)
* [Vue Chat App](https://github.com/rethinkdb/horizon/tree/next/examples/vue-chat-app)
* [Vue TodoMVC App](https://github.com/rethinkdb/horizon/tree/next/examples/vue-todo-app)
## Extending Horizon Server
We also have a few examples of how you can extend Horizon Server. We imagine that once your application
grows beyond the needs of simply providing the Horizon Client API, you'll want to expand and build upon
Horizon Server. Here are a few examples of how to extend Horizon Server with some popular Node web frameworks.
* [Extending with Koa Server](https://github.com/rethinkdb/horizon/tree/next/examples/koa-server)
* [Extending with Hapi Server](https://github.com/rethinkdb/horizon/tree/next/examples/hapi-server)
* [Extending with Express Server](https://github.com/rethinkdb/horizon/tree/next/examples/express-server)
[above]: https://github.com/rethinkdb/horizon/tree/next/client#above-limit-integer--key-value-closed-string-
[below]: https://github.com/rethinkdb/horizon/tree/next/client#below-limit-integer--key-value-closed-string-
[Collection]: https://github.com/rethinkdb/horizon/tree/next/client#collection
[fetch]: https://github.com/rethinkdb/horizon/tree/next/client#fetch
[find]: https://github.com/rethinkdb/horizon/tree/next/client#find---id-any-
[findAll]: https://github.com/rethinkdb/horizon/tree/next/client#findall--id-any----id-any--
[Horizon]: https://github.com/rethinkdb/horizon/tree/next/client#horizon
[limit]: https://github.com/rethinkdb/horizon/tree/next/client#limit-num-integer-
[order]: https://github.com/rethinkdb/horizon/tree/next/client#order---directionascending-
[remove]: https://github.com/rethinkdb/horizon/tree/next/client#remove-id-any--id-any-
[removeAll]: https://github.com/rethinkdb/horizon/tree/next/client#removeall--id-any--id-any-----id-any---id-any---
[replace]: https://github.com/rethinkdb/horizon/tree/next/client#replace--
[store]: https://github.com/rethinkdb/horizon/tree/next/client#store-------
[store]: https://github.com/rethinkdb/horizon/tree/next/client#store-------
[upsert]: https://github.com/rethinkdb/horizon/tree/next/client#upsert------
[watch]: https://github.com/rethinkdb/horizon/tree/next/client#watch--rawchanges-false--
================================================
FILE: ISSUE_TEMPLATE.md
================================================
If you're reporting a bug please include the server version and client version.
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 RethinkDB, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
<img style="width:100%;" src="/github-banner.png">
# Horizon
[Official Repository](https://github.com/rethinkdb/horizon)
## What is Horizon?
Horizon is an open-source developer platform for building sophisticated realtime
apps. It provides a complete backend that makes it dramatically simpler to
build, deploy, manage, and scale engaging JavaScript web and mobile apps.
Horizon is extensible, integrates with the Node.js stack, and allows building
modern, arbitrarily complex applications.
Horizon is built on top of [RethinkDB](https://www.rethinkdb.com) and consists of
four components:
- [__Horizon server__](/server) -- a middleware server that connects to/is built on
top of RethinkDB, and exposes a simple API/protocol to front-end
applications.
- [__Horizon client library__](/client) -- a JavaScript client library that wraps
Horizon server's protocol in a convenient API for front-end
developers.
- [__Horizon CLI - `hz`__](/cli) -- a command-line tool aiding in scaffolding, development, and deployment
- [__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.
Horizon currently has all the following services available to developers:
- ✅ __Subscribe__ -- a streaming API for building realtime apps directly from the
browser without writing any backend code.
- ✅ __Auth__ -- an authentication API that connects to common auth providers
(e.g. Facebook, Google, GitHub).
- ✅ __Identity__ -- an API for listing and manipulating user accounts.
- ✅ __Permissions__ -- a security model that allows the developer to protect
data from unauthorized access.
Upcoming versions of Horizon will likely expose the following
additional services:
- __Session management__ -- manage browser session and session
information.
- __Geolocation__ -- an API that makes it very easy to build
location-aware apps.
- __Presence__ -- an API for detecting presence information for a given
user and sharing it with others.
- __Plugins__ -- a system for extending Horizon with user-defined services
in a consistent, discoverable way.
- __Backend__ -- an API/protocol to integrate custom backend code with
Horizon server/client-libraries.
## Why Horizon?
While technologies like [RethinkDB](http://www.rethinkdb.com) and
[WebSocket](https://en.wikipedia.org/wiki/WebSocket) make it possible to build
engaging realtime apps, empirically there is still too much friction for most
developers. Building realtime apps now requires understanding and manually
orchestrating multiple systems across the software stack, understanding
distributed stream processing, and learning how to deploy and scale realtime systems. The
learning curve is quite steep, and most of the initial work involves boilerplate
code that is far removed from the primary task of building a realtime app.
Horizon sets out to solve this problem. Developers can start building
apps using their favorite front-end framework using Horizon's APIs
without having to write any backend code.
Since Horizon stores data in RethinkDB, once the app gets sufficiently
complex to need custom business logic on the backend, developers can
incrementally add backend code at any time in the development cycle of
their app.
## Get Involved
We'd love for you to help us build Horizon. If you'd like to be a contributor,
check out our [Contributing guide](/CONTRIBUTING.md).
Also, to stay up-to-date on all Horizon related news and the community you should
definitely [join us on Slack](http://slack.rethinkdb.com) or [follow us on Twitter](https://twitter.com/horizonjs).

## FAQ
Check out our FAQ at [horizon.io/faq](https://horizon.io/faq/)
### How will Horizon be licensed?
The Horizon server, client and cli are available under the MIT license
================================================
FILE: circle.yml
================================================
## Customize the test machine
machine:
#timezone:
# America/Los_Angeles # Set the timezone
# Set version of node to use
#node:
# version:
# 5.7.0
post:
- source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list
- wget -qO- https://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add -
- 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"
- sudo apt-get install rethinkdb
## Set artifacts
# general:
# artifacts:
# - "client/npm-debug.log"
# - "server/npm-debug.log"
# - "cli/npm-debug.log"
## Customize dependencies
dependencies:
# Cache directories for speed
cache_directories:
- client/node_modules
- server/node_modules
- cli/node_modules
override:
# Stop default services
#- sudo service redis-server stop
#- sudo service postgresql stop
#- sudo service mysql stop
# Prepare for client tests
#- npm prune --production:
# pwd: client
# Prepare for server tests
#- npm prune --production:
# pwd: server
#- npm prune --production:
# pwd: cli
- ./setupDev.sh:
pwd: test
## Customize test commands
test:
pre:
- ./test/serve.js:
background: true
# - mkdir -p $CIRCLE_TEST_REPORTS/xunit
# - touch $CIRCLE_TEST_REPORTS/xunit/cli-tests.xml
# - touch $CIRCLE_TEST_REPORTS/xunit/client-tests.xml
# - touch $CIRCLE_TEST_REPORTS/xunit/server-tests.xml
override:
# Run client tests
- ./node_modules/.bin/mocha --timeout 100000 dist/test.js:
pwd: client
parallel: false
# Run server tests
- ./node_modules/.bin/mocha --timeout 100000 test/test.js test/schema.js:
pwd: server
parallel: false
# Run cli tests
- ./node_modules/.bin/mocha --timeout 100000 test:
pwd: cli
parallel: false
================================================
FILE: cli/.eslintrc.js
================================================
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
extends: "../.eslintrc.js",
rules: {
"max-len": [ ERROR, 100 ],
"no-invalid-this": [ OFF ]
},
env: {
"es6": true,
"node": true,
"mocha": true,
},
};
================================================
FILE: cli/.gitignore
================================================
# Coverage directory used by tools like istanbul
coverage
================================================
FILE: cli/README.md
================================================
# **Horizon** is a realtime, open-source backend for JavaScript apps.
Rapidly build and deploy web or mobile apps using a simple JavaScript API. Scale your apps to millions of users without any backend code.
Horizon consists of three components:
* Horizon server: a middleware server that connects to/is built on top of RethinkDB, and exposes a simple API/protocol to front-end applications.
* Horizon client: a JavaScript client library that wraps Horizon server’s protocol in a convenient API for front-end developers.
* Horizon CLI: a command line tool, hz, aiding in scaffolding, development, and deployment.
Built by the [RethinkDB](https://rethinkdb.com) team and an open-source community, Horizon lets you build sophisticated apps with lightning speed.
## Installing Horizon
https://horizon.io/install/
## Getting Started
https://horizon.io/docs/getting-started/
================================================
FILE: cli/package.json
================================================
{
"name": "horizon",
"version": "2.0.0",
"description": "An open-source developer platform for building realtime, scalable web apps.",
"main": "src/main.js",
"repository": {
"type": "git",
"url": "git+https://github.com/rethinkdb/horizon.git"
},
"scripts": {
"lint": "eslint src test",
"test": "mocha test test/unit --timeout 10000",
"coverage": "istanbul cover _mocha test"
},
"author": "RethinkDB",
"license": "MIT",
"bin": {
"hz": "src/main.js",
"horizon": "src/main.js"
},
"bugs": {
"url": "https://github.com/rethinkdb/horizon/issues"
},
"homepage": "https://github.com/rethinkdb/horizon#readme",
"dependencies": {
"@horizon/server": "2.0.0",
"argparse": "^1.0.3",
"bluebird": "^3.4.1",
"chalk": "^1.1.3",
"hasbin": "^1.2.1",
"joi": "^8.0.5",
"jsonwebtoken": "^5.5.4",
"mime-types": "^2.0.4",
"open": "7.0.4",
"rethinkdb": "^2.1.1",
"toml": "^2.3.0"
},
"devDependencies": {
"chai": "^3.5.0",
"eslint": "^7.3.1",
"istanbul": "^0.4.3",
"mocha": "2.4.5",
"mock-fs": "^3.10.0",
"sinon": "1.17.3",
"strip-ansi": "^3.0.1",
"toml": "^2.3.0"
},
"engines": {
"node": ">=4.0.0",
"npm": ">=3.0.0"
},
"preferGlobal": true
}
================================================
FILE: cli/src/create-cert.js
================================================
'use strict';
const hasbin = require('hasbin');
const spawn = require('child_process').spawn;
const run = (args) => {
if (args.length) {
throw new Error('create-cert takes no arguments');
}
// TODO: user configuration?
const settings = {
binaryName: 'openssl',
keyOutName: 'horizon-key.pem',
certOutName: 'horizon-cert.pem',
algo: 'rsa',
bits: '2048',
days: '365',
};
// generate the arguments to the command
const binArgs = [ 'req', '-x509', '-nodes', '-batch',
'-newkey', `${settings.algo}:${settings.bits}`,
'-keyout', settings.keyOutName,
'-out', settings.certOutName,
'-days', settings.days,
];
return new Promise((resolve, reject) => {
hasbin(settings.binaryName, (hasOpenSSL) => {
// show the invocation that's about to be run
console.log(`> ${settings.binaryName} ${binArgs.join(' ')}`);
// if we don't have openssl, bail
if (!hasOpenSSL) {
reject(new Error(`Missing ${settings.binaryName}. Make sure it is on the path.`));
}
// otherwise start openssl
const sslProc = spawn(settings.binaryName, binArgs);
// pipe output appropriately
sslProc.stdout.pipe(process.stdout, { end: false });
sslProc.stderr.pipe(process.stderr, { end: false });
// say nice things to the user when it's done
sslProc.on('error', reject);
sslProc.on('close', (code) => {
if (code) {
reject(new Error(`OpenSSL failed with code ${code}.`));
} else {
console.log('Everything seems to be fine. ' +
'Remember to add your shiny new certificates to your Horizon config!');
resolve();
}
});
});
});
};
module.exports = {
run,
description: 'Generate a certificate',
};
================================================
FILE: cli/src/init.js
================================================
/* global require, module */
'use strict';
const fs = require('fs');
const crypto = require('crypto');
const process = require('process');
const argparse = require('argparse');
const checkProjectName = require('./utils/check-project-name');
const rethrow = require('./utils/rethrow');
const makeIndexHTML = (projectName) => `\
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script src="/horizon/horizon.js"></script>
<script>
var horizon = Horizon();
horizon.onReady(function() {
document.querySelector('h1').innerHTML = '${projectName} works!'
});
horizon.connect();
</script>
</head>
<body>
<marquee direction="left"><h1></h1></marquee>
</body>
</html>
`;
const makeDefaultConfig = (projectName) => `\
# This is a TOML file
###############################################################################
# IP options
# 'bind' controls which local interfaces will be listened on
# 'port' controls which port will be listened on
#------------------------------------------------------------------------------
# bind = [ "localhost" ]
# port = 8181
###############################################################################
# HTTPS Options
# 'secure' will disable HTTPS and use HTTP instead when set to 'false'
# 'key_file' and 'cert_file' are required for serving HTTPS
#------------------------------------------------------------------------------
# secure = true
# key_file = "horizon-key.pem"
# cert_file = "horizon-cert.pem"
###############################################################################
# App Options
# 'project_name' sets the name of the RethinkDB database used to store the
# application state
# 'serve_static' will serve files from the given directory over HTTP/HTTPS
#------------------------------------------------------------------------------
project_name = "${projectName}"
# serve_static = "dist"
###############################################################################
# Data Options
# WARNING: these should probably not be enabled on a publically accessible
# service. Tables and indexes are not lightweight objects, and allowing them
# to be created like this could open the service up to denial-of-service
# attacks.
# 'auto_create_collection' creates a collection when one is needed but does not exist
# 'auto_create_index' creates an index when one is needed but does not exist
#------------------------------------------------------------------------------
# auto_create_collection = false
# auto_create_index = false
###############################################################################
# RethinkDB Options
# 'connect' and 'start_rethinkdb' are mutually exclusive
# 'connect' will connect to an existing RethinkDB instance
# 'start_rethinkdb' will run an internal RethinkDB instance
# 'rdb_timeout' is the number of seconds to wait when connecting to RethinkDB
#------------------------------------------------------------------------------
# connect = "localhost:28015"
# start_rethinkdb = false
# rdb_timeout = 30
###############################################################################
# Debug Options
# 'debug' enables debug log statements
#------------------------------------------------------------------------------
# debug = false
###############################################################################
# Authentication Options
# Each auth subsection will add an endpoint for authenticating through the
# specified provider.
# 'token_secret' is the key used to sign jwts
# 'allow_anonymous' issues new accounts to users without an auth provider
# 'allow_unauthenticated' allows connections that are not tied to a user id
# 'auth_redirect' specifies where users will be redirected to after login
# 'access_control_allow_origin' sets a host that can access auth settings
# (typically your frontend host)
#------------------------------------------------------------------------------
# allow_anonymous = false
# allow_unauthenticated = false
# auth_redirect = "/"
# access_control_allow_origin = ""
#
`;
const makeDefaultSchema = () => `\
[groups.admin]
[groups.admin.rules.carte_blanche]
template = "any()"
`;
const makeDefaultSecrets = () => `\
token_secret = "${crypto.randomBytes(64).toString('base64')}"
###############################################################################
# RethinkDB Options
# 'rdb_user' is the user account to log in with when connecting to RethinkDB
# 'rdb_password' is the password for the user account specified by 'rdb_user'
#------------------------------------------------------------------------------
# rdb_user = 'admin'
# rdb_password = ''
# [auth.auth0]
# host = "0000.00.auth0.com"
# id = "0000000000000000000000000"
# secret = "00000000000000000000000000000000000000000000000000"
#
# [auth.facebook]
# id = "000000000000000"
# secret = "00000000000000000000000000000000"
#
# [auth.google]
# id = "00000000000-00000000000000000000000000000000.apps.googleusercontent.com"
# secret = "000000000000000000000000"
#
# [auth.twitter]
# id = "0000000000000000000000000"
# secret = "00000000000000000000000000000000000000000000000000"
#
# [auth.github]
# id = "00000000000000000000"
# secret = "0000000000000000000000000000000000000000"
#
# [auth.twitch]
# id = "0000000000000000000000000000000"
# secret = "0000000000000000000000000000000"
#
# [auth.slack]
# id = "0000000000000000000000000000000"
# secret = "0000000000000000000000000000000"
`;
const gitignore = () => `\
rethinkdb_data
**/*.log
.hz/secrets.toml
node_modules
`;
const parseArguments = (args) => {
const parser = new argparse.ArgumentParser({ prog: 'hz init' });
parser.addArgument([ 'projectName' ],
{ action: 'store',
help: 'Name of directory to create. Defaults to current directory',
nargs: '?',
}
);
return parser.parseArgs(args);
};
const fileExists = (pathName) => {
try {
fs.statSync(pathName);
return true;
} catch (e) {
return false;
}
};
const maybeMakeDir = (createDir, dirName) => {
if (createDir) {
try {
fs.mkdirSync(dirName);
console.info(`Created new project directory ${dirName}`);
} catch (e) {
throw rethrow(e,
`Couldn't make directory ${dirName}: ${e.message}`);
}
} else {
console.info(`Initializing in existing directory ${dirName}`);
}
};
const maybeChdir = (chdirTo) => {
if (chdirTo) {
try {
process.chdir(chdirTo);
} catch (e) {
if (e.code === 'ENOTDIR') {
throw rethrow(e, `${chdirTo} is not a directory`);
} else {
throw rethrow(e, `Couldn't chdir to ${chdirTo}: ${e.message}`);
}
}
}
};
const populateDir = (projectName, dirWasPopulated, chdirTo, dirName) => {
const niceDir = chdirTo ? `${dirName}/` : '';
if (!dirWasPopulated && !fileExists('src')) {
fs.mkdirSync('src');
console.info(`Created ${niceDir}src directory`);
}
if (!dirWasPopulated && !fileExists('dist')) {
fs.mkdirSync('dist');
console.info(`Created ${niceDir}dist directory`);
fs.appendFileSync('./dist/index.html', makeIndexHTML(projectName));
console.info(`Created ${niceDir}dist/index.html example`);
}
if (!fileExists('.hz')) {
fs.mkdirSync('.hz');
console.info(`Created ${niceDir}.hz directory`);
}
// Default permissions
const permissionGeneral = {
encoding: 'utf8',
mode: 0o666,
};
const permissionSecret = {
encoding: 'utf8',
mode: 0o600, // Secrets are put in this config, so set it user, read/write only
};
// Create .gitignore if it doesn't exist
if (!fileExists('.gitignore')) {
fs.appendFileSync(
'.gitignore',
gitignore(),
permissionGeneral
);
console.info(`Created ${niceDir}.gitignore`);
} else {
console.info('.gitignore already exists, not touching it.');
}
// Create .hz/config.toml if it doesn't exist
if (!fileExists('.hz/config.toml')) {
fs.appendFileSync(
'.hz/config.toml',
makeDefaultConfig(projectName),
permissionGeneral
);
console.info(`Created ${niceDir}.hz/config.toml`);
} else {
console.info('.hz/config.toml already exists, not touching it.');
}
// Create .hz/schema.toml if it doesn't exist
if (!fileExists('.hz/schema.toml')) {
fs.appendFileSync(
'.hz/schema.toml',
makeDefaultSchema(),
permissionGeneral
);
console.info(`Created ${niceDir}.hz/schema.toml`);
} else {
console.info('.hz/schema.toml already exists, not touching it.');
}
// Create .hz/secrets.toml if it doesn't exist
if (!fileExists('.hz/secrets.toml')) {
fs.appendFileSync(
'.hz/secrets.toml',
makeDefaultSecrets(),
permissionSecret
);
console.info(`Created ${niceDir}.hz/secrets.toml`);
} else {
console.info('.hz/secrets.toml already exists, not touching it.');
}
};
const run = (args) =>
Promise.resolve(args)
.then(parseArguments)
.then((parsed) => {
const check = checkProjectName(
parsed.projectName,
process.cwd(),
fs.readdirSync('.')
);
const projectName = check.projectName;
const dirName = check.dirName;
const chdirTo = check.chdirTo;
const createDir = check.createDir;
maybeMakeDir(createDir, dirName);
maybeChdir(chdirTo);
// Before we create things, check if the directory is empty
const dirWasPopulated = fs.readdirSync(process.cwd()).length !== 0;
populateDir(projectName, dirWasPopulated, chdirTo, dirName);
});
module.exports = {
run,
description: 'Initialize a horizon app directory',
};
================================================
FILE: cli/src/main.js
================================================
#!/usr/bin/env node
'use strict';
// To support `pidof horizon`, by default it shows in `pidof node`
process.title = 'horizon';
const chalk = require('chalk');
const path = require('path');
const initCommand = require('./init');
const serveCommand = require('./serve');
const versionCommand = require('./version');
const createCertCommand = require('./create-cert');
const schemaCommand = require('./schema');
const makeTokenCommand = require('./make-token');
const migrateCommand = require('./migrate');
const NiceError = require('./utils/nice_error');
// Mapping from command line strings to modules. To add a new command,
// add an entry in this object, and create a module with the following
// exported:
// - run: main function for the command
// - description: a string to display in the hz help text
const commands = {
init: initCommand,
serve: serveCommand,
version: versionCommand,
'create-cert': createCertCommand,
'make-token': makeTokenCommand,
schema: schemaCommand,
migrate: migrateCommand,
};
const programName = path.basename(process.argv[1]);
const help = () => {
console.log(`Usage: ${programName} subcommand [args...]`);
console.log('Available subcommands:');
Object.keys(commands).forEach((cmdName) =>
console.log(` ${cmdName} - ${commands[cmdName].description}`)
);
};
const allArgs = process.argv.slice(2);
if (allArgs.length === 0) {
help();
process.exit(1);
}
const cmdName = allArgs[0];
const cmdArgs = allArgs.slice(1);
if (cmdName === '-h' || cmdName === '--help' || cmdName === 'help') {
help();
process.exit(0);
}
const command = commands[cmdName];
if (!command) {
console.error(chalk.red.bold(
`No such subcommand ${cmdName}, run with -h for help`));
process.exit(1);
}
const done = (err) => {
if (err) {
const errMsg = (err instanceof NiceError) ?
err.niceString({ contextSize: 2 }) : err.message;
console.error(chalk.red.bold(errMsg));
process.exit(1);
} else {
process.exit(0);
}
};
try {
command.run(cmdArgs).then(() => done()).catch(done);
} catch (err) {
done(err);
}
================================================
FILE: cli/src/make-token.js
================================================
'use strict';
const interrupt = require('./utils/interrupt');
const config = require('./utils/config');
const horizon_server = require('@horizon/server');
const path = require('path');
const jwt = require('jsonwebtoken');
const r = horizon_server.r;
const logger = horizon_server.logger;
const argparse = require('argparse');
const parseArguments = (args) => {
const parser = new argparse.ArgumentParser({ prog: 'hz make-token' });
parser.addArgument(
[ '--token-secret' ],
{ type: 'string', metavar: 'SECRET',
help: 'Secret key for signing the token.' });
parser.addArgument(
[ 'user' ],
{ type: 'string', metavar: 'USER_ID',
help: 'The ID of the user to issue a token for.' });
return parser.parseArgs(args);
};
const processConfig = (parsed) => {
let options;
options = config.default_options();
options = config.merge_options(
options, config.read_from_config_file(parsed.project_path));
options = config.merge_options(
options, config.read_from_secrets_file(parsed.project_path));
options = config.merge_options(options, config.read_from_env());
options = config.merge_options(options, config.read_from_flags(parsed));
if (options.project_name === null) {
options.project_name = path.basename(path.resolve(options.project_path));
}
return Object.assign(options, { user: parsed.user });
};
const run = (args) => Promise.resolve().then(() => {
const options = processConfig(parseArguments(args));
if (options.token_secret === null) {
throw new Error('No token secret specified, unable to sign the token.');
}
const token = jwt.sign(
{ id: options.user, provider: null },
new Buffer(options.token_secret, 'base64'),
{ expiresIn: '1d', algorithm: 'HS512' }
);
console.log(`${token}`);
});
module.exports = {
run,
description: 'Generate a token to log in as a user',
};
================================================
FILE: cli/src/migrate.js
================================================
'use strict';
const chalk = require('chalk');
const r = require('rethinkdb');
const Promise = require('bluebird');
const argparse = require('argparse');
const runSaveCommand = require('./schema').runSaveCommand;
const fs = require('fs');
const accessAsync = Promise.promisify(fs.access);
const config = require('./utils/config');
const procPromise = require('./utils/proc-promise');
const interrupt = require('./utils/interrupt');
const change_to_project_dir = require('./utils/change_to_project_dir');
const parse_yes_no_option = require('./utils/parse_yes_no_option');
const start_rdb_server = require('./utils/start_rdb_server');
const NiceError = require('./utils/nice_error.js');
const VERSION_2_0 = [ 2, 0, 0 ];
function run(cmdArgs) {
const options = processConfig(cmdArgs);
interrupt.on_interrupt(() => teardown());
return Promise.resolve().bind({ options })
.then(setup)
.then(validateMigration)
.then(makeBackup)
.then(renameUserTables)
.then(moveInternalTables)
.then(renameIndices)
.then(rewriteHzCollectionDocs)
.then(exportNewSchema)
.finally(teardown);
}
function green() {
const args = Array.from(arguments);
args[0] = chalk.green(args[0]);
console.log.apply(console, args);
}
function white() {
const args = Array.from(arguments);
args[0] = chalk.white(args[0]);
console.log.apply(console, args);
}
function processConfig(cmdArgs) {
// do boilerplate to get config args :/
const parser = new argparse.ArgumentParser({ prog: 'hz migrate' });
parser.addArgument([ 'project_path' ], {
default: '.',
nargs: '?',
help: 'Change to this directory before migrating',
});
parser.addArgument([ '--project-name', '-n' ], {
help: 'Name of the Horizon project server',
});
parser.addArgument([ '--connect', '-c' ], {
metavar: 'host:port',
default: undefined,
help: 'Host and port of the RethinkDB server to connect to.',
});
parser.addArgument([ '--rdb-user' ], {
default: 'admin',
metavar: 'USER',
help: 'RethinkDB User',
});
parser.addArgument([ '--rdb-password' ], {
default: undefined,
metavar: 'PASSWORD',
help: 'RethinkDB Password',
});
parser.addArgument([ '--start-rethinkdb' ], {
metavar: 'yes|no',
default: 'yes',
constant: 'yes',
nargs: '?',
help: 'Start up a RethinkDB server in the current directory',
});
parser.addArgument([ '--skip-backup' ], {
metavar: 'yes|no',
default: 'no',
constant: 'yes',
nargs: '?',
help: 'Whether to perform a backup of rethinkdb_data' +
' before migrating',
});
parser.addArgument([ '--nonportable-backup' ], {
metavar: 'yes|no',
default: 'no',
constant: 'yes',
nargs: '?',
help: 'Allows creating a backup that is not portable, ' +
"but doesn't require the RethinkDB Python driver to be " +
'installed.',
});
const parsed = parser.parseArgs(cmdArgs);
const confOptions = config.read_from_config_file(parsed.project_path);
const envOptions = config.read_from_env();
config.merge_options(confOptions, envOptions);
// Pull out the relevant settings from the config file
const options = {
project_path: parsed.project_path || '.',
project_name: parsed.project_name || confOptions.project_name,
rdb_host: parsed.rdb_host || confOptions.rdb_host || 'localhost',
rdb_port: parsed.rdb_port || confOptions.rdb_port || 28015,
rdb_user: parsed.rdb_user || confOptions.rdb_user || 'admin',
rdb_password: parsed.rdb_password || confOptions.rdb_password || '',
start_rethinkdb: parse_yes_no_option(parsed.start_rethinkdb),
skip_backup: parse_yes_no_option(parsed.skip_backup),
nonportable_backup: parse_yes_no_option(parsed.nonportable_backup),
};
// sets rdb_host and rdb_port from connect if necessary
if (parsed.connect) {
config.parse_connect(parsed.connect, options);
}
if (options.project_name == null) {
throw new NiceError('No project_name given', {
description: `\
The project_name is needed to migrate from the v1.x format the v.2.0 format. \
It wasn't passed on the command line or found in your config.`,
suggestions: [
'pass the --project-name option to hz migrate',
'add the "project_name" key to your .hz/config.toml',
] });
}
return options;
}
function setup() {
// Start rethinkdb server if necessary
// Connect to whatever rethinkdb server we're using
white('Setup');
return Promise.resolve().then(() => {
if (this.options.project_path && this.options.project_path !== '.') {
green(` ├── Changing to directory ${this.options.project_path}`);
change_to_project_dir(this.options.project_path);
}
}).then(() => {
// start rethinkdb server if necessary
if (this.options.start_rethinkdb) {
green(' ├── Starting RethinkDB server');
return start_rdb_server({ quiet: true }).then((server) => {
this.rdb_server = server;
this.options.rdb_host = 'localhost';
this.options.rdb_port = server.driver_port;
});
}
}).then(() => {
green(' ├── Connecting to RethinkDB');
return r.connect({
host: this.options.rdb_host,
port: this.options.rdb_port,
user: this.options.rdb_user,
password: this.options.rdb_password,
});
}).then((conn) => {
green(' └── Successfully connected');
this.conn = conn;
});
}
function teardown() {
return Promise.resolve().then(() => {
white('Cleaning up...');
// close the rethinkdb connection
if (this.conn) {
green(' ├── Closing rethinkdb connection');
return this.conn.close();
}
}).then(() => {
// shut down the rethinkdb server if we started it
if (this.rdb_server) {
green(' └── Shutting down rethinkdb server');
return this.rdb_server.close();
}
});
}
function validateMigration() {
// check that `${project}_internal` exists
const project = this.options.project_name;
const internalNotFound = `Database named '${project}_internal' wasn't found`;
const tablesHaveHzPrefix = `Some tables in ${project} have an hz_ prefix`;
const checkForHzTables = r.db('rethinkdb')
.table('table_config')
.filter({ db: project })('name')
.contains((x) => x.match('^hz_'))
.branch(r.error(tablesHaveHzPrefix), true);
const waitForCollections = r.db(`${project}_internal`)
.table('collections')
.wait({ timeout: 30 })
.do(() => r.db(project).tableList())
.forEach((tableName) =>
r.db(project).table(tableName).wait({ timeout: 30 })
);
return Promise.resolve().then(() => {
white('Validating current schema version');
return r.dbList().contains(`${project}_internal`)
.branch(true, r.error(internalNotFound))
.do(() => checkForHzTables)
.do(() => waitForCollections)
.run(this.conn)
.then(() => green(' └── Pre-2.0 schema found'))
.catch((e) => {
if (e.msg === internalNotFound) {
throw new NiceError(e.msg, {
description: `\
This could happen if you don't have a Horizon app in this database, or if \
you've already migrated this database to the v2.0 format.`,
});
} else if (e.msg === tablesHaveHzPrefix) {
throw new NiceError(e.msg, {
description: `This could happen if you've already migrated \
this database to the v2.0 format.`,
});
} else {
throw e;
}
});
});
}
function makeBackup() {
// shell out to rethinkdb dump
const rdbHost = this.options.rdb_host;
const rdbPort = this.options.rdb_port;
if (this.options.skip_backup) {
return Promise.resolve();
}
white('Backing up rethinkdb_data directory');
if (this.options.nonportable_backup) {
return nonportableBackup();
}
return procPromise('rethinkdb', [
'dump',
'--connect',
`${rdbHost}:${rdbPort}`,
]).then(() => {
green(' └── Backup completed');
}).catch((e) => {
if (e.message.match(/Python driver/)) {
throw new NiceError('The RethinkDB Python driver is not installed.', {
description: `Before we migrate to the v2.0 format, we should do a \
backup of your RethinkDB database in case anything goes wrong. Unfortunately, \
we can't use the rethinkdb dump command to do a backup because you don't have \
the RethinkDB Python driver installed on your system.`,
suggestions: [
`Install the Python driver with the instructions found at: \
http://www.rethinkdb.com/docs/install-drivers/python/`,
`Pass the --nonportable-backup flag to hz migrate. This flag uses \
the tar command to make a backup, but the backup is not safe to use on \
another machine or to create replicas from. This option should not be used \
if RethinkDB is currently running. It should also not be used if the \
rethinkdb_data/ directory is not in the current directory.`,
] });
} else {
throw e;
}
});
}
function nonportableBackup() {
// Uses tar to do an unsafe backup
const timestamp = new Date().toISOString().replace(/:/g, '_');
return procPromise('tar', [
'-zcvf', // gzip, compress, verbose, filename is...
`rethinkdb_data.nonportable-backup.${timestamp}.tar.gz`,
'rethinkdb_data', // directory to back up
]).then(() => {
green(' └── Nonportable backup completed');
});
}
function renameUserTables() {
// for each table listed in ${project}_internal.collections
// rename the table name to the collection name
const project = this.options.project_name;
return Promise.resolve().then(() => {
white('Removing suffix from user tables');
return r.db(`${project}_internal`).wait({ timeout: 30 }).
do(() => r.db(`${project}_internal`).table('collections')
.forEach((collDoc) => r.db('rethinkdb').table('table_config')
.filter({ db: project, name: collDoc('table') })
.update({ name: collDoc('id') }))
).run(this.conn)
.then(() => green(' └── Suffixes removed'));
});
}
function moveInternalTables() {
// find project_internal
// move all tables from ${project}_internal.${table} to ${project}.hz_${table}
// - except for users, don't add hz_prefix, but move its db
const project = this.options.project_name;
return Promise.resolve().then(() => {
white(`Moving internal tables from ${project}_internal to ${project}`);
return r.db('rethinkdb').table('table_config')
.filter({ db: `${project}_internal` })
.update((table) => ({
db: project,
name: r.branch(
table('name').ne('users'),
r('hz_').add(table('name')),
'users'),
})).run(this.conn)
.then(() => green(' ├── Internal tables moved'));
}).then(() => {
// delete project_internal
green(` └── Deleting empty "${project}_internal" database`);
return r.dbDrop(`${project}_internal`).run(this.conn);
});
}
function renameIndices() {
// for each user $table in ${project}
// for each index in ${table}
// parse the old name into array of field names.
// rename to `hz_${JSON.stringify(fields)}`
const project = this.options.project_name;
return Promise.resolve().then(() => {
white('Renaming indices to new JSON format');
return r.db(project).tableList().forEach((tableName) =>
r.db(project).table(tableName).indexList().forEach((indexName) =>
r.db(project).table(tableName)
.indexRename(indexName, rename(indexName))
)
).run(this.conn)
.then(() => green(' └── Indices renamed.'));
});
function rename(name) {
// ReQL to rename the index name to the new format
const initialState = {
escaped: false,
field: '',
fields: [ ],
};
return name.split('')
.fold(initialState, (acc, c) =>
r.branch(
acc('escaped'),
acc.merge({
escaped: false,
field: acc('field').add(c),
}),
c.eq('\\'),
acc.merge({ escaped: true }),
c.eq('_'),
acc.merge({
fields: acc('fields').append(acc('field')),
field: '',
}),
acc.merge({ field: acc('field').add(c) })
)
).do((state) =>
// last field needs to be appended to running list
state('fields').append(state('field'))
// wrap each field in an array
.map((field) => [ field ])
)
.toJSON()
.do((x) => r('hz_').add(x));
}
}
function rewriteHzCollectionDocs() {
// for each document in ${project}.hz_collections
// delete the table field
const project = this.options.project_name;
return Promise.resolve().then(() => {
white('Rewriting hz_collections to new format');
return r.db(project).table('hz_collections')
.update({ table: r.literal() })
.run(this.conn);
}).then(() => green(' ├── "table" field removed'))
.then(() => r.db(project).table('hz_collections')
.insert({ id: 'users' })
.run(this.conn))
.then(() => green(' ├── Added document for "users" table'))
.then(() => r.db(project).table('hz_collections')
.insert({ id: 'hz_metadata', version: VERSION_2_0 })
.run(this.conn))
.then(() => green(' └── Adding the metadata document with schema version:' +
`${JSON.stringify(VERSION_2_0)}`));
}
function exportNewSchema() {
// Import and run schema save process, giving it a different
// filename than schema.toml
const timestamp = new Date().toISOString().replace(/:/g, '_');
return accessAsync('.hz/schema.toml', fs.R_OK | fs.F_OK)
.then(() => `.hz/schema.toml.migrated.${timestamp}`)
.catch(() => '.hz/schema.toml') // if no schema.toml
.then((schemaFile) => {
white(`Exporting the new schema to ${schemaFile}`);
return runSaveCommand({
rdb_host: this.options.rdb_host,
rdb_port: this.options.rdb_port,
rdb_user: this.options.rdb_user,
rdb_password: this.options.rdb_password,
out_file: schemaFile,
project_name: this.options.project_name,
});
}).then(() => green(' └── Schema exported'));
}
module.exports = {
run,
description: 'migrate an older version of horizon to a newer one',
};
================================================
FILE: cli/src/schema.js
================================================
'use strict';
const horizon_server = require('@horizon/server');
const horizon_index = require('@horizon/server/src/metadata/index');
const horizon_metadata = require('@horizon/server/src/metadata/metadata');
const config = require('./utils/config');
const interrupt = require('./utils/interrupt');
const start_rdb_server = require('./utils/start_rdb_server');
const parse_yes_no_option = require('./utils/parse_yes_no_option');
const change_to_project_dir = require('./utils/change_to_project_dir');
const initialize_joi = require('./utils/initialize_joi');
const fs = require('fs');
const Joi = require('joi');
const path = require('path');
const argparse = require('argparse');
const toml = require('toml');
const r = horizon_server.r;
const create_collection = horizon_metadata.create_collection;
const initialize_metadata = horizon_metadata.initialize_metadata;
initialize_joi(Joi);
const parseArguments = (args) => {
const parser = new argparse.ArgumentParser({ prog: 'hz schema' });
const subparsers = parser.addSubparsers({
title: 'subcommands',
dest: 'subcommand_name',
});
const apply = subparsers.addParser('apply', { addHelp: true });
const save = subparsers.addParser('save', { addHelp: true });
// Set options shared between both subcommands
[ apply, save ].map((subcmd) => {
subcmd.addArgument([ 'project_path' ],
{ type: 'string', nargs: '?',
help: 'Change to this directory before serving' });
subcmd.addArgument([ '--project-name', '-n' ],
{ type: 'string', action: 'store', metavar: 'NAME',
help: 'Name of the Horizon Project server' });
subcmd.addArgument([ '--connect', '-c' ],
{ type: 'string', metavar: 'HOST:PORT',
help: 'Host and port of the RethinkDB server to connect to.' });
subcmd.addArgument([ '--rdb-timeout' ],
{ type: 'int', metavar: 'TIMEOUT',
help: 'Timeout period in seconds for the RethinkDB connection to be opened' });
subcmd.addArgument([ '--rdb-user' ],
{ type: 'string', metavar: 'USER',
help: 'RethinkDB User' });
subcmd.addArgument([ '--rdb-password' ],
{ type: 'string', metavar: 'PASSWORD',
help: 'RethinkDB Password' });
subcmd.addArgument([ '--start-rethinkdb' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Start up a RethinkDB server in the current directory' });
subcmd.addArgument([ '--debug' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Enable debug logging.' });
});
// Options exclusive to HZ SCHEMA APPLY
apply.addArgument([ '--update' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Only add new items and update existing, no removal.' });
apply.addArgument([ '--force' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Allow removal of existing collections.' });
apply.addArgument([ 'schema_file' ],
{ type: 'string', metavar: 'SCHEMA_FILE_PATH',
help: 'File to get the horizon schema from, use "-" for stdin.' });
// Options exclusive to HZ SCHEMA SAVE
save.addArgument([ '--out-file', '-o' ],
{ type: 'string', metavar: 'PATH', defaultValue: '.hz/schema.toml',
help: 'File to write the horizon schema to, defaults to .hz/schema.toml.' });
return parser.parseArgs(args);
};
const schema_schema = Joi.object().unknown(false).keys({
collections: Joi.object().unknown(true).pattern(/.*/,
Joi.object().unknown(false).keys({
indexes: Joi.array().items(
Joi.alternatives(
Joi.string(),
Joi.object().unknown(false).keys({
fields: Joi.array().items(Joi.array().items(Joi.string())).required(),
})
)
).optional().default([ ]),
})
).optional().default({ }),
groups: Joi.object().unknown(true).pattern(/.*/,
Joi.object().keys({
rules: Joi.object().unknown(true).pattern(/.*/,
Joi.object().unknown(false).keys({
template: Joi.string().required(),
validator: Joi.string().optional(),
})
).optional().default({ }),
})
).optional().default({ }),
});
// Preserved for interpreting old schemas
const v1_0_name_to_fields = (name) => {
let escaped = false;
let field = '';
const fields = [ ];
for (const c of name) {
if (escaped) {
if (c !== '\\' && c !== '_') {
throw new Error(`Unexpected index name: "${name}"`);
}
escaped = false;
field += c;
} else if (c === '\\') {
escaped = true;
} else if (c === '_') {
fields.push(field);
field = '';
} else {
field += c;
}
}
if (escaped) {
throw new Error(`Unexpected index name: "${name}"`);
}
fields.push([ field ]);
return fields;
};
const parse_schema = (schema_toml) => {
const parsed = Joi.validate(toml.parse(schema_toml), schema_schema);
const schema = parsed.value;
if (parsed.error) {
throw parsed.error;
}
const collections = [ ];
for (const name in schema.collections) {
collections.push({
id: name,
indexes: schema.collections[name].indexes.map((index) => {
if (typeof index === 'string') {
return { fields: v1_0_name_to_fields(index), multi: false, geo: false };
} else {
return { fields: index.fields, multi: false, geo: false };
}
}),
});
}
// Make sure the 'users' collection is present, as some things depend on
// its existence.
if (!schema.collections || !schema.collections.users) {
collections.push({ id: 'users', indexes: [ ] });
}
const groups = [ ];
for (const name in schema.groups) {
groups.push(Object.assign({ id: name }, schema.groups[name]));
}
return { groups, collections };
};
const processApplyConfig = (parsed) => {
let options, in_file;
options = config.default_options();
options = config.merge_options(options,
config.read_from_config_file(parsed.project_path));
options = config.merge_options(options, config.read_from_env());
options = config.merge_options(options, config.read_from_flags(parsed));
if (parsed.schema_file === '-') {
in_file = process.stdin;
} else {
in_file = fs.createReadStream(parsed.schema_file, { flags: 'r' });
}
if (options.project_name === null) {
options.project_name = path.basename(path.resolve(options.project_path));
}
return {
subcommand_name: 'apply',
start_rethinkdb: options.start_rethinkdb,
rdb_host: options.rdb_host,
rdb_port: options.rdb_port,
rdb_user: options.rdb_user || undefined,
rdb_password: options.rdb_password || undefined,
project_name: options.project_name,
project_path: options.project_path,
debug: options.debug,
update: parse_yes_no_option(parsed.update),
force: parse_yes_no_option(parsed.force),
in_file,
};
};
const processSaveConfig = (parsed) => {
let options, out_file;
options = config.default_options();
options.start_rethinkdb = true;
options = config.merge_options(options,
config.read_from_config_file(parsed.project_path));
options = config.merge_options(options, config.read_from_env());
options = config.merge_options(options, config.read_from_flags(parsed));
if (parsed.out_file === '-') {
out_file = process.stdout;
} else {
out_file = parsed.out_file;
}
if (options.project_name === null) {
options.project_name = path.basename(path.resolve(options.project_path));
}
return {
subcommand_name: 'save',
start_rethinkdb: options.start_rethinkdb,
rdb_host: options.rdb_host,
rdb_port: options.rdb_port,
rdb_user: options.rdb_user || undefined,
rdb_password: options.rdb_password || undefined,
project_name: options.project_name,
project_path: options.project_path,
debug: options.debug,
out_file,
};
};
const schema_to_toml = (collections, groups) => {
const res = [ '# This is a TOML document' ];
for (const c of collections) {
res.push('');
res.push(`[collections.${c.id}]`);
c.indexes.forEach((index) => {
const info = horizon_index.name_to_info(index);
res.push(`[[collections.${c.id}.indexes]]`);
res.push(`fields = ${JSON.stringify(info.fields)}`);
});
}
for (const g of groups) {
res.push('');
res.push(`[groups.${g.id}]`);
if (g.rules) {
for (const key in g.rules) {
const template = g.rules[key].template;
const validator = g.rules[key].validator;
res.push(`[groups.${g.id}.rules.${key}]`);
res.push(`template = ${JSON.stringify(template)}`);
if (validator) {
res.push(`validator = ${JSON.stringify(validator)}`);
}
}
}
}
res.push('');
return res.join('\n');
};
const runApplyCommand = (options) => {
let conn, schema, rdb_server;
let obsolete_collections = [ ];
const db = options.project_name;
const cleanup = () =>
Promise.all([
conn ? conn.close() : Promise.resolve(),
rdb_server ? rdb_server.close() : Promise.resolve(),
]);
interrupt.on_interrupt(() => cleanup());
return Promise.resolve().then(() => {
if (options.start_rethinkdb) {
change_to_project_dir(options.project_path);
}
return new Promise((resolve, reject) => {
let schema_toml = '';
options.in_file.on('data', (buffer) => (schema_toml += buffer));
options.in_file.on('end', () => resolve(schema_toml));
options.in_file.on('error', reject);
});
}).then((schema_toml) => {
schema = parse_schema(schema_toml);
if (options.start_rethinkdb) {
return start_rdb_server({ quiet: !options.debug }).then((server) => {
rdb_server = server;
options.rdb_host = 'localhost';
options.rdb_port = server.driver_port;
});
}
}).then(() =>
r.connect({ host: options.rdb_host,
port: options.rdb_port,
user: options.rdb_user,
password: options.rdb_password,
timeout: options.rdb_timeout })
).then((rdb_conn) => {
conn = rdb_conn;
return initialize_metadata(db, conn);
}).then((initialization_result) => {
if (initialization_result.tables_created) {
console.log('Initialized new application metadata.');
}
// Wait for metadata tables to be writable
return r.expr([ 'hz_collections', 'hz_groups' ])
.forEach((table) =>
r.db(db).table(table)
.wait({ waitFor: 'ready_for_writes', timeout: 30 }))
.run(conn);
}).then(() => {
// Error if any collections will be removed
if (!options.update) {
return r.db(db).table('hz_collections')
.filter((row) => row('id').match('^hz_').not())
.getField('id')
.coerceTo('array')
.setDifference(schema.collections.map((c) => c.id))
.run(conn)
.then((res) => {
if (!options.force && res.length > 0) {
throw new Error('Run with "--force" to continue.\n' +
'These collections would be removed along with their data:\n' +
`${res.join(', ')}`);
}
obsolete_collections = res;
});
}
}).then(() => {
if (options.update) {
// Update groups
return Promise.all(schema.groups.map((group) => {
const literal_group = JSON.parse(JSON.stringify(group));
Object.keys(literal_group.rules).forEach((key) => {
literal_group.rules[key] = r.literal(literal_group.rules[key]);
});
return r.db(db).table('hz_groups')
.get(group.id).replace((old_row) =>
r.branch(old_row.eq(null),
group,
old_row.merge(literal_group)))
.run(conn).then((res) => {
if (res.errors) {
throw new Error(`Failed to update group: ${res.first_error}`);
}
});
}));
} else {
// Replace and remove groups
const groups_obj = { };
schema.groups.forEach((g) => { groups_obj[g.id] = g; });
return Promise.all([
r.expr(groups_obj).do((groups) =>
r.db(db).table('hz_groups')
.replace((old_row) =>
r.branch(groups.hasFields(old_row('id')),
old_row,
null))
).run(conn).then((res) => {
if (res.errors) {
throw new Error(`Failed to write groups: ${res.first_error}`);
}
}),
r.db(db).table('hz_groups')
.insert(schema.groups, { conflict: 'replace' })
.run(conn).then((res) => {
if (res.errors) {
throw new Error(`Failed to write groups: ${res.first_error}`);
}
}),
]);
}
}).then(() => {
// Ensure all collections exist and remove any obsolete collections
const promises = [ ];
for (const c of schema.collections) {
promises.push(
create_collection(db, c.id, conn).then((res) => {
if (res.error) {
throw new Error(res.error);
}
}));
}
for (const c of obsolete_collections) {
promises.push(
r.db(db)
.table('hz_collections')
.get(c)
.delete({ returnChanges: 'always' })('changes')(0)
.do((res) =>
r.branch(res.hasFields('error'),
res,
res('old_val').eq(null),
res,
r.db(db).tableDrop(res('old_val')('id')).do(() => res)))
.run(conn).then((res) => {
if (res.error) {
throw new Error(res.error);
}
}));
}
return Promise.all(promises);
}).then(() => {
const promises = [ ];
// Ensure all indexes exist
for (const c of schema.collections) {
for (const info of c.indexes) {
const name = horizon_index.info_to_name(info);
promises.push(
r.branch(r.db(db).table(c.id).indexList().contains(name), { },
r.db(db).table(c.id).indexCreate(name, horizon_index.info_to_reql(info),
{ geo: Boolean(info.geo), multi: (info.multi !== false) }))
.run(conn)
.then((res) => {
if (res.errors) {
throw new Error(`Failed to create index ${name} ` +
`on collection ${c.id}: ${res.first_error}`);
}
}));
}
}
// Remove obsolete indexes
if (!options.update) {
for (const c of schema.collections) {
const names = c.indexes.map(horizon_index.info_to_name);
promises.push(
r.db(db).table(c.id).indexList().filter((name) => name.match('^hz_'))
.setDifference(names)
.forEach((name) => r.db(db).table(c.id).indexDrop(name))
.run(conn)
.then((res) => {
if (res.errors) {
throw new Error('Failed to remove old indexes ' +
`on collection ${c.id}: ${res.first_error}`);
}
}));
}
}
return Promise.all(promises);
}).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));
};
const file_exists = (filename) => {
try {
fs.accessSync(filename);
} catch (e) {
return false;
}
return true;
};
const runSaveCommand = (options) => {
let conn, rdb_server;
const db = options.project_name;
const cleanup = () =>
Promise.all([
conn ? conn.close() : Promise.resolve(),
rdb_server ? rdb_server.close() : Promise.resolve(),
]);
interrupt.on_interrupt(() => cleanup());
return Promise.resolve().then(() => {
if (options.start_rethinkdb) {
change_to_project_dir(options.project_path);
}
}).then(() => {
if (options.start_rethinkdb) {
return start_rdb_server({ quiet: !options.debug }).then((server) => {
rdb_server = server;
options.rdb_host = 'localhost';
options.rdb_port = server.driver_port;
});
}
}).then(() =>
r.connect({ host: options.rdb_host,
port: options.rdb_port,
user: options.rdb_user,
password: options.rdb_password,
timeout: options.rdb_timeout })
).then((rdb_conn) => {
conn = rdb_conn;
return r.db(db).wait({ waitFor: 'ready_for_reads', timeout: 30 }).run(conn);
}).then(() =>
r.object('collections',
r.db(db).table('hz_collections')
.filter((row) => row('id').match('^hz_').not())
.coerceTo('array')
.map((row) =>
row.merge({ indexes: r.db(db).table(row('id')).indexList() })),
'groups', r.db(db).table('hz_groups').coerceTo('array'))
.run(conn)
).then((res) =>
new Promise((resolve) => {
// Only rename old file if saving to default .hz/schema.toml
if (options.out_file === '.hz/schema.toml' &&
file_exists(options.out_file)) {
// Rename existing file to have the current time appended to its name
const oldPath = path.resolve(options.out_file);
const newPath = `${path.resolve(options.out_file)}.${new Date().toISOString()}`;
fs.renameSync(oldPath, newPath);
}
const output = (options.out_file === '-') ? process.stdout :
fs.createWriteStream(options.out_file, { flags: 'w', defaultEncoding: 'utf8' });
// Output toml_str to schema.toml
const toml_str = schema_to_toml(res.collections, res.groups);
output.end(toml_str, resolve);
})
).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));
};
const processConfig = (options) => {
// Determine if we are saving or applying and use appropriate config processing
switch (options.subcommand_name) {
case 'apply':
return processApplyConfig(options);
case 'save':
return processSaveConfig(options);
default:
throw new Error(`Unrecognized schema subcommand: "${options.subcommand_name}"`);
}
};
// Avoiding cyclical depdendencies
module.exports = {
run: (args) =>
Promise.resolve().then(() => {
const options = processConfig(parseArguments(args));
// Determine if we are saving or applying and use appropriate run function
switch (options.subcommand_name) {
case 'apply':
return runApplyCommand(options);
case 'save':
return runSaveCommand(options);
default:
throw new Error(`Unrecognized schema subcommand: "${options.subcommand_name}"`);
}
}),
description: 'Apply and save the schema from a horizon database',
processApplyConfig,
runApplyCommand,
runSaveCommand,
parse_schema,
};
================================================
FILE: cli/src/serve.js
================================================
'use strict';
const chalk = require('chalk');
const crypto = require('crypto');
const fs = require('fs');
const get_type = require('mime-types').contentType;
const http = require('http');
const https = require('https');
const open = require('open');
const path = require('path');
const argparse = require('argparse');
const url = require('url');
const config = require('./utils/config');
const start_rdb_server = require('./utils/start_rdb_server');
const change_to_project_dir = require('./utils/change_to_project_dir');
const NiceError = require('./utils/nice_error.js');
const interrupt = require('./utils/interrupt');
const schema = require('./schema');
const horizon_server = require('@horizon/server');
const logger = horizon_server.logger;
const TIMEOUT_30_SECONDS = 30 * 1000;
const default_rdb_host = 'localhost';
const default_rdb_port = 28015;
const default_rdb_timeout = 20;
const parseArguments = (args) => {
const parser = new argparse.ArgumentParser({ prog: 'hz serve' });
parser.addArgument([ 'project_path' ],
{ type: 'string', nargs: '?',
help: 'Change to this directory before serving' });
parser.addArgument([ '--project-name', '-n' ],
{ type: 'string', action: 'store', metavar: 'NAME',
help: 'Name of the Horizon project. Determines the name of ' +
'the RethinkDB database that stores the project data.' });
parser.addArgument([ '--bind', '-b' ],
{ type: 'string', action: 'append', metavar: 'HOST',
help: 'Local hostname to serve horizon on (repeatable).' });
parser.addArgument([ '--port', '-p' ],
{ type: 'int', metavar: 'PORT',
help: 'Local port to serve horizon on.' });
parser.addArgument([ '--connect', '-c' ],
{ type: 'string', metavar: 'HOST:PORT',
help: 'Host and port of the RethinkDB server to connect to.' });
parser.addArgument([ '--rdb-timeout' ],
{ type: 'int', metavar: 'TIMEOUT',
help: 'Timeout period in seconds for the RethinkDB connection to be opened' });
parser.addArgument([ '--rdb-user' ],
{ type: 'string', metavar: 'USER',
help: 'RethinkDB User' });
parser.addArgument([ '--rdb-password' ],
{ type: 'string', metavar: 'PASSWORD',
help: 'RethinkDB Password' });
parser.addArgument([ '--key-file' ],
{ type: 'string', metavar: 'PATH',
help: 'Path to the key file to use, defaults to "./horizon-key.pem".' });
parser.addArgument([ '--cert-file' ],
{ type: 'string', metavar: 'PATH',
help: 'Path to the cert file to use, defaults to "./horizon-cert.pem".' });
parser.addArgument([ '--token-secret' ],
{ type: 'string', metavar: 'SECRET',
help: 'Key for signing jwts' });
parser.addArgument([ '--allow-unauthenticated' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Whether to allow unauthenticated Horizon connections.' });
parser.addArgument([ '--allow-anonymous' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Whether to allow anonymous Horizon connections.' });
parser.addArgument([ '--max-connections' ],
{ type: 'int', metavar: 'MAX_CONNECTIONS',
help: 'Maximum number of simultaneous connections server will accept.' });
parser.addArgument([ '--debug' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Enable debug logging.' });
parser.addArgument([ '--secure' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Serve secure websockets, requires --key-file and ' +
'--cert-file if true, on by default.' });
parser.addArgument([ '--start-rethinkdb' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Start up a RethinkDB server in the current directory' });
parser.addArgument([ '--auto-create-collection' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Create collections used by requests if they do not exist.' });
parser.addArgument([ '--auto-create-index' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Create indexes used by requests if they do not exist.' });
parser.addArgument([ '--permissions' ],
{ type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?',
help: 'Enables or disables checking permissions on requests.' });
parser.addArgument([ '--serve-static' ],
{ type: 'string', metavar: 'PATH', nargs: '?', constant: './dist',
help: 'Serve static files from a directory, defaults to "./dist".' });
parser.addArgument([ '--dev' ],
{ action: 'storeTrue',
help: 'Runs the server in development mode, this sets ' +
'--secure=no, ' +
'--permissions=no, ' +
'--auto-create-collection=yes, ' +
'--auto-create-index=yes, ' +
'--start-rethinkdb=yes, ' +
'--allow-unauthenticated=yes, ' +
'--allow-anonymous=yes ' +
'and --serve-static=./dist.' });
parser.addArgument([ '--schema-file' ],
{ type: 'string', metavar: 'SCHEMA_FILE_PATH',
help: 'Path to the schema file to use, ' +
'will attempt to apply schema before starting Horizon server".' });
parser.addArgument([ '--auth' ],
{ type: 'string', action: 'append', metavar: 'PROVIDER,ID,SECRET', defaultValue: [ ],
help: 'Auth provider and options comma-separated, e.g. "facebook,<id>,<secret>".' });
parser.addArgument([ '--auth-redirect' ],
{ type: 'string', metavar: 'URL',
help: 'The URL to redirect to upon completed authentication, defaults to "/".' });
parser.addArgument([ '--access-control-allow-origin' ],
{ type: 'string', metavar: 'URL',
help: 'The URL of the host that can access auth settings, defaults to "".' });
parser.addArgument([ '--open' ],
{ action: 'storeTrue',
help: 'Open index.html in the static files folder once Horizon is ready to' +
' receive connections' });
return parser.parseArgs(args);
};
// Simple file server. 404s if file not found, 500 if file error,
// otherwise serve it with a mime-type suggested by its file extension.
const serve_file = (filePath, res) => {
fs.access(filePath, fs.R_OK | fs.F_OK, (exists) => {
if (exists) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end(`File "${filePath}" not found\n`);
} else {
fs.lstat(filePath, (err, stats) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`${err}\n`);
} else if (stats.isFile()) {
fs.readFile(filePath, 'binary', (err2, file) => {
if (err2) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`${err2}\n`);
} else {
const type = get_type(path.extname(filePath)) || false;
if (type) {
res.writeHead(200, { 'Content-Type': type });
} else {
res.writeHead(200);
}
res.end(file, 'binary');
}
});
} else if (stats.isDirectory()) {
serve_file(path.join(filePath, 'index.html'), res);
}
});
}
});
};
const file_server = (distDir) => (req, res) => {
const reqPath = url.parse(req.url).pathname;
// Serve client files directly
if (reqPath === '/' || reqPath === '') {
serve_file(path.join(distDir, 'index.html'), res);
} else if (!reqPath.match(/\/horizon\/.*$/)) {
// All other static files come from the dist directory
serve_file(path.join(distDir, reqPath), res);
}
// Fall through otherwise. Should be handled by horizon server
};
const initialize_servers = (ctor, opts) => {
const servers = [ ];
let numReady = 0;
return new Promise((resolve, reject) => {
opts.bind.forEach((host) => {
const srv = ctor().listen(opts.port, host);
servers.push(srv);
if (opts.serve_static) {
if (opts.serve_static === 'dist') {
// do nothing, this is the default
} else if (opts.project_path !== '.') {
const pth = path.join(opts.project_path, opts.serve_static);
console.info(`Static files being served from ${pth}`);
} else {
console.info(`Static files being served from ${opts.serve_static}`);
}
srv.on('request', file_server(opts.serve_static));
} else {
srv.on('request', (req, res) => {
res.writeHead(404);
res.end('404 Not Found');
});
}
srv.on('listening', () => {
const protocol = opts.secure ? 'https' : 'http';
console.info(`App available at ${protocol}://${srv.address().address}:` +
`${srv.address().port}`);
if (++numReady === servers.length) {
resolve(servers);
}
});
srv.on('error', (err) => {
reject(new Error(`HTTP${opts.secure ? 'S' : ''} server: ${err}`));
});
});
});
};
const create_insecure_servers = (opts) => {
if (!opts._dev_flag_used) {
console.error(chalk.red.bold('WARNING: Serving app insecurely.'));
}
return initialize_servers(() => new http.Server(), opts);
};
const read_cert_file = (file, type) => {
try {
return fs.readFileSync(path.resolve(file));
} catch (err) {
const wasDefault = file.endsWith(`horizon-${type}.pem`);
let description;
const suggestions = [
`If you're running horizon for the first time, we recommend \
running horizon like ${chalk.white('hz serve --dev')} to get started without \
having to configure certificates.`,
];
if (wasDefault) {
suggestions.push(
`If you have a ${type} file you'd like to use but they aren't in the \
default location, pass them in with the \
${chalk.white(`hz serve --${type}-file`)} option.`,
`You can explicitly disable security by passing \
${chalk.white('--secure=no')} to ${chalk.white('hz serve')}.`,
`You can generate a cert and key file for development by using the \
${chalk.white('hz create-cert')} command. Note that these certs won't be \
signed by a certificate authority, so you will need to explicitly authorize \
them in your browser.`
);
description = `In order to run the server in secure mode (the default), \
Horizon needs both a certificate file and a key file to encrypt websockets. \
By default, it looks for horizon-key.pem and horizon-cert.pem \
files in the current directory.`;
} else {
// They supplied a cert or key file, so don't give the long
// explanation and irrelevant suggestions.
suggestions.unshift(`See if the ${type} filename was misspelled.`);
description = null;
}
throw new NiceError(
`Could not access the ${type} file ${file}`, {
description,
suggestions,
});
}
};
const create_secure_servers = (opts) => {
const cert = read_cert_file(opts.cert_file, 'cert');
const key = read_cert_file(opts.key_file, 'key');
return initialize_servers(() => new https.Server({ key, cert }), opts);
};
// Command-line flags have the highest precedence, followed by environment variables,
// then the config file, and finally the default values.
const processConfig = (parsed) => {
let options;
options = config.default_options();
options = config.merge_options(options,
config.read_from_config_file(parsed.project_path));
options = config.merge_options(options,
config.read_from_secrets_file(parsed.project_path));
options = config.merge_options(options, config.read_from_env());
options = config.merge_options(options, config.read_from_flags(parsed));
if (options.project_name === null) {
options.project_name = path.basename(path.resolve(options.project_path));
}
if (options.bind.indexOf('all') !== -1) {
options.bind = [ '0.0.0.0' ];
}
if (!options.rdb_host) {
options.rdb_host = default_rdb_host;
}
if (!options.rdb_port) {
options.rdb_port = default_rdb_port;
}
if (!options.rdb_timeout) {
options.rdb_timeout = default_rdb_timeout;
}
return options;
};
const start_horizon_server = (http_servers, opts) =>
new horizon_server.Server(http_servers, {
auto_create_collection: opts.auto_create_collection,
auto_create_index: opts.auto_create_index,
permissions: opts.permissions,
project_name: opts.project_name,
access_control_allow_origin: opts.access_control_allow_origin,
auth: {
token_secret: opts.token_secret,
allow_unauthenticated: opts.allow_unauthenticated,
allow_anonymous: opts.allow_anonymous,
success_redirect: opts.auth_redirect,
failure_redirect: opts.auth_redirect,
},
rdb_host: opts.rdb_host,
rdb_port: opts.rdb_port,
rdb_user: opts.rdb_user || null,
rdb_password: opts.rdb_password || null,
rdb_timeout: opts.rdb_timeout || null,
max_connections: opts.max_connections || null,
});
// `interruptor` is meant for use by tests to stop the server without relying on SIGINT
const run = (args, interruptor) => {
let opts, http_servers, hz_server, rdb_server;
const old_log_level = logger.level;
const cleanup = () => {
logger.level = old_log_level;
return Promise.all([
hz_server ? hz_server.close() : Promise.resolve(),
rdb_server ? rdb_server.close() : Promise.resolve(),
http_servers ? Promise.all(http_servers.map((s) =>
new Promise((resolve) => s.close(resolve)))) : Promise.resolve(),
]);
};
interrupt.on_interrupt(() => cleanup());
return Promise.resolve().then(() => {
opts = processConfig(parseArguments(args));
logger.level = opts.debug ? 'debug' : 'warn';
if (!opts.secure && opts.auth && Array.from(Object.keys(opts.auth)).length > 0) {
logger.warn('Authentication requires that the server be accessible via HTTPS. ' +
'Either specify "secure=true" or use a reverse proxy.');
}
change_to_project_dir(opts.project_path);
if (opts.secure) {
return create_secure_servers(opts);
} else {
return create_insecure_servers(opts);
}
}).then((servers) => {
http_servers = servers;
if (opts.start_rethinkdb) {
return start_rdb_server().then((server) => {
rdb_server = server;
// Don't need to check for host, always localhost.
opts.rdb_host = 'localhost';
opts.rdb_port = server.driver_port;
console.log('RethinkDB');
console.log(` ├── Admin interface: http://localhost:${server.http_port}`);
console.log(` └── Drivers can connect to port ${server.driver_port}`);
});
}
}).then(() => {
// Ensure schema from schema.toml file is set
if (opts.schema_file) {
console.log(`Ensuring schema "${opts.schema_file}" is applied`);
try {
fs.accessAsync(opts.schema_file, fs.R_OK | fs.F_OK);
} catch (e) {
console.error(
chalk.yellow.bold('No .hz/schema.toml file found'));
return;
}
const schemaOptions = schema.processApplyConfig({
project_name: opts.project_name,
schema_file: opts.schema_file,
start_rethinkdb: false,
connect: `${opts.rdb_host}:${opts.rdb_port}`,
update: true,
force: false,
});
return schema.runApplyCommand(schemaOptions);
}
}).then(() => {
console.log('Starting Horizon...');
hz_server = start_horizon_server(http_servers, opts);
return new Promise((resolve, reject) => {
const timeoutObject = setTimeout(() => {
reject(new Error('Horizon failed to start after 30 seconds.\n' +
'Try running hz serve again with the --debug flag'));
}, TIMEOUT_30_SECONDS);
hz_server.ready().then(() => {
clearTimeout(timeoutObject);
console.log(chalk.green.bold('🌄 Horizon ready for connections'));
resolve(hz_server);
}).catch(reject);
});
}).then(() => {
if (opts.auth) {
for (const name in opts.auth) {
const provider = horizon_server.auth[name];
if (!provider) {
throw new Error(`Unrecognized auth provider "${name}"`);
}
hz_server.add_auth_provider(provider,
Object.assign({}, { path: name }, opts.auth[name]));
}
}
}).then(() => {
// Automatically open up index.html in the `dist` directory only if
// `--open` flag specified and an index.html exists in the directory.
if (opts.open && opts.serve_static) {
try {
// Check if index.html exists and readable in serve static_static directory
fs.accessSync(`${opts.serve_static}/index.html`, fs.R_OK | fs.F_OK);
// Determine scheme from options
const scheme = opts.secure ? 'https://' : 'http://';
// Open up index.html in default browser
console.log('Attempting open of index.html in default browser');
open(`${scheme}${opts.bind}:${opts.port}/index.html`);
} catch (open_err) {
console.log(chalk.red('Error occurred while trying to open ' +
`${opts.serve_static}/index.html`));
console.log(open_err);
}
}
return Promise.race([
hz_server._interruptor.catch(() => { }),
interruptor ? interruptor.catch(() => { }) : new Promise(() => { }),
]);
}).then(cleanup).catch((err) => cleanup().then(() => { throw err; }));
};
module.exports = {
run,
description: 'Serve a Horizon app',
parseArguments,
processConfig,
};
================================================
FILE: cli/src/utils/change_to_project_dir.js
================================================
'use strict';
const is_directory = require('./is_directory');
module.exports = (project_path) => {
if (is_directory(project_path)) {
process.chdir(project_path);
} else {
throw new Error(`${project_path} is not a directory`);
}
if (!is_directory('.hz')) {
const nice_path = (project_path === '.' ? 'this directory' : project_path);
throw new Error(`${nice_path} doesn't contain an .hz directory`);
}
};
================================================
FILE: cli/src/utils/check-project-name.js
================================================
'use strict';
const path = require('path');
const basename = path.basename;
const join = path.join;
const fixableProjectName = /^[A-Za-z0-9_-]+$/;
const unfixableChars = /[^A-Za-z0-9_-]/g;
const dehyphenate = (name) => name.replace(/-/g, '_');
const shouldCreateDir = (prospectiveName, dirList) => {
if (prospectiveName === '.' ||
prospectiveName == null ||
!fixableProjectName.test(prospectiveName)) {
return false;
} else if (dirList.indexOf(prospectiveName) === -1) {
return true;
} else {
return false;
}
};
module.exports = (prospectiveName, cwd, dirList) => {
let chdirTo = prospectiveName != null ?
join(cwd, prospectiveName) : cwd;
const createDir = shouldCreateDir(prospectiveName, dirList);
if (prospectiveName === '.' || prospectiveName == null) {
// eslint-disable-next-line no-param-reassign
prospectiveName = basename(cwd);
chdirTo = false;
}
if (fixableProjectName.test(prospectiveName)) {
return {
dirName: prospectiveName,
projectName: dehyphenate(prospectiveName),
chdirTo,
createDir,
};
} else {
const invalids = prospectiveName.match(unfixableChars).join('');
throw new Error(`Invalid characters in project name: ${invalids}`);
}
};
================================================
FILE: cli/src/utils/config.js
================================================
'use strict';
const parse_yes_no_option = require('./parse_yes_no_option');
const NiceError = require('./nice_error.js');
const fs = require('fs');
const url = require('url');
const toml = require('toml');
const chalk = require('chalk');
const default_config_file = '.hz/config.toml';
const default_secrets_file = '.hz/secrets.toml';
const default_rdb_port = 28015;
const make_default_options = () => ({
config: null,
debug: false,
// Default to current directory for path
project_path: '.',
// Default to current directory name for project name
project_name: null,
bind: [ 'localhost' ],
port: 8181,
start_rethinkdb: false,
serve_static: null,
open: false,
secure: true,
permissions: true,
key_file: './horizon-key.pem',
cert_file: './horizon-cert.pem',
schema_file: null,
auto_create_collection: false,
auto_create_index: false,
rdb_host: null,
rdb_port: null,
rdb_user: null,
rdb_password: null,
rdb_timeout: null,
token_secret: null,
allow_anonymous: false,
allow_unauthenticated: false,
auth_redirect: '/',
access_control_allow_origin: '',
max_connections: null,
auth: { },
});
const default_options = make_default_options();
const yes_no_options = [ 'debug',
'secure',
'permissions',
'start_rethinkdb',
'auto_create_index',
'auto_create_collection',
'allow_unauthenticated',
'allow_anonymous' ];
const parse_connect = (connect, config) => {
// support rethinkdb:// style connection uri strings
// expects rethinkdb://host:port` at a minimum but can optionally take a user:pass and db
// e.g. rethinkdb://user:pass@host:port/db
const rdb_uri = url.parse(connect);
if (rdb_uri.protocol === 'rethinkdb:') {
if (rdb_uri.hostname) {
config.rdb_host = rdb_uri.hostname;
config.rdb_port = rdb_uri.port || default_rdb_port;
// check for user/pass
if (rdb_uri.auth) {
const user_pass = rdb_uri.auth.split(':');
config.rdb_user = user_pass[0];
config.rdb_password = user_pass[1];
}
// set the project name based on the db
if (rdb_uri.path && rdb_uri.path.replace('/', '') !== '') {
config.project_name = rdb_uri.path.replace('/', '');
}
} else {
throw new Error(`Expected --connect rethinkdb://HOST, but found "${connect}".`);
}
} else {
// support legacy HOST:PORT connection strings
const host_port = connect.split(':');
if (host_port.length === 1) {
config.rdb_host = host_port[0];
config.rdb_port = default_rdb_port;
} else if (host_port.length === 2) {
config.rdb_host = host_port[0];
config.rdb_port = parseInt(host_port[1]);
if (isNaN(config.rdb_port) || config.rdb_port < 0 || config.rdb_port > 65535) {
throw new Error(`Invalid port: "${host_port[1]}".`);
}
} else {
throw new Error(`Expected --connect HOST:PORT, but found "${connect}".`);
}
}
};
const read_from_config_file = (project_path) => {
const config = { auth: { } };
let fileData, configFilename, fileConfig;
if (project_path) {
configFilename = `${project_path}/${default_config_file}`;
} else {
configFilename = default_config_file;
}
try {
fileData = fs.readFileSync(configFilename);
} catch (err) {
return config;
}
try {
fileConfig = toml.parse(fileData);
} catch (e) {
if (e.name === 'SyntaxError') {
throw new NiceError(
`There was a syntax error when parsing ${configFilename}`, {
description: `Something was wrong with the format of \
${configFilename}, causing it not be a valid TOML file.`,
src: {
filename: configFilename,
contents: fileData,
line: e.line,
column: e.column,
},
suggestions: [
'Check that all strings values have quotes around them',
'Check that key/val pairs use equals and not colons',
'See https://github.com/toml-lang/toml#user-content-spec',
],
});
} else {
throw e;
}
}
for (const field in fileConfig) {
if (field === 'connect') {
parse_connect(fileConfig.connect, config);
} else if (yes_no_options.indexOf(field) !== -1) {
config[field] = parse_yes_no_option(fileConfig[field], field);
} else if (default_options[field] !== undefined) {
config[field] = fileConfig[field];
} else {
throw new Error(`Unknown config parameter: "${field}".`);
}
}
return config;
};
const read_from_secrets_file = (projectPath) => {
const config = { auth: { } };
let fileData, secretsFilename;
if (projectPath) {
secretsFilename = `${projectPath}/${default_secrets_file}`;
} else {
secretsFilename = default_secrets_file;
}
try {
fileData = fs.readFileSync(secretsFilename);
} catch (err) {
return config;
}
const fileConfig = toml.parse(fileData);
for (const field in fileConfig) {
if (field === 'connect') {
parse_connect(fileConfig.connect, config);
} else if (yes_no_options.indexOf(field) !== -1) {
config[field] = parse_yes_no_option(fileConfig[field], field);
} else if (default_options[field] !== undefined) {
config[field] = fileConfig[field];
} else {
throw new Error(`Unknown config parameter: "${field}".`);
}
}
return config;
};
const env_regex = /^HZ_([A-Z]+([_]?[A-Z]+)*)$/;
const read_from_env = () => {
const config = { auth: { } };
for (const env_var in process.env) {
const matches = env_regex.exec(env_var);
if (matches && matches[1]) {
const destVarName = matches[1].toLowerCase();
const varPath = destVarName.split('_');
const value = process.env[env_var];
if (destVarName === 'connect') {
parse_connect(value, config);
} else if (destVarName === 'bind') {
config[destVarName] = value.split(',');
} else if (varPath[0] === 'auth') {
if (varPath.length !== 3) {
console.log(`Ignoring malformed Horizon environment variable: "${env_var}", ` +
'should be HZ_AUTH_{PROVIDER}_{OPTION}.');
} else {
config.auth[varPath[1]] = config.auth[varPath[1]] || { };
config.auth[varPath[1]][varPath[2]] = value;
}
} else if (yes_no_options.indexOf(destVarName) !== -1) {
config[destVarName] = parse_yes_no_option(value, destVarName);
} else if (default_options[destVarName] !== undefined) {
config[destVarName] = value;
}
}
}
return config;
};
// Handles reading configuration from the parsed flags
const read_from_flags = (parsed) => {
const config = { auth: { } };
// Dev mode
if (parsed.dev) {
config.access_control_allow_origin = '*';
config.allow_unauthenticated = true;
config.allow_anonymous = true;
config.secure = false;
config.permissions = false;
config.start_rethinkdb = true;
config.auto_create_collection = true;
config.auto_create_index = true;
config.serve_static = 'dist';
config._dev_flag_used = true;
if (parsed.start_rethinkdb === null || parsed.start_rethinkdb === undefined) {
config._start_rethinkdb_implicit = true;
}
}
for (const key in parsed) {
if (key === 'auth' && parsed.auth != null) {
// Auth options
parsed.auth.forEach((auth_options) => {
const params = auth_options.split(',');
if (params.length !== 3) {
throw new Error(`Expected --auth PROVIDER,ID,SECRET, but found "${auth_options}"`);
}
config.auth[params[0]] = { id: params[1], secret: params[2] };
});
} else if (key === 'connect' && parsed.connect != null) {
// Normalize RethinkDB connection options
parse_connect(parsed.connect, config);
} else if (yes_no_options.indexOf(key) !== -1 && parsed[key] != null) {
// Simple 'yes' or 'no' (or 'true' or 'false') flags
config[key] = parse_yes_no_option(parsed[key], key);
} else if (parsed[key] != null) {
config[key] = parsed[key];
}
}
return config;
};
const merge_options = (old_options, new_options) => {
// Disable start_rethinkdb if it was enabled by dev mode but we already have a host
if (new_options._start_rethinkdb_implicit) {
if (old_options.rdb_host) {
delete new_options.start_rethinkdb;
}
} else if (new_options.start_rethinkdb && new_options.rdb_host) {
throw new Error('Cannot provide both --start-rethinkdb and --connect.');
}
for (const key in new_options) {
if (key === 'rdb_host') {
old_options.start_rethinkdb = false;
}
if (key === 'auth') {
for (const provider in new_options.auth) {
old_options.auth[provider] = old_options.auth[provider] || { };
for (const field in new_options.auth[provider]) {
old_options.auth[provider][field] = new_options.auth[provider][field];
}
}
} else {
old_options[key] = new_options[key];
}
}
return old_options;
};
module.exports = {
default_config_file,
default_secrets_file,
default_options: make_default_options,
read_from_config_file,
read_from_secrets_file,
read_from_env,
read_from_flags,
merge_options,
parse_connect,
};
================================================
FILE: cli/src/utils/each_line_in_pipe.js
================================================
'use strict';
module.exports = (pipe, callback) => {
let buffer = '';
pipe.on('data', (data) => {
buffer += data.toString();
let endline_pos = buffer.indexOf('\n');
while (endline_pos !== -1) {
const line = buffer.slice(0, endline_pos);
buffer = buffer.slice(endline_pos + 1);
callback(line);
endline_pos = buffer.indexOf('\n');
}
});
};
================================================
FILE: cli/src/utils/initialize_joi.js
================================================
'use strict';
// Issues a dummy joi validation to force joi to initialize its scripts.
// This is used because tests will mock the filesystem, and the lazy
// `require`s done by joi will no longer work at that point.
module.exports = (joi) =>
joi.validate('', joi.any().when('', { is: '', then: joi.any() }));
================================================
FILE: cli/src/utils/interrupt.js
================================================
'use strict';
const handlers = [ ];
const on_interrupt = (cb) => {
handlers.push(cb);
};
const run_handlers = () => {
if (handlers.length > 0) {
return handlers.shift()().then(() => run_handlers);
}
};
const interrupt = () => {
process.removeAllListeners('SIGTERM');
process.removeAllListeners('SIGINT');
process.on('SIGTERM', () => process.exit(1));
process.on('SIGINT', () => process.exit(1));
return run_handlers();
};
process.on('SIGTERM', interrupt);
process.on('SIGINT', interrupt);
module.exports = { on_interrupt, interrupt };
================================================
FILE: cli/src/utils/is_directory.js
================================================
'use strict';
const path = require('path');
const fs = require('fs');
module.exports = (dirname) => {
try {
return fs.statSync(path.resolve(dirname)).isDirectory();
} catch (e) {
return false;
}
};
================================================
FILE: cli/src/utils/nice_error.js
================================================
'use strict';
/*
A nice error type that allows you to associate a longer description, a
source file and suggestions with it.
*/
const chalk = require('chalk');
class NiceError extends Error {
constructor(message, options) {
super(message);
const opts = options || {};
this.description = opts.description || null;
this.suggestions = opts.suggestions || null;
// TODO: maybe allow multiple source locations and spans of text
// instead of a single column offset
this.src = (opts.src) ? Object.assign({}, opts.src) : null;
}
toString() {
return this.message;
}
niceString(options) {
const opts = options || {};
const cSize = opts.contextSize != null ? opts.contextSize : 2;
const results = [ this.message ];
if (this.description) {
results.push('', this.description);
}
if (this.src != null) {
const formattedSrc = NiceError._formatContext(
this.src.contents,
this.src.line,
this.src.column,
cSize
);
if (formattedSrc.length > 0) {
results.push(`\nIn ${this.src.filename}, ` +
`line ${this.src.line}, ` +
`column ${this.src.column}:`);
results.push.apply(results, formattedSrc);
}
}
if (this.suggestions) {
results.push(
'', // extra newline before suggestions
chalk.red(
this.suggestions.length > 1 ? 'Suggestions:' : 'Suggestion:'));
results.push.apply(
results, this.suggestions.map((note) => ` ➤ ${note}`));
}
results.push(''); // push a final newline on
return results.join('\n');
}
static _sourceLine(ln) {
return `${chalk.blue(`${ln.line}:`)} ${chalk.white(ln.src)}`;
}
static _extractContext(sourceContents, line, contextSize) {
const lines = sourceContents.toString().split('\n');
const minLine = Math.max(line - contextSize - 1, 0);
const maxLine = Math.min(line + contextSize, lines.length);
if (line > lines.length) {
return [];
} else {
return lines.slice(minLine, maxLine).map((src, i) => ({
line: i + minLine + 1,
src,
}));
}
}
static _formatContext(sourceContents, line, col, contextSize) {
return this._extractContext(sourceContents, line, contextSize)
.map((srcLine) => {
let formatted = this._sourceLine(srcLine);
if (srcLine.line === line) {
const prefix = `${line}: `;
formatted +=
`\n${' '.repeat(prefix.length + col - 1)}${chalk.green('^')}`;
}
return formatted;
});
}
}
module.exports = NiceError;
================================================
FILE: cli/src/utils/parse_yes_no_option.js
================================================
'use strict';
module.exports = (value, option_name) => {
if (value !== undefined && value !== null) {
const lower = value.toLowerCase ? value.toLowerCase() : value;
if (lower === true || lower === 'true' || lower === 'yes') {
return true;
} else if (lower === false || lower === 'false' || lower === 'no') {
return false;
}
throw new Error(`Unexpected value "${option_name}=${value}", should be yes or no.`);
}
};
================================================
FILE: cli/src/utils/proc-promise.js
================================================
'use strict';
const Promise = require('bluebird');
const childProcess = require('child_process');
function procPromise() {
// Takes the same arguments as child_process.spawn
const args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
const proc = childProcess.spawn.apply(childProcess, args);
proc.stderr.setEncoding('utf8');
proc.stdout.setEncoding('utf8');
proc.on('exit', (code) => {
if (code === 0) {
resolve(proc);
} else {
const err = new Error(proc.stderr.read());
err.exitCode = code;
reject(err);
}
});
});
}
module.exports = procPromise;
================================================
FILE: cli/src/utils/rethrow.js
================================================
'use strict';
// Returns a new Error with the given message. Combines the stack
// traces with the old error, and removes itself from the stack trace.
module.exports = (e, newMessage) => {
let e2;
if (typeof newMessage === 'string') {
e2 = new Error(newMessage);
e2.stack = e2.stack.split('\n');
e2.stack.splice(1, 1); // Remove rethrow from stack trace
} else {
e2 = newMessage;
}
e2.stack += '\n\n ==== Original stack trace ====\n\n';
e2.stack += e.stack;
return e2;
};
================================================
FILE: cli/src/utils/rm_sync_recursive.js
================================================
'use strict';
const fs = require('fs');
const path = require('path');
const rmdirSyncRecursive = (dir) => {
try {
fs.readdirSync(dir).forEach((item) => {
const full_path = path.join(dir, item);
if (fs.statSync(full_path).isDirectory()) {
rmdirSyncRecursive(full_path);
} else {
fs.unlinkSync(full_path);
}
});
fs.rmdirSync(dir);
} catch (err) { /* Do nothing */ }
};
module.exports = rmdirSyncRecursive;
================================================
FILE: cli/src/utils/start_rdb_server.js
================================================
'use strict';
const each_line_in_pipe = require('./each_line_in_pipe');
const horizon_server = require('@horizon/server');
const execSync = require('child_process').execSync;
const spawn = require('child_process').spawn;
const hasbinSync = require('hasbin').sync;
const defaultDatadir = 'rethinkdb_data';
const infoLevelLog = (msg) => /^Running/.test(msg) || /^Listening/.test(msg);
const r = horizon_server.r;
const logger = horizon_server.logger;
const version_check = horizon_server.utils.rethinkdb_version_check;
class RethinkdbServer {
constructor(options) {
const quiet = Boolean(options.quiet);
const bind = options.bind || [ '127.0.0.1' ];
const dataDir = options.dataDir || defaultDatadir;
const driverPort = options.rdbPort;
const httpPort = options.rdbHttpPort;
const cacheSize = options.cacheSize || 200;
// Check if `rethinkdb` in PATH
if (!hasbinSync('rethinkdb')) {
throw new Error('`rethinkdb` not found in $PATH, please install RethinkDB.');
}
// Check if RethinkDB is sufficient version for Horizon
version_check(execSync('rethinkdb --version', { timeout: 5000 }).toString());
const args = [ '--http-port', String(httpPort || 0),
'--cluster-port', '0',
'--driver-port', String(driverPort || 0),
'--cache-size', String(cacheSize),
'--directory', dataDir,
'--no-update-check' ];
bind.forEach((host) => args.push('--bind', host));
this.proc = spawn('rethinkdb', args);
this.ready_promise = new Promise((resolve, reject) => {
this.proc.once('error', reject);
this.proc.once('exit', (exit_code) => {
if (exit_code !== 0) {
reject(new Error(`RethinkDB process terminated with error code ${exit_code}.`));
}
});
process.on('exit', () => {
if (this.proc.exitCode === null) {
logger.error('Unclean shutdown - killing RethinkDB child process');
this.proc.kill('SIGKILL');
}
});
const maybe_resolve = () => {
// Once we have both ports determined, callback with all settings.
if (this.http_port !== undefined &&
this.driver_port !== undefined) {
resolve(this);
}
};
each_line_in_pipe(this.proc.stdout, (line) => {
if (!quiet) {
if (infoLevelLog(line)) {
logger.info('RethinkDB', line);
} else {
logger.debug('RethinkDB stdout:', line);
}
}
if (this.driver_port === undefined) {
const matches = line.match(
/^Listening for client driver connections on port (\d+)$/);
if (matches !== null && matches.length === 2) {
this.driver_port = parseInt(matches[1]);
maybe_resolve();
}
}
if (this.http_port === undefined) {
const matches = line.match(
/^Listening for administrative HTTP connections on port (\d+)$/);
if (matches !== null && matches.length === 2) {
this.http_port = parseInt(matches[1]);
maybe_resolve();
}
}
});
each_line_in_pipe(this.proc.stderr, (line) =>
logger.error(`rethinkdb stderr: ${line}`));
});
}
ready() {
return this.ready_promise;
}
// This is only used by tests - cli commands use a more generic method as
// the database may be launched elsewhere.
connect() {
return r.connect({ host: 'localhost', port: this.driver_port });
}
close() {
return new Promise((resolve) => {
if (this.proc.exitCode !== null) {
resolve();
} else {
this.proc.kill('SIGTERM');
this.proc.once('exit', () => {
resolve();
});
setTimeout(() => {
this.proc.kill('SIGKILL');
resolve();
}, 20000).unref();
}
});
}
}
// start_rdb_server
// Options:
// quiet: boolean, suppresses rethinkdb log messages
// bind: array of ip addresses to bind to, or 'all'
// dataDir: name of rethinkdb data directory. Defaults to `rethinkdb_data`
// driverPort: port number for rethinkdb driver connections. Auto-assigned by default.
// httpPort: port number for webui. Auto-assigned by default.
// cacheSize: cacheSize to give to rethinkdb in MB. Default 200.
module.exports = (options) => new RethinkdbServer(options || { }).ready();
module.exports.r = r;
================================================
FILE: cli/src/version.js
================================================
'use strict';
const package_json = require('../package.json');
const run = (args) =>
Promise.resolve().then(() => {
if (args && args.length) {
throw new Error('create-cert takes no arguments');
}
console.info(package_json.version);
});
module.exports = {
run,
description: 'Print the version number of horizon',
};
================================================
FILE: cli/test/config.js
================================================
'use strict';
const serve = require('../src/serve');
const processConfig = serve.processConfig;
const assert = require('assert');
const mockFs = require('mock-fs');
const make_flags = (flags) => Object.assign({}, serve.parseArguments([]), flags);
const write_config = (config) => {
let data = '';
const recursive_add = (obj, path) => {
const value_keys = [ ];
const object_keys = [ ];
for (const key in obj) {
const val = obj[key];
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
object_keys.push(key);
} else {
value_keys.push(key);
}
}
if (value_keys.length > 0) {
if (path) {
data += `[${path}]\n`;
}
value_keys.forEach((key) => {
data += `${key} = ${JSON.stringify(obj[key])}\n`;
});
}
object_keys.forEach((key) => {
recursive_add(obj[key], `${path}${path ? '.' : ''}${key}`);
});
};
recursive_add(config, '');
mockFs({ '.hz': { 'config.toml': data } });
};
describe('Config', () => {
let original_env;
before('Save env', () => {
original_env = Object.assign({}, process.env);
});
beforeEach('Create empty config file', () => {
write_config({ });
});
after('Restore fs', () => {
mockFs.restore();
});
afterEach('Restore env', () => {
process.env = Object.assign({}, original_env);
});
it('precedence', () => {
// Test a yes/no flag (in this case `secure`)
// Make a list of all possible states to test, each item contains
// the state of the config file, the env, and the flags
const states = [ ];
const values = [ 'yes', 'no', 'false', 'true', false, true, null ];
for (let i = 0; i < Math.pow(values.length, 3); ++i) {
states.push([ values[i % 3], values[Math.floor(i / 3) % 3], values[Math.floor(i / 9) % 3] ]);
}
states.forEach((state) => {
const parsed = { };
let expected = true; // default value
if (state[0] !== null) {
write_config({ secure: state[0] });
expected = state[0];
} else {
write_config({ });
}
if (state[1] !== null) {
process.env.HZ_INSECURE = `${state[1]}`;
expected = state[1];
} else {
delete process.env.HZ_INSECURE;
}
if (state[2] !== null) {
parsed.secure = state[2];
expected = state[2];
}
expected = (expected === 'yes' || expected === 'true') || expected;
expected = (expected === 'no' || expected === 'false') ? false : expected;
assert.strictEqual(processConfig(make_flags(parsed)).secure, expected);
});
});
// An unrecognized parameter in a config file should cause an error
it('unknown field in file', () => {
write_config({ fake_field: 'foo' });
assert.throws(() => processConfig(make_flags({ })),
/Unknown config parameter: "fake_field"./);
});
// An unrecognized environment variable that matches the pattern should be ignored
it('unknown field in env', () => {
process.env.HZ_FAKE_FIELD = 'foo';
const config = processConfig(make_flags({ }));
assert.strictEqual(config.fake_field, undefined);
});
// The port parameter should always be stored as a number
describe('connect', () => {
it('valid in file', () => {
write_config({ connect: 'localhost:123' });
const config = processConfig(make_flags({ }));
assert.strictEqual(config.rdb_port, 123);
});
it('valid in env', () => {
process.env.HZ_CONNECT = 'localhost:456';
const config = processConfig(make_flags({ }));
assert.strictEqual(config.rdb_port, 456);
});
// Make sure an error is thrown if the format is wrong
it('invalid format in file', () => {
write_config({ connect: 'local:host:111' });
assert.throws(() => processConfig(make_flags({ })),
/Expected --connect HOST:PORT, but found "local:host:111"./);
});
it('invalid format in env', () => {
process.env.HZ_CONNECT = 'local:host:111';
assert.throws(() => processConfig(make_flags({ })),
/Expected --connect HOST:PORT, but found "local:host:111"./);
});
it('invalid format in flags', () => {
assert.throws(() => processConfig(make_flags({ connect: 'local:host:111' })),
/Expected --connect HOST:PORT, but found "local:host:111"./);
});
// Make sure an error is thrown if the port cannot be parsed
it('invalid port in file', () => {
write_config({ connect: 'localhost:cat' });
assert.throws(() => processConfig(make_flags({ })),
/Invalid port: "cat"./);
});
it('invalid port in env', () => {
process.env.HZ_CONNECT = 'localhost:dog';
assert.throws(() => processConfig(make_flags({ })),
/Invalid port: "dog"./);
});
it('invalid port in flags', () => {
assert.throws(() => processConfig(make_flags({ connect: 'localhost:otter' })),
/Invalid port: "otter"./);
});
it('with start_rethinkdb in file', () => {
write_config({ connect: 'localhost:123', start_rethinkdb: true });
assert.throws(() => processConfig(make_flags({ })),
/Cannot provide both --start-rethinkdb and --connect./);
});
it('with start_rethinkdb in env', () => {
process.env.HZ_CONNECT = 'localhost:123';
process.env.HZ_START_RETHINKDB = 'true';
assert.throws(() => processConfig(make_flags({ })),
/Cannot provide both --start-rethinkdb and --connect./);
});
it('with start_rethinkdb in flags', () => {
assert.throws(() => processConfig(make_flags({ connect: 'localhost:123',
start_rethinkdb: true })),
/Cannot provide both --start-rethinkdb and --connect./);
});
it('with enabling and disabling start_rethinkdb', () => {
write_config({ start_rethinkdb: true });
process.env.HZ_CONNECT = 'example:123';
const config = processConfig(make_flags({ start_rethinkdb: false }));
assert.strictEqual(config.start_rethinkdb, false);
assert.strictEqual(config.rdb_host, 'example');
assert.strictEqual(config.rdb_port, 123);
});
it('with start_rethinkdb across configs', () => {
let config;
write_config({ connect: 'example:123' });
config = processConfig(
make_flags({ start_rethinkdb: true }));
assert.strictEqual(config.start_rethinkdb, true);
write_config({ start_rethinkdb: true });
config = processConfig(
make_flags({ connect: 'example:123' }));
assert.strictEqual(config.start_rethinkdb, false);
assert.strictEqual(config.rdb_host, 'example');
assert.strictEqual(config.rdb_port, 123);
});
it('with dev mode and start_rethinkdb across configs', () => {
let config;
write_config({ connect: 'example:123' });
config = processConfig(make_flags({ dev: true }));
assert.strictEqual(config.start_rethinkdb, false);
assert.strictEqual(config.rdb_host, 'example');
assert.strictEqual(config.rdb_port, 123);
write_config({ connect: 'example:123' });
config = processConfig(
make_flags({ start_rethinkdb: false, dev: true }));
assert.strictEqual(config.start_rethinkdb, false);
assert.strictEqual(config.rdb_host, 'example');
assert.strictEqual(config.rdb_port, 123);
write_config({ connect: 'example:123' });
config = processConfig(
make_flags({ start_rethinkdb: true, dev: true }));
assert.strictEqual(config.start_rethinkdb, true);
write_config({ start_rethinkdb: true });
config = processConfig(make_flags({ dev: true }));
assert.strictEqual(config.start_rethinkdb, true);
write_config({ start_rethinkdb: true });
config = processConfig(
make_flags({ connect: 'example:123', dev: true }));
assert.strictEqual(config.start_rethinkdb, false);
assert.strictEqual(config.rdb_host, 'example');
assert.strictEqual(config.rdb_port, 123);
});
});
// The bind parameter must be stored as an array of hostnames
describe('bind', () => {
it('in file', () => {
write_config({ bind: [ 'foo', 'bar' ] });
const config = processConfig(make_flags({ }));
assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);
});
it('in env', () => {
process.env.HZ_BIND = 'foo,bar';
const config = processConfig(make_flags({ }));
assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);
});
it('in flags', () => {
const config = processConfig(make_flags({ bind: [ 'foo', 'bar' ] }));
assert.deepStrictEqual(config.bind, [ 'foo', 'bar' ]);
});
});
// Auth parameters are handled slightly differently to other parameters
// They add to an object, rather than having an explicit option name
// for each auth provider.
it('auth', () => {
// provider 'foo' and 'far' through config file
write_config({ auth: {
foo: { id: 'foo_id', secret: 'foo_secret' },
far: { id: 'far_id', secret: 'far_secret' },
} });
// provider 'bar' and 'baz' through env
process.env.HZ_AUTH_BAR_ID = 'bar_id';
process.env.HZ_AUTH_BAR_SECRET = 'bar_secret';
process.env.HZ_AUTH_BAZ_ID = 'baz_id';
process.env.HZ_AUTH_BAZ_SECRET = 'baz_secret';
// overwrite 'far' through env
process.env.HZ_AUTH_FAR_ID = 'far_id_new';
process.env.HZ_AUTH_FAR_SECRET = 'far_secret_new';
// provider 'bamf' through command-line
// overwrite 'baz' through command-line
const config = processConfig(make_flags({
auth: [ 'bamf,bamf_id,bamf_secret',
'baz,baz_id_new,baz_secret_new' ] }));
assert.deepStrictEqual(config.auth, {
foo: { id: 'foo_id', secret: 'foo_secret' },
far: { id: 'far_id_new', secret: 'far_secret_new' },
bar: { id: 'bar_id', secret: 'bar_secret' },
baz: { id: 'baz_id_new', secret: 'baz_secret_new' },
bamf: { id: 'bamf_id', secret: 'bamf_secret' },
});
});
});
================================================
FILE: cli/test/init.spec.js
================================================
/* global beforeEach, describe, process, afterEach, it, require */
'use strict';
const init = require('../src/init');
const assert = require('chai').assert;
const fs = require('fs');
const mockFs = require('mock-fs');
const path = require('path');
const sinon = require('sinon');
const toml = require('toml');
const original_dir = path.resolve(process.cwd());
const hz_dir = `${original_dir}/.hz`;
const assertNameExists = (baseDir, fileName) => {
const files = fs.readdirSync(baseDir);
assert.include(files, fileName);
};
const assertNameDoesntExist = (baseDir, fileName) => {
const files = fs.readdirSync(baseDir);
assert.notInclude(files, fileName);
};
const assertDirExists = (baseDir, dirName) => {
assertNameExists(baseDir, dirName);
assert.isTrue(fs.statSync(`${baseDir}/${dirName}`).isDirectory());
};
const assertDirDoesntExist = (baseDir, dirName) => {
assertNameDoesntExist(baseDir, dirName);
};
const assertFileExists = (baseDir, fileName) => {
assertNameExists(baseDir, fileName);
assert.isTrue(fs.statSync(`${baseDir}/${fileName}`).isFile());
};
const assertFileDoesntExist = (baseDir, fileName) => {
assertNameDoesntExist(baseDir, fileName);
};
const getFileString = (filepath) =>
fs.readFileSync(filepath, { encoding: 'utf8' });
const readToml = (filepath) => {
const tomlData = getFileString(filepath);
return toml.parse(tomlData);
};
const assertValidConfig = (filepath) => {
const configObject = readToml(filepath);
// Need an uncommented project name
assert.property(configObject, 'project_name');
};
const assertValidSecrets = (filepath) => {
const secretsObject = readToml(filepath);
// Need an uncommented token_secret
assert.property(secretsObject, 'token_secret');
};
describe('hz init', () => {
beforeEach('redirect console out', () => {
sinon.stub(console, 'error');
sinon.stub(console, 'info');
});
afterEach('restore console out', () => {
console.error.restore();
console.info.restore();
});
afterEach('restore cwd', () => process.chdir(original_dir));
afterEach('clear mockfs', () => mockFs.restore());
describe('when passed a project name', () => {
const testDirName = 'test-app';
const projectDir = `${original_dir}/${testDirName}`;
const args = [ testDirName ];
it("creates the project dir if it doesn't exist", () => {
mockFs();
return init.run(args).then(() =>
assertDirExists(original_dir, testDirName)
);
});
it('moves into the project dir', () => {
mockFs({ [testDirName]: { } });
return init.run(args).then(() =>
assert.equal(process.cwd(), `${original_dir}/${testDirName}`)
);
});
describe('when the project dir is empty', () => {
beforeEach('initialize mockfs', () => {
mockFs({ [projectDir]: {} });
});
it('creates the src dir', () =>
init.run(args).then(() =>
assertDirExists(projectDir, 'src')));
it('creates the dist dir', () =>
init.run(args).then(() =>
assertDirExists(projectDir, 'dist')));
it('creates an example dist/index.html', () =>
init.run(args).then(() =>
assertFileExists(`${projectDir}/dist`, 'index.html')));
it('creates the .hz dir', () =>
init.run(args).then(() =>
assertDirExists(projectDir, '.hz')));
it('creates the .gitignore file', () =>
init.run(args).then(() =>
assertFileExists(`${projectDir}`, '.gitignore')));
it('creates the .hz/config.toml file', () =>
init.run(args).then(() =>
assertFileExists(`${projectDir}/.hz`, 'config.toml')));
it('creates the .hz/secrets.toml file', () =>
init.run(args).then(() =>
assertFileExists(`${projectDir}/.hz`, 'secrets.toml')));
it('creates the .hz/schema.toml file', () =>
init.run(args).then(() =>
assertFileExists(`${projectDir}/.hz`, 'schema.toml')));
});
describe('when the project dir is not empty', () => {
beforeEach('initialize mockfs', () => {
mockFs({
[projectDir]: {
lib: { 'someFile.html': '<blink>Something</blink>' },
},
});
});
it("doesn't create the src dir", () =>
init.run(args).then(() =>
assertDirDoesntExist(projectDir, 'src')));
it("doesn't create the dist dir", () =>
init.run(args).then(() =>
assertDirDoesntExist(projectDir, 'dist')));
it("doesn't create an example dist/index.html", () => {
fs.mkdirSync(`${projectDir}/dist`);
return init.run(args).then(() =>
assertFileDoesntExist(`${projectDir}/dist`, 'index.html')
);
});
it("still creates the .hz dir if it doesn't exist", () =>
init.run(args).then(() =>
assertDirExists(projectDir, '.hz')));
it("doesn't create the .hz dir if it already exists", () => {
fs.mkdirSync(`${projectDir}/.hz`);
const beforeMtime = fs.statSync(`${projectDir}/.hz`).birthtime.getTime();
return init.run(args).then(() => {
const afterMtime = fs.statSync(`${projectDir}/.hz`).birthtime.getTime();
assert.equal(beforeMtime, afterMtime, '.hz was modified');
});
});
it("creates a valid config.toml if it doesn't exist", () => {
fs.mkdirSync(`${projectDir}/.hz`);
return init.run(args).then(() => {
assertFileExists(`${projectDir}/.hz`, 'config.toml');
assertValidConfig(`${projectDir}/.hz/config.toml`);
});
});
it("creates a valid secrets.toml if it doesn't exist", () => {
fs.mkdirSync(`${projectDir}/.hz`);
return init.run(args).then(() => {
assertFileExists(`${projectDir}/.hz`, 'secrets.toml');
assertValidSecrets(`${projectDir}/.hz/secrets.toml`);
});
});
it("creates a valid schema.toml if it doesn't exist", () => {
fs.mkdirSync(`${projectDir}/.hz`);
return init.run(args).then(() => {
assertFileExists(`${projectDir}/.hz`, 'schema.toml');
// TODO: assertValidSchema(`${projectDir}/.hz/schema.toml`);
});
});
it("doesn't touch the config.toml if it already exists", () => {
fs.mkdirSync(`${projectDir}/.hz`);
const filename = `${projectDir}/.hz/config.toml`;
fs.appendFileSync(filename, '#Hoo\n');
const beforeMtime = fs.statSync(filename).mtime.getTime();
return init.run(args).then(() => {
const afterMtime = fs.statSync(filename).mtime.getTime();
assert.equal(beforeMtime, afterMtime);
const afterContents = getFileString(filename);
assert.equal('#Hoo\n', afterContents);
});
});
it("doesn't touch the secrets.toml if it already exists", () => {
fs.mkdirSync(`${projectDir}/.hz`);
const filename = `${projectDir}/.hz/secrets.toml`;
fs.appendFileSync(filename, '#Hoo\n');
const beforeMtime = fs.statSync(filename).mtime.getTime();
return init.run(args).then(() => {
const afterMtime = fs.statSync(filename).mtime.getTime();
assert.equal(beforeMtime, afterMtime);
const afterContents = getFileString(filename);
assert.equal('#Hoo\n', afterContents);
});
});
it("doesn't touch the schema.toml if it already exists", () => {
fs.mkdirSync(`${projectDir}/.hz`);
const filename = `${projectDir}/.hz/schema.toml`;
fs.appendFileSync(filename, '#Hoo\n');
const beforeMtime = fs.statSync(filename).mtime.getTime();
return init.run(args).then(() => {
const afterMtime = fs.statSync(filename).mtime.getTime();
assert.equal(beforeMtime, afterMtime);
const afterContents = getFileString(filename);
assert.equal('#Hoo\n', afterContents);
});
});
});
});
describe('when not passed a project name', () => {
const args = [ ];
it('stays in the current directory', () => {
mockFs();
return init.run(args).then(() => {
const afterCwd = process.cwd();
assert.equal(original_dir, afterCwd, 'init changed directories');
});
});
describe('in an empty directory', () => {
beforeEach('initialize mockfs', () => mockFs({}));
it('creates the src dir', () =>
init.run(args).then(() =>
assertDirExists(original_dir, 'src')));
it('creates the dist dir', () =>
init.run(args).then(() =>
assertDirExists(original_dir, 'dist')));
it('creates an example dist/index.html', () =>
init.run(args).then(() =>
assertFileExists(`${original_dir}/dist`, 'index.html')));
it('creates the .hz dir', () =>
init.run(args).then(() =>
assertDirExists(original_dir, '.hz')));
it('creates the .hz/config.toml file', () =>
init.run(args).then(() => {
assertFileExists(hz_dir, 'config.toml');
assertValidConfig(`${hz_dir}/config.toml`);
}));
});
describe('in a directory with files in it', () => {
beforeEach('initialize mocks', () => {
mockFs({
lib: { 'some_file.txt': 'Some file content' },
});
});
it("doesn't create the src dir", () =>
init.run(args).then(() =>
assertDirDoesntExist(original_dir, 'src')));
it("doesn't create the dist dir", () =>
init.run(args).then(() =>
assertDirDoesntExist(original_dir, 'dist')));
it("doesn't create an example dist/index.html", () => {
fs.mkdirSync(`${original_dir}/dist`);
return init.run(args).then(() =>
assertFileDoesntExist(`${original_dir}/dist`, 'index.html')
);
});
it("creates the .hz dir if it doesn't exist", () =>
init.run(args).then(() =>
assertDirExists(original_dir, '.hz')));
it("doesn't create the .hz dir if it exists", () => {
fs.mkdirSync(hz_dir);
const beforeTime = fs.statSync(hz_dir).birthtime.getTime();
return init.run(args).then(() => {
assertDirExists(original_dir, '.hz');
const afterTime = fs.statSync(hz_dir).birthtime.getTime();
assert.equal(beforeTime, afterTime, '.hz birthtime changed');
});
});
it("creates the config.toml if it doesn't exist", () =>
init.run(args).then(() => {
assertFileExists(hz_dir, 'config.toml');
assertValidConfig(`${hz_dir}/config.toml`);
})
);
it("doesn't touch the config.toml if it already exists", () => {
fs.mkdirSync(hz_dir);
const filename = `${hz_dir}/config.toml`;
fs.appendFileSync(filename, '#Hoo\n');
const beforeMtime = fs.statSync(filename).mtime.getTime();
return init.run(args).then(() => {
const afterMtime = fs.statSync(filename).mtime.getTime();
assert.equal(beforeMtime, afterMtime);
const afterContents = getFileString(filename);
assert.equal('#Hoo\n', afterContents);
});
});
});
});
});
================================================
FILE: cli/test/schema.spec.js
================================================
'use strict';
const processApplyConfig = require('../src/schema').processApplyConfig;
const runApplyCommand = require('../src/schema').runApplyCommand;
const runSaveCommand = require('../src/schema').runSaveCommand;
const parse_schema = require('../src/schema').parse_schema;
const start_rdb_server = require('../src/utils/start_rdb_server');
const rm_sync_recursive = require('../src/utils/rm_sync_recursive');
const assert = require('assert');
const fs = require('fs');
const r = start_rdb_server.r;
const mockFs = require('mock-fs');
const tmpdir = require('os').tmpdir;
const project_name = 'schema_test';
const v1_schema = `
[collections.test_messages]
indexes = ["datetime"]
[groups.admin]
[groups.admin.rules.carte_blanche]
template = "any()"
`;
const v2_schema = `# This is a TOML document
[collections.users]
[collections.test_messages]
[[collections.test_messages.indexes]]
fields = [["a","b"],["c"]]
[[collections.test_messages.indexes]]
fields = [["datetime"]]
[groups.admin]
[groups.admin.rules.carte_blanche]
template = "any()"
`;
const brokenTestSchema = `
[collectiosfklajsfns.test_messages]
indexes = ["datetime"]
[groups.adminasklfjasf]
[groups.admin.rules.carte_blanche]
template = "any()a;lkdfjlakjf;ladkfjal;kfj"
`;
const fs_with_schema = (schema) => {
mockFs({ '.hz': { 'schema.toml': schema } });
};
describe('hz schema', () => {
const rdb_data_dir = `${tmpdir()}/horizon-test-${process.pid}`;
let rdb_conn, rdb_server;
before('start rethinkdb', () =>
start_rdb_server({
quiet: true,
dataDir: rdb_data_dir,
}).then((server) => {
rdb_server = server;
})
);
after('stop rethinkdb', () => rdb_server && rdb_server.close());
after('delete rethinkdb data directory', () => rm_sync_recursive(rdb_data_dir));
before('connect to rethinkdb', () =>
rdb_server.connect().then((conn) => {
rdb_conn = conn;
})
);
beforeEach('initialize mockfs', () => fs_with_schema(v2_schema));
afterEach('restore fs', () => mockFs.restore());
describe('save', () => {
before('initialize database', () => {
fs_with_schema(v2_schema);
return runApplyCommand(processApplyConfig({
start_rethinkdb: false,
schema_file: '.hz/schema.toml',
project_name,
connect: `localhost:${rdb_server.driver_port}`,
}));
});
after('clear database', () =>
r.branch(
r.dbList().contains(project_name),
r.dbDrop(project_name),
null
).run(rdb_conn)
);
it('renames previous schema.toml if it already exists', () =>
runSaveCommand({
start_rethinkdb: false,
rdb_host: 'localhost',
rdb_port: rdb_server.driver_port,
out_file: '.hz/schema.toml',
project_name,
}).then(() =>
assert.equal(fs.readdirSync('.hz').length, 2, 'backup schema file not created')
)
);
it('saves schema to schema.toml from rdb', () =>
runSaveCommand({
start_rethinkdb: false,
rdb_host: 'localhost',
rdb_port: rdb_server.driver_port,
out_file: 'out.toml',
project_name,
}).then(() =>
assert.strictEqual(fs.readFileSync('out.toml', 'utf8'), v2_schema)
)
);
});
describe('apply', () => {
afterEach('clear database', () =>
r.branch(
r.dbList().contains(project_name),
r.dbDrop(project_name),
null
).run(rdb_conn)
);
it('applies v1.x schema to rdb from schema.toml', () => {
fs_with_schema(v1_schema);
const config = processApplyConfig({
connect: `localhost:${rdb_server.driver_port}`,
schema_file: '.hz/schema.toml',
start_rethinkdb: false,
update: true,
force: true,
secure: false,
permissions: false,
project_name,
});
// Apply settings into RethinkDB
return runApplyCommand(config).then(() =>
// Check that the project database exists
r.dbList().contains(project_name).run(rdb_conn)
).then((res) =>
assert(res, `${project_name} database is missing.`)
).then(() =>
r.db(project_name).table('test_messages').indexList().run(rdb_conn)
).then((indexes) => {
// Check that the expected indexes exist on the expected table
assert(indexes.indexOf('hz_[["datetime"]]') !== -1, '"datetime" index is missing');
});
});
it('applies v2.x schema to rdb from schema.toml', () => {
const config = processApplyConfig({
connect: `localhost:${rdb_server.driver_port}`,
schema_file: '.hz/schema.toml',
start_rethinkdb: false,
update: true,
force: true,
secure: false,
permissions: false,
project_name,
});
// Apply settings into RethinkDB
return runApplyCommand(config).then(() =>
// Check that the project database exists
r.dbList().contains(project_name).run(rdb_conn)
).then((res) =>
assert(res, `${project_name} database is missing.`)
).then(() =>
r.db(project_name).table('test_messages').indexList().run(rdb_conn)
).then((indexes) => {
// Check that the expected indexes exist on the expected table
assert(indexes.indexOf('hz_[["datetime"]]') !== -1, '"datetime" index is missing');
assert(indexes.indexOf('hz_[["a","b"],["c"]]') !== -1, '[["a","b"],["c"]] index is missing');
});
});
});
describe('given a schema.toml file', () => {
it('can parse a valid v1.x schema.toml file', () => {
parse_schema(v1_schema);
});
it('can parse a valid v2.x schema.toml file', () => {
parse_schema(v2_schema);
});
it('fails parsing invalid schema.toml file', () => {
assert.throws(() => {
parse_schema(brokenTestSchema);
}, /"collectiosfklajsfns" is not allowed/);
});
it('can read a vaild schema.toml file', () =>
processApplyConfig({
start_rethinkdb: true,
schema_file: '.hz/schema.toml',
update: true,
force: true,
})
);
});
});
================================================
FILE: cli/test/serve.spec.js
================================================
'use strict';
const serve = require('../src/serve');
const schema = require('../src/schema');
const start_rdb_server = require('../src/utils/start_rdb_server');
const rm_sync_recursive = require('../src/utils/rm_sync_recursive');
const tmpdir = require('os').tmpdir;
const assert = require('chai').assert;
const mockFs = require('mock-fs');
const project_name = 'test_app';
const serve_args = [
'--secure=false',
'--port=0',
'--auto-create-collection',
`--project-name=${project_name}`,
`--token-secret=test-token`
];
const make_args = (args) => serve_args.concat(args);
const valid_project_data = {
'.hz': {
'config.toml': 'project_name = "projectName"\n',
},
};
describe('hz serve', () => {
const rdb_data_dir = `${tmpdir()}/horizon-test-${process.pid}`;
let rdb_server;
before('start rethinkdb', () =>
start_rdb_server({
quiet: true,
dataDir: rdb_data_dir,
}).then((server) => {
rdb_server = server;
serve_args.push(`--connect=localhost:${rdb_server.driver_port}`);
})
);
// Run schema apply with a blank schema
before('initialize rethinkdb', () => {
mockFs({ 'schema.toml': '' });
return schema.run([ 'apply', 'schema.toml',
`--connect=localhost:${rdb_server.driver_port}`,
`--project-name=${project_name}` ])
.then(() => mockFs.restore());
});
after('stop rethinkdb', () => rdb_server && rdb_server.close());
after('delete rethinkdb data directory', () => rm_sync_recursive(rdb_data_dir));
afterEach('restore mockfs', () => mockFs.restore());
describe('with a project path', () => {
beforeEach('initialize mockfs', () => mockFs({ [project_name]: valid_project_data }));
it('changes to the project directory', () => {
const before_dir = process.cwd();
return serve.run(make_args([ project_name ]), Promise.resolve()).then(() =>
assert.strictEqual(`${before_dir}/${project_name}`, process.cwd(),
'directory should have changed')
);
});
it('fails if the .hz dir does not exist', () => {
mockFs({ [project_name]: {} });
return serve.run(make_args([ project_name ]), Promise.resolve()).then(() =>
assert(false, 'should have failed because the .hz directory is missing')
).catch((err) =>
assert.throws(() => { throw err; }, /doesn't contain an .hz directory/)
);
});
it('continues if .hz dir does exist', () =>
serve.run(make_args([ project_name ]), Promise.resolve())
);
});
describe('without a project path', () => {
beforeEach('initialize mockfs', () => mockFs(valid_project_data));
it('does not change directories', () => {
const before_dir = process.cwd();
return serve.run(make_args([ '.' ]), Promise.resolve()).then(() =>
assert.strictEqual(before_dir, process.cwd(), 'directory should not have changed')
);
});
it('fails if the .hz dir does not exist', () => {
mockFs({ });
return serve.run(make_args([ '.' ]), Promise.resolve()).then(() =>
assert(false, 'should have failed because the .hz directory is missing')
).catch((err) =>
assert.throws(() => { throw err; }, /doesn't contain an .hz directory/)
);
});
it('continues if .hz dir does exist', () =>
serve.run(make_args([ '.' ]), Promise.resolve())
);
});
});
================================================
FILE: cli/test/unit/check-project-name.js
================================================
/* global describe, it */
'use strict';
const checkProjectName = require('../../src/utils/check-project-name');
const assert = require('chai').assert;
describe('checkProjectName', () => {
describe('when passed null for a directory', () => {
const prospectiveName = null;
const dirList = [];
it("doesn't change directory", (done) => {
const goodCwd = '/foo/bar/Ba_z';
const res = checkProjectName(prospectiveName, goodCwd, dirList);
assert.propertyVal(res, 'chdirTo', false);
assert.propertyVal(res, 'dirName', 'Ba_z');
assert.propertyVal(res, 'projectName', 'Ba_z');
assert.propertyVal(res, 'createDir', false);
done();
});
it('throws if the cwd has invalid chars', (done) => {
const badCwd = '/foo/bar/b*a&z';
assert.throws(() => {
checkProjectName(prospectiveName, badCwd, dirList);
}, '*&');
done();
});
it('sets projectName to dehyphenated cwd if fixable', (done) => {
const fixableCwd = '/foo/bar/ba-z';
const res = checkProjectName(
prospectiveName, fixableCwd, dirList);
assert.propertyVal(res, 'projectName', 'ba_z');
assert.propertyVal(res, 'dirName', 'ba-z');
assert.propertyVal(res, 'chdirTo', false);
assert.propertyVal(res, 'createDir', false);
done();
});
});
describe('when passed "." as a directory', () => {
const prospectiveName = '.';
const dirList = [];
it("doesn't change directory", (done) => {
const goodCwd = '/foo/bar/Ba_z';
const res = checkProjectName(prospectiveName, goodCwd, dirList);
assert.propertyVal(res, 'chdirTo', false);
assert.propertyVal(res, 'dirName', 'Ba_z');
assert.propertyVal(res, 'projectName', 'Ba_z');
assert.propertyVal(res, 'createDir', false);
done();
});
it('throws if the cwd has invalid chars', (done) => {
const badCwd = '/foo/bar/b*a&z';
assert.throws(() => {
checkProjectName(prospectiveName, badCwd, dirList);
}, '*&');
done();
});
it('sets projectName to dehyphenated cwd if fixable', (done) => {
const fixableCwd = '/foo/bar/ba-z';
const res = checkProjectName(prospectiveName, fixableCwd, dirList);
assert.propertyVal(res, 'projectName', 'ba_z');
assert.propertyVal(res, 'dirName', 'ba-z');
assert.propertyVal(res, 'chdirTo', false);
assert.propertyVal(res, 'createDir', false);
done();
});
});
describe('when passed a non-existing directory', () => {
const dirList = [ 'a', 'b', 'c' ];
const cwd = '/foo/bar';
it('creates the directory when name is valid', (done) => {
const results = checkProjectName('Ba_z9', cwd, dirList);
assert.propertyVal(results, 'projectName', 'Ba_z9');
assert.propertyVal(results, 'createDir', true);
assert.propertyVal(results, 'chdirTo', '/foo/bar/Ba_z9');
assert.propertyVal(results, 'dirName', 'Ba_z9');
done();
});
it('creates the directory when name is fixable', (done) => {
const results = checkProjectName('Ba-z9', cwd, dirList);
assert.propertyVal(results, 'projectName', 'Ba_z9');
assert.propertyVal(results, 'createDir', true);
assert.propertyVal(results, 'chdirTo', '/foo/bar/Ba-z9');
assert.propertyVal(results, 'dirName', 'Ba-z9');
done();
});
it('throws an error if the name is not fixable', (done) => {
assert.throws(() => {
checkProjectName('Some*Bad+Name', cwd, dirList);
}, '*+');
done();
});
});
describe('when passed an existing directory', () => {
const dirList = [ 'a', 'Ba-z', 'B^a%z', 'Ba_z9' ];
const cwd = '/foo/bar';
it('errors if given an invalid projectName', (done) => {
assert.throws(() => {
checkProjectName('B^%z', cwd, dirList);
}, '^%');
done();
});
it('changes directory if the name is good', (done) => {
const res = checkProjectName('Ba_z9', cwd, dirList);
assert.propertyVal(res, 'dirName', 'Ba_z9');
assert.propertyVal(res, 'projectName', 'Ba_z9');
assert.propertyVal(res, 'chdirTo', '/foo/bar/Ba_z9');
assert.propertyVal(res, 'createDir', false);
done();
});
it('changes directory if the name is fixable', (done) => {
const res = checkProjectName('Ba-z', cwd, dirList);
assert.propertyVal(res, 'dirName', 'Ba-z');
assert.propertyVal(res, 'projectName', 'Ba_z');
assert.propertyVal(res, 'chdirTo', '/foo/bar/Ba-z');
assert.propertyVal(res, 'createDir', false);
done();
});
});
});
================================================
FILE: cli/test/unit/nice_error.js
================================================
'use strict';
const stripAnsi = require('strip-ansi');
const assert = require('chai').assert;
const NiceError = require('../../src/utils/nice_error');
const fakeFile = `\
some = fake, syntax
next := some(1, 2, 3)
def foo(bar) {
-- what language is this?
}
`;
describe('NiceError', () => {
describe('._sourceLine', () => {
it('should have blue line number and white source text', (done) => {
const inputs = [
{ src: 'foo bar', line: 2 },
{ src: 'baz wux', line: 200 },
{ src: ' a b c d e', line: 2000 },
];
const expected = [
'\u001b[34m2:\u001b[39m' + ' ' + '\u001b[37mfoo bar\u001b[39m',
'\u001b[34m200:\u001b[39m' + ' ' + '\u001b[37mbaz wux\u001b[39m',
'\u001b[34m2000:\u001b[39m' + ' ' + '\u001b[37m a b c d e\u001b[39m',
];
const results = inputs.map(NiceError._sourceLine.bind(NiceError));
assert.deepEqual(results, expected);
done();
});
});
describe('._extractContext', () => {
it('can get one line of context from the middle', (done) => {
const line = 3;
const contextSize = 1;
const expected = [
{ line: 2, src: 'next := some(1, 2, 3)' },
{ line: 3, src: 'def foo(bar) {' },
{ line: 4, src: ' -- what language is this?' },
];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
it('can get a size 2 context', (done) => {
const line = 3;
const contextSize = 2;
const expected = [
{ line: 1, src: 'some = fake, syntax' },
{ line: 2, src: 'next := some(1, 2, 3)' },
{ line: 3, src: 'def foo(bar) {' },
{ line: 4, src: ' -- what language is this?' },
{ line: 5, src: '}' },
];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
it('can gets a size 2 context with 1 line below it', (done) => {
const line = 2;
const contextSize = 2;
const expected = [
{ line: 1, src: 'some = fake, syntax' },
{ line: 2, src: 'next := some(1, 2, 3)' },
{ line: 3, src: 'def foo(bar) {' },
{ line: 4, src: ' -- what language is this?' },
];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
it('can gets a size 3 context with 0 lines below it', (done) => {
const line = 1;
const contextSize = 3;
const expected = [
{ line: 1, src: 'some = fake, syntax' },
{ line: 2, src: 'next := some(1, 2, 3)' },
{ line: 3, src: 'def foo(bar) {' },
{ line: 4, src: ' -- what language is this?' },
];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
it('can gets a size 3 context with 0 lines after it', (done) => {
const line = 6;
const contextSize = 3;
const expected = [
{ line: 3, src: 'def foo(bar) {' },
{ line: 4, src: ' -- what language is this?' },
{ line: 5, src: '}' },
{ line: 6, src: '' },
];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
it('returns an empty array if line out of bounds', (done) => {
const line = 7;
const contextSize = 3;
const expected = [];
const results = NiceError._extractContext(fakeFile, line, contextSize);
assert.deepEqual(results, expected);
done();
});
});
describe('.toString', () => {
const message = 'This is an error message';
it('is compatible with a basic Error', (done) => {
const error = new NiceError(message);
const result = error.toString();
assert.deepEqual(result, message);
done();
});
it('only displays the basic message', (done) => {
const error = new NiceError(message, {
description: 'Some long description',
suggestions: [
'Suggestion A',
'Suggestion B',
],
src: {
filename: 'fakety.txt',
contents: 'File contents',
line: 0,
column: 0,
},
});
const result = error.toString();
assert.deepEqual(result, message);
done();
});
});
describe('.niceString', () => {
const message = 'Some kinda message';
const description = `A much longer description here that may span \
many lines and be just really ridiculously long in order to completely \
explain what's going on.`
const filename = './fake.dx';
const line = 2;
const column = 6;
const suggestions = [
'Always call your mother',
'Never lie to your mother about being robbed in Rio',
];
let error;
beforeEach(() => {
error = new NiceError(message, {
description: description,
src: {
filename,
contents: fakeFile,
line,
column,
},
suggestions,
});
});
it('shows the description if present', (done) => {
error.src = null;
error.suggestions = null;
const expected = `\
${message}
${description}
`;
const result = stripAnsi(error.niceString());
assert.deepEqual(result, expected);
done();
});
it('returns a carrot in the right place with source', (done) => {
error.suggestions = null;
const expected = `\
${message}
${description}
In ./fake.dx, line 2, column 6:
1: some = fake, syntax
2: next := some(1, 2, 3)
^
3: def foo(bar) {
4: -- what language is this?
`;
const result = stripAnsi(error.niceString());
assert.deepEqual(result, expected);
done();
});
it('returns a list of suggestions if present', (done) => {
error.src = null;
error.description = null;
const expected = `\
${message}
Suggestions:
➤ ${suggestions[0]}
➤ ${suggestions[1]}
`;
const results = stripAnsi(error.niceString());
assert.deepEqual(results, expected);
done();
});
it('shows both suggestions and source if present', (done) => {
error.description = null;
error.suggestions.shift();
error.src.line = 5;
error.src.column = 1;
const expected = `\
${message}
In ./fake.dx, line 5, column 1:
2: next := some(1, 2, 3)
3: def foo(bar) {
4: -- what language is this?
5: }
^
6: \
Suggestion:
➤ ${suggestions[0]}
`;
/* Note: there's an extra space in the string literal above on
* the line starting with "6:". This could be removed, but a lot
* of text editors are set to remove trailing spaces on save, so
* a backslash and an extra newline are a workaround to avoid
* the editor mucking with it. */
const results = stripAnsi(error.niceString({ contextSize: 3 }));
assert.deepEqual(results, expected);
done();
});
});
});
================================================
FILE: cli/test/version.spec.js
================================================
'use strict';
const versionCommand = require('../src/version');
const assert = require('assert');
const sinon = require('sinon');
const pkg = require('../package.json');
describe('hz version', () => {
beforeEach(() => sinon.stub(console, 'info'));
afterEach(() => console.info.restore());
it('prints the version and exits', () =>
versionCommand.run().then(() =>
assert.equal(console.info.args[0][0], pkg.version)));
});
================================================
FILE: client/.eslintrc.js
================================================
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
extends: "../.eslintrc.js",
rules: {
"arrow-parens": [ ERROR, "as-needed" ],
"no-confusing-arrow": [ OFF ],
"no-use-before-define": [ OFF ],
"semi": [ ERROR, "never" ],
"max-len": [ ERROR, 80, 2 ],
},
env: {
"browser": true,
"commonjs": true,
"es6": true,
"mocha": true,
},
parser: "babel-eslint",
};
================================================
FILE: client/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
build/
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# database stuff
rethinkdb_data/
rethinkdb_data_test/
================================================
FILE: client/README.md
================================================
# Horizon Client Library
The 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.
## Building
Running `npm install` for the first time will build the browser bundle and lib files.
1. `npm install`
2. `npm run dev` (or `npm run build` or `npm run compile`, see below)
### Build Options
Command | Description
--------------------|----------------------------
npm run build | Build dist/horizon.js minified production browser bundle
npm run builddebug | Build with webpack and output debug logging
npm run compile | Compile src to lib for CommonJS module loaders (such as webpack, browserify)
npm run coverage | Run code coverage tool - `istanbul`
npm run dev | Watch directory for changes, build dist/horizon.js unminified browser bundle
npm run devtest | Serve `dist` directory to build app and continuously run tests
npm test | Run tests in node
npm run lint -s | Lint src
npm run test | Run tests
## Running tests
* `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`.
* 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.
## Docs
### Getting Started
[samuelhughes.com/rethinkdb/horizon-docs/docs/getting-started.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/getting-started.html)
### APIs
* Horizon API - [samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html](http://samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html)
* Collection API - [samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html](http://samuelhughes.com/rethinkdb/horizon-docs/api/horizon.html)
## Users and Groups
[samuelhughes.com/rethinkdb/horizon-docs/docs/users.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/users.html)
## Setting Permissions
[samuelhughes.com/rethinkdb/horizon-docs/docs/permissions.html](http://samuelhughes.com/rethinkdb/horizon-docs/docs/permissions.html)
### Clearing tokens
Sometimes you may wish to delete all authentication tokens from localStorage. You can do that with:
``` js
// Note the 'H'
Horizon.clearAuthTokens()
```
================================================
FILE: client/index.d.ts
================================================
import { Observable } from 'rxjs';
declare namespace hz {
interface Feed {
watch (options?: { rawChanges: boolean }): Observable<any>;
fetch (): Observable<any>;
}
type Bound = 'open' | 'closed';
type Direction = 'ascending' | 'descending';
type Primitive = boolean | number | string | Date;
type IdValue = Primitive | Primitive[] | { id: Primitive };
type WriteOp = Object | Object[];
interface TermBase extends Feed {
find (value: IdValue): TermBase;
findAll (...values: IdValue[]): TermBase;
order (fields: string[], direction?: Direction): TermBase;
limit (size: Number): TermBase;
above (spec: any, bound?: Bound): TermBase;
below (spec: any, bound?: Bound): TermBase;
}
interface Collection extends TermBase {
store (docs: WriteOp): Observable<any>;
upsert (docs: WriteOp): Observable<any>;
insert (docs: WriteOp): Observable<any>;
replace (docs: WriteOp): Observable<any>;
update (docs: WriteOp): Observable<any>;
remove (docs: IdValue): Observable<any>;
removeAll (docs: IdValue[]): Observable<any>;
}
interface User extends Feed {}
interface HorizonInstance {
(name: string): Collection;
currentUser (): User;
hasAuthToken (): boolean;
authEndpoint (name: string): Observable<string>;
aggregate (aggs: any): TermBase;
model (fn: Function): TermBase;
disconnect (): void;
connect (): void;
status (): Observable<any>;
onReady (): Observable<any>;
onDisconnected (): Observable<any>;
onSocketError (): Observable<any>;
}
interface HorizonOptions {
host?: string;
path?: string;
secure?: boolean;
authType?: string;
lazyWrites?: boolean;
keepalive?: number;
WebSocketCtor?: any;
}
interface Horizon {
(options: HorizonOptions): HorizonInstance;
clearAuthTokens (): void;
}
}
export type HorizonOptions = hz.HorizonOptions;
export type HorizonInstance = hz.HorizonInstance;
export type TermBase = hz.TermBase;
export type Collection = hz.Collection;
export type User = hz.User;
declare var Horizon: hz.Horizon;
export default Horizon;
================================================
FILE: client/package.json
================================================
{
"name": "@horizon/client",
"version": "2.0.0",
"description": "RethinkDB Horizon is an open-source developer platform for building realtime, scalable web apps.",
"scripts": {
"coverage": "cross-env NODE_ENV=test nyc mocha test/test.js",
"dev": "webpack --watch --progress --colors",
"devtest": "nodemon --watch dist --exec 'npm test -- --reporter dot && npm run lint -s'",
"builddebug": "webpack --progress --colors --display-modules --display-reasons",
"build": "cross-env NODE_ENV=production webpack --progress --colors",
"compile": "node ./scripts/compile.js",
"lint": "eslint src",
"test": "mocha dist/test.js --inline-diffs --timeout 10000",
"prepublish": "npm run compile && npm run build"
},
"dependencies": {
"babel-runtime": "^6.6.1",
"core-js": "^2.1.0",
"deep-equal": "^1.0.1",
"es6-promise": "^3.2.1",
"is-plain-object": "^2.0.1",
"rxjs": "5.0.0-beta.11",
"snake-case": "^2.1.0",
"ws": "^1.1.0"
},
"engines": {
"node": ">=4.0.0"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-core": "^6.10.4",
"babel-eslint": "^6.0.0-beta",
"babel-loader": "^6.2.4",
"babel-plugin-istanbul": "^1.0.3",
"babel-plugin-transform-runtime": "^6.6.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-loose": "^7.0.0",
"babel-register": "^6.9.0",
"chai": "^3.5.0",
"copy-webpack-plugin": "^3.0.1",
"cross-env": "^2.0.0",
"eslint": "^7.3.1",
"exports-loader": "^0.6.3",
"imports-loader": "^0.6.5",
"istanbul": "^0.4.2",
"lodash.clonedeep": "^4.4.1",
"lodash.sortby": "^4.6.1",
"mocha": "^2.5.3",
"nodemon": "^1.9.1",
"nyc": "^7.0.0",
"shelljs": "^0.7.0",
"source-map-support": "^0.4.0",
"webpack": "^1.12.14"
},
"main": "lib/index.js",
"jsmain:next": "src/index.js",
"repository": {
"type": "git",
"url": "https://github.com/rethinkdb/horizon.git"
},
"author": "RethinkDB",
"license": "ISC",
"bugs": {
"url": "https://github.com/rethinkdb/horizon/issues"
},
"homepage": "https://github.com/rethinkdb/horizon",
"files": [
"dist/horizon.js",
"dist/horizon.js.map",
"dist/horizon-dev.js",
"dist/horizon-dev.js.map",
"dist/horizon-core.js",
"dist/horizon-core.js.map",
"dist/horizon-core-dev.js",
"dist/horizon-core-dev.js.map",
"lib/*"
],
"babel": {
"presets": [
"es2015-loose",
{
"plugins": [
[
"transform-runtime",
{
"polyfill": false
}
]
]
}
],
"env": {
"test": {
"plugins": [
"istanbul"
]
}
}
},
"nyc": {
"all": true,
"statements": 82.02,
"branches": 72.51,
"functions": 87.42,
"lines": 81.89,
"cache": true,
"check-coverage": true,
"include": [
"src/**/*.js"
],
"require": [
"babel-register"
],
"reporter": [
"lcov",
"text-summary",
"html"
],
"sourceMap": false,
"instrument": false
}
}
================================================
FILE: client/scripts/compile.js
================================================
require('shelljs/global')
// remove existing lib files
rm('-rf', 'lib/**/*')
// compile with babel
if (exec('babel src --out-dir lib --extends src/.babelrc --source-maps true').code !== 0) {
echo('error: babel couldn\'t build source, if EACCESS error, check access rights')
exit(1)
}
================================================
FILE: client/src/ast.js
================================================
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/empty'
import 'rxjs/add/operator/publishReplay'
import 'rxjs/add/operator/scan'
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/toArray'
import 'rxjs/add/operator/defaultIfEmpty'
import 'rxjs/add/operator/ignoreElements'
import 'rxjs/add/operator/merge'
import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/operator/take'
import snakeCase from 'snake-case'
import deepEqual from 'deep-equal'
import checkArgs from './util/check-args'
import validIndexValue from './util/valid-index-value.js'
import { serialize } from './serialization.js'
import watchRewrites from './hacks/watch-rewrites'
/**
@this TermBase
Validation check to throw an exception if a method is chained onto a
query that already has it. It belongs to TermBase, but we don't want
to pollute the objects with it (since it isn't useful to api users),
so it's dynamically bound with .call inside methods that use it.
*/
function checkIfLegalToChain(key) {
if (this._legalMethods.indexOf(key) === -1) {
throw new Error(`${key} cannot be called on the current query`)
}
if (snakeCase(key) in this._query) {
throw new Error(`${key} has already been called on this query`)
}
}
// Abstract base class for terms
export class TermBase {
constructor(sendRequest, query, legalMethods) {
this._sendRequest = sendRequest
this._query = query
this._legalMethods = legalMethods
}
toString() {
let string = `Collection('${this._query.collection}')`
if (this._query.find) {
string += `.find(${JSON.stringify(this._query.find)})`
}
if (this._query.find_all) {
string += `.findAll(${JSON.stringify(this._query.find_all)})`
}
if (this._query.order) {
string += `.order(${JSON.stringify(this._query.order[0])}, ` +
`${JSON.stringify(this._query.order[1])})`
}
if (this._query.above) {
string += `.above(${JSON.stringify(this.query.above[0])}, ` +
`${JSON.stringify(this.query.above[1])})`
}
if (this._query.below) {
string += `.below(${JSON.stringify(this.query.below[0])}, ` +
`${JSON.stringify(this.query.below[1])})`
}
if (this._query.limit) {
string += `.limit(${JSON.stringify(this._query.limit)})`
}
return string
}
// Returns a sequence of the result set. Every time it changes the
// updated sequence will be emitted. If raw change objects are
// needed, pass the option 'rawChanges: true'. An observable is
// returned which will lazily emit the query when it is subscribed
// to
watch({ rawChanges = false } = {}) {
const query = watchRewrites(this, this._query)
const raw = this._sendRequest('subscribe', query)
if (rawChanges) {
return raw
} else {
return makePresentable(raw, this._query)
}
}
// Grab a snapshot of the current query (non-changefeed). Emits an
// array with all results. An observable is returned which will
// lazily emit the query when subscribed to
fetch() {
const raw = this._sendRequest('query', this._query).map(val => {
delete val.$hz_v$
return val
})
if (this._query.find) {
return raw.defaultIfEmpty(null)
} else {
return raw.toArray()
}
}
findAll(...fieldValues) {
checkIfLegalToChain.call(this, 'findAll')
checkArgs('findAll', arguments, { maxArgs: 100 })
return new FindAll(this._sendRequest, this._query, fieldValues)
}
find(idOrObject) {
checkIfLegalToChain.call(this, 'find')
checkArgs('find', arguments)
return new Find(this._sendRequest, this._query, idOrObject)
}
order(fields, direction = 'ascending') {
checkIfLegalToChain.call(this, 'order')
checkArgs('order', arguments, { minArgs: 1, maxArgs: 2 })
return new Order(this._sendRequest, this._query, fields, direction)
}
above(aboveSpec, bound = 'closed') {
checkIfLegalToChain.call(this, 'above')
checkArgs('above', arguments, { minArgs: 1, maxArgs: 2 })
return new Above(this._sendRequest, this._query, aboveSpec, bound)
}
below(belowSpec, bound = 'open') {
checkIfLegalToChain.call(this, 'below')
checkArgs('below', arguments, { minArgs: 1, maxArgs: 2 })
return new Below(this._sendRequest, this._query, belowSpec, bound)
}
limit(size) {
checkIfLegalToChain.call(this, 'limit')
checkArgs('limit', arguments)
return new Limit(this._sendRequest, this._query, size)
}
}
// Turn a raw observable of server responses into user-presentable events
//
// `observable` is the base observable with full responses coming from
// the HorizonSocket
// `query` is the value of `options` in the request
function makePresentable(observable, query) {
// Whether the entire data structure is in each change
const pointQuery = Boolean(query.find)
if (pointQuery) {
let hasEmitted = false
const seedVal = null
// Simplest case: just pass through new_val
return observable
.filter(change => !hasEmitted || change.type !== 'state')
.scan((previous, change) => {
hasEmitted = true
if (change.new_val != null) {
delete change.new_val.$hz_v$
}
if (change.old_val != null) {
delete change.old_val.$hz_v$
}
if (change.state === 'synced') {
return previous
} else {
return change.new_val
}
}, seedVal)
} else {
const seedVal = { emitted: false, val: [] }
return observable
.scan((state, change) => {
if (change.new_val != null) {
delete change.new_val.$hz_v$
}
if (change.old_val != null) {
delete change.old_val.$hz_v$
}
if (change.state === 'synced') {
state.emitted = true
}
state.val = applyChange(state.val.slice(), change)
return state
}, seedVal)
.filter(state => state.emitted)
.map(x => x.val)
}
}
export function applyChange(arr, change) {
switch (change.type) {
case 'remove':
case 'uninitial': {
// Remove old values from the array
if (change.old_offset != null) {
arr.splice(change.old_offset, 1)
} else {
const index = arr.findIndex(x => deepEqual(x.id, change.old_val.id))
if (index === -1) {
// Programming error. This should not happen
throw new Error(
`change couldn't be applied: ${JSON.stringify(change)}`)
}
arr.splice(index, 1)
}
break
}
case 'add':
case 'initial': {
// Add new values to the array
if (change.new_offset != null) {
// If we have an offset, put it in the correct location
arr.splice(change.new_offset, 0, change.new_val)
} else {
// otherwise for unordered results, push it on the end
arr.push(change.new_val)
}
break
}
case 'change': {
// Modify in place if a change is happening
if (change.old_offset != null) {
// Remove the old document from the results
arr.splice(change.old_offset, 1)
}
if (change.new_offset != null) {
// Splice in the new val if we have an offset
arr.splice(change.new_offset, 0, change.new_val)
} else {
// If we don't have an offset, find the old val and
// replace it with the new val
const index = arr.findIndex(x => deepEqual(x.id, change.old_val.id))
if (index === -1) {
// indicates a programming bug. The server gives us the
// ordering, so if we don't find the id it means something is
// buggy.
throw new Error(
`change couldn't be applied: ${JSON.stringify(change)}`)
}
arr[index] = change.new_val
}
break
}
case 'state': {
// This gets hit if we have not emitted yet, and should
// result in an empty array being output.
break
}
default:
throw new Error(
`unrecognized 'type' field from server ${JSON.stringify(change)}`)
}
return arr
}
/** @this Collection
Implements writeOps for the Collection class
*/
function writeOp(name, args, documents) {
checkArgs(name, args)
let isBatch = true
let wrappedDocs = documents
if (!Array.isArray(documents)) {
// Wrap in an array if we need to
wrappedDocs = [ documents ]
isBatch = false
} else if (documents.length === 0) {
// Don't bother sending no-ops to the server
return Observable.empty()
}
const options = Object.assign(
{}, this._query, { data: serialize(wrappedDocs) })
let observable = this._sendRequest(name, options)
if (isBatch) {
// If this is a batch writeOp, each document may succeed or fail
// individually.
observable = observable.map(
resp => resp.error ? new Error(resp.error) : resp)
} else {
// If this is a single writeOp, the entire operation should fail
// if any fails.
const _prevOb = observable
observable = Observable.create(subscriber => {
_prevOb.subscribe({
next(resp) {
if (resp.error) {
// TODO: handle error ids when we get them
subscriber.error(new Error(resp.error))
} else {
subscriber.next(resp)
}
},
error(err) { subscriber.error(err) },
complete() { subscriber.complete() },
})
})
}
if (!this._lazyWrites) {
// Need to buffer response since this becomes a hot observable and
// when we subscribe matters
observable = observable.publishReplay().refCount()
observable.subscribe()
}
return observable
}
export class Collection extends TermBase {
constructor(sendRequest, collectionName, lazyWrites) {
const query = { collection: collectionName }
const legalMethods = [
'find', 'findAll', 'order', 'above', 'below', 'limit' ]
super(sendRequest, query, legalMethods)
this._lazyWrites = lazyWrites
}
store(documents) {
return writeOp.call(this, 'store', arguments, documents)
}
upsert(documents) {
return writeOp.call(this, 'upsert', arguments, documents)
}
insert(documents) {
return writeOp.call(this, 'insert', arguments, documents)
}
replace(documents) {
return writeOp.call(this, 'replace', arguments, documents)
}
update(documents) {
return writeOp.call(this, 'update', arguments, documents)
}
remove(documentOrId) {
const wrapped = validIndexValue(documentOrId) ?
{ id: documentOrId } : documentOrId
return writeOp.call(this, 'remove', arguments, wrapped)
}
removeAll(documentsOrIds) {
if (!Array.isArray(documentsOrIds)) {
throw new Error('removeAll takes an array as an argument')
}
const wrapped = documentsOrIds.map(item => {
if (validIndexValue(item)) {
return { id: item }
} else {
return item
}
})
return writeOp.call(this, 'removeAll', arguments, wrapped)
}
}
export class Find extends TermBase {
constructor(sendRequest, previousQuery, idOrObject) {
const findObject = validIndexValue(idOrObject) ?
{ id: idOrObject } : idOrObject
const query = Object.assign({}, previousQuery, { find: findObject })
super(sendRequest, query, [])
}
}
export class FindAll extends TermBase {
constructor(sendRequest, previousQuery, fieldValues) {
const wrappedFields = fieldValues
.map(item => validIndexValue(item) ? { id: item } : item)
const options = { find_all: wrappedFields }
const findAllQuery = Object.assign({}, previousQuery, options)
let legalMethods
if (wrappedFields.length === 1) {
legalMethods = [ 'order', 'above', 'below', 'limit' ]
} else {
// The vararg version of findAll cannot have anything chained to it
legalMethods = []
}
super(sendRequest, findAllQuery, legalMethods)
}
}
export class Above extends TermBase {
constructor(sendRequest, previousQuery, aboveSpec, bound) {
const option = { above: [ aboveSpec, bound ] }
const query = Object.assign({}, previousQuery, option)
const legalMethods = [ 'findAll', 'order', 'below', 'limit' ]
super(sendRequest, query, legalMethods)
}
}
export class Below extends TermBase {
constructor(sendRequest, previousQuery, belowSpec, bound) {
const options = { below: [ belowSpec, bound ] }
const query = Object.assign({}, previousQuery, options)
const legalMethods = [ 'findAll', 'order', 'above', 'limit' ]
super(sendRequest, query, legalMethods)
}
}
export class Order extends TermBase {
constructor(sendRequest, previousQuery, fields, direction) {
const wrappedFields = Array.isArray(fields) ? fields : [ fields ]
const options = { order: [ wrappedFields, direction ] }
const query = Object.assign({}, previousQuery, options)
const legalMethods = [ 'findAll', 'above', 'below', 'limit' ]
super(sendRequest, query, legalMethods)
}
}
export class Limit extends TermBase {
constructor(sendRequest, previousQuery, size) {
const query = Object.assign({}, previousQuery, { limit: size })
// Nothing is legal to chain after .limit
super(sendRequest, query, [])
}
}
export class UserDataTerm {
constructor(hz, handshake, socket) {
this._hz = hz
this._before = socket.ignoreElements().merge(handshake)
}
_query(userId) {
return this._hz('users').find(userId)
}
fetch() {
return this._before.mergeMap(handshake => {
if (handshake.id == null) {
throw new Error('Unauthenticated users have no user document')
} else {
return this._query(handshake.id).fetch()
}
}).take(1) // necessary so that we complete, since _before is
// infinite
}
watch(...args) {
return this._before.mergeMap(handshake => {
if (handshake.id === null) {
throw new Error('Unauthenticated users have no user document')
} else {
return this._query(handshake.id).watch(...args)
}
})
}
}
================================================
FILE: client/src/auth.js
================================================
import queryParse from './util/query-parse'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/do'
import 'rxjs/add/operator/map'
import 'rxjs/add/observable/dom/ajax'
const HORIZON_JWT = 'horizon-jwt'
/** @this Horizon **/
export function authEndpoint(name) {
const endpointForName = methods => {
if (methods.hasOwnProperty(name)) {
return this._root + methods[name]
} else {
throw new Error(`Unconfigured auth type: ${name}`)
}
}
if (!this._authMethods) {
return Observable.ajax(`${this._horizonPath}/auth_methods`)
.map(ajax => ajax.response)
.do(authMethods => {
this._authMethods = authMethods
}).map(endpointForName)
} else {
return Observable.of(this._authMethods).map(endpointForName)
}
}
// Simple shim to make a Map look like local/session storage
export class FakeStorage {
constructor() { this._storage = new Map() }
setItem(a, b) { return this._storage.set(a, b) }
getItem(a) { return this._storage.get(a) }
removeItem(a) { return this._storage.delete(a) }
}
function getStorage(storeLocally = true) {
let storage
try {
if (!storeLocally ||
typeof window !== 'object' ||
window.localStorage === undefined) {
storage = new FakeStorage()
} else {
// Mobile safari in private browsing has a localStorage, but it
// has a size limit of 0
gitextract_ib87vw2t/ ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── GETTING-STARTED.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── circle.yml ├── cli/ │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── create-cert.js │ │ ├── init.js │ │ ├── main.js │ │ ├── make-token.js │ │ ├── migrate.js │ │ ├── schema.js │ │ ├── serve.js │ │ ├── utils/ │ │ │ ├── change_to_project_dir.js │ │ │ ├── check-project-name.js │ │ │ ├── config.js │ │ │ ├── each_line_in_pipe.js │ │ │ ├── initialize_joi.js │ │ │ ├── interrupt.js │ │ │ ├── is_directory.js │ │ │ ├── nice_error.js │ │ │ ├── parse_yes_no_option.js │ │ │ ├── proc-promise.js │ │ │ ├── rethrow.js │ │ │ ├── rm_sync_recursive.js │ │ │ └── start_rdb_server.js │ │ └── version.js │ └── test/ │ ├── config.js │ ├── init.spec.js │ ├── schema.spec.js │ ├── serve.spec.js │ ├── unit/ │ │ ├── check-project-name.js │ │ └── nice_error.js │ └── version.spec.js ├── client/ │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── scripts/ │ │ └── compile.js │ ├── src/ │ │ ├── ast.js │ │ ├── auth.js │ │ ├── hacks/ │ │ │ └── watch-rewrites.js │ │ ├── index-polyfill.js │ │ ├── index.js │ │ ├── model.js │ │ ├── serialization.js │ │ ├── shim.js │ │ ├── socket.js │ │ └── util/ │ │ ├── check-args.js │ │ ├── glob.js │ │ ├── ordinal.js │ │ ├── query-parse.js │ │ └── valid-index-value.js │ ├── test/ │ │ ├── above.js │ │ ├── aboveSub.js │ │ ├── aggregate.js │ │ ├── aggregateSub.js │ │ ├── api.js │ │ ├── auth.js │ │ ├── below.js │ │ ├── belowSub.js │ │ ├── chaining.js │ │ ├── collection.js │ │ ├── find.js │ │ ├── findAll.js │ │ ├── findAllSub.js │ │ ├── findSub.js │ │ ├── horizonObject.js │ │ ├── insert.js │ │ ├── limit.js │ │ ├── order.js │ │ ├── orderLimitSub.js │ │ ├── remove.js │ │ ├── removeAll.js │ │ ├── replace.js │ │ ├── store.js │ │ ├── test.html │ │ ├── test.js │ │ ├── times.js │ │ ├── unit/ │ │ │ ├── ast.js │ │ │ ├── auth.js │ │ │ └── utilsTest.js │ │ ├── update.js │ │ ├── upsert.js │ │ └── utils.js │ ├── webpack.config.js │ ├── webpack.horizon.config.js │ └── webpack.test.config.js ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── examples/ │ ├── .eslintrc │ ├── README.md │ ├── angularjs-todo-app/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── dist/ │ │ ├── css/ │ │ │ └── style.css │ │ ├── index.html │ │ ├── js/ │ │ │ ├── app.js │ │ │ └── controllers/ │ │ │ └── TodoController.js │ │ └── package.json │ ├── auth-app/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── dist/ │ │ ├── app.css │ │ ├── app.js │ │ └── index.html │ ├── cyclejs-chat-app/ │ │ ├── README.md │ │ └── dist/ │ │ ├── app.css │ │ ├── app.js │ │ └── index.html │ ├── express-server/ │ │ ├── main.js │ │ └── package.json │ ├── hapi-server/ │ │ ├── main.js │ │ └── package.json │ ├── koa-server/ │ │ ├── main.js │ │ └── package.json │ ├── react-chat-app/ │ │ ├── README.md │ │ └── dist/ │ │ ├── app.css │ │ ├── app.jsx │ │ ├── index.html │ │ └── package.json │ ├── react-todo-app/ │ │ ├── .gitignore │ │ ├── dist/ │ │ │ ├── index.html │ │ │ ├── js/ │ │ │ │ ├── app.jsx │ │ │ │ ├── footer.jsx │ │ │ │ ├── todoItem.jsx │ │ │ │ ├── todoModel.js │ │ │ │ └── utils.js │ │ │ └── package.json │ │ └── readme.md │ ├── riotjs-chat-app/ │ │ └── dist/ │ │ ├── app.css │ │ ├── chat.tag │ │ └── index.html │ ├── vue-chat-app/ │ │ ├── .gitignore │ │ ├── README.md │ │ └── dist/ │ │ ├── app.css │ │ ├── app.js │ │ └── index.html │ └── vue-todo-app/ │ ├── .gitignore │ ├── dist/ │ │ ├── index.html │ │ ├── js/ │ │ │ ├── app.js │ │ │ ├── routes.js │ │ │ └── store.js │ │ └── package.json │ └── readme.md ├── protocol.md ├── rfcs/ │ ├── identity_mgmt.md │ └── permissions.md ├── server/ │ ├── .babelrc │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── auth/ │ │ │ ├── auth0.js │ │ │ ├── facebook.js │ │ │ ├── github.js │ │ │ ├── google.js │ │ │ ├── slack.js │ │ │ ├── twitch.js │ │ │ ├── twitter.js │ │ │ └── utils.js │ │ ├── auth.js │ │ ├── client.js │ │ ├── endpoint/ │ │ │ ├── common.js │ │ │ ├── insert.js │ │ │ ├── query.js │ │ │ ├── remove.js │ │ │ ├── replace.js │ │ │ ├── store.js │ │ │ ├── subscribe.js │ │ │ ├── update.js │ │ │ ├── upsert.js │ │ │ └── writes.js │ │ ├── error.js │ │ ├── horizon.js │ │ ├── logger.js │ │ ├── metadata/ │ │ │ ├── collection.js │ │ │ ├── index.js │ │ │ ├── metadata.js │ │ │ └── table.js │ │ ├── permissions/ │ │ │ ├── group.js │ │ │ ├── rule.js │ │ │ ├── template.js │ │ │ └── validator.js │ │ ├── reql_connection.js │ │ ├── request.js │ │ ├── schema/ │ │ │ ├── horizon_protocol.js │ │ │ └── server_options.js │ │ ├── server.js │ │ └── utils.js │ └── test/ │ ├── http_tests.js │ ├── permissions.js │ ├── prereq_tests.js │ ├── protocol_tests.js │ ├── query_tests.js │ ├── schema.js │ ├── subscribe_tests.js │ ├── test.js │ ├── utils.js │ └── write_tests.js ├── test/ │ ├── serve.js │ └── setupDev.sh └── update_versions.py
SYMBOL INDEX (362 symbols across 79 files)
FILE: .eslintrc.js
constant OFF (line 1) | const OFF = 0;
constant WARN (line 2) | const WARN = 1;
constant ERROR (line 3) | const ERROR = 2;
FILE: cli/.eslintrc.js
constant OFF (line 1) | const OFF = 0;
constant WARN (line 2) | const WARN = 1;
constant ERROR (line 3) | const ERROR = 2;
FILE: cli/src/migrate.js
constant VERSION_2_0 (line 17) | const VERSION_2_0 = [ 2, 0, 0 ];
function run (line 19) | function run(cmdArgs) {
function green (line 34) | function green() {
function white (line 40) | function white() {
function processConfig (line 46) | function processConfig(cmdArgs) {
function setup (line 139) | function setup() {
function teardown (line 172) | function teardown() {
function validateMigration (line 189) | function validateMigration() {
function makeBackup (line 234) | function makeBackup() {
function nonportableBackup (line 277) | function nonportableBackup() {
function renameUserTables (line 289) | function renameUserTables() {
function moveInternalTables (line 305) | function moveInternalTables() {
function renameIndices (line 329) | function renameIndices() {
function rewriteHzCollectionDocs (line 381) | function rewriteHzCollectionDocs() {
function exportNewSchema (line 402) | function exportNewSchema() {
FILE: cli/src/serve.js
constant TIMEOUT_30_SECONDS (line 24) | const TIMEOUT_30_SECONDS = 30 * 1000;
FILE: cli/src/utils/nice_error.js
class NiceError (line 10) | class NiceError extends Error {
method constructor (line 11) | constructor(message, options) {
method toString (line 20) | toString() {
method niceString (line 24) | niceString(options) {
method _sourceLine (line 57) | static _sourceLine(ln) {
method _extractContext (line 61) | static _extractContext(sourceContents, line, contextSize) {
method _formatContext (line 75) | static _formatContext(sourceContents, line, col, contextSize) {
FILE: cli/src/utils/proc-promise.js
function procPromise (line 5) | function procPromise() {
FILE: cli/src/utils/start_rdb_server.js
class RethinkdbServer (line 18) | class RethinkdbServer {
method constructor (line 19) | constructor(options) {
method ready (line 99) | ready() {
method connect (line 105) | connect() {
method close (line 109) | close() {
FILE: client/.eslintrc.js
constant OFF (line 1) | const OFF = 0;
constant WARN (line 2) | const WARN = 1;
constant ERROR (line 3) | const ERROR = 2;
FILE: client/index.d.ts
type Feed (line 6) | interface Feed {
type Bound (line 11) | type Bound = 'open' | 'closed';
type Direction (line 12) | type Direction = 'ascending' | 'descending';
type Primitive (line 13) | type Primitive = boolean | number | string | Date;
type IdValue (line 14) | type IdValue = Primitive | Primitive[] | { id: Primitive };
type WriteOp (line 15) | type WriteOp = Object | Object[];
type TermBase (line 17) | interface TermBase extends Feed {
type Collection (line 27) | interface Collection extends TermBase {
type User (line 38) | interface User extends Feed {}
type HorizonInstance (line 40) | interface HorizonInstance {
type HorizonOptions (line 60) | interface HorizonOptions {
type Horizon (line 72) | interface Horizon {
type HorizonOptions (line 79) | type HorizonOptions = hz.HorizonOptions;
type HorizonInstance (line 80) | type HorizonInstance = hz.HorizonInstance;
type TermBase (line 81) | type TermBase = hz.TermBase;
type Collection (line 82) | type Collection = hz.Collection;
type User (line 83) | type User = hz.User;
FILE: client/src/ast.js
function checkIfLegalToChain (line 33) | function checkIfLegalToChain(key) {
class TermBase (line 43) | class TermBase {
method constructor (line 44) | constructor(sendRequest, query, legalMethods) {
method toString (line 50) | toString() {
method watch (line 80) | watch({ rawChanges = false } = {}) {
method fetch (line 92) | fetch() {
method findAll (line 103) | findAll(...fieldValues) {
method find (line 108) | find(idOrObject) {
method order (line 113) | order(fields, direction = 'ascending') {
method above (line 118) | above(aboveSpec, bound = 'closed') {
method below (line 123) | below(belowSpec, bound = 'open') {
method limit (line 128) | limit(size) {
function makePresentable (line 140) | function makePresentable(observable, query) {
function applyChange (line 185) | function applyChange(arr, change) {
function writeOp (line 254) | function writeOp(name, args, documents) {
class Collection (line 302) | class Collection extends TermBase {
method constructor (line 303) | constructor(sendRequest, collectionName, lazyWrites) {
method store (line 310) | store(documents) {
method upsert (line 313) | upsert(documents) {
method insert (line 316) | insert(documents) {
method replace (line 319) | replace(documents) {
method update (line 322) | update(documents) {
method remove (line 325) | remove(documentOrId) {
method removeAll (line 330) | removeAll(documentsOrIds) {
class Find (line 345) | class Find extends TermBase {
method constructor (line 346) | constructor(sendRequest, previousQuery, idOrObject) {
class FindAll (line 354) | class FindAll extends TermBase {
method constructor (line 355) | constructor(sendRequest, previousQuery, fieldValues) {
class Above (line 371) | class Above extends TermBase {
method constructor (line 372) | constructor(sendRequest, previousQuery, aboveSpec, bound) {
class Below (line 380) | class Below extends TermBase {
method constructor (line 381) | constructor(sendRequest, previousQuery, belowSpec, bound) {
class Order (line 389) | class Order extends TermBase {
method constructor (line 390) | constructor(sendRequest, previousQuery, fields, direction) {
class Limit (line 399) | class Limit extends TermBase {
method constructor (line 400) | constructor(sendRequest, previousQuery, size) {
class UserDataTerm (line 408) | class UserDataTerm {
method constructor (line 409) | constructor(hz, handshake, socket) {
method _query (line 414) | _query(userId) {
method fetch (line 418) | fetch() {
method watch (line 429) | watch(...args) {
FILE: client/src/auth.js
constant HORIZON_JWT (line 7) | const HORIZON_JWT = 'horizon-jwt'
function authEndpoint (line 10) | function authEndpoint(name) {
class FakeStorage (line 30) | class FakeStorage {
method constructor (line 31) | constructor() { this._storage = new Map() }
method setItem (line 32) | setItem(a, b) { return this._storage.set(a, b) }
method getItem (line 33) | getItem(a) { return this._storage.get(a) }
method removeItem (line 34) | removeItem(a) { return this._storage.delete(a) }
function getStorage (line 37) | function getStorage(storeLocally = true) {
class TokenStorage (line 61) | class TokenStorage {
method constructor (line 62) | constructor({ authType = 'token',
method _getHash (line 75) | _getHash() {
method _setHash (line 84) | _setHash(hash) {
method set (line 88) | set(jwt) {
method get (line 94) | get() {
method remove (line 98) | remove() {
method setAuthFromQueryParams (line 104) | setAuthFromQueryParams() {
method handshake (line 115) | handshake() {
method hasAuthToken (line 130) | hasAuthToken() {
function clearAuthTokens (line 146) | function clearAuthTokens() {
FILE: client/src/hacks/watch-rewrites.js
function watchRewrites (line 14) | function watchRewrites(self, query) {
FILE: client/src/index.js
function Horizon (line 18) | function Horizon({
function subscribeOrObservable (line 128) | function subscribeOrObservable(observable) {
FILE: client/src/model.js
function checkWatchArgs (line 13) | function checkWatchArgs(args) {
function isTerm (line 19) | function isTerm(term) {
function isPromise (line 24) | function isPromise(term) {
function isObservable (line 28) | function isObservable(term) {
function isPrimitive (line 35) | function isPrimitive(value) {
class PrimitiveTerm (line 55) | class PrimitiveTerm {
method constructor (line 56) | constructor(value) {
method toString (line 60) | toString() {
method fetch (line 64) | fetch() {
method watch (line 68) | watch(...watchArgs) {
class ObservableTerm (line 77) | class ObservableTerm {
method constructor (line 78) | constructor(value) {
method toString (line 82) | toString() {
method fetch (line 86) | fetch() {
method watch (line 90) | watch(...watchArgs) {
class ArrayTerm (line 97) | class ArrayTerm {
method constructor (line 98) | constructor(value) {
method _reducer (line 103) | _reducer(...args) {
method _query (line 107) | _query(operation) {
method toString (line 111) | toString() {
method fetch (line 115) | fetch() {
method watch (line 124) | watch(...watchArgs) {
class AggregateTerm (line 136) | class AggregateTerm {
method constructor (line 137) | constructor(value) {
method _reducer (line 142) | _reducer(...pairs) {
method _query (line 149) | _query(operation) {
method toString (line 154) | toString() {
method fetch (line 159) | fetch() {
method watch (line 168) | watch(...watchArgs) {
function aggregate (line 180) | function aggregate(spec) {
function model (line 200) | function model(constructor) {
FILE: client/src/serialization.js
constant PRIMITIVES (line 1) | const PRIMITIVES = [
function modifyObject (line 4) | function modifyObject(doc) {
function deserialize (line 11) | function deserialize(value) {
function jsonifyObject (line 27) | function jsonifyObject(doc) {
function serialize (line 34) | function serialize(value) {
FILE: client/src/socket.js
constant PROTOCOL_VERSION (line 17) | const PROTOCOL_VERSION = 'rethinkdb-horizon-v0'
constant STATUS_UNCONNECTED (line 20) | const STATUS_UNCONNECTED = { type: 'unconnected' }
constant STATUS_READY (line 22) | const STATUS_READY = { type: 'ready' }
constant STATUS_ERROR (line 24) | const STATUS_ERROR = { type: 'error' }
constant STATUS_DISCONNECTED (line 26) | const STATUS_DISCONNECTED = { type: 'disconnected' }
class ProtocolError (line 28) | class ProtocolError extends Error {
method constructor (line 29) | constructor(msg, errorCode) {
method toString (line 33) | toString() {
class HorizonSocket (line 42) | class HorizonSocket extends WebSocketSubject {
method resultSelector (line 45) | resultSelector(e) {
method next (line 52) | next(value) {
method constructor (line 57) | constructor({
method deactivateRequest (line 107) | deactivateRequest(req) {
method activateRequest (line 114) | activateRequest(req) {
method filterRequest (line 121) | filterRequest(req) {
method getRequest (line 125) | getRequest(request) {
method sendHandshake (line 132) | sendHandshake() {
method makeRequest (line 163) | makeRequest(rawRequest) {
method hzRequest (line 181) | hzRequest(rawRequest) {
FILE: client/src/util/check-args.js
function checkArgs (line 4) | function checkArgs(name, args, {
FILE: client/src/util/ordinal.js
function ordinal (line 1) | function ordinal(x) {
FILE: client/src/util/valid-index-value.js
function validIndexValue (line 3) | function validIndexValue(val) {
FILE: client/test/above.js
function aboveSuite (line 9) | function aboveSuite(getData) {
FILE: client/test/aboveSub.js
function aboveSubscriptionSuite (line 5) | function aboveSubscriptionSuite(getData) {
FILE: client/test/aggregate.js
function arrayHasSameElements (line 9) | function arrayHasSameElements(a, b) {
function aggregateSuite (line 18) | function aggregateSuite(getData, getHorizon) {
FILE: client/test/aggregateSub.js
function aggregateSubSuite (line 7) | function aggregateSubSuite(getData, getHorizon) {
FILE: client/test/api.js
function wrappedDone (line 76) | function wrappedDone(...args) {
FILE: client/test/auth.js
function authSuite (line 5) | function authSuite(getHorizon) {
FILE: client/test/below.js
function belowSuite (line 9) | function belowSuite(getData) {
FILE: client/test/belowSub.js
function belowSubscriptionSuite (line 5) | function belowSubscriptionSuite(getData) {
FILE: client/test/chaining.js
function chainingSuite (line 6) | function chainingSuite(getData) {
FILE: client/test/collection.js
function collectionSuite (line 6) | function collectionSuite(getHorizon, getData, getTestData) {
FILE: client/test/find.js
function findSuite (line 9) | function findSuite(getData) {
FILE: client/test/findAll.js
function findAllSuite (line 9) | function findAllSuite(getData) {
FILE: client/test/findAllSub.js
function findAllSubscriptionSuite (line 5) | function findAllSubscriptionSuite(getData) {
FILE: client/test/findSub.js
function findSubscriptionSuite (line 5) | function findSubscriptionSuite(getData) {
FILE: client/test/horizonObject.js
function doneWrap (line 6) | function doneWrap(done) {
function horizonObjectSuite (line 16) | function horizonObjectSuite() {
FILE: client/test/insert.js
function insertSuite (line 12) | function insertSuite(getData) {
FILE: client/test/limit.js
function limitSuite (line 9) | function limitSuite(getData) {
FILE: client/test/order.js
function orderSuite (line 12) | function orderSuite(getData, getTestData) {
FILE: client/test/orderLimitSub.js
function orderLimitSubSuite (line 5) | function orderLimitSubSuite(getData) {
FILE: client/test/remove.js
function removeSuite (line 15) | function removeSuite(getData) {
FILE: client/test/removeAll.js
function removeAllSuite (line 15) | function removeAllSuite(getData) {
FILE: client/test/replace.js
function replaceSuite (line 11) | function replaceSuite(getData) {
FILE: client/test/store.js
function storeSuite (line 12) | function storeSuite(getData) {
FILE: client/test/test.js
constant BROWSER (line 1) | const BROWSER = (typeof window !== 'undefined')
FILE: client/test/times.js
function timesSuite (line 8) | function timesSuite(getData) {
FILE: client/test/unit/ast.js
function unitAstSuite (line 3) | function unitAstSuite() {
FILE: client/test/unit/auth.js
function unitAuthSuite (line 3) | function unitAuthSuite() {
FILE: client/test/unit/utilsTest.js
function unitUtilsSuite (line 3) | function unitUtilsSuite() {
FILE: client/test/update.js
function updateSuite (line 11) | function updateSuite(getData) {
FILE: client/test/upsert.js
function upsertSuite (line 12) | function upsertSuite(getData) {
FILE: client/test/utils.js
function removeAllDataObs (line 10) | function removeAllDataObs(collection) {
function removeAllData (line 19) | function removeAllData(collection, done) {
function doneObserver (line 24) | function doneObserver(done) {
function doneErrorObserver (line 33) | function doneErrorObserver(done, regex) {
function assertThrows (line 54) | function assertThrows(message, callback) {
function assertCompletes (line 72) | function assertCompletes(observable) {
function assertErrors (line 78) | function assertErrors(observable, regex) {
function observableInterleave (line 93) | function observableInterleave(options) {
function compareWithoutVersion (line 131) | function compareWithoutVersion(actual, expected, message) {
function compareSetsWithoutVersion (line 137) | function compareSetsWithoutVersion(actual, expected, message) {
FILE: client/webpack.config.js
constant BUILD_ALL (line 1) | const BUILD_ALL = (process.env.NODE_ENV === 'production')
FILE: client/webpack.test.config.js
constant DEV_BUILD (line 5) | const DEV_BUILD = (process.env.NODE_ENV !== 'production')
constant SOURCEMAPS (line 6) | const SOURCEMAPS = !process.env.NO_SOURCEMAPS
FILE: examples/angularjs-todo-app/dist/js/controllers/TodoController.js
function TodoCtrl (line 10) | function TodoCtrl($filter, $q) {
FILE: examples/cyclejs-chat-app/dist/app.js
function intent (line 15) | function intent(sources) {
function model (line 58) | function model(inputValue$, messages$) {
function view (line 68) | function view(state$) {
function main (line 92) | function main(sources) {
function makeHorizonDriver (line 125) | function makeHorizonDriver() {
FILE: examples/react-todo-app/dist/js/app.jsx
function render (line 171) | function render() {
FILE: server/.eslintrc.js
constant OFF (line 1) | const OFF = 0;
constant WARN (line 2) | const WARN = 1;
constant ERROR (line 3) | const ERROR = 2;
FILE: server/src/auth.js
class JWT (line 14) | class JWT {
method constructor (line 15) | constructor(options) {
method sign (line 30) | sign(payload) {
method verify (line 40) | verify(token) {
class Auth (line 47) | class Auth {
method constructor (line 48) | constructor(server, user_options) {
method handshake (line 63) | handshake(request) {
method auth_key (line 83) | auth_key(provider, info) {
method new_user_row (line 91) | new_user_row(id) {
method generate (line 100) | generate(provider, info) {
FILE: server/src/auth/auth0.js
function auth0 (line 19) | function auth0(horizon, raw_options) {
FILE: server/src/auth/facebook.js
function facebook (line 17) | function facebook(horizon, raw_options) {
FILE: server/src/auth/github.js
function github (line 16) | function github(horizon, raw_options) {
FILE: server/src/auth/google.js
function google (line 17) | function google(horizon, raw_options) {
FILE: server/src/auth/slack.js
function slack (line 16) | function slack(horizon, raw_options) {
FILE: server/src/auth/twitch.js
function twitch (line 17) | function twitch(horizon, raw_options) {
FILE: server/src/auth/twitter.js
function twitter (line 40) | function twitter(horizon, raw_options) {
FILE: server/src/client.js
class Client (line 10) | class Client {
method constructor (line 11) | constructor(socket, server, metadata) {
method handle_websocket_close (line 36) | handle_websocket_close() {
method handle_websocket_error (line 48) | handle_websocket_error(code, msg) {
method error_wrap_socket (line 52) | error_wrap_socket(cb) {
method parse_request (line 63) | parse_request(data, schema) {
method group_changed (line 89) | group_changed(group_name) {
method handle_handshake (line 95) | handle_handshake(data) {
method handle_request (line 142) | handle_request(data) {
method remove_request (line 168) | remove_request(raw_request) {
method is_open (line 176) | is_open() {
method close (line 180) | close(info) {
method send_response (line 193) | send_response(request, data) {
method send_error (line 202) | send_error(request, err, code) {
FILE: server/src/error.js
class IndexMissing (line 11) | class IndexMissing extends Error {
method constructor (line 12) | constructor(collection, fields) {
class CollectionMissing (line 19) | class CollectionMissing extends Error {
method constructor (line 20) | constructor(name) {
class IndexNotReady (line 26) | class IndexNotReady extends Error {
method constructor (line 27) | constructor(collection, index) {
class CollectionNotReady (line 34) | class CollectionNotReady extends Error {
method constructor (line 35) | constructor(collection) {
FILE: server/src/metadata/collection.js
class Collection (line 8) | class Collection {
method constructor (line 9) | constructor(db, name) {
method _close (line 17) | _close() {
method _update_table (line 27) | _update_table(table_id, indexes, conn) {
method _register (line 47) | _register() {
method _unregister (line 51) | _unregister() {
method _is_safe_to_remove (line 55) | _is_safe_to_remove() {
method _on_ready (line 59) | _on_ready(done) {
method _get_table (line 67) | _get_table() {
method _create_index (line 74) | _create_index(fields, conn, done) {
method get_matching_index (line 78) | get_matching_index(fuzzy_fields, ordered_fields) {
FILE: server/src/metadata/index.js
class Index (line 104) | class Index {
method constructor (line 105) | constructor(name, table, conn) {
method close (line 139) | close() {
method ready (line 144) | ready() {
method on_ready (line 148) | on_ready(done) {
method is_match (line 163) | is_match(fuzzy_fields, ordered_fields) {
FILE: server/src/metadata/metadata.js
class Metadata (line 45) | class Metadata {
method constructor (line 46) | constructor(project_name,
method close (line 277) | close() {
method is_ready (line 295) | is_ready() {
method ready (line 299) | ready() {
method collection (line 303) | collection(name) {
method handle_error (line 315) | handle_error(err, done) {
method create_collection (line 337) | create_collection(name, done) {
method get_user_feed (line 357) | get_user_feed(id) {
method get_group (line 363) | get_group(group_name) {
method connection (line 367) | connection() {
FILE: server/src/metadata/table.js
class Table (line 9) | class Table {
method constructor (line 10) | constructor(reql_table, conn) {
method close (line 31) | close() {
method ready (line 39) | ready() {
method on_ready (line 43) | on_ready(done) {
method update_indexes (line 53) | update_indexes(indexes, conn) {
method create_index (line 81) | create_index(fields, conn, done) {
method get_matching_index (line 110) | get_matching_index(fuzzy_fields, ordered_fields) {
FILE: server/src/permissions/group.js
class Group (line 4) | class Group {
method constructor (line 5) | constructor(row_data) {
FILE: server/src/permissions/rule.js
class Rule (line 5) | class Rule {
method constructor (line 6) | constructor(name, info) {
method is_match (line 14) | is_match(query, context) {
method is_valid (line 18) | is_valid() {
class Ruleset (line 26) | class Ruleset {
method constructor (line 27) | constructor() {
method clear (line 31) | clear() {
method empty (line 35) | empty() {
method update (line 39) | update(rules) {
method validation_required (line 43) | validation_required() {
method validate (line 56) | validate() {
FILE: server/src/permissions/template.js
class Any (line 12) | class Any {
method constructor (line 13) | constructor(values) {
method matches (line 17) | matches(value, context) {
class AnyObject (line 36) | class AnyObject {
method constructor (line 37) | constructor(obj) {
method matches (line 41) | matches(value, context) {
class AnyArray (line 58) | class AnyArray {
method constructor (line 59) | constructor(values) {
method matches (line 63) | matches(value, context) {
class UserId (line 85) | class UserId { }
class Template (line 225) | class Template {
method constructor (line 226) | constructor(str) {
method is_match (line 244) | is_match(raw_query, context) {
FILE: server/src/permissions/validator.js
class Validator (line 9) | class Validator {
method constructor (line 10) | constructor(str) {
method is_valid (line 19) | is_valid() {
FILE: server/src/reql_connection.js
class ReqlConnection (line 11) | class ReqlConnection {
method constructor (line 12) | constructor(host, port, db,
method _reconnect (line 49) | _reconnect() {
method _init_connection (line 74) | _init_connection() {
method is_ready (line 111) | is_ready() {
method ready (line 115) | ready() {
method connection (line 119) | connection() {
method metadata (line 124) | metadata() {
FILE: server/src/request.js
class Request (line 6) | class Request {
method constructor (line 7) | constructor(raw_request, endpoint, client) {
method evaluate_rules (line 15) | evaluate_rules() {
method run (line 36) | run() {
method close (line 67) | close() {
method handle_error (line 74) | handle_error(err) {
FILE: server/src/server.js
class Server (line 67) | class Server {
method constructor (line 68) | constructor(http_servers, user_opts) {
method add_request_handler (line 172) | add_request_handler(request_name, endpoint) {
method get_request_handler (line 178) | get_request_handler(request) {
method remove_request_handler (line 182) | remove_request_handler(request_name) {
method add_http_handler (line 186) | add_http_handler(sub_path, handler) {
method remove_http_handler (line 193) | remove_http_handler(sub_path) {
method add_auth_provider (line 197) | add_auth_provider(provider, options) {
method ready (line 205) | ready() {
method close (line 209) | close() {
FILE: server/src/utils.js
constant MIN_VERSION (line 3) | const MIN_VERSION = [ 2, 3, 1 ];
FILE: update_versions.py
function rewrite (line 13) | def rewrite(filename):
function main (line 24) | def main(version):
Condensed preview — 211 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (690K chars).
[
{
"path": ".dockerignore",
"chars": 258,
"preview": "**/node_modules\ndocs\nexamples\nserver/test\n\nclient/test\nclient/lib/\nclient/build\nclient/dist\n\n**/README.md\ndocker-compose"
},
{
"path": ".eslintrc.js",
"chars": 3378,
"preview": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n extends: \"eslint:recommended\",\n rules: {\n \"arr"
},
{
"path": ".gitignore",
"chars": 156,
"preview": "client/dist/\nclient/lib/\nrethinkdb_data_test\nrethinkdb_data/\n**/*.log\n.#*\n**/*-key.pem\n**/*-cert.pem\nnode_modules/\n**/.D"
},
{
"path": "CONTRIBUTING.md",
"chars": 1126,
"preview": "# Contributing\n\nWe're happy you want to contribute! You can help us in different ways:\n\n- [Open an issue][1] with sugges"
},
{
"path": "Dockerfile",
"chars": 578,
"preview": "# REQUIREMENTS\n# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_UR"
},
{
"path": "Dockerfile.dev",
"chars": 259,
"preview": "# REQUIREMENTS\n# * Needs a RETHINKDB_URI environment variable pushed into the container at runtime, with -e RETHINKDB_UR"
},
{
"path": "GETTING-STARTED.md",
"chars": 22330,
"preview": "\n\n# Getting Started with Horizon\n\n**Getting Started**\n* [Installation](#installation)\n* [Creating your "
},
{
"path": "ISSUE_TEMPLATE.md",
"chars": 80,
"preview": "If you're reporting a bug please include the server version and client version.\n"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "The MIT License (MIT)\nCopyright (c) 2016 RethinkDB, Inc.\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 3997,
"preview": "<img style=\"width:100%;\" src=\"/github-banner.png\">\n\n# Horizon\n\n[Official Repository](https://github.com/rethinkdb/horizo"
},
{
"path": "circle.yml",
"chars": 2005,
"preview": "## Customize the test machine\nmachine:\n\n #timezone:\n # America/Los_Angeles # Set the timezone\n\n # Set version of nod"
},
{
"path": "cli/.eslintrc.js",
"chars": 247,
"preview": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n extends: \"../.eslintrc.js\",\n rules: {\n \"max-le"
},
{
"path": "cli/.gitignore",
"chars": 58,
"preview": "# Coverage directory used by tools like istanbul\ncoverage\n"
},
{
"path": "cli/README.md",
"chars": 881,
"preview": "# **Horizon** is a realtime, open-source backend for JavaScript apps.\n\nRapidly build and deploy web or mobile apps using"
},
{
"path": "cli/package.json",
"chars": 1277,
"preview": "{\n \"name\": \"horizon\",\n \"version\": \"2.0.0\",\n \"description\": \"An open-source developer platform for building realtime, "
},
{
"path": "cli/src/create-cert.js",
"chars": 1798,
"preview": "'use strict';\nconst hasbin = require('hasbin');\nconst spawn = require('child_process').spawn;\n\nconst run = (args) => {\n "
},
{
"path": "cli/src/init.js",
"chars": 9618,
"preview": "/* global require, module */\n\n'use strict';\n\nconst fs = require('fs');\nconst crypto = require('crypto');\nconst process ="
},
{
"path": "cli/src/main.js",
"chars": 2097,
"preview": "#!/usr/bin/env node\n'use strict';\n\n// To support `pidof horizon`, by default it shows in `pidof node`\nprocess.title = 'h"
},
{
"path": "cli/src/make-token.js",
"chars": 1883,
"preview": "'use strict';\n\nconst interrupt = require('./utils/interrupt');\nconst config = require('./utils/config');\nconst horizon_s"
},
{
"path": "cli/src/migrate.js",
"chars": 14353,
"preview": "'use strict';\nconst chalk = require('chalk');\nconst r = require('rethinkdb');\nconst Promise = require('bluebird');\nconst"
},
{
"path": "cli/src/schema.js",
"chars": 18765,
"preview": "'use strict';\n\nconst horizon_server = require('@horizon/server');\nconst horizon_index = require('@horizon/server/src/met"
},
{
"path": "cli/src/serve.js",
"chars": 17481,
"preview": "'use strict';\n\nconst chalk = require('chalk');\nconst crypto = require('crypto');\nconst fs = require('fs');\nconst get_typ"
},
{
"path": "cli/src/utils/change_to_project_dir.js",
"chars": 431,
"preview": "'use strict';\n\nconst is_directory = require('./is_directory');\n\nmodule.exports = (project_path) => {\n if (is_directory("
},
{
"path": "cli/src/utils/check-project-name.js",
"chars": 1264,
"preview": "'use strict';\n\nconst path = require('path');\nconst basename = path.basename;\nconst join = path.join;\n\nconst fixableProje"
},
{
"path": "cli/src/utils/config.js",
"chars": 9407,
"preview": "'use strict';\n\nconst parse_yes_no_option = require('./parse_yes_no_option');\nconst NiceError = require('./nice_error.js'"
},
{
"path": "cli/src/utils/each_line_in_pipe.js",
"chars": 386,
"preview": "'use strict';\n\nmodule.exports = (pipe, callback) => {\n let buffer = '';\n pipe.on('data', (data) => {\n buffer += dat"
},
{
"path": "cli/src/utils/initialize_joi.js",
"chars": 313,
"preview": "'use strict';\n\n// Issues a dummy joi validation to force joi to initialize its scripts.\n// This is used because tests wi"
},
{
"path": "cli/src/utils/interrupt.js",
"chars": 562,
"preview": "'use strict';\n\nconst handlers = [ ];\n\nconst on_interrupt = (cb) => {\n handlers.push(cb);\n};\n\nconst run_handlers = () =>"
},
{
"path": "cli/src/utils/is_directory.js",
"chars": 214,
"preview": "'use strict';\n\nconst path = require('path');\nconst fs = require('fs');\n\nmodule.exports = (dirname) => {\n try {\n retu"
},
{
"path": "cli/src/utils/nice_error.js",
"chars": 2637,
"preview": "'use strict';\n\n/*\nA nice error type that allows you to associate a longer description, a\nsource file and suggestions wit"
},
{
"path": "cli/src/utils/parse_yes_no_option.js",
"chars": 451,
"preview": "'use strict';\n\nmodule.exports = (value, option_name) => {\n if (value !== undefined && value !== null) {\n const lower"
},
{
"path": "cli/src/utils/proc-promise.js",
"chars": 662,
"preview": "'use strict';\nconst Promise = require('bluebird');\nconst childProcess = require('child_process');\n\nfunction procPromise("
},
{
"path": "cli/src/utils/rethrow.js",
"chars": 504,
"preview": "'use strict';\n\n// Returns a new Error with the given message. Combines the stack\n// traces with the old error, and remov"
},
{
"path": "cli/src/utils/rm_sync_recursive.js",
"chars": 463,
"preview": "'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst rmdirSyncRecursive = (dir) => {\n try {\n "
},
{
"path": "cli/src/utils/start_rdb_server.js",
"chars": 4485,
"preview": "'use strict';\n\nconst each_line_in_pipe = require('./each_line_in_pipe');\nconst horizon_server = require('@horizon/server"
},
{
"path": "cli/src/version.js",
"chars": 344,
"preview": "'use strict';\n\nconst package_json = require('../package.json');\n\nconst run = (args) =>\n Promise.resolve().then(() => {\n"
},
{
"path": "cli/test/config.js",
"chars": 10141,
"preview": "'use strict';\n\nconst serve = require('../src/serve');\nconst processConfig = serve.processConfig;\n\nconst assert = require"
},
{
"path": "cli/test/init.spec.js",
"chars": 11169,
"preview": "/* global beforeEach, describe, process, afterEach, it, require */\n\n'use strict';\n\nconst init = require('../src/init');\n"
},
{
"path": "cli/test/schema.spec.js",
"chars": 6104,
"preview": "'use strict';\n\nconst processApplyConfig = require('../src/schema').processApplyConfig;\nconst runApplyCommand = require('"
},
{
"path": "cli/test/serve.spec.js",
"chars": 3410,
"preview": "'use strict';\n\nconst serve = require('../src/serve');\nconst schema = require('../src/schema');\nconst start_rdb_server = "
},
{
"path": "cli/test/unit/check-project-name.js",
"chars": 4593,
"preview": "/* global describe, it */\n\n'use strict';\n\nconst checkProjectName = require('../../src/utils/check-project-name');\nconst "
},
{
"path": "cli/test/unit/nice_error.js",
"chars": 7056,
"preview": "'use strict';\nconst stripAnsi = require('strip-ansi');\nconst assert = require('chai').assert;\nconst NiceError = require("
},
{
"path": "cli/test/version.spec.js",
"chars": 439,
"preview": "'use strict';\n\nconst versionCommand = require('../src/version');\nconst assert = require('assert');\nconst sinon = require"
},
{
"path": "client/.eslintrc.js",
"chars": 417,
"preview": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n extends: \"../.eslintrc.js\",\n rules: {\n \"arrow-"
},
{
"path": "client/.gitignore",
"chars": 603,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
},
{
"path": "client/README.md",
"chars": 2636,
"preview": "# Horizon Client Library\n\nThe Horizon client library. Built to interact with the [Horizon Server](/server) API. Provides"
},
{
"path": "client/index.d.ts",
"chars": 2322,
"preview": "\nimport { Observable } from 'rxjs';\n\ndeclare namespace hz {\n\n interface Feed {\n watch (options?: { rawChanges:"
},
{
"path": "client/package.json",
"chars": 3109,
"preview": "{\n \"name\": \"@horizon/client\",\n \"version\": \"2.0.0\",\n \"description\": \"RethinkDB Horizon is an open-source developer pla"
},
{
"path": "client/scripts/compile.js",
"chars": 290,
"preview": "require('shelljs/global')\n\n// remove existing lib files\nrm('-rf', 'lib/**/*')\n\n// compile with babel\nif (exec('babel src"
},
{
"path": "client/src/ast.js",
"chars": 13982,
"preview": "import { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/observable/empty'\n\nimport 'rxjs/add/operator/publishReplay"
},
{
"path": "client/src/auth.js",
"chars": 3773,
"preview": "import queryParse from './util/query-parse'\nimport { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/operator/do'\ni"
},
{
"path": "client/src/hacks/watch-rewrites.js",
"chars": 1027,
"preview": "/*\n Some common queries run on an entire collection or on a collection of\n indeterminate size. RethinkDB doesn't actuall"
},
{
"path": "client/src/index-polyfill.js",
"chars": 483,
"preview": "// Ensures these features are present or polyfilled\n// See http://kangax.github.io/compat-table/es6/\nrequire('core-js/fn"
},
{
"path": "client/src/index.js",
"chars": 4348,
"preview": "import 'rxjs/add/observable/of'\nimport 'rxjs/add/observable/from'\nimport 'rxjs/add/operator/catch'\nimport 'rxjs/add/oper"
},
{
"path": "client/src/model.js",
"chars": 4313,
"preview": "import { Observable } from 'rxjs/Observable'\n\nimport 'rxjs/add/observable/of'\nimport 'rxjs/add/observable/forkJoin'\nimpo"
},
{
"path": "client/src/serialization.js",
"chars": 1201,
"preview": "const PRIMITIVES = [\n 'string', 'number', 'boolean', 'function', 'symbol' ]\n\nfunction modifyObject(doc) {\n Object.keys"
},
{
"path": "client/src/shim.js",
"chars": 257,
"preview": "/* global WebSocket */\n\n// Check for websocket\nif (typeof WebSocket !== 'undefined') {\n module.exports.WebSocket = WebS"
},
{
"path": "client/src/socket.js",
"chars": 6626,
"preview": "import { AsyncSubject } from 'rxjs/AsyncSubject'\nimport { BehaviorSubject } from 'rxjs/BehaviorSubject'\nimport { WebSock"
},
{
"path": "client/src/util/check-args.js",
"chars": 1139,
"preview": "import ordinal from './ordinal.js'\n\n// Validation helper\nexport default function checkArgs(name, args, {\n "
},
{
"path": "client/src/util/glob.js",
"chars": 180,
"preview": "module.exports = function glob() {\n return typeof self !== 'undefined' ?\n self :\n typeof window !== 'undefined' ?\n w"
},
{
"path": "client/src/util/ordinal.js",
"chars": 270,
"preview": "export default function ordinal(x) {\n if ([ 11, 12, 13 ].indexOf(x) !== -1) {\n return `${x}th`\n } else if (x % 10 ="
},
{
"path": "client/src/util/query-parse.js",
"chars": 1069,
"preview": "/* Pulled from @sindresorhus query-string module and reformatted.\nThis is simply to avoid requiring the other methods in"
},
{
"path": "client/src/util/valid-index-value.js",
"chars": 548,
"preview": "// Checks whether the return value is a valid primary or secondary\n// index value in RethinkDB.\nexport default function "
},
{
"path": "client/test/above.js",
"chars": 4397,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/aboveSub.js",
"chars": 3360,
"preview": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default funct"
},
{
"path": "client/test/aggregate.js",
"chars": 6874,
"preview": "import Rx from 'rxjs/Rx'\n\nimport { assertCompletes,\n removeAllDataObs,\n observableInterleave } from './u"
},
{
"path": "client/test/aggregateSub.js",
"chars": 7195,
"preview": "import { Observable } from 'rxjs/Observable'\n\nimport { assertCompletes,\n removeAllDataObs,\n observableIn"
},
{
"path": "client/test/api.js",
"chars": 5552,
"preview": "import 'rxjs/add/operator/ignoreElements'\nimport 'rxjs/add/operator/concat'\nimport 'rxjs/add/operator/do'\nimport 'rxjs/a"
},
{
"path": "client/test/auth.js",
"chars": 2564,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add/operator/mergeMapTo'\n\nexport default "
},
{
"path": "client/test/below.js",
"chars": 4348,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/belowSub.js",
"chars": 3301,
"preview": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default funct"
},
{
"path": "client/test/chaining.js",
"chars": 2108,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes, assertThrows, compareWithout"
},
{
"path": "client/test/collection.js",
"chars": 1359,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes, removeAllData, compareSetsWi"
},
{
"path": "client/test/find.js",
"chars": 4009,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/findAll.js",
"chars": 4213,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/findAllSub.js",
"chars": 2817,
"preview": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default funct"
},
{
"path": "client/test/findSub.js",
"chars": 2414,
"preview": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default funct"
},
{
"path": "client/test/horizonObject.js",
"chars": 1755,
"preview": "'use strict'\n\n// Test object creation, the `disconnect` method, and `connected/disconnected`\n// events.\n\nfunction doneWr"
},
{
"path": "client/test/insert.js",
"chars": 5220,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add"
},
{
"path": "client/test/limit.js",
"chars": 2350,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/order.js",
"chars": 3676,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n assertThrows,\n "
},
{
"path": "client/test/orderLimitSub.js",
"chars": 3982,
"preview": "import 'rxjs/add/operator/concat'\n\nimport { assertCompletes, observableInterleave } from './utils'\n\nexport default funct"
},
{
"path": "client/test/remove.js",
"chars": 2640,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/"
},
{
"path": "client/test/removeAll.js",
"chars": 4175,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\nimport 'rxjs/add/"
},
{
"path": "client/test/replace.js",
"chars": 4214,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertC"
},
{
"path": "client/test/store.js",
"chars": 5164,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add"
},
{
"path": "client/test/test.html",
"chars": 288,
"preview": "<!doctype html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <link href=\"mocha.css\" rel=\"stylesheet\"/>\n <script src=\"horizo"
},
{
"path": "client/test/test.js",
"chars": 1903,
"preview": "const BROWSER = (typeof window !== 'undefined')\nconst path = require('path')\nconst glob = require('../src/util/glob')\n\nc"
},
{
"path": "client/test/times.js",
"chars": 2415,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertCompletes,\n compareWithoutVersi"
},
{
"path": "client/test/unit/ast.js",
"chars": 1317,
"preview": "import { applyChange } from '../../src/ast'\n\nexport default function unitAstSuite() {\n describe('applyChanges', () => {"
},
{
"path": "client/test/unit/auth.js",
"chars": 1873,
"preview": "import { TokenStorage, FakeStorage } from '../../src/auth'\n\nexport default function unitAuthSuite() {\n describe('TokenS"
},
{
"path": "client/test/unit/utilsTest.js",
"chars": 2105,
"preview": "import validIndexValue from '../../src/util/valid-index-value'\n\nexport default function unitUtilsSuite() {\n describe('v"
},
{
"path": "client/test/update.js",
"chars": 4183,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/toArray'\n\nimport { assertC"
},
{
"path": "client/test/upsert.js",
"chars": 4850,
"preview": "import 'rxjs/add/operator/do'\nimport 'rxjs/add/operator/mergeMapTo'\nimport 'rxjs/add/operator/mergeMap'\nimport 'rxjs/add"
},
{
"path": "client/test/utils.js",
"chars": 4206,
"preview": "import { Observable } from 'rxjs/Observable'\nimport 'rxjs/add/observable/empty'\nimport 'rxjs/add/operator/toArray'\nimpor"
},
{
"path": "client/webpack.config.js",
"chars": 816,
"preview": "const BUILD_ALL = (process.env.NODE_ENV === 'production')\n\nconst build = require('./webpack.horizon.config.js')\nconst te"
},
{
"path": "client/webpack.horizon.config.js",
"chars": 3354,
"preview": "const path = require('path')\n\nconst BannerPlugin = require('webpack/lib/BannerPlugin')\nconst DedupePlugin = require('web"
},
{
"path": "client/webpack.test.config.js",
"chars": 2000,
"preview": "const path = require('path')\n\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\nconst DEV_BUILD = (process.env.N"
},
{
"path": "docker-compose.dev.yml",
"chars": 305,
"preview": "rethinkdb:\n image: rethinkdb\n ports:\n - \"28015:28015\"\n - \"8080:8080\"\nhorizon:\n image: rethinkdb/horizon\n comma"
},
{
"path": "docker-compose.prod.yml",
"chars": 299,
"preview": "rethinkdb:\n image: rethinkdb\n ports:\n - \"28015:28015\"\n - \"8080:8080\"\nhorizon:\n image: rethinkdb/horizon\n comma"
},
{
"path": "examples/.eslintrc",
"chars": 2753,
"preview": "{\n \"rules\": {\n \"array-bracket-spacing\": [ 2, \"always\" ],\n \"arrow-parens\": [ 2, \"as-needed\" ],\n \"arrow-spacing\""
},
{
"path": "examples/README.md",
"chars": 1439,
"preview": "# Community Examples\n\nComing soon!\n\n# Horizon Extension Examples\n\nWe decided it was important to leverage Horizon alongs"
},
{
"path": "examples/angularjs-todo-app/.gitignore",
"chars": 43,
"preview": "node_modules/\nrethinkdb_data\n**/*.log\n.hz/\n"
},
{
"path": "examples/angularjs-todo-app/README.md",
"chars": 659,
"preview": "#TodoMVC\n\nA basic example of using [AngularJS](http://angularjs.org/) and Horizon to create real-time TodoMVC app.\n\n## P"
},
{
"path": "examples/angularjs-todo-app/dist/css/style.css",
"chars": 113,
"preview": "[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",
"chars": 2845,
"preview": "<!doctype html>\n<html>\n\n <head>\n <meta charset=\"UTF-8\">\n <script src=\"/horizon/horizon.js\"></script>\n "
},
{
"path": "examples/angularjs-todo-app/dist/js/app.js",
"chars": 31,
"preview": "angular.module('todomvc', []);\n"
},
{
"path": "examples/angularjs-todo-app/dist/js/controllers/TodoController.js",
"chars": 2498,
"preview": "(function () {\n 'use strict';\n\n angular\n .module('todomvc')\n .controller('TodoCtrl', TodoCtrl);\n\n "
},
{
"path": "examples/angularjs-todo-app/dist/package.json",
"chars": 136,
"preview": "{\n \"private\": true,\n \"dependencies\": {\n \"angular\": \"^1.5.8\",\n \"todomvc-app-css\": \"^2.0.0\",\n \"todomvc-common\":"
},
{
"path": "examples/auth-app/.gitignore",
"chars": 31,
"preview": "node_modules\ndist/node_modules\n"
},
{
"path": "examples/auth-app/README.md",
"chars": 0,
"preview": ""
},
{
"path": "examples/auth-app/dist/app.css",
"chars": 204,
"preview": "#steps {\n margin-top:25%;\n}\n\n#success > div > div {\n margin-top:10"
},
{
"path": "examples/auth-app/dist/app.js",
"chars": 1682,
"preview": "'use strict'\nvar horizon = Horizon({\n authType: 'anonymous'\n});\n\nconst crossOut = (element) => {\n element.setAttri"
},
{
"path": "examples/auth-app/dist/index.html",
"chars": 1809,
"preview": "<html>\n\n<head>\n <link href=\"app.css\" rel=\"stylesheet\">\n <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0."
},
{
"path": "examples/cyclejs-chat-app/README.md",
"chars": 330,
"preview": "# Horizon Chat example using Cycle.js\n\n<center></center>\n\n\n## Ideas for future\n\n- [ "
},
{
"path": "examples/cyclejs-chat-app/dist/app.css",
"chars": 810,
"preview": "/* always present */\n.expand-transition {\n transition: all .4s ease;\n height: 30px;\n padding: 10px;\n background-colo"
},
{
"path": "examples/cyclejs-chat-app/dist/app.js",
"chars": 4064,
"preview": "'use strict'\n\n;(function() {\n const makeDOMDriver = CycleDOM.makeDOMDriver\n const div = CycleDOM.div\n const input = C"
},
{
"path": "examples/cyclejs-chat-app/dist/index.html",
"chars": 851,
"preview": "<!doctype html>\n<html lang=\"en\" data-framework=\"react\">\n <head>\n <meta charset=\"UTF-8\">\n <link rel=\"sty"
},
{
"path": "examples/express-server/main.js",
"chars": 341,
"preview": "#!/usr/bin/env node\n'use strict'\n\nconst express = require('express');\nconst horizon = require('@horizon/server');\n\nconst"
},
{
"path": "examples/express-server/package.json",
"chars": 461,
"preview": "{\n \"name\": \"express-horizon-example\",\n \"version\": \"0.0.1\",\n \"description\": \"Example of a horizon server within a expr"
},
{
"path": "examples/hapi-server/main.js",
"chars": 356,
"preview": "#!/usr/bin/env node\n'use strict'\n\nconst Hapi = require('hapi');\nconst horizon = require('horizon-server');\n\nconst server"
},
{
"path": "examples/hapi-server/package.json",
"chars": 462,
"preview": "{\n \"name\": \"hapi-horizon-example\",\n \"version\": \"0.0.1\",\n \"description\": \"Example of a horizon server within a hapi se"
},
{
"path": "examples/koa-server/main.js",
"chars": 249,
"preview": "#!/usr/bin/env node\n'use strict'\n\nconst koa = require('koa');\nconst horizon = require('horizon-server');\n\nconst app = ko"
},
{
"path": "examples/koa-server/package.json",
"chars": 458,
"preview": "{\n \"name\": \"koa-horizon-example\",\n \"version\": \"0.0.1\",\n \"description\": \"Example of a horizon server within a koa serv"
},
{
"path": "examples/react-chat-app/README.md",
"chars": 327,
"preview": "# Horizon Chat example using React\n\n<center></center>\n\n\n## Ideas for future\n\n- [ ] i"
},
{
"path": "examples/react-chat-app/dist/app.css",
"chars": 810,
"preview": "/* always present */\n.expand-transition {\n transition: all .4s ease;\n height: 30px;\n padding: 10px;\n background-colo"
},
{
"path": "examples/react-chat-app/dist/app.jsx",
"chars": 4358,
"preview": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n\nvar app = app ||"
},
{
"path": "examples/react-chat-app/dist/index.html",
"chars": 742,
"preview": "<!doctype html>\n<html lang=\"en\" data-framework=\"react\">\n<head>\n <link rel=\"stylesheet\" href=\"app.css\">\n <link href='ht"
},
{
"path": "examples/react-chat-app/dist/package.json",
"chars": 70,
"preview": "{\n \"private\": true,\n \"dependencies\": {\n \"react\": \"^0.13.3\"\n }\n}\n"
},
{
"path": "examples/react-todo-app/.gitignore",
"chars": 31,
"preview": "node_modules\ndist/node_modules\n"
},
{
"path": "examples/react-todo-app/dist/index.html",
"chars": 1597,
"preview": "<!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"
},
{
"path": "examples/react-todo-app/dist/js/app.jsx",
"chars": 3867,
"preview": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n/*global React, R"
},
{
"path": "examples/react-todo-app/dist/js/footer.jsx",
"chars": 1338,
"preview": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\n/*global React */"
},
{
"path": "examples/react-todo-app/dist/js/todoItem.jsx",
"chars": 2926,
"preview": "/*jshint quotmark: false */\n/*jshint white: false */\n/*jshint trailing: false */\n/*jshint newcap: false */\n/*global Reac"
},
{
"path": "examples/react-todo-app/dist/js/todoModel.js",
"chars": 2774,
"preview": "/*jshint quotmark:false */\n/*jshint white:false */\n/*jshint trailing:false */\n/*jshint newcap:false */\nvar app = app || "
},
{
"path": "examples/react-todo-app/dist/js/utils.js",
"chars": 1018,
"preview": "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"
},
{
"path": "examples/react-todo-app/dist/package.json",
"chars": 189,
"preview": "{\n \"private\": true,\n \"dependencies\": {\n \"classnames\": \"^2.1.5\",\n \"director\": \"^1.2.0\",\n \"react\": \"^0.13.3\",\n "
},
{
"path": "examples/react-todo-app/readme.md",
"chars": 196,
"preview": "# React TodoMVC example using React\n\n## Ideas for future\n\n- [ ] implement `presence` for other connected people ToDo-ing"
},
{
"path": "examples/riotjs-chat-app/dist/app.css",
"chars": 835,
"preview": "/* always present */\n/*.expand-transition {\n transition: all .2s ease;\n height: 30px;\n padding: 10px;\n background-co"
},
{
"path": "examples/riotjs-chat-app/dist/chat.tag",
"chars": 1258,
"preview": "<chat>\n <div class=\"container\">\n <div class=\"row messages\">\n <ul>\n <li each={messages.slice().reverse().sl"
},
{
"path": "examples/riotjs-chat-app/dist/index.html",
"chars": 596,
"preview": "<html>\n<head>\n <link rel=\"stylesheet\" href=\"app.css\">\n <link href='https://fonts.googleapis.com/css?family=Source+Code"
},
{
"path": "examples/vue-chat-app/.gitignore",
"chars": 31,
"preview": "node_modules\ndist/node_modules\n"
},
{
"path": "examples/vue-chat-app/README.md",
"chars": 316,
"preview": "# RethinkDB Horizon Chat example using Vue.js\n\n<center></center>\n\n- [ ] implement `p"
},
{
"path": "examples/vue-chat-app/dist/app.css",
"chars": 855,
"preview": "/* always present */\n.expand-transition {\n transition: all .2s ease;\n height: 30px;\n padding: 10px;\n background-colo"
},
{
"path": "examples/vue-chat-app/dist/app.js",
"chars": 827,
"preview": "'use strict'\n\nconst horizon = Horizon();\nconst chat = horizon('chat')\nconst app = new Vue({\n\n el: '#app',\n data: {\n "
},
{
"path": "examples/vue-chat-app/dist/index.html",
"chars": 1236,
"preview": "<html>\n<head>\n <link rel=\"stylesheet\" href=\"app.css\">\n <link href='https://fonts.googleapis.com/css?family=Source+Code"
},
{
"path": "examples/vue-todo-app/.gitignore",
"chars": 31,
"preview": "dist/node_modules\nnode_modules\n"
},
{
"path": "examples/vue-todo-app/dist/index.html",
"chars": 2646,
"preview": "<!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 r"
},
{
"path": "examples/vue-todo-app/dist/js/app.js",
"chars": 5299,
"preview": "/*global Vue, todoStorage */\n\n(function (exports) {\n 'use strict';\n\n var filters = {\n all: "
},
{
"path": "examples/vue-todo-app/dist/js/routes.js",
"chars": 390,
"preview": "/*global app, Router */\n\n(function (app, Router) {\n\n\t'use strict';\n\n\tvar router = new Router();\n\n\t['all', 'active', 'com"
},
{
"path": "examples/vue-todo-app/dist/js/store.js",
"chars": 567,
"preview": "/*jshint unused:false */\n(function(exports) {\n\n 'use strict';\n\n const horizon = Horizon();\n const todos = horizon(\"vu"
},
{
"path": "examples/vue-todo-app/dist/package.json",
"chars": 158,
"preview": "{\n \"private\": true,\n \"dependencies\": {\n \"director\": \"^1.2.0\",\n \"vue\": \"^1.0.1\",\n \"todomvc-common\": \"^1.0.1\",\n"
},
{
"path": "examples/vue-todo-app/readme.md",
"chars": 229,
"preview": "# RethinkDB Horizon TodoMVC example using Vue.js\n\n## Ideas for future\n\n- [ ] implement `presence` for other connected pe"
},
{
"path": "protocol.md",
"chars": 7665,
"preview": "### Handshake\nThe handshake is required before any requests can be served. If the first message sent cannot be parsed a"
},
{
"path": "rfcs/identity_mgmt.md",
"chars": 2123,
"preview": "# Identity Management\n\n### Related issue:\nrethinkdb/horizon#3\n\n### Problem\n\nHorizon needs a concept of a user, and clien"
},
{
"path": "rfcs/permissions.md",
"chars": 15797,
"preview": "# Permissions\n\n## Description\n\n[Related issue (#4)](https://github.com/rethinkdb/horizon/issues/4)\n\nAny real application"
},
{
"path": "server/.babelrc",
"chars": 28,
"preview": "{\n \"presets\": [\"es2015\"]\n}\n"
},
{
"path": "server/.eslintrc.js",
"chars": 248,
"preview": "const OFF = 0;\nconst WARN = 1;\nconst ERROR = 2;\n\nmodule.exports = {\n extends: \"../.eslintrc.js\",\n rules: {\n \"max-le"
},
{
"path": "server/.gitignore",
"chars": 150,
"preview": "cert.pem\nkey.pem\nnode_modules\n*.log\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# RethinkDB stuff\nrethin"
},
{
"path": "server/README.md",
"chars": 1338,
"preview": "# Horizon Server\n\nAn extensible middleware server built on top of [RethinkDB](https://github.com/rethinkdb/rethinkdb) wh"
},
{
"path": "server/package.json",
"chars": 1126,
"preview": "{\n \"name\": \"@horizon/server\",\n \"version\": \"2.0.0\",\n \"description\": \"Server for RethinkDB Horizon, an open-source deve"
},
{
"path": "server/src/auth/auth0.js",
"chars": 1804,
"preview": "'use strict';\n\nconst auth_utils = require('./utils');\nconst logger = require('../logger');\n\nconst https = require('https"
},
{
"path": "server/src/auth/facebook.js",
"chars": 2470,
"preview": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https"
},
{
"path": "server/src/auth/github.js",
"chars": 1649,
"preview": "'use strict';\n\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst"
},
{
"path": "server/src/auth/google.js",
"chars": 1687,
"preview": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https"
},
{
"path": "server/src/auth/slack.js",
"chars": 1821,
"preview": "'use strict';\n\nconst auth_utils = require('./utils');\n\nconst https = require('https');\nconst Joi = require('joi');\nconst"
},
{
"path": "server/src/auth/twitch.js",
"chars": 1767,
"preview": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst https = require('https"
},
{
"path": "server/src/auth/twitter.js",
"chars": 5546,
"preview": "'use strict';\n\nconst logger = require('../logger');\nconst auth_utils = require('./utils');\n\nconst Joi = require('joi');\n"
},
{
"path": "server/src/auth/utils.js",
"chars": 7076,
"preview": "'use strict';\n\nconst logger = require('../logger');\n\nconst cookie = require('cookie');\nconst crypto = require('crypto');"
},
{
"path": "server/src/auth.js",
"chars": 4095,
"preview": "'use strict';\n\nconst logger = require('./logger');\nconst options_schema = require('./schema/server_options').auth;\nconst"
},
{
"path": "server/src/client.js",
"chars": 7200,
"preview": "'use strict';\n\nconst logger = require('./logger');\nconst schemas = require('./schema/horizon_protocol');\nconst Request ="
},
{
"path": "server/src/endpoint/common.js",
"chars": 124,
"preview": "'use strict';\n\nconst reql_options = {\n timeFormat: 'raw',\n binaryFormat: 'raw',\n};\n\nmodule.exports = {\n reql_options,"
},
{
"path": "server/src/endpoint/insert.js",
"chars": 1144,
"preview": "'use strict';\n\nconst insert = require('../schema/horizon_protocol').insert;\nconst writes = require('./writes');\nconst re"
},
{
"path": "server/src/endpoint/query.js",
"chars": 4506,
"preview": "'use strict';\n\nconst query = require('../schema/horizon_protocol').query;\nconst check = require('../error.js').check;\nco"
},
{
"path": "server/src/endpoint/remove.js",
"chars": 2145,
"preview": "'use strict';\n\nconst remove = require('../schema/horizon_protocol').remove;\nconst reql_options = require('./common').req"
},
{
"path": "server/src/endpoint/replace.js",
"chars": 1804,
"preview": "'use strict';\n\nconst replace = require('../schema/horizon_protocol').replace;\nconst reql_options = require('./common').r"
},
{
"path": "server/src/endpoint/store.js",
"chars": 2519,
"preview": "'use strict';\n\nconst store = require('../schema/horizon_protocol').store;\nconst reql_options = require('./common').reql_"
},
{
"path": "server/src/endpoint/subscribe.js",
"chars": 1291,
"preview": "'use strict';\n\nconst make_reql = require('./query').make_reql;\nconst reql_options = require('./common').reql_options;\n\nc"
},
{
"path": "server/src/endpoint/update.js",
"chars": 1997,
"preview": "'use strict';\n\nconst update = require('../schema/horizon_protocol').update;\nconst reql_options = require('./common').req"
},
{
"path": "server/src/endpoint/upsert.js",
"chars": 2785,
"preview": "'use strict';\n\nconst upsert = require('../schema/horizon_protocol').upsert;\nconst reql_options = require('./common').req"
},
{
"path": "server/src/endpoint/writes.js",
"chars": 6490,
"preview": "'use strict';\n\nconst check = require('../error').check;\n\nconst r = require('rethinkdb');\n\n// Common functionality used b"
},
{
"path": "server/src/error.js",
"chars": 1051,
"preview": "'use strict';\n\nconst check = (pred, message) => {\n if (!pred) {\n throw new Error(message);\n }\n};\n\nconst fail = (mes"
},
{
"path": "server/src/horizon.js",
"chars": 928,
"preview": "'use strict';\n\nconst joi = require('joi');\n\n// Issue a dummy joi validation to force joi to initialize its scripts.\n// T"
},
{
"path": "server/src/logger.js",
"chars": 78,
"preview": "'use strict';\n\nconst winston = require('winston');\n\nmodule.exports = winston;\n"
},
{
"path": "server/src/metadata/collection.js",
"chars": 2278,
"preview": "'use strict';\n\nconst error = require('../error');\nconst Table = require('./table').Table;\n\nconst r = require('rethinkdb'"
},
{
"path": "server/src/metadata/index.js",
"chars": 5641,
"preview": "'use strict';\n\nconst check = require('../error').check;\nconst logger = require('../logger');\n\n// Index names are of the "
},
{
"path": "server/src/metadata/metadata.js",
"chars": 13840,
"preview": "'use strict';\n\nconst error = require('../error');\nconst logger = require('../logger');\nconst Group = require('../permiss"
},
{
"path": "server/src/metadata/table.js",
"chars": 3534,
"preview": "'use strict';\n\nconst error = require('../error');\nconst index = require('./index');\nconst logger = require('../logger');"
},
{
"path": "server/src/permissions/group.js",
"chars": 254,
"preview": "'use strict';\nconst Rule = require('./rule').Rule;\n\nclass Group {\n constructor(row_data) {\n this.name = row_data.id;"
},
{
"path": "server/src/permissions/rule.js",
"chars": 1435,
"preview": "'use strict';\nconst Template = require('./template').Template;\nconst Validator = require('./validator').Validator;\n\nclas"
},
{
"path": "server/src/permissions/template.js",
"chars": 6880,
"preview": "'use strict';\n\nconst check = require('../error').check;\nconst remake_error = require('../utils').remake_error;\n\nconst as"
},
{
"path": "server/src/permissions/validator.js",
"chars": 752,
"preview": "'use strict';\n\nconst check = require('../error').check;\nconst logger = require('../logger');\nconst remake_error = requir"
},
{
"path": "server/src/reql_connection.js",
"chars": 3745,
"preview": "'use strict';\n\nconst check = require('./error').check;\nconst logger = require('./logger');\nconst Metadata = require('./m"
},
{
"path": "server/src/request.js",
"chars": 2441,
"preview": "'use strict';\n\nconst logger = require('./logger');\nconst rule = require('./permissions/rule');\n\nclass Request {\n constr"
},
{
"path": "server/src/schema/horizon_protocol.js",
"chars": 2852,
"preview": "'use strict';\n\nconst Joi = require('joi');\n\nconst handshake = Joi.object().keys({\n request_id: Joi.number().required(),"
},
{
"path": "server/src/schema/server_options.js",
"chars": 1242,
"preview": "'use strict';\n\nconst Joi = require('joi');\n\nconst server = Joi.object({\n project_name: Joi.string().default('horizon'),"
},
{
"path": "server/src/server.js",
"chars": 7572,
"preview": "'use strict';\n\nconst Auth = require('./auth').Auth;\nconst make_client = require('./client').make_client;\nconst ReqlConne"
},
{
"path": "server/src/utils.js",
"chars": 1589,
"preview": "'use strict';\n\nconst MIN_VERSION = [ 2, 3, 1 ];\n\n// Recursive version compare, could be flatter but opted for instant re"
},
{
"path": "server/test/http_tests.js",
"chars": 2835,
"preview": "'use strict';\n\nconst horizon = require('../');\n\nconst assert = require('assert');\nconst child_process = require('child_p"
},
{
"path": "server/test/permissions.js",
"chars": 29428,
"preview": "'use strict';\n\nconst hz_rule = require('../src/permissions/rule');\nconst hz_validator = require('../src/permissions/vali"
}
]
// ... and 11 more files (download for full content)
About this extraction
This page contains the full source code of the rethinkdb/horizon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 211 files (638.1 KB), approximately 166.8k tokens, and a symbol index with 362 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.