1 | # Horizon Client Library
|
2 |
|
3 | The Horizon client library. Built to interact with the [Horizon Server](/server) websocket API. Provides all the tooling to build a fully-functional and reactive front-end web application.
|
4 |
|
5 | ## Building
|
6 |
|
7 | Running `npm install` for the first time will build the browser bundle and lib files.
|
8 |
|
9 | 1. `npm install`
|
10 | 2. `npm run dev` (or `npm run build` or `npm run compile`, see below)
|
11 |
|
12 | ### Build Options
|
13 |
|
14 | Command | Description
|
15 | --------------------|----------------------------
|
16 | npm run dev | Watch directory for changes, build dist/horizon.js unminified browser bundle
|
17 | npm run build | Build dist/horizon.js minified production browser bundle
|
18 | npm run compile | Compile src to lib for CommonJS module loaders (such as webpack, browserify)
|
19 | npm test | Run tests in node
|
20 | npm run lint -s | Lint src
|
21 | npm run devtest | Run tests and linting continually
|
22 |
|
23 | ## Running tests
|
24 |
|
25 | * `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`.
|
26 | * 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.
|
27 |
|
28 | ## Docs
|
29 |
|
30 |
|
31 | ### Getting Started
|
32 | [Check out our Getting Started guide.](/GETTING-STARTED.md)
|
33 |
|
34 | ### API
|
35 |
|
36 | * [Horizon](#horizon)
|
37 | * [Collection](#collection)
|
38 | * [above](#above-limit-integer--key-value-closed-string-)
|
39 | * [below](#below-limit-integer--key-value-closed-string-)
|
40 | * [fetch](#fetch)
|
41 | * [find](#find---id-any-)
|
42 | * [findAll](#findall--id-any----id-any--)
|
43 | * [limit](#limit-num-integer-)
|
44 | * [order](#order---directionascending-)
|
45 | * [remove](#remove-id-any--id-any-)
|
46 | * [removeAll](#removeall--id-any--id-any-----id-any---id-any---)
|
47 | * [replace](#replace--)
|
48 | * [store](#store-------)
|
49 | * [upsert](#upsert------)
|
50 | * [watch](#watch--rawchanges-false--)
|
51 |
|
52 | #### Horizon
|
53 |
|
54 | Object which initializes the connection to a Horizon Server.
|
55 |
|
56 | If Horizon server has been started with `--insecure` then you will need to connect unsecurely by passing `{secure: false}` as a second parameter.
|
57 |
|
58 | ###### Example
|
59 |
|
60 | ```js
|
61 | const Horizon = require("@horizon/client")
|
62 | const horizon = Horizon()
|
63 |
|
64 | const unsecure_horizon = Horizon({ secure: false })
|
65 | ```
|
66 |
|
67 | #### Collection
|
68 |
|
69 | Object which represents a collection of documents on which queries can be performed.
|
70 |
|
71 | ###### Example
|
72 | ```js
|
73 | // Setup connection the Horizon server
|
74 | const Horizon = require("@horizon/client")
|
75 | const horizon = Horizon()
|
76 |
|
77 | // Create horizon collection
|
78 | const messages = horizon('messages')
|
79 | ```
|
80 |
|
81 | ##### above( *limit* *<integer>* || *{key: value}*, *closed* *<string>* )
|
82 |
|
83 | The `.above` method can be chained onto all methods with the exception of `.find` and `.limit` and restricts the range of results returned.
|
84 |
|
85 | The first parameter if an integer will limit based on `id` and if an object is provided the limit will be on the key provided and its value.
|
86 |
|
87 | The second parameter allows only either "closed" or "open" as arguments for inclusive or exclusive behavior for the limit value.
|
88 |
|
89 | ###### Example
|
90 |
|
91 | ```js
|
92 |
|
93 | // {
|
94 | // id: 1,
|
95 | // text: "Top o' the morning to ya! 🇮🇪",
|
96 | // author: "kittybot"
|
97 | // }, {
|
98 | // id: 2,
|
99 | // text: "Howdy! 🇺🇸",
|
100 | // author: "grey"
|
101 | // }, {
|
102 | // id: 3,
|
103 | // text: "Bonjour 🇫🇷",
|
104 | // author: "coffeemug"
|
105 | // }, {
|
106 | // id: 4,
|
107 | // text: "Gutentag 🇩🇪",
|
108 | // author: "deontologician"
|
109 | // }, {
|
110 | // id: 5,
|
111 | // text: "G'day 🇦🇺",
|
112 | // author: "dalanmiller"
|
113 | // }
|
114 |
|
115 | // Returns docs with id 4 and 5
|
116 | chat.messages.order("id").above(3).fetch().forEach(doc => console.log(doc));
|
117 |
|
118 | // Returns docs with id 3, 4, and 5
|
119 | chat.messages.order("id").above(3, "closed").fetch().forEach(doc => console.log(doc));
|
120 |
|
121 | // Returns the documents with ids 1, 2, 4, and 5 (alphabetical)
|
122 | chat.messages.order("id").above({author: "d"}).fetch().forEach(doc => console.log(doc));
|
123 | ```
|
124 |
|
125 | ##### below( *limit* *<integer>* || *{key: value}*, *closed* *<string>* )
|
126 |
|
127 | The `.below` method can only be chained onto an `.order(...)` method and limits the range of results returned.
|
128 |
|
129 | The first parameter if an integer will limit based on `id` and if an object is provided the limit will be on the key provided and its value.
|
130 |
|
131 | The second parameter allows only either "closed" or "open" as arguments for inclusive or exclusive behavior for the limit value.
|
132 |
|
133 | ###### Example
|
134 |
|
135 | ```javascript
|
136 |
|
137 | // {
|
138 | // id: 1,
|
139 | // text: "Top o' the morning to ya! 🇮🇪",
|
140 | // author: "kittybot"
|
141 | // }, {
|
142 | // id: 2,
|
143 | // text: "Howdy! 🇺🇸",
|
144 | // author: "grey"
|
145 | // }, {
|
146 | // id: 3,
|
147 | // text: "Bonjour 🇫🇷",
|
148 | // author: "coffeemug"
|
149 | // }, {
|
150 | // id: 4,
|
151 | // text: "Gutentag 🇩🇪",
|
152 | // author: "deontologician"
|
153 | // }, {
|
154 | // id: 5,
|
155 | // text: "G'day 🇦🇺",
|
156 | // author: "dalanmiller"
|
157 | // }
|
158 |
|
159 | // Returns docs with id 1 and 2
|
160 | chat.messages.order("id").below(3).fetch().forEach(doc => console.log(doc));
|
161 |
|
162 | // Returns docs with id 1, 2, and 3
|
163 | chat.messages.order("id").below(3, "closed").fetch().forEach(doc => console.log(doc));
|
164 |
|
165 | // Returns the document with id 3 (alphabetical)
|
166 | chat.messages.order("id").below({author: "d"}).fetch().forEach(doc => console.log(doc));
|
167 | ```
|
168 |
|
169 | ##### fetch()
|
170 |
|
171 | Queries for the results of a query currently, without updating results when they change. This is used to complete and send
|
172 | the query request.
|
173 |
|
174 | ##### Example
|
175 |
|
176 | ```js
|
177 |
|
178 | // Returns the entire contents of the collection
|
179 | horizon('chats').fetch().forEach(
|
180 | result => console.log('Result:', result),
|
181 | err => console.error(err),
|
182 | () => console.log('Results fetched, query done!')
|
183 | )
|
184 |
|
185 | // Sample output
|
186 | // Result: { id: 1, chat: 'Hey there' }
|
187 | // Result: { id: 2, chat: 'Ho there' }
|
188 | // Results fetched, query done!
|
189 | ```
|
190 |
|
191 | If you would rather get the results all at once as an array, you can
|
192 | chain `.toArray()` to the call:
|
193 |
|
194 | ```js
|
195 | horizon('chats').fetch().toArray().forEach(
|
196 | results => console.log('All results: ', results),
|
197 | err => console.error(err),
|
198 | () => console.log('Results fetched, query done!')
|
199 | )
|
200 |
|
201 | // Sample output
|
202 | // All results: [ { id: 1, chat: 'Hey there' }, { id: 2, chat: 'Ho there' } ]
|
203 | // Results fetched, query done!
|
204 | ```
|
205 |
|
206 | ##### find( *{}* || *id* *<any>* )
|
207 |
|
208 | Retrieve a single object from the Horizon collection.
|
209 |
|
210 | ###### Example
|
211 |
|
212 | ```js
|
213 | // Using id, both are equivalent
|
214 | chats.find(1).fetch().forEach(doc => console.log(doc));
|
215 | chats.find({ id: 1 }).fetch().forEach(doc => console.log(doc));
|
216 |
|
217 | // Using another field
|
218 | chats.find({ name: "dalan" }).fetch().forEach(doc => console.log(doc));
|
219 | ```
|
220 |
|
221 | ##### findAll( *{ id:* *<any> }* [, *{ id:* *<any> }*] )
|
222 |
|
223 | Retrieve multiple objects from the Horizon collection. Returns `[]` if queried documents do not exist.
|
224 |
|
225 | ###### Example
|
226 |
|
227 | ```js
|
228 | chats.findAll({ id: 1 }, { id: 2 }).fetch().forEach(doc => console.log(doc));
|
229 |
|
230 | chats.findAll({ name: "dalan" }, { id: 3 }).fetch().forEach(doc => console.log(doc));
|
231 | ```
|
232 |
|
233 | ##### forEach( *readResult[s]* *<function>*, *error* *<function>*, *completed* *<function>* || *writeResult[s] *<function>*, *error* *<function>* || *changefeedHandler* *<function>*, *error* *<function>*)
|
234 |
|
235 | Means of providing handlers to a query on a Horizon collection.
|
236 |
|
237 | ###### Example
|
238 |
|
239 | When `.forEach` is chained off of a read operation it accepts three functions as parameters. A results handler, a error handler, and a result completion handler.
|
240 |
|
241 | ```js
|
242 | // Documents are returned one at a time.
|
243 | chats.fetch().forEach(
|
244 | (result) => { console.log("A single document =>" + result ) },
|
245 | (error) => { console.log ("Danger Will Robinson 🤖! || " + error ) },
|
246 | () => { console.log("Read is now complete" ) }
|
247 | );
|
248 |
|
249 | // To wait and retrieve all documents as a single array instead of immediately one at a time.
|
250 | chats.toArray().fetch().forEach(
|
251 | (result) => { console.log("A single document =>" + result ) },
|
252 | (error) => { console.log ("Danger Will Robinson 🤖! || " + error ) },
|
253 | () => { console.log("Read is now complete" ) }
|
254 | );
|
255 | ```
|
256 |
|
257 | When `.forEach` is chained off of a write operation it accepts two functions, one which handles successful writes and handles the returned `id` of the document from the server as well as an error handler.
|
258 |
|
259 | ```js
|
260 | chats.store([
|
261 | { text: "So long, and thanks for all the 🐟!" },
|
262 | { id: 2, text: "Don't forget your towel!" }
|
263 | ]).forEach(
|
264 | (id) => { console.log("A saved document id =>" + id ) },
|
265 | (error) => { console.log ("An error has occurred || " + error ) },
|
266 | );
|
267 |
|
268 | // Output:
|
269 | // f8dd67dc-2301-487a-85ab-c4b573acad2d
|
270 | // 2 (because `id` was provided)
|
271 | ```
|
272 |
|
273 | When `.forEach` is chained off of a changefeed it accepts two functions, one which handles the changefeed results as well as an error handler.
|
274 |
|
275 | ```js
|
276 | chats.watch().forEach(
|
277 | (chats) => { console.log("The entire chats collection triggered by changes =>" + chats ) },
|
278 | (error) => { console.log ("An error has occurred || " + error ) },
|
279 | );
|
280 | ```
|
281 |
|
282 | ##### limit( *num* *<integer>* )
|
283 |
|
284 | Limit the output of a query to the provided number of documents. If the result of the query prior to `.limit(...)` is fewer than the value passed to `.limit` then the results returned will be limited to that amount.
|
285 |
|
286 | If using `.limit(...)` it must be the final method in your query.
|
287 |
|
288 | ###### Example
|
289 |
|
290 | ```js
|
291 |
|
292 | chats.limit(5).fetch().forEach(doc => console.log(doc));
|
293 |
|
294 | chats.findAll({ author: "dalan" }).limit(5).fetch().forEach(doc => console.log(doc));
|
295 |
|
296 | chats.order("datetime", "descending").limit(5).fetch().forEach(doc => console.log(doc));
|
297 | ```
|
298 |
|
299 | ##### order( *<string>* [, *direction*="ascending"] )
|
300 |
|
301 | Order the results of the query by the given field string. The second parameter is also a string that determines order direction. Default is ascending ⏫.
|
302 |
|
303 | ###### Example
|
304 |
|
305 | ```js
|
306 | chats.order("id").fetch().forEach(doc => console.log(doc));
|
307 |
|
308 | // Equal result
|
309 | chats.order("name").fetch().forEach(doc => console.log(doc));
|
310 | chats.order("name", "ascending").fetch().forEach(doc => console.log(doc));
|
311 |
|
312 | chats.order("age", "descending").fetch().forEach(doc => console.log(doc));
|
313 | ```
|
314 |
|
315 | ##### remove( *id* *<any>* || *{id:* *\<any>}* )
|
316 |
|
317 | Remove a single document from the collection. Takes an `id` representing the `id` of the document to remove or an object that has an `id` key.
|
318 |
|
319 | ###### Example
|
320 |
|
321 | ```javascript
|
322 |
|
323 | // Equal results
|
324 | chat.remove(1);
|
325 | chat.remove({ id: 1 })
|
326 |
|
327 | ```
|
328 | ##### removeAll( [ *id* *<any>* [, *id* *<any>* ]] || [ *{* *id:* *<any>* [, *{* *id:* *<any>* *}* ]] )
|
329 |
|
330 | Remove multiple documents from the collection via an array of `id` integers or an array of objects that have an `id` key.
|
331 |
|
332 | ###### Example
|
333 |
|
334 | ```js
|
335 |
|
336 | // Equal results
|
337 | chat.removeAll([1, 2, 3]);
|
338 | chat.removeAll([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
339 | ```
|
340 |
|
341 | ##### replace( *{}* )
|
342 |
|
343 | The `replace` command replaces documents already in the database. An error will occur if the document does not exist.
|
344 |
|
345 | ###### Example
|
346 |
|
347 | ```js
|
348 |
|
349 | // Will result in error
|
350 | chat.replace({
|
351 | id: 1,
|
352 | text: "Oh, hello"
|
353 | });
|
354 |
|
355 | // Store a document
|
356 | chat.store({
|
357 | id: 1,
|
358 | text: "Howdy!"
|
359 | });
|
360 |
|
361 | // Replace will be successful
|
362 | chat.replace({
|
363 | id: 1,
|
364 | text: "Oh, hello!"
|
365 | });
|
366 | ```
|
367 |
|
368 | ##### store( *{}* || [ *{}* [, *{}*] )
|
369 |
|
370 | The `store` method stores objects or arrays of objects. One can also chain `.forEach` off of `.store` which takes two
|
371 | functions to handle store succeses and errors.
|
372 |
|
373 | ###### Example
|
374 |
|
375 | ```js
|
376 | chat.store({
|
377 | id:1,
|
378 | text: "Hi 😁"
|
379 | });
|
380 |
|
381 | chat.find({ id: 1 }).fetch().forEach((doc) => {
|
382 | console.log(doc); // matches stored document above
|
383 | });
|
384 |
|
385 | chat.store({ id: 2, text: "G'day!" }).forEach(
|
386 | (id) => { console.log("saved doc id: " + id) },
|
387 | (error) => { console.log(err) }
|
388 | );
|
389 |
|
390 | ```
|
391 |
|
392 | ##### upsert( *{}* || [ *{}* [, *{}* ]] )
|
393 |
|
394 | The `upsert` method allows storing a single or multiple documents in a single call. If any of them exist, the existing version of the document will be updated with the new version supplied to the method. Replacements are determined by already existing documents with an equal `id`.
|
395 |
|
396 | ###### Example
|
397 |
|
398 | ```javascript
|
399 |
|
400 | chat.store({
|
401 | id: 1,
|
402 | text: "Hi 😁"
|
403 | });
|
404 |
|
405 | chat.upsert([{
|
406 | id: 1,
|
407 | text: "Howdy 😅"
|
408 | }, {
|
409 | id: 2,
|
410 | text: "Hello there!"
|
411 | }, {
|
412 | id: 3,
|
413 | text: "How have you been?"
|
414 | }]);
|
415 |
|
416 | chat.find(1).fetch().forEach((doc) => {
|
417 | // Returns "Howdy 😅"
|
418 | console.log(message.text);
|
419 | });
|
420 |
|
421 | ```
|
422 |
|
423 | ##### watch( *{ rawChanges: false }* )
|
424 | Turns the query into a changefeed query, returning an observable that receives a live-updating view of the results every time they change.
|
425 |
|
426 | ###### Example
|
427 |
|
428 | This query will get all chats in an array every time a chat is added,
|
429 | removed or deleted.
|
430 |
|
431 | ```js
|
432 | horizon('chats').watch().forEach(allChats => {
|
433 | console.log('Chats: ', allChats)
|
434 | })
|
435 |
|
436 | // Sample output
|
437 | // Chats: []
|
438 | // Chats: [{ id: 1, chat: 'Hey there' }]
|
439 | // Chats: [{ id: 1, chat: 'Hey there' }, {id: 2, chat: 'Ho there' }]
|
440 | // Chats: [{ id: 2, chat: 'Ho there' }]
|
441 | ```
|
442 |
|
443 | Alternately, you can provide the `rawChanges: true` option to receive change documents from the server directly, instead of having the client maintain the array of results for you.
|
444 |
|
445 | ```js
|
446 | horizon('chats').watch({ rawChanges: true }).forEach(change => {
|
447 | console.log('Chats changed:', change)
|
448 | })
|
449 |
|
450 | // Sample output
|
451 | // Chat changed: { type: 'state', state: 'synced' }
|
452 | // Chat changed: { type: 'added', new_val: { id: 1, chat: 'Hey there' }, old_val: null }
|
453 | // Chat changed: { type: 'added', new_val: { id: 2, chat: 'Ho there' }, old_val: null }
|
454 | // Chat changed: { type: 'removed', new_val: null, old_val: { id: 1, chat: 'Hey there' } }
|
455 | ```
|
456 |
|
457 | ## Authenticating
|
458 |
|
459 | There are three types of authentication types that Horizon recognizes.
|
460 |
|
461 | ### Unauthenticated
|
462 |
|
463 | The first auth type is unauthenticated. One [JWT](https://jwt.io/) is shared by all unauthenticated users. To create a connection using the 'unauthenticated' method do:
|
464 |
|
465 | ``` js
|
466 | const horizon = Horizon({ authType: 'unauthenticated' });
|
467 | ```
|
468 |
|
469 | This is the default authentication method and provides no means to separate user permissions or data in the Horizon application.
|
470 |
|
471 | ### Anonymous
|
472 |
|
473 | The second auth type is anonymous. If anonymous authentication is enabled in the config, any user requesting anonymous authentication will be given a new JWT, with no other confirmation necessary. The server will create a user entry in the users table for this JWT, with no other way to authenticate as this user than by passing the JWT back. (This is done under the hood with the jwt being stored in localStorage and passed back on subsequent requests automatically).
|
474 |
|
475 | ``` js
|
476 | const horizon = Horizon({ authType: 'anonymous' });
|
477 | ```
|
478 |
|
479 | This type of authentication is useful when you need to differentiate users but don't want to use a popular 3rd party to authenticate them. This is essentially the means of "Creating an account" or "Signing up" for people who use your website.
|
480 |
|
481 | ### Token
|
482 |
|
483 | This is the only method of authentication that verifies a user's identity with a third party. To authenticate, first pick an OAuth identity provider. For example, to use Twitter for authentication, you might do something like:
|
484 |
|
485 | ``` js
|
486 | const horizon = Horizon({ authType: 'token' });
|
487 | if (!horizon.hasAuthToken()) {
|
488 | horizon.authEndpoint('twitter').toPromise()
|
489 | .then((endpoint) => {
|
490 | window.location.pathname = endpoint;
|
491 | })
|
492 | } else {
|
493 | // We have a token already, do authenticated horizon stuff here...
|
494 | }
|
495 | ```
|
496 | After logging in with Twitter, the user will be redirected back to the app, where the Horizon client will grab the JWT from the redirected url, which will be used on subsequent connections where `authType = 'token'`. If the token is lost (because of a browser wipe, or changing computers etc), the user can be recovered by re-authenticating with Twitter.
|
497 |
|
498 | This is type of authentication is useful for quickly getting your application running with information relevant to your application provided by a third party. Users don't need to create yet another user acount for your application and can reuse the ones they already have.
|
499 |
|
500 | ### Clearing tokens
|
501 |
|
502 | Sometimes you may wish to delete all authentication tokens from localStorage. You can do that with:
|
503 |
|
504 | ``` js
|
505 | // Note the 'H'
|
506 | Horizon.clearAuthTokens()
|
507 | ```
|