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