add example #1

Merged
merged 9 commits into from May 10, 2014
View
@@ -23,6 +23,20 @@ for the redpanda project. I intend to end up with a complete implementation of
the fauna v1 API, so please file an issue or contact me (Twitter & email
below) if you have suggestions, bug reports, or patches.
+Basic usage is to create a new `Fauna` object, register your model classes
+with it, authenticate, and then do whatever you want.
+
+```javascript
+var fauna = require('fauna-js');
+
+var client = new fauna.Fauna();
+client.addClasses(Song, Album);
+client.setKey(ClientKey);
+```
+
+Registering the model classes allows the library to encode and decode objects
+(see below).
+
## Models
@@ -78,8 +92,12 @@ The definition of "extends" is shown only for convenience -- you should use
the subclass mechanism your framework uses.
The fauna tracking info for an object will be stored in a field named
-'_fauna'. The tracking info contains the instance id (as `id`), timestamp (as
-`ts`), and class name in the database (as `className`).
+'_fauna'. The tracking info contains:
+
+- `id`: the fauna resource reference ID
+- `className`: fauna class name
+- `ts`: javascript timestamp (milliseconds) of the last update
+- `deleted`: true/false if this refers to a deleted object
```coffeescript
console.log "Song title is '#{song.title}' (fauna id #{song._fauna.id})"
@@ -96,11 +114,12 @@ class Song extends fauna.Class
## Event sets
-Custom event sets can be declared on a model class with `@eventSet`:
+Custom event sets can be declared on a model class with `@eventSet`, with
+optional config data attached:
```coffeescript
class Album extends fauna.Class
- @eventSet "tracks"
+ @eventSet "tracks", { ordered: true }
```
This will create a special field 'tracks' on any Album object, which refers to
@@ -126,10 +145,27 @@ an object with methods for manipulating the set. The methods are:
`after`.
-License
-=======
+## Developing
+
+Install dependencies using node:
+
+ $ npm install
+
+Run tests using cake (or npm):
+
+ $ cake test
+
+Build the library by compiling the coffeescript into javascript:
+
+ $ cake build
+
+Pull requests and bug reports are tracked on github:
+https://github.com/robey/fauna-js
+
+
+## License
-Apache 2 license, included in 'LICENSE.txt'.
+Apache 2 (open-source) license, included in 'LICENSE.txt'.
Authors
View
@@ -0,0 +1,305 @@
+
+# An example
+
+The best way to get the feel of this library is probably through a simple
@zerotrickpony
zerotrickpony May 5, 2013

You might add a one-liner that says something like "this doc assumes that you've read and understand the Fauna REST API [link], and uses its terms and concepts"

+example. Let's say we'd like to build a small chat server, like redpanda.
+
+This example assumes you're familiar with the basic concepts of fauna, and
+have browsed the API.
+
+## Promises
+
+Fauna-js is written in coffeescript and uses the Q promises library, but
+neither is necessary to use it. Coffeescript is just a functional veneer on
+javascript, and Q is just an advanced form of the callback system used in
+node and jquery.
+
+Instead of calling a fauna-js function with a callback as the last argument,
+like this:
+
+```javascript
+squirrel.findNuts(forest, function (error, nuts) {
+ ...
+});
+```
+
+the function will return a new object which has a `then` method. You call the
+`then` method with your callback function (and optionally an error-handling
+function), like this:
+
+```javascript
+squirrel.findNuts(forest).then(function (nuts) {
+ ...
+}, function (error) {
+ ...
+});
+```
+
+In both cases, the callback functions are called when the `findNuts` operation
+is finished, either by finding nuts, or because there was an error.
+
+## Schema
+
+The first thing we need to do is define our fauna object schema. For the
+littlest chat server, we just need three classes:
+
+- users
+- messages
+- chat rooms
+
+Users already exist as a native class in fauna, but we'll create the other
+two. In addition, we need to define relationships between them.
+
+- A message is written by a user.
+- A chat room contains many messages.
+
+The first type of relationship is what database people call "one to many":
+Each message is written by exactly one user, but each user may write many
+messages. In this case, we don't care about looking up which messages a user
+wrote -- we'll never ask "which messages did jack write?" -- so we only need
+to keep a reference on the message, pointing to the user who wrote it.
+
+The second type is also "one to many". Each message is in one chat room, but
+each chat room may contain many messages. In this case, we'll be asking
+questions in the opposite direction: What are the messages in each chat room?
+Luckily, this relationship fits well with fauna's event system. Each message
+can be added to a chat room as an event, and we can page through them in order
+of recency.
+
+So, in coffeescript, we'd have:
@zerotrickpony
zerotrickpony May 5, 2013

It looks like the rest of the example is in javascript, so maybe the coffeescript here is distracting? If I were a JS programmer I think I would basically like to not see any coffeescript at all. And if I'm a coffeescript programmer i probably wouldn't mind reading the whole thing in JS (they must all be used to that) and then seeing the coffeescript analog all at once at the end.

@robey
robey Jun 4, 2013 owner

hm, true, this is the only coffee-script on the page. the section below is meant to be the "also in javascript" bit, but it's too far separated. i'm going to merge it together. the coffee-script code is much clearer because js doesn't really have subclasses or static methods, so i do want to show the simple version first.

+
+```coffeescript
+fauna = require 'fauna'
+
+class User extends fauna.Class
@zerotrickpony
zerotrickpony May 5, 2013

Since User is a built-in class, could fauna-js just provide it so that the caller doesn't have to define it? Or is there some reason why the caller ought to make their own User flavor?

@robey
robey Jun 4, 2013 owner

i think i've clarified this in the new paragraph...

+ @native()
+ @field "name"
+
+class Message extends fauna.Class
+ @field "text"
+ @reference "author"
+
+class ChatRoom extends fauna.Class
+ @field "name"
+ @eventSet "messages"
+```
+
+It looks a little more opaque in javascript, because of the lack of syntax
+for extending objects and calling static methods:
+
+```javascript
+function User() {
+ fauna.Class.apply(this, arguments);
+}
+extends(User, fauna.Class);
+User.native();
+User.field("name");
+
+function Message() {
+ fauna.Class.apply(this, arguments);
+}
+extends(Message, fauna.Class);
+Message.field("text");
+Message.reference("author");
+
+function ChatRoom() {
+ fauna.Class.apply(this, arguments);
+}
+extends(ChatRoom, fauna.Class);
+ChatRoom.field("name");
+ChatRoom.eventSet("messages");
+```
+
+User-defined classes live in the `classes/` namespace in fauna, so normally
+fauna-js will determine the fauna class name by converting the javascript
+class name to lowercase, adding an "s", and using the `classes/` namespace:
+`Message` becomes `classes/messages`. We've marked `User` as a native fauna
+class, so it will use the fauna built-in class `users` instead of creating a
+new user-defined class. But we can add our own fields and references here,
+just like any other class, so we add a display name.
+
+`Message` has a text field for the message content, and a reference named
+"author". Marking it as a reference lets fauna know that it's a reference to
+another object in the database (usually with an identifier like
+`user/95183857239` or `classes/message/18374838271`). When responding to a
+query, fauna will helpfully look up these objects and fill them in, so that
+our response doesn't contain just the object id, but the whole object.
+
+`ChatRoom` has a name field, and also an event set for holding messages. Event
+sets are like containers with history -- but read the fauna documentation for
+the finer points.
+
+## Initialize
+
+First, we need to register our classes with the fauna-js library. This lets
+fauna-js map incoming json objects into our model classes:
+
+```javascript
+var faunaClient = require('fauna-js').FaunaClient;
+faunaClient.addPrototypes(User, Message, ChatRoom);
+```
+
+We can tell fauna about our user-defined classes (or models) as often as we
+want, and fauna will just ignore us if it already knows about these models.
+Since it requires publisher-level access, though, we should do it from a tool
+outside the app.
+
+Fauna authentication uses different types of tokens to grant different access
+levels. Fauna-js knows which API methods require which kind of authentication,
+so usually you can set authentication keys once, and the library will use the
+right kind of authentication for each call. If a fauna request can be
+authenticated multiple ways, fauna-js prefers to use a less- capable key
+first. For example, if a fauna operation can be done by either a user or
+publisher, then fauna-js will use a user token if one has been set, falling
+back to a publisher token otherwise. If a key of the right type hasn't been
+set, an exception will be thrown without bothering to talk to the server.
+
+So, to set the publisher key (which is needed to define new classes) and
+install our schema on the fauna servers:
+
+```javascript
+faunaClient.setPublisherKey("...");
+faunaClient.installSchema();
+```
+
+The 'installSchema' call just tells fauna about the names of the classes we've
+defined, and any event sets on those classes. This code should live in a setup
+script, and never in the client, because the publisher key should remain
+secret.
+
+## Create a new user
+
+Okay, let's login to fauna as a user, make a chat room, post a message to it,
+and read the last 20 messages posted. Simple!
+
+If no users exist yet, we should create one:
+
+```javascript
+var user = new User({ name, "Rocky", email: "rocky@example.com", password: "acornz" });
+user.persist().then(function (user) {
+ console.log("Created a new user: " + user);
+ console.log("The new user's id is: " + user._fauna.id);
+});
+```
+
+To create a new object ("instance") in fauna, just create a new javascript
+object out of a fauna model prototype, and persist it. This works fine for
+native (built-in) types like `User` as well as our own user-defined classes.
+
+The callback receives a "user" object, but its the same object you passed in,
+and is only chained into the callback for convenience. The fauna identifiers
+are attached to the object in a new `_fauna` field, which contains the fauna
+reference id in `id`. The `_fauna` object contains these useful fields:
+
+- `id`: the fauna resource reference ID
+- `className`: fauna class name
+- `ts`: javascript timestamp (milliseconds) of the last update
+- `deleted`: true/false if this refers to a deleted object
+
+The default constructor for a fauna-js `Class` takes an object and copies the
+fields into the new fauna object. This is just a convenience; you can also set
+them manually:
+
+```javascript
+var user = new User();
+user.name = "Rocky";
+user.email = "rocky@example.com";
+user.password = "acornz";
+user.persist().then( ... );
+```
+
+For user objects, fauna-js knows that `email` and `password` are built-in
+fields. For other fields or references on the class (like `name` in our
+example) fauna-js fills in the `data` and `references` sections of the object
+inside the fauna database.
+
+The `persist` call may look a little magical, but it's using the `FaunaClient`
+that the `User` prototype was registered with. If you're juggling multiple
+`FaunaClient`s for some reason, you can pass one to `persist` to override the
+default.
+
+## Login as a user
+
+If you already have an auth token for a user, setting it is easy:
+
+```javascript
+faunaClient.loginWithToken(userId, token).then(function (user) {
+ ...
+});
+```
+
+Fauna-js will use the token to attempt to fetch the user object, so you can
+tell immediately if the token is invalid. The user object is passed into the
+promise.
+
+If you don't have an auth token for a user, you can get one by logging in:
+
+```javascript
+faunaClient.login(email, password).then(function (token) {
+ console.log("auth token is: " + token.token);
+});
+```
+
+## Create a new chat room and post a message
+
+Creating a new chat room is identical to creating a new user.
+
+```javascript
+var room = new ChatRoom({ name: "woodland creatures" });
+room.persist().then(function (room) {
@zerotrickpony
zerotrickpony May 5, 2013

in the fauna docs they show the JSON block that comes back from the server. I found that really helpful, and so I'd recommend putting something like that here. What does "room" look like? What other fields are in it besides what I got from the example?

@robey
robey Jun 4, 2013 owner

i dunno. i just added a section on the "_fauna" object, but the rest of it should just be whatever's in the schema -- there isn't really a format.

+ console.log("The new room's id is: " + room._fauna.id);
+});
+```
+
+Creating a new message is more of the same, but this time we'll use the
+promise callback to add the new message to our chat room's "messages" event
+set.
+
+```javascript
+var message = new Message({ text: "hello everybody!", author: user });
+message.persist().then(function (message) {
+ room.messages.add(message).then(function () {
+ ...
+ });
+});
+```
+
+Notice that we pass an object for the `author` reference in the new message.
+Since we told fauna-js that it's a reference, it'll pull out the object
+identifier and put it in the "references" section of the object.
+
+## Read recent messages
+
+To read the most recent 20 messages, we ask for the most recent events on the
+chat room's "messages" event set. Fauna time values are in microseconds, and
+javascript's are in milliseconds, so we have to multiply the current time by
+1000, and ask for events before that time (now).
+
+```javascript
+var now = Date.now() * 1000;
+room.messages.page({ count: 20, before: now }).then(function (page) {
+ var messages = page.toArray().reverse();
+ for (var i = 0; i < messages.length; i++) {
+ console.log("<" + messages[i].author.name + "> " + messages[i].text);
+ }
+});
+```
+
+The `page` method requests a page of events from the event set, and returns an
+event set object with `before`, `after`, and `events` fields. Since we aren't
+going to keep paging through the events, we ignore the `before` and `after`
+fields, and call `toArray` to convert the event list into a list of objects.
+
+(If the events included modifications or deletions, `toArray` would only
+return items that still exist after matching "create" and "delete" operations.
+So for a true stream of events, you would want to use the `page.events` array
+directly. Our littlest chat server never deletes messages, and treats all
+messages as immutable, so we know all of the events are "create" events.)
+
+The objects are in reverse time order, so we reverse them back to display the
+oldest messages first. Fauna helpfully supplies the user object for our
+`author` field, so we dig out the user's name and display it next to their
+message.
+
+And that's it! We're chatting, like 1990s IRC all over again!
+
Oops, something went wrong. Retry.