How to perform CRUD operations in Fauna

Fauna allows you to store documents and query them in a relational fashion. This section walks you through a basic example of creating, retrieving, updating, and deleting (CRUD) documents in Fauna, including working with collections. If you are new to Fauna, make sure to check out our Glossary for definitions.

Introduction

To demonstrate how to perform CRUD operations in Fauna, we are going to use the example of blog posts: creating blog posts, updating them with additional attributes, and querying for specific posts.

The steps are:

We have set up this example so you can follow along from start to finish. Feel free to skip straight to Create a post if you are just looking for examples of the create, retrieve, update, and delete process.

Requirements

This section walks you through setting up your environment, installing a driver, importing the driver, obtaining an admin key, and instantiating the client.

Supported runtimes

Before you install the driver, it’s important to ensure you’re running a compatible version of the language runtime and have satisfied other dependencies.

The JavaScript driver is supported on:

  • Node.js

    • LTS (v12)

    • Stable (v10+)

  • Chrome

  • Firefox

  • Safari

  • Internet Explorer 11

Currently, the driver is tested on Go versions:

  • 1.11

  • 1.12

  • 1.13

  • 1.14

Compatible with:

  • .NET SDK

  • Mono (on macOS or Linux)

Shared
Java
  • Java 8

Shared
Scala
  • Scala 2.11.x

  • Scala 2.12.x

The following versions of Python are supported:

  • Python 2.7

  • Python 3.3

  • Python 3.4

  • Python 3.5

  • Python 3.6

  • Python 3.7

  • Python 3.8

Install the driver

Node.js

To install the JavaScript driver, run this command in the terminal:

npm install --save faunadb

See faunadb on NPM for more information. Note that not all Node.js/serverless environments are supported.

Browsers

The JavaScript driver can be included via CDN:

<script src="//cdn.jsdelivr.net/npm/faunadb@latest/dist/faunadb.js"></script>

The minified version of the driver can also be used via CDN:

<script src="//cdn.jsdelivr.net/npm/faunadb@latest/dist/faunadb-min.js"></script>

See the driver’s repository for more details: fauna/faunadb-js

To install the Go driver, run this in the terminal:

go get github.com/fauna/faunadb-go/{driver-go-get)/faunadb

First install the Nuget package by adding the package reference to your MSBuild project:

<PackageReference Include="FaunaDB.Client" Version="4.2.0" />

or by using your IDE and searching for FaunaDB.Client.

Download from the Maven central repository:

faunadb-java/pom.xml
<dependencies>
  ...
  <dependency>
    <groupId>com.faunadb</groupId>
    <artifactId>faunadb-java</artifactId>
    <version>4.3.0</version>
    <scope>compile</scope>
  </dependency>
  ...
</dependencies>
faunadb-scala/sbt
libraryDependencies += ("com.faunadb" %% "faunadb-scala" % "4.3.0")
pip install faunadb

Import the driver

Import the client and the query language helpers.

using FaunaDB.Client;
using FaunaDB.Types;

using static FaunaDB.Query.Language;

We recommend that you import this driver with an alias import such as:

import (
  "fmt"
  f "github.com/fauna/faunadb-go/v4/faunadb"
)
import com.faunadb.client.*;
import com.faunadb.client.types.*;
import com.faunadb.client.types.Value.*;
var faunadb = require('faunadb')
var q = faunadb.query;

This is the recommended require stanza. The faunadb.query module contains all of the functions to create Fauna query expressions.

Similarly with ES6 modules:

import faunadb, { query as q } from "faunadb"

The CDN package exposes a global faunadb variable.

from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient
import faunadb.FaunaClient
import faunadb.query._
import faunadb.values._

Obtain an admin key

To follow along, you need an admin key, which you can create using the Fauna Dashboard.

Instantiate an admin client

var adminClient = new FaunaClient(secret: YOUR_FAUNADB_ADMIN_SECRET);
adminClient := f.NewFaunaClient("YOUR_FAUNADB_ADMIN_SECRET")
FaunaClient adminClient = FaunaClient.builder()
      .withSecret("YOUR_FAUNADB_ADMIN_SECRET")
      .build();
var adminClient = new faunadb.Client({ secret: 'YOUR_FAUNADB_ADMIN_SECRET' });
adminClient = FaunaClient(secret="YOUR_FAUNADB_ADMIN_SECRET")
val adminClient = FaunaClient(secret = "YOUR_FAUNADB_ADMIN_SECRET")

Create a database

Create a database called my_app in Fauna:

try
{
    Value result = await adminClient.Query(
        CreateDatabase(Obj("name", "my_app"))
    );

    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "my_app", collection = RefV(id = "databases")),ts: LongV(1622574227070000),name: StringV(my_app),global_id: StringV(yoijz8a4sybyy))
result, err := adminClient.Query(
	f.CreateDatabase(f.Obj{"name": "my_app"}))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[global_id:yoijzpsy6ybyy name:my_app ref:{my_app 0xc0001037a0 0xc0001037a0 <nil>} ts:1622574323940000]
System.out.println(
    adminClient.query(
        CreateDatabase(Obj("name", Value("my_app")))
    ).get());
{ref: ref(id = "my_app", collection = ref(id = "databases")), ts: 1622574355920000, name: "my_app", global_id: "yoijzxwxsybyy"}
adminClient.query(
  q.CreateDatabase({ name: 'my_app' })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Database("my_app"),
  ts: 1622574499020000,
  name: 'my_app',
  global_id: 'yoijzam91ybyy'
}
result = adminClient.query(
  q.create_database({"name": "my_app"})
)
print(result)
{'ref': Ref(id=my_app, collection=Ref(id=databases)), 'ts': 1622574506710000, 'name': 'my_app', 'global_id': 'yoijza49hydyy'}
CreateDatabase({ name: 'my_app' })
{
  ref: Database("my_app"),
  ts: 1624310594390000,
  name: 'my_app',
  global_id: 'yoat8d8erydyy'
}
Query metrics:
  •    bytesIn:   48

  •   bytesOut:  152

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   19

  • writeBytes:  470

  •  queryTime: 45ms

  •    retries:    0

Create a server key to access the my_app database

Create a server key. The server key has unrestricted access to a single database; in this case, the server key allows access only to the my_app database that we just created.

try
{
    Value result = await adminClient.Query(
        CreateKey(
            Obj(
                "database", Database("my_app"),
                "role", "server"
            )
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "300223437306069504", collection = RefV(id = "keys")),ts: LongV(1622574231350000),database: RefV(id = "my_app", collection = RefV(id = "databases")),role: StringV(server),secret: StringV(fnAEKpugJeACABxgtt0QWzlpfy0hJevB3yaiT4FZ),hashed_secret: StringV($2a$05$.DPcB6qo3ZYP3AKzaqH/r.gBDsvf2l0arylIrL0d02Rgf0EG5BIpm))
result, err := adminClient.Query(
	f.CreateKey(
		f.Obj{
			"database": f.Database("my_app"),
			"role": "server",
		},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[database:{my_app 0xc0001805a0 0xc0001805a0 <nil>} hashed_secret:$2a$05$EnvOSmQkFDzdY1Uv5GcE3uAQOm/sowh63w18On0RZs8HYKB/GhET6 ref:{300223535791473152 0xc0001804b0 0xc0001804b0 <nil>} role:server secret:fnAEKpu3FBACAO5Y68g1aIGP4Bj-9-3XuAvGvlnq ts:1622574325280000]
System.out.println(
    adminClient.query(
        CreateKey(
            Obj(
                "database", Database(Value("my_app")),
                "role", Value("server")
            )
        )
    ).get());
{ref: ref(id = "300223578563936768", collection = ref(id = "keys")), ts: 1622574366070000, database: ref(id = "my_app", collection = ref(id = "databases")), role: "server", secret: "fnAEKpvBCYACAGh1X1AByGzUVqd6-BajTEdOdtCO", hashed_secret: "$2a$05$.mcEBa9OANfdF.6iYsquuue0di.W8erLc6ki6pPeRzlcRnq0u6xra"}
adminClient.query(
  q.CreateKey({
    database: q.Database('my_app'),
    role: 'server',
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Keys(), "300223718348554752"),
  ts: 1622574499380000,
  database: Database("my_app"),
  role: 'server',
  secret: 'fnAEKpvhlVACAGnxyrjFa-zJiNatObJjyXFc3V8L',
  hashed_secret: '$2a$05$mCeHKqZYZHN5.MGC6RIIRO3M7JSt6UHmoziYWRzezBHrpuUiEdRFi'
}
result = adminClient.query(
  q.create_key(
    {"database": q.database("my_app"), "role": "server"}
  )
)
print(result)
{'ref': Ref(id=300223726396375552, collection=Ref(id=keys)), 'ts': 1622574507050000, 'database': Ref(id=my_app, collection=Ref(id=databases)), 'role': 'server', 'secret': 'fnAEKpvjdQACAFruhIE6q_-k9h8y_CJLLwFmzXNd', 'hashed_secret': '$2a$05$bpLlW.51p54oU.F9HRTxXOyBZ7G8T8ATiMxGPPq7elGG667ThDLtG'}
CreateKey({
  database: Database('my_app'),
  role: 'server',
})
{
  ref: Ref(Keys(), "302044146566169088"),
  ts: 1624310595010000,
  database: Database("my_app"),
  role: 'server',
  secret: 'fnAEMRONDpACAMrMtkE9PJk2CjjqPBk71xnFs44W',
  hashed_secret: '$2a$05$8J9xjhMmc3LJSrS5bng95u1WSC2vYeuDRanwGOe1v5uqcAxaIYm4W'
}
Query metrics:
  •    bytesIn:   76

  •   bytesOut:  339

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  151

  • writeBytes:  487

  •  queryTime: 41ms

  •    retries:    0

Save this key’s secret; it is only displayed once, and if you lose it, you would have to generate a new one. The key is used to perform all of the remaining database setup steps.

Your key will be different than the key you see displayed in the command output in this documentation. The key you saved just above will be different.

Instantiate a client that has server key privileges

Instantiate a client that uses the server key that we just set up, to perform the rest of the tasks in this tutorial. Be sure to copy the secret returned in the previous step and replace YOUR_FAUNADB_SERVER_SECRET with that value.

var serverClient = new FaunaClient(secret: YOUR_FAUNADB_SERVER_SECRET);
serverClient := f.NewFaunaClient("YOUR_FAUNADB_SERVER_SECRET")
FaunaClient serverClient = FaunaClient.builder()
      .withSecret("YOUR_FAUNADB_SERVER_SECRET")
      .build();
var serverClient = new faunadb.Client({ secret: 'YOUR_FAUNADB_SERVER_SECRET' });
serverClient = FaunaClient(secret="YOUR_FAUNADB_SERVER_SECRET")
val serverClient = FaunaClient(secret = "YOUR_FAUNADB_SERVER_SECRET")

Create a collection

Fauna stores documents in the form of nested containers. A database contains collections, and collections contain documents. Each document belongs to a specific collection. So in order to create a document for a post, we need to first create a collection for posts.

Create a collection using the CreateCollection function with a param_object containing the name of the collection. We shall name our collection "Posts":

try
{
    Value result = await serverClient.Query(
        CreateCollection(Obj("name", "Posts"))
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "Posts", collection = RefV(id = "collections")),ts: LongV(1622574236210000),history_days: LongV(30),name: StringV(Posts))
result, err := serverClient.Query(
	f.CreateCollection(f.Obj{"name": "Posts"}))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[history_days:30 name:Posts ref:{Posts 0xc00009b7a0 0xc00009b7a0 <nil>} ts:1622574326590000]
System.out.println(
    serverClient.query(
        CreateCollection(Obj("name", Value("Posts")))
    ).get());
{ref: ref(id = "Posts", collection = ref(id = "collections")), ts: 1622574376120000, history_days: 30, name: "Posts"}
serverClient.query(
  q.CreateCollection({ name: 'Posts' })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Collection("Posts"),
  ts: 1622574499730000,
  history_days: 30,
  name: 'Posts'
}
result = serverClient.query(
  q.create_collection({"name": "Posts"})
)
print(result)
{'ref': Ref(id=Posts, collection=Ref(id=collections)), 'ts': 1622574507400000, 'history_days': 30, 'name': 'Posts'}
CreateCollection({ name: 'Posts' })
{
  ref: Collection("Posts"),
  ts: 1624310595640000,
  history_days: 30,
  name: 'Posts'
}
Query metrics:
  •    bytesIn:   49

  •   bytesOut:  142

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  804

  • writeBytes:  325

  •  queryTime: 29ms

  •    retries:    0

Create an index

Before we create any document, let’s ensure that we can easily access them. We do this by creating an index using the CreateIndex function. We create this index now to help make the examples clear; in production, you can create an index at any time.

The customary way to access documents within a collection is by specifying a criteria for one of the fields. To enable criteria-based searches, we need to first create an index using the path of the field within the document.

Create an index on the post’s title:

try
{
    Value result = await serverClient.Query(
        CreateIndex(
            Obj(
                "name", "posts_by_title",
                "source", Collection("Posts"),
                "terms", Arr(Obj("field", Arr("data", "title")))
            )
        )
    );

    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "posts_by_title", collection = RefV(id = "indexes")),ts: LongV(1622574241570000),active: BooleanV(True),serialized: BooleanV(True),name: StringV(posts_by_title),source: RefV(id = "Posts", collection = RefV(id = "collections")),terms: Arr(ObjectV(field: Arr(StringV(data), StringV(title)))),partitions: LongV(1))
result, err := serverClient.Query(
	f.CreateIndex(
		f.Obj{
			"name": "posts_by_title",
			"source": f.Collection("Posts"),
			"terms": f.Arr{f.Obj{"field": f.Arr{"data", "title"}}},
		},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[active:true name:posts_by_title partitions:1 ref:{posts_by_title 0xc0000924b0 0xc0000924b0 <nil>} serialized:true source:{Posts 0xc0000925a0 0xc0000925a0 <nil>} terms:[map[field:[data title]]] ts:1622574327960000]
System.out.println(
    serverClient.query(
        CreateIndex(
            Obj(
                "name", Value("posts_by_title"),
                "source", Collection(Value("Posts")),
                "terms", Arr(
                    Obj("field", Arr(Value("data"), Value("title")))
                )
            )
        )
    ).get());
{ref: ref(id = "posts_by_title", collection = ref(id = "indexes")), ts: 1622574386080000, active: true, serialized: true, name: "posts_by_title", source: ref(id = "Posts", collection = ref(id = "collections")), terms: [{field: ["data", "title"]}], partitions: 1}
serverClient.query(
  q.CreateIndex({
    name: 'posts_by_title',
    source: q.Collection('Posts'),
    terms: [{ field: ['data', 'title'] }],
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Index("posts_by_title"),
  ts: 1622574500130000,
  active: true,
  serialized: true,
  name: 'posts_by_title',
  source: Collection("Posts"),
  terms: [ { field: [ 'data', 'title' ] } ],
  partitions: 1
}
result = serverClient.query(
  q.create_index(
    {
      "name": "posts_by_title",
      "source": q.collection("Posts"),
      "terms": [{"field": ["data", "title"]}]
    }
  )
)
print(result)
{'ref': Ref(id=posts_by_title, collection=Ref(id=indexes)), 'ts': 1622574507770000, 'active': True, 'serialized': True, 'name': 'posts_by_title', 'source': Ref(id=Posts, collection=Ref(id=collections)), 'terms': [{'field': ['data', 'title']}], 'partitions': 1}
CreateIndex({
  name: 'posts_by_title',
  source: Collection('Posts'),
  terms: [{ field: ['data', 'title'] }],
})
{
  ref: Index("posts_by_title"),
  ts: 1624310596280000,
  active: true,
  serialized: true,
  name: 'posts_by_title',
  source: Collection("Posts"),
  terms: [ { field: [ 'data', 'title' ] } ],
  partitions: 1
}
Query metrics:
  •    bytesIn:   133

  •   bytesOut:   298

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     1

  •  readBytes: 1,233

  • writeBytes:   424

  •  queryTime:  63ms

  •    retries:     0

It is also possible to specify the values of the document that should be returned when querying the index. We also create an index for a post’s tags:

try
{
    Value result = await serverClient.Query(
        CreateIndex(
            Obj(
                "name", "posts_by_tags_with_title",
                "source", Collection("Posts"),
                "terms", Arr(Obj("field", Arr("data", "tags"))),
                "values", Arr(Obj("field", Arr("data", "title")))
            )
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "posts_by_tags_with_title", collection = RefV(id = "indexes")),ts: LongV(1622574247700000),active: BooleanV(True),serialized: BooleanV(True),name: StringV(posts_by_tags_with_title),source: RefV(id = "Posts", collection = RefV(id = "collections")),terms: Arr(ObjectV(field: Arr(StringV(data), StringV(tags)))),values: Arr(ObjectV(field: Arr(StringV(data), StringV(title)))),partitions: LongV(1))
result, err := serverClient.Query(
	f.CreateIndex(
		f.Obj{
			"name": "posts_by_tags_with_title",
			"source": f.Collection("Posts"),
			"terms": f.Arr{f.Obj{"field": f.Arr{"data", "tags"}}},
			"values": f.Arr{f.Obj{"field": f.Arr{"data", "title"}}},
		},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[active:true name:posts_by_tags_with_title partitions:1 ref:{posts_by_tags_with_title 0xc000282240 0xc000282240 <nil>} serialized:true source:{Posts 0xc000282330 0xc000282330 <nil>} terms:[map[field:[data tags]]] ts:1622574329310000 values:[map[field:[data title]]]]
System.out.println(
    serverClient.query(
        CreateIndex(
            Obj(
                "name", Value("posts_by_tags_with_title"),
                "source", Collection(Value("Posts")),
                "terms", Arr(
                    Obj("field", Arr(Value("data"), Value("tags")))
                ),
                "values", Arr(
                    Obj("field", Arr(Value("data"), Value("title")))
                )
            )
        )
    ).get());
{ref: ref(id = "posts_by_tags_with_title", collection = ref(id = "indexes")), ts: 1622574396410000, active: true, serialized: true, name: "posts_by_tags_with_title", source: ref(id = "Posts", collection = ref(id = "collections")), terms: [{field: ["data", "tags"]}], values: [{field: ["data", "title"]}], partitions: 1}
serverClient.query(
  q.CreateIndex({
    name: 'posts_by_tags_with_title',
    source: q.Collection('Posts'),
    terms: [{ field: ['data', 'tags'] }],
    values: [{ field: ['data', 'title'] }],
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Index("posts_by_tags_with_title"),
  ts: 1622574500460000,
  active: true,
  serialized: true,
  name: 'posts_by_tags_with_title',
  source: Collection("Posts"),
  terms: [ { field: [ 'data', 'tags' ] } ],
  values: [ { field: [ 'data', 'title' ] } ],
  partitions: 1
}
result = serverClient.query(
  q.create_index(
    {
      "name": "posts_by_tags_with_title",
      "source": q.collection("Posts"),
      "terms": [{"field": ["data", "tags"]}],
      "values": [{"field": ["data", "title"]}]
    }
  )
)
print(result)
{'ref': Ref(id=posts_by_tags_with_title, collection=Ref(id=indexes)), 'ts': 1622574508140000, 'active': True, 'serialized': True, 'name': 'posts_by_tags_with_title', 'source': Ref(id=Posts, collection=Ref(id=collections)), 'terms': [{'field': ['data', 'tags']}], 'values': [{'field': ['data', 'title']}], 'partitions': 1}
CreateIndex({
  name: 'posts_by_tags_with_title',
  source: Collection('Posts'),
  terms: [{ field: ['data', 'tags'] }],
  values: [{ field: ['data', 'title'] }],
})
{
  ref: Index("posts_by_tags_with_title"),
  ts: 1624310596920000,
  active: true,
  serialized: true,
  name: 'posts_by_tags_with_title',
  source: Collection("Posts"),
  terms: [ { field: [ 'data', 'tags' ] } ],
  values: [ { field: [ 'data', 'title' ] } ],
  partitions: 1
}
Query metrics:
  •    bytesIn:   191

  •   bytesOut:   355

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     1

  •  readBytes: 1,297

  • writeBytes:   483

  •  queryTime:  68ms

  •    retries:     0

Create a post

Posts are created by calling the Create function with the ref of the posts collection and a param_object that specifies the structure of the document to be created, as well as permissions for the document. The post’s data is included in the param_object's data field.

try
{
    Value result = await serverClient.Query(
        Create(
            Collection("Posts"),
            Obj("data", Obj("title", "What I had for breakfast .."))
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "300223461515592192", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574254500000),data: ObjectV(title: StringV(What I had for breakfast ..)))
result, err := serverClient.Query(
	f.Create(
		f.Collection("Posts"),
		f.Obj{"data": f.Obj{"title": "What I had for breakfast .."}},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:What I had for breakfast ..] ref:{300223541417083392 0xc00011dad0 0xc00011dad0 <nil>} ts:1622574330710000]
System.out.println(
    serverClient.query(
        Create(
            Collection(Value("Posts")),
            Obj(
                "data", Obj(
                    "title", Value("What I had for breakfast ..")
                )
            )
        )
    ).get());
{ref: ref(id = "300223621377294848", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574406990000, data: {title: "What I had for breakfast .."}}
serverClient.query(
  q.Create(
    q.Collection('Posts'),
    { data: { title: 'What I had for breakfast ..' } },
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "300223719783006720"),
  ts: 1622574500780000,
  data: { title: 'What I had for breakfast ..' }
}
result = serverClient.query(
  q.create(
    q.collection("Posts"),
    {"data": {"title": "What I had for breakfast .."}}
  )
)
print(result)
{'ref': Ref(id=300223727893742080, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574508510000, 'data': {'title': 'What I had for breakfast ..'}}
Create(
  Collection('Posts'),
  { data: { title: 'What I had for breakfast ..' } },
)
{
  ref: Ref(Collection("Posts"), "302044149229552128"),
  ts: 1624310597570000,
  data: { title: 'What I had for breakfast ..' }
}
Query metrics:
  •    bytesIn:  113

  •   bytesOut:  206

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:    0

  • writeBytes:  402

  •  queryTime: 38ms

  •    retries:    0

The Create function returns the post document just created. As you can see in the output, the ref of the document is an automatically-generated identifier that is unique to the document within its database.

Using the Ref function, you can specify a document ID to use instead of the auto-generated number, but it must be a unique string-encoded 64-bit integer. For example:

try
{
    Value result = await serverClient.Query(
        Create(
            Ref(Collection("Posts"), 1),
            Obj("data", Obj("title", "The first post"))
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "1", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574261840000),data: ObjectV(title: StringV(The first post)))
result, err := serverClient.Query(
	f.Create(
		f.Ref(f.Collection("Posts"), "1"),
		f.Obj{"data": f.Obj{"title": "The first post"}}))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:The first post] ref:{1 0xc00009dbc0 0xc00009dbc0 <nil>} ts:1622574332010000]
System.out.println(
    serverClient.query(
        Create(
            Ref(Collection(Value("Posts")), Value(1)),
            Obj(
                "data", Obj(
                    "title", Value("The first post")
                )
            )
        )
    ).get());
{ref: ref(id = "1", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574416990000, data: {title: "The first post"}}
serverClient.query(
  q.Create(
    q.Ref(q.Collection('Posts'), '1'),
    { data: { title: 'The first post' } },
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1622574501060000,
  data: { title: 'The first post' }
}
result = serverClient.query(
  q.create(
    q.ref(q.collection("Posts"), 1),
    {"data": {"title": "The first post"}}
  )
)
print(result)
{'ref': Ref(id=1, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574508850000, 'data': {'title': 'The first post'}}
Create(
  Ref(Collection('Posts'), '1'),
  { data: { title: 'The first post' } },
)
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1624310598180000,
  data: { title: 'The first post' }
}
Query metrics:
  •    bytesIn:  117

  •   bytesOut:  176

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   14

  • writeBytes:  352

  •  queryTime: 23ms

  •    retries:    0

Create several posts

It can quickly become tedious to repeat the Create function for multiple posts.

This is where Fauna’s transaction language really shines. Let’s use a Map function to create several posts at once:

try
{
    Value result = await serverClient.Query(
        Map(
            Arr(
                "My cat and other marvels",
                "Pondering during a commute",
                "Deep meanings in a latte"
            ),
            Lambda(
                "post_title",
                Create(
                    Collection("Posts"),
                    Obj(
                        "data", Obj(
                            "title", Var("post_title")
                        )
                    )
                )
            )
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
Arr(ObjectV(ref: RefV(id = "300223477212774912", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574269420000),data: ObjectV(title: StringV(My cat and other marvels))), ObjectV(ref: RefV(id = "300223477212775936", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574269420000),data: ObjectV(title: StringV(Pondering during a commute))), ObjectV(ref: RefV(id = "300223477212776960", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574269420000),data: ObjectV(title: StringV(Deep meanings in a latte))))
result, err := serverClient.Query(
	f.Map(
		f.Arr{
			"My cat and other marvels",
			"Pondering during a commute",
			"Deep meanings in a latte",
		},
		f.Lambda(
			"post_title",
			f.Create(
				f.Collection("Posts"),
				f.Obj{"data": f.Obj{"title": f.Var("post_title")}},
			),
		)))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
[map[data:map[title:My cat and other marvels] ref:{300223544222024192 0xc000113dd0 0xc000113dd0 <nil>} ts:1622574333320000] map[data:map[title:Pondering during a commute] ref:{300223544222025216 0xc0001a4000 0xc0001a4000 <nil>} ts:1622574333320000] map[data:map[title:Deep meanings in a latte] ref:{300223544222026240 0xc0001a4210 0xc0001a4210 <nil>} ts:1622574333320000]]
System.out.println(
    serverClient.query(
        Map(
            Arr(
                Value("My cat and other marvels"),
                Value("Pondering during a commute"),
                Value("Deep meanings in a latte")
            ),
            Lambda(
                Value("post_title"),
                Create(
                    Collection(Value("Posts")),
                    Obj(
                        "data",
                        Obj("title", Var("post_title"))
                    )
                )
            )
        )
    ).get());
[{ref: ref(id = "300223642454721024", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574427010000, data: {title: "My cat and other marvels"}}, {ref: ref(id = "300223642454722048", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574427010000, data: {title: "Pondering during a commute"}}, {ref: ref(id = "300223642454723072", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574427010000, data: {title: "Deep meanings in a latte"}}]
serverClient.query(
  q.Map(
    [
      'My cat and other marvels',
      'Pondering during a commute',
      'Deep meanings in a latte',
    ],
    q.Lambda(
      'post_title',
      q.Create(
        q.Collection('Posts'),
        { data: { title: q.Var('post_title') } },
      )
    ),
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
[
  {
    ref: Ref(Collection("Posts"), "300223720476115456"),
    ts: 1622574501410000,
    data: { title: 'My cat and other marvels' }
  },
  {
    ref: Ref(Collection("Posts"), "300223720476116480"),
    ts: 1622574501410000,
    data: { title: 'Pondering during a commute' }
  },
  {
    ref: Ref(Collection("Posts"), "300223720476117504"),
    ts: 1622574501410000,
    data: { title: 'Deep meanings in a latte' }
  }
]
result = serverClient.query(
  q.map_(
    lambda post_title: q.create(
      q.collection("Posts"),
      {"data": {"title": post_title}}
    ),
    [
      "My cat and other marvels",
      "Pondering during a commute",
      "Deep meanings in a latte"
    ]
  )
)
print(result)
[{'ref': Ref(id=300223728646619648, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574509200000, 'data': {'title': 'My cat and other marvels'}}, {'ref': Ref(id=300223728646620672, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574509200000, 'data': {'title': 'Pondering during a commute'}}, {'ref': Ref(id=300223728646621696, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574509200000, 'data': {'title': 'Deep meanings in a latte'}}]
Map(
  [
    'My cat and other marvels',
    'Pondering during a commute',
    'Deep meanings in a latte',
  ],
  Lambda(
    'post_title',
    Create(
      Collection('Posts'),
      { data: { title: Var('post_title') } },
    )
  ),
)
[
  {
    ref: Ref(Collection("Posts"), "302044150538174976"),
    ts: 1624310598800000,
    data: { title: 'My cat and other marvels' }
  },
  {
    ref: Ref(Collection("Posts"), "302044150538176000"),
    ts: 1624310598800000,
    data: { title: 'Pondering during a commute' }
  },
  {
    ref: Ref(Collection("Posts"), "302044150538177024"),
    ts: 1624310598800000,
    data: { title: 'Deep meanings in a latte' }
  }
]
Query metrics:
  •    bytesIn:   241

  •   bytesOut:   589

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     3

  •  readBytes:     0

  • writeBytes: 1,185

  •  queryTime:  35ms

  •    retries:     0

Using the Map function, we can restructure the data as an array and wrap the Create in a lambda function, which then runs over each document in the collection. The anonymous lambda function specifies a variable post_title which is used as a placeholder in the parameters sent to the create function. This way, multiple documents in a collection can be created using a single query.

Retrieve posts

The easiest way to retrieve a document is by using its reference value:

try
{
    Value result = await serverClient.Query(
        Get(Ref(Collection("Posts"), "1"))
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "1", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574261840000),data: ObjectV(title: StringV(The first post)))
result, err := serverClient.Query(
	f.Get(f.Ref(f.Collection("Posts"), "1")))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:The first post] ref:{1 0xc000092570 0xc000092570 <nil>} ts:1622574332010000]
System.out.println(
    serverClient.query(
        Get(Ref(Collection("Posts"),Value("1")))
    ).get());
{ref: ref(id = "1", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574416990000, data: {title: "The first post"}}
serverClient.query(
  q.Get(q.Ref(q.Collection('Posts'), '1'))
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1622574501060000,
  data: { title: 'The first post' }
}
result = serverClient.query(
  q.get(q.ref(q.collection("Posts"), "1"))
)
print(result)
{'ref': Ref(id=1, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574508850000, 'data': {'title': 'The first post'}}
Get(Ref(Collection('Posts'), '1'))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1624310598180000,
  data: { title: 'The first post' }
}
Query metrics:
  •    bytesIn:   47

  •   bytesOut:  176

  • computeOps:    1

  •    readOps:    1

  •   writeOps:    0

  •  readBytes:   70

  • writeBytes:    0

  •  queryTime: 10ms

  •    retries:    0

You can query for posts with a specific title using the match function and the index we created earlier:

try
{
    Value result = await serverClient.Query(
        Get(
            Match(
                Index("posts_by_title"),
                "My cat and other marvels"
            )
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "300223477212774912", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574269420000),data: ObjectV(title: StringV(My cat and other marvels)))
result, err := serverClient.Query(
	f.Get(
		f.MatchTerm(
			f.Index("posts_by_title"),
			"My cat and other marvels",
		),
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:My cat and other marvels] ref:{300223544222024192 0xc000184570 0xc000184570 <nil>} ts:1622574333320000]
System.out.println(
    serverClient.query(
        Get(
            Match(
                Index(Value("posts_by_title")),
                Value("My cat and other marvels")
            )
        )
    ).get());
{ref: ref(id = "300223642454721024", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574427010000, data: {title: "My cat and other marvels"}}
serverClient.query(
  q.Get(
    q.Match(q.Index('posts_by_title'), 'My cat and other marvels')
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "300223720476115456"),
  ts: 1622574501410000,
  data: { title: 'My cat and other marvels' }
}
result = serverClient.query(
  q.get(
    q.match(
      q.index("posts_by_title"),
      "My cat and other marvels"
    )
  )
)
print(result)
{'ref': Ref(id=300223728646619648, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574509200000, 'data': {'title': 'My cat and other marvels'}}
Get(
  Match(Index('posts_by_title'), 'My cat and other marvels')
)
{
  ref: Ref(Collection("Posts"), "302044150538174976"),
  ts: 1624310598800000,
  data: { title: 'My cat and other marvels' }
}
Query metrics:
  •    bytesIn:   79

  •   bytesOut:  203

  • computeOps:    1

  •    readOps:    1

  •   writeOps:    0

  •  readBytes:  173

  • writeBytes:    0

  •  queryTime: 12ms

  •    retries:    0

The match function returns a logical set of elements, which can be combined with other sets with set-operations like join, intersect, subtract, etc.

Update posts

You can easily modify documents by supplying the new data along with the reference to the document. For example, we want to add tags to each of our blog posts:

try
{
    Value result = await serverClient.Query(
        Update(
            Ref(Collection("Posts"), "1"),
            Obj("data", Obj("tags", Arr("welcome", "short")))
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "1", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574293510000),data: ObjectV(title: StringV(The first post),tags: Arr(StringV(welcome), StringV(short))))
result, err := serverClient.Query(
	f.Update(
		f.Ref(f.Collection("Posts"), "1"),
		f.Obj{"data": f.Obj{"tags": f.Arr{"welcome", "short"}}},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[tags:[welcome short] title:The first post] ref:{1 0xc000109bf0 0xc000109bf0 <nil>} ts:1622574337210000]
System.out.println(
    serverClient.query(
        Update(
            Ref(Collection("Posts"),Value("1")),
            Obj("data", Obj("tags", Arr(Value("welcome"), Value("short"))))
        )
    ).get());
{ref: ref(id = "1", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574459880000, data: {title: "The first post", tags: ["welcome", "short"]}}
serverClient.query(
  q.Update(
    q.Ref(q.Collection('Posts'), '1'),
    { data: { tags: ['welcome', 'short'] } },
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1622574502370000,
  data: { title: 'The first post', tags: [ 'welcome', 'short' ] }
}
result = serverClient.query(
  q.update(
    q.ref(q.collection("Posts"), "1"),
    {"data": {"tags": ["welcome", "short"]}}
  )
)
print(result)
{'ref': Ref(id=1, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574510200000, 'data': {'title': 'The first post', 'tags': ['welcome', 'short']}}
Update(
  Ref(Collection('Posts'), '1'),
  { data: { tags: ['welcome', 'short'] } },
)
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1624310600630000,
  data: { title: 'The first post', tags: [ 'welcome', 'short' ] }
}
Query metrics:
  •    bytesIn:  119

  •   bytesOut:  203

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   70

  • writeBytes:  434

  •  queryTime: 29ms

  •    retries:    0

The Update function updates specific fields in a document. It preserves the old fields if they are not specified in params. In the case of nested values (known as objects, due to the JSON data format), the old and the new values are merged. If null is specified as a value for a field, it is removed.

Replace posts

The Replace function replaces the document’s data with the fields provided in params. Old fields not mentioned in params are removed.

try
{
    Value result = await serverClient.Query(
        Replace(
            Ref(Collection("Posts"), "1"),
            Obj("data", Obj("title", "The replacement first post"))
        )
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "1", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574301140000),data: ObjectV(title: StringV(The replacement first post)))
result, err := serverClient.Query(
	f.Replace(
		f.Ref(f.Collection("Posts"), "1"),
		f.Obj{"data": f.Obj{"title": "The replacement first post"}},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:The replacement first post] ref:{1 0xc000096570 0xc000096570 <nil>} ts:1622574338510000]
System.out.println(
    serverClient.query(
        Replace(
            Ref(Collection("Posts"),Value("1")),
            Obj("data", Obj("title", Value("The replacement first post")))
        )
    )
    .get());
{ref: ref(id = "1", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574471430000, data: {title: "The replacement first post"}}
serverClient.query(
  q.Replace(
    q.Ref(q.Collection('Posts'), '1'),
    { data: { title: 'The replacement first post' } },
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1622574502680000,
  data: { title: 'The replacement first post' }
}
result = serverClient.query(
  q.replace(
    q.ref(q.collection("Posts"), "1"),
    {"data": {"title": "The replacement first post"}}
  )
)
print(result)
{'ref': Ref(id=1, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574510530000, 'data': {'title': 'The replacement first post'}}
Replace(
  Ref(Collection('Posts'), '1'),
  { data: { title: 'The replacement first post' } },
)
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1624310601250000,
  data: { title: 'The replacement first post' }
}
Query metrics:
  •    bytesIn:  130

  •   bytesOut:  188

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  170

  • writeBytes:  780

  •  queryTime: 52ms

  •    retries:    0

Note that the title has been updated, but tags has been deleted.

Delete a post

Lastly, a post can be removed using the Delete function:

try
{
    Value result = await serverClient.Query(
        Delete(Ref(Collection("Posts"), "1"))
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ObjectV(ref: RefV(id = "1", collection = RefV(id = "Posts", collection = RefV(id = "collections"))),ts: LongV(1622574301140000),data: ObjectV(title: StringV(The replacement first post)))
result, err := serverClient.Query(
	f.Delete(f.Ref(f.Collection("Posts"), "1")),
)

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
map[data:map[title:The replacement first post] ref:{1 0xc000109980 0xc000109980 <nil>} ts:1622574338510000]
System.out.println(
    serverClient.query(
        Delete(Ref(Collection("Posts"),Value("1")))
    ).get());
{ref: ref(id = "1", collection = ref(id = "Posts", collection = ref(id = "collections"))), ts: 1622574471430000, data: {title: "The replacement first post"}}
serverClient.query(
  q.Delete(q.Ref(q.Collection('Posts'), '1'))
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1622574502680000,
  data: { title: 'The replacement first post' }
}
result = serverClient.query(
  q.delete(q.ref(q.collection("Posts"), "1"))
)
print(result)
{'ref': Ref(id=1, collection=Ref(id=Posts, collection=Ref(id=collections))), 'ts': 1622574510530000, 'data': {'title': 'The replacement first post'}}
Delete(Ref(Collection('Posts'), '1'))
{
  ref: Ref(Collection("Posts"), "1"),
  ts: 1624310601250000,
  data: { title: 'The replacement first post' }
}
Query metrics:
  •    bytesIn:   50

  •   bytesOut:  188

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  298

  • writeBytes:  403

  •  queryTime: 24ms

  •    retries:    0

Once the post is deleted, attempting to retrieve the post results in an error:

try
{
    Value result = await serverClient.Query(
        Get(Ref(Collection("Posts"), "1"))
    );
    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
ERROR: instance not found: Document not found.
result, err := serverClient.Query(
	f.Get(f.Ref(f.Collection("Posts"), "1")))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
Response error 404. Errors: [](instance not found): Document not found., details: []
try {
    System.out.println(
        serverClient.query(
            Get(Ref(Collection("Posts"), Value("1")))
        ).get());
} catch (Exception e) {
    System.out.println("ERROR " + e.getMessage());
}
ERROR com.faunadb.client.errors.NotFoundException: instance not found: Document not found.
serverClient.query(
  q.Get(q.Ref(q.Collection('Posts'), '1'))
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
Error: [NotFound] instance not found: Document not found.
try:
  result = serverClient.query(
    q.get(q.ref(q.collection("Posts"), "1"))
  )
  print(result)
except:
  print("Error: ", sys.exc_info()[0], sys.exc_info()[1])
Error:  <class 'faunadb.errors.NotFound'> ErrorData(code='instance not found', description='Document not found.', position=[], failures=None)
Get(Ref(Collection("Posts"), "1"))
{
  errors: [
    {
      position: [],
      code: 'instance not found',
      description: 'Document not found.'
    }
  ]
}
Query metrics:
  •    bytesIn:   47

  •   bytesOut:   92

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    0

  •  readBytes:    0

  • writeBytes:    0

  •  queryTime: 10ms

  •    retries:    0

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!