Skip to main content

Reads

The interface for read operations is a simpler version of the ActiveRecord Query Interface. Instead of generating SQL, we'll be generating JSONAPI requests.

Basic Finders

Execute queries with .all(), find(), or .first():

let response = await Post.all()
response.data // array of Post instances
Post.all().then(function(response) {
response.data // array of Post instances
});

GET /posts

let response = await Post.find(123)
response.data // Post instance
Post.find(123).then(function(response) {
response.data // Post instance
});

GET /posts/123

let response = await Post.first()
response.data // Post instance
Post.first().then(function(response) {
response.data // Post instance
});

GET /posts?page[size]=1

Composable Queries with Scopes

The beauty of ORMs is their ability to compose queries. We'll be doing this by chaining together Scopes (query fragments). All of the methods you see on this page can be chained together - the request will not fire until the chain ends with all(), first(), or find. Example:

let scope = Post.order({ name: "desc" })

if (someCheckboxIsChecked) {
scope = scope.where({ important: true })
} else {
scope = scope.where({ important: false })
}

scope.all() // request fires
var scope = Post.order({ name: "desc" });

if (someCheckboxIsChecked) {
scope = scope.where({ important: true });
} else {
scope = scope.where({ important: false });
}

scope.all() // request fires

/posts?sort=-name&filter[important]=true

/posts?sort=-name&filter[important]=false

In practice, you'll probably have some scopes you want to re-use across different contexts. A best practice is to store these scopes as class methods (static methods) in the model:

class Post extends ApplicationRecord {
// ... code ...
static superImportant() {
return this
.where({ ranking_gt: 8 })
.order({ ranking: 'desc' })
.stats({ total 'count' })
}
}

// get 10 super important posts
let scope = Post.superImportant().per(10)
scope.all() // fire query
const Post = ApplicationRecord.extend({
// ... code ...
static: {
superImportant() {
return this
.where({ ranking_gt: 8 })
.order({ ranking: 'desc' })
.stats({ total 'count' })
}
}
})

// get 10 super important posts
var scope = Post.superImportant().per(10);
scope.all() // fire query

/posts?sort=-ranking&stats[total]=count&page[size]=10&filter[ranking_gt]=8

Metadata

The meta information of the JSONAPI response is available as a POJO on the response:

let response = await Post.all()
response.meta // { stats: { total: { count: 100 } } }
await Post.all().then(function(response) {
response.meta // { stats: { total: { count: 100 } } }
})

Promises and Async/Await

The result of all(), first() or find is a Promise. The promise will resolve to a Response object.

A Response object has three keys - data, meta, and raw. data - the one you'll be using the most - will be a Model instance (or array of Model) instances. meta will be the Meta Information returned by the API (mostly used for statistics in our case). raw is only used to introspect the raw response document.

Post.all().then((response) => {
response.data // array of Post instances
response.meta // js object from the server
response.raw // js response document
})
Post.all().then(function(response) {
response.data // array of Post instances
response.meta // js object from the server
response.raw // js response document
});

/posts

Hopefully you're running in an environment that supports ES7's Async/Await. This makes things even easier:

let { data } = await Post.all()
data // array of Post instances

// alternatively

let posts = (await Post.all()).data
posts // array of Post instances

/posts

Filtering

Use #where() to apply filters:

Post.where({ important: true }).all()

/posts?filter[important]=true

#where() clauses can be chained together. If the same key is seen twice, it will be overridden:

Post
.where({ important: true })
.where({ ranking: 10 })
.where({ important: false })
.all()

/posts?filter[important]=false&filter[ranking]=10

#where() clauses are based on server implementation. The key should be exactly as the server understands it. Here are some common conventions we promote:

// id greater than 5
Post.where({ id_gt: 5 }).all()

// id greater than or equal to 5
Post.where({ id_gte: 5 }).all()

// id less than 5
Post.where({ id_lt: 5 }).all()

// id less or equal to 5
Post.where({ id_lte: 5 }).all()

// title starts with "foo"
Post.where({ title: { prefix: "foo" } }).all()

// OR these two values
Post.where({ status_or: ['draft', 'review'] })

// AND these two values (default)
Post.where({ status: ['draft', 'review'] })

Escaping Values

Graphiti treats a comma as a delimiter of multiple values. To escape the comma and tell Graphiti this is a single value, wrap it in {{curlies}}:

Post.where({ title: "{{Hello World, here I am}}" })

Sorting

Use #order() to sort.

If passed a string, it will default to ascending:

Post.order("title").all()

/posts?sort=title

Otherwise, pass an object:

Post.order({ title: "desc" }).all()

/posts?sort=-title

For multisort, chain multiple #order() clauses:

Post
.order({ title: "desc" })
.order("ranking")
.all()

/posts?sort=-title,ranking

Pagination

Use #per() to set the limit per page:

Post.per(10).all()

/posts?page[size]=10

Use #page() to set the current page:

Post.page(5).all()

/posts?page[number]=5

When chained together (10 per page, the 5th page):

Post.page(5).per(10).all()

/posts?page[size]=10&page[number]=5

Fieldsets

Sparse Fieldsets

Use #select() to limit the fields returned by the server:

Post.select(['title', 'status']).all()

/posts?fields[posts]=title,status

When dealing with relationships, it may be easier to pass an object, where the key is the corresponding JSONAPI type. This will be exactly what's sent to the server in ?fields:

Post.select({
posts: ['title', 'status'],
comments: ['created_at']
}).all()

/posts?fields[posts]=title,status&fields[comments]=created_at

Extra Fieldsets

Use #selectExtra() to explicitly request a field that doesn't usually come back (often computationally expensive):

Post.selectExtra(['highlights', 'cumulative_ranking']).all()

/posts?extra_fields[posts]=highlights,cumulative_ranking

Just like the select example above, feel free to pass an object specifying the fields for each relationship.

Includes

Use #includes() to "sideload" associations:

Post.includes("comments").all()

/posts?include=comments

You can also pass an array of associations:

Post.includes(["blog", "comments"]).all()

/posts?include=blog,comments

Or an object for nested associations:

Post.includes(["blog", { comments: "author" }]).all()

/posts?include=blog,comments.author

Nested Queries

We can nest all read operations at any level of the graph. Let's say we wanted to fetch all Posts and their Comments...but only return comments that are active, sorted by created_at descending. We can create a Comment scope as normal, then #merge() it into our Post scope:

let commentScope = Comment
.where({ active: true })
.order({ created_at: "desc" })
Post
.includes("comments")
.merge({ comments: commentScope })
.all()
var commentScope = Comment
.where({ active: true })
.order({ created_at: "desc" })
Post
.includes("comments")
.merge({ comments: commentScope })
.all()

/posts?include=comments&filter[comments][active]=true&sort=-comments.active

Because this can get verbose, it's often desirable to store it on the class:

class Comment extends ApplicationRecord {
// ... code ...
static recent() {
return this
.where({ active: true })
.order({ created_at: "desc" })
}
}

Post.merge({ comments: Comment.recent() }).all()
const Comment = ApplicationRecord.extend({
// ... code ...
static: {
recent: function() {
return this
.where({ active: true })
.order({ created_at: "desc" })
}
}
})

Post
.includes("comments")
.merge({ comments: Comment.recent() })
.all()

Any number of scopes can be merged in. Just remember to #include() and #merge() relationship names as the server understands them:

class Dog extends ApplicationRecord {
@BelongsTo() person: Person
}

// We've modeled this as Dog > person in javascript
// And Person is jsonapiType "people"
// But the server defined the relationship as "owner"
Dog.includes("owner").merge({ owner: Person.limitedFields() })
const Dog = ApplicationRecord.extend({
// ... code ...
methods: {
person: belongsTo()
}
})

// We've modeled this as Dog > person in javascript
// And Person is jsonapiType "people"
// But the server defined the relationship as "owner"
Dog.includes("owner").merge({ owner: Person.limitedFields() })

Statistics

Use #stats() to request statistics. Access stats within meta:

let { data } = await Post.stats({ total: "count" }).all()
data.meta.stats.total.count // the total count
Post.stats({ total: "count" }).all().then(function(response) {
response.meta.stats.total.count // the total count
})

/posts?stats[total]=count

Stats are always independent of pagination. If you request the total count, you'll get the total count even if you're limiting to 10 per page. This means to get only statistics - avoid returning Post instances altogether - request 0 results per page:

let { data } = await Post.per(0)stats({ total: "count" }).all()
data.meta.stats.total.count // the total count
Post
.per(0)
.stats({ total: "count" })
.all().then(function(response) {
response.meta.stats.total.count // the total count
})

/posts?stats[total]=count&page[size]=0

NEXT: Writes »