This library provides an interface to APIs following the JSONAPI spec. Though we're not striving for 1:1 compatibility with Active Record, you'll see it's the same basic usage, from scopes to error handling.
The first step is to define your models. These will match the resource objects returned from your API. Your code can be written in Typescript, ES6, or vanilla JS/Coffeescript.
Let's say we have a /api/v1/people
endpoint:
// es6 import syntax
// vanilla JS would expose 'jsorm' as a global
import "isomorphic-fetch"
import { Model, attr } from "jsorm"
var Person = Model.extend({
static: {
baseUrl: "http://localhost:3000", // set to '' in browser for relative URLs
apiNamespace: "/api/v1",
jsonapiType: "people"
},
firstName: attr(),
lastName: attr()
})
Alternatively, in Typescript:
// typescript
class Person extends Model {
static baseUrl = "http://localhost:3000"
static apiNamespace = "/api/v1"
static jsonapiType = "people"
firstName = attr()
lastName = attr()
}
NOTE: *Once your models are defined, you must call Config.setup()
:
import { Config } from "jsorm"
Config.setup()
ES6 and TypeScript classes do not have an inherited
hook. Because this hook provides critical functionality, you have three options:
-
Edit your
.tsconfig
:- Set
target
toes5
. - Add
noEmitHelpers: "true"
- Set
-
Call the inherited hook manually after each class definition:
class Author extends Person { ... } Person.inherited(Author);
-
Use the
let Person = Model.extend({ ... })
pattern shown above instead of native classes.
All queries are
- Chainable
- Overrideable
Call all()
, first()
, or find()
to actually fire the query.
All of the following examples can be chained together:
let scope = new Person()
if (should_include_admins) {
scope = scope.where({ admin: true })
}
scope.all().then(people => {
people.data.map(p => {
return p.firstName
}) // => ['Joe', 'Jane', 'Bill']
})
scope
.page(2)
.all()
.then(people => {
people.data.map(p => {
return p.firstName
}) // => ['Chris', 'Sarah', 'Ben']
})
Use per
and page
. To limit 10 per page, viewing the second page:
Person.per(10)
.page(2)
.all()
GET /people?page[number]=2&page[size]=10
Use order
. Passing an attribute will default to ascending order.
Ascending:
Person.order("name")
GET /people?sort=name
Descending:
Person.order({ name: "desc" })
GET /people?sort=-name
Use where
:
Person.where({ name: "Joe" })
.where({ age: 30 })
.all()
GET /people?filter[name]=Joe&filter[age]=30
Filters are based on swagger documentation, not object attributes. This means you can do stuff like:
Person.where({ age_greater_than: 30 }).all()
GET /people?filter[age_greater_than]=30
Arrays are supported automatically, defaulting to an OR clause:
Person.where({ name: ["Joe", "Bill"] }).all()
GET /people?&filter[name][]=Joe&filter[name][]=Bill
Use select
:
Person.select({ people: ["name", "age"] }).all()
GET /people?fields[people]=name,age
This functionality is enabled by jsonapi_suite. It works the same as Sparse fieldsets, but is for requesting additional fields, not limiting them.
Use selectExtra
:
Person.selectExtra({ people: ["name", "age"] }).all()
GET /people?extra_fields[people]=name,age
JSONAPI Docs on Includes (sideloads)
Use includes
. This can be a symbol, array, hash, or combination of all. In short - it works exactly like it works in ActiveRecord:
// a person has many tags, and has many pets
// a pet has many toys, and many tags
Person.includes(["tags", { pets: ["toys", "tags"] }])
GET /people?include=tags,pets.toys,pets.tags
The included resources will now be present:
Person.includes("tags")
.all()
.then(person => {
person.data.tags.map(t => {
return t.name
}) // #=> ['funny', 'smart']
})
GET /people?include=tags
all
, first
, and find
can be used in conjunction with scopes.
Person.all()
GET /people
scope = Person.where({ name: 'Bill' }) # no query
scope.all(); # => fires query, returns a Promise that resolves to an array of Person objects
GET /people?filter[name]=bill
Use first
to grab the first result:
// Limits per_page to 1, result is first element in the array
Person.where({ name: "Bill" })
.first()
.then(person => {
// ...
})
GET /people?page[size]=1&page[number]=1&filter[name]=Bill
Finally, use find
to find a record by ID. This will hit the show
action.
By default we will use console
to log to STDOUT (or the browser's console log). If you are using node and want more in-depth options, inject another logger (we suggest winston):
import { Config } from "jsorm"
let winston = require("winston")
winston.level = "warn"
Config.logger = winston
This will log colorized request/responses when the log_level is debug, and only the request when the log level is info.
- Create a Github Issue
- Contact richmolj@gmail.com