|
| 1 | +# What It Is |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +# How to Use It |
| 6 | + |
| 7 | +### Defining Models |
| 8 | + |
| 9 | +The first step is to define your models. These will match the [resource objects](http://jsonapi.org/format/#document-resource-objects) returned from your API. Your code can be written in Typescript, ES6, or vanilla JS/Coffeescript. |
| 10 | + |
| 11 | +Let's say we have a `/api/v1/people` endpoint: |
| 12 | + |
| 13 | +```es6 |
| 14 | +// es6 import syntax |
| 15 | +// vanilla JS would expose 'jsorm' as a global |
| 16 | +import { Model, attr } from 'jsorm'; |
| 17 | + |
| 18 | +var Person = Model.extend({ |
| 19 | + static: { |
| 20 | + baseUrl: 'http://localhost:3000', // set to '' in browser for relative URLs |
| 21 | + apiNamespace: '/api/v1', |
| 22 | + jsonapiType: 'people' |
| 23 | + }, |
| 24 | + |
| 25 | + firstName: attr(), |
| 26 | + lastName: attr() |
| 27 | +}); |
| 28 | +``` |
| 29 | + |
| 30 | +Alternatively, in Typescript: |
| 31 | + |
| 32 | +```ts |
| 33 | +// typescript |
| 34 | +class Person extends Model { |
| 35 | + static baseUrl: 'http://localhost:3000'; |
| 36 | + static apiNamespace: '/api/v1'; |
| 37 | + static jsonapiType: 'people'; |
| 38 | + |
| 39 | + firstName: attr(); |
| 40 | + lastName: attr(); |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +**NB**: *Once your models are defined, you must call `Config.setup()`: |
| 45 | + |
| 46 | +```js |
| 47 | +import { Config } from 'jsorm'; |
| 48 | +Config.setup(); |
| 49 | +``` |
| 50 | + |
| 51 | +## Basic Usage |
| 52 | + |
| 53 | +### Querying |
| 54 | + |
| 55 | +All queries are |
| 56 | + |
| 57 | +* Chainable |
| 58 | +* Overrideable |
| 59 | + |
| 60 | +Call `all()`, `first()`, or `find()` to actually fire the query. |
| 61 | + |
| 62 | +All of the following examples can be chained together: |
| 63 | + |
| 64 | +```es6 |
| 65 | +let scope = new Person(); |
| 66 | +if (should_include_admins) { |
| 67 | + scope = scope.where({ admin: true }); |
| 68 | +} |
| 69 | +scope.all().then((people) => { |
| 70 | + people.map((p) => { return p.firstName; }); // => ['Joe', 'Jane', 'Bill'] |
| 71 | +}); |
| 72 | + |
| 73 | +scope.page(2).all().then((people) => { |
| 74 | + people.map((p) => { return p.firstName; }); // => ['Chris', 'Sarah', 'Ben'] |
| 75 | +}); |
| 76 | +``` |
| 77 | + |
| 78 | +### Pagination |
| 79 | + |
| 80 | +[JSONAPI Pagination Docs](http://jsonapi.org/format/#fetching-pagination) |
| 81 | + |
| 82 | +Use `per` and `page`. To limit 10 per page, viewing the second page: |
| 83 | + |
| 84 | +```es6 |
| 85 | +Person.per(10).page(2).all(); |
| 86 | +``` |
| 87 | + |
| 88 | +> GET /people?page[number]=2&page[size]=10 |
| 89 | +
|
| 90 | +### Sorting |
| 91 | + |
| 92 | +[JSONAPI Sorting Docs](http://jsonapi.org/format/#fetching-sorting) |
| 93 | + |
| 94 | +Use `order`. Passing an attribute will default to ascending order. |
| 95 | + |
| 96 | +Ascending: |
| 97 | + |
| 98 | +```es6 |
| 99 | +Person.order('name'); |
| 100 | +``` |
| 101 | + |
| 102 | +> GET /people?sort=name |
| 103 | +
|
| 104 | +Descending: |
| 105 | + |
| 106 | +```es6 |
| 107 | +Person.order({ name: 'desc' }); |
| 108 | +``` |
| 109 | + |
| 110 | +> GET /people?sort=-name |
| 111 | +
|
| 112 | +### Filtering |
| 113 | + |
| 114 | +[JSONAPI Filtering Docs](http://jsonapi.org/format/#fetching-filtering) |
| 115 | + |
| 116 | +Use `where`: |
| 117 | + |
| 118 | +```es6 |
| 119 | +Person.where({ name: 'Joe' }).where({ age: 30 }).all(); |
| 120 | +``` |
| 121 | + |
| 122 | +> GET /people?filter[name]=Joe&filter[age]=30 |
| 123 | +
|
| 124 | +Filters are based on swagger documentation, not object attributes. This means you can do stuff like: |
| 125 | + |
| 126 | +```es6 |
| 127 | +Person.where({ age_greater_than: 30 }).all(); |
| 128 | +``` |
| 129 | + |
| 130 | +> GET /people?filter[age_greater_than]=30 |
| 131 | +
|
| 132 | +Arrays are supported automatically, defaulting to an OR clause: |
| 133 | + |
| 134 | +```es6 |
| 135 | +Person.where({ name: ['Joe', 'Bill'] }).all(); |
| 136 | +``` |
| 137 | + |
| 138 | +> GET /people?&filter[name][]=Joe&filter[name][]=Bill |
| 139 | +
|
| 140 | +### Sparse Fieldsets |
| 141 | + |
| 142 | +[JSONAPI Sparse Fieldset Docs](http://jsonapi.org/format/#fetching-sparse-fieldsets) |
| 143 | + |
| 144 | +Use `select`: |
| 145 | + |
| 146 | +```es6 |
| 147 | +Person.select({ people: ['name', 'age'] }).all(); |
| 148 | +``` |
| 149 | + |
| 150 | +> GET /people?fields[people]=name,age |
| 151 | +
|
| 152 | +### Extra Fields |
| 153 | + |
| 154 | +This functionality is enabled by [jsonapi_suite](https://jsonapi-suite.github.io/jsonapi_suite). It works the same as Sparse fieldsets, but is for requesting additional fields, not limiting them. |
| 155 | + |
| 156 | +Use `selectExtra`: |
| 157 | + |
| 158 | +```es6 |
| 159 | +Person.selectExtra({ people: ['name', 'age'] }).all(); |
| 160 | +``` |
| 161 | + |
| 162 | +> GET /people?extra_fields[people]=name,age |
| 163 | +
|
| 164 | +### Inclusion of Related Resources |
| 165 | + |
| 166 | +[JSONAPI Docs on Includes (sideloads)](http://jsonapi.org/format/#fetching-includes) |
| 167 | + |
| 168 | +Use `includes`. This can be a symbol, array, hash, or combination of all. In short - it works exactly like it works in ActiveRecord: |
| 169 | + |
| 170 | +```es6 |
| 171 | +// a person has many tags, and has many pets |
| 172 | +// a pet has many toys, and many tags |
| 173 | +Person.includes(['tags', { pets: ['toys', 'tags'] }]); |
| 174 | +``` |
| 175 | + |
| 176 | +> GET /people?include=tags,pets.toys,pets.tags |
| 177 | +
|
| 178 | +The included resources will now be present: |
| 179 | + |
| 180 | +```es6 |
| 181 | +Person.includes('tags').then((person) => { |
| 182 | + person.tags.map((t) => { return t.name; }); // #=> ['funny', 'smart'] |
| 183 | +}); |
| 184 | +``` |
| 185 | + |
| 186 | +> GET /people?include=tags |
| 187 | +
|
| 188 | +### Basic Finders |
| 189 | + |
| 190 | +`all`, `first`, and `find` can be used in conjunction with scopes. |
| 191 | + |
| 192 | +```es6 |
| 193 | +Person.all(); |
| 194 | +``` |
| 195 | + |
| 196 | +> GET /people |
| 197 | +
|
| 198 | +```es6 |
| 199 | +scope = Person.where({ name: 'Bill' }) # no query |
| 200 | +scope.all(); # => fires query, returns a Promise that resolves to an array of Person objects |
| 201 | +``` |
| 202 | + |
| 203 | +> GET /people?filter[name]=bill |
| 204 | +
|
| 205 | +Use `first` to grab the first result: |
| 206 | + |
| 207 | +```es6 |
| 208 | +// Limits per_page to 1, result is first element in the array |
| 209 | +Person.where({ name: 'Bill' }).first().then((person) => { |
| 210 | + // ... |
| 211 | +}); |
| 212 | +``` |
| 213 | + |
| 214 | +> GET /people?page[size]=1&page[number]=1&filter[name]=Bill |
| 215 | +
|
| 216 | +Finally, use `find` to find a record by ID. This will hit the `show` action. |
| 217 | + |
| 218 | +### Debugging |
| 219 | + |
| 220 | +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](https://github.com/winstonjs/winston)): |
| 221 | + |
| 222 | +```es6 |
| 223 | +import { Config } from 'jsorm'; |
| 224 | +let winston = require('winston'); |
| 225 | + |
| 226 | +winston.level = 'warn'; |
| 227 | +Config.logger = winston; |
| 228 | +``` |
| 229 | + |
| 230 | +This will log colorized request/responses when the log_level is debug, and only the request when the log level is info. |
| 231 | + |
| 232 | +### Support or Contact |
| 233 | + |
| 234 | +* Create a Github Issue |
| 235 | + |
0 commit comments