sinja 0.2.0.beta2 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +12 -2
- data/Gemfile +2 -0
- data/README.md +526 -99
- data/Rakefile +7 -3
- data/demo-app/Gemfile +3 -0
- data/demo-app/app.rb +39 -0
- data/demo-app/base.rb +9 -0
- data/demo-app/boot.rb +4 -0
- data/demo-app/classes/author.rb +88 -0
- data/demo-app/classes/comment.rb +91 -0
- data/demo-app/classes/post.rb +118 -0
- data/demo-app/classes/tag.rb +70 -0
- data/demo-app/database.rb +12 -0
- data/demo-app/test.rb +17 -0
- data/lib/sinja.rb +157 -29
- data/lib/sinja/config.rb +123 -29
- data/lib/sinja/errors.rb +69 -0
- data/lib/sinja/{relationship_routes → extensions}/sequel.rb +1 -1
- data/lib/sinja/helpers/nested.rb +10 -0
- data/lib/sinja/helpers/relationships.rb +16 -7
- data/lib/sinja/helpers/sequel.rb +16 -11
- data/lib/sinja/helpers/serializers.rb +127 -53
- data/lib/sinja/method_override.rb +15 -0
- data/lib/sinja/relationship_routes/has_many.rb +14 -1
- data/lib/sinja/relationship_routes/has_one.rb +14 -1
- data/lib/sinja/resource.rb +40 -59
- data/lib/sinja/resource_routes.rb +46 -26
- data/lib/sinja/version.rb +2 -2
- data/sinja.gemspec +18 -7
- metadata +137 -25
- data/lib/role_list.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ca76b5776f261ca35dbf3d7728b8f6c5e17b720
|
4
|
+
data.tar.gz: eb1238aab2e77a50558b6445a0cd5055e3fe9f39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9134e5396394e7b0f92305e88df7777e4abbd39e0f1b0da839e5382629dd59d97bdd41c72cc45bd2455578bc332d2381b6aa49548653333b21f7965fe4db5e28
|
7
|
+
data.tar.gz: 6907c31c9ecbe601b2eb1f93accedf39288d2cf20be57df6e96a295a1baddc8dbc4f662d1a50e367eb0806718d69e8daf8d77fe54e971ea3a7c1e6ccc89ff71f
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
|
-
- 2.3.
|
5
|
-
|
4
|
+
- 2.3.3
|
5
|
+
- ruby-head
|
6
|
+
- jruby-9.1.6.0
|
7
|
+
- jruby-head
|
8
|
+
env:
|
9
|
+
- sinatra=1.4.7 rails=4.2.7.1
|
10
|
+
- sinatra=2.0.0.beta2 rails=5.0.0.1
|
11
|
+
jdk:
|
12
|
+
- oraclejdk8
|
13
|
+
before_install:
|
14
|
+
- gem uninstall bundler
|
15
|
+
- gem install bundler -v 1.11.2
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[![Build Status](https://travis-ci.org/mwpastore/sinja.svg?branch=master)](https://travis-ci.org/mwpastore/sinja)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/sinja.svg)](https://badge.fury.io/rb/sinja)
|
5
5
|
|
6
|
-
Sinja is a [Sinatra
|
6
|
+
Sinja is a [Sinatra][1] [extension][10] for quickly building [RESTful][11],
|
7
7
|
[JSON:API][2]-[compliant][7] web services, leveraging the excellent
|
8
8
|
[JSONAPI::Serializers][3] gem and [Sinatra::Namespace][21] extension. It
|
9
9
|
enhances Sinatra's DSL to enable resource-, relationship-, and role-centric
|
@@ -13,10 +13,6 @@ Sinja aims to be lightweight (to the extent that Sinatra is), ORM-agnostic (to
|
|
13
13
|
the extent that JSONAPI::Serializers is), and opinionated (to the extent that
|
14
14
|
the JSON:API specification is).
|
15
15
|
|
16
|
-
**CAVEAT EMPTOR: This gem is still very new and under active development. The
|
17
|
-
API is mostly stable, but there still may be significant breaking changes. It
|
18
|
-
has not yet been thoroughly tested or vetted in a production environment.**
|
19
|
-
|
20
16
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
21
17
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
22
18
|
|
@@ -32,6 +28,7 @@ has not yet been thoroughly tested or vetted in a production environment.**
|
|
32
28
|
- [Configuration](#configuration)
|
33
29
|
- [Sinatra](#sinatra)
|
34
30
|
- [Sinja](#sinja)
|
31
|
+
- [Resource Locator](#resource-locator)
|
35
32
|
- [Action Helpers](#action-helpers)
|
36
33
|
- [`resource`](#resource)
|
37
34
|
- [`index {..}` => Array](#index---array)
|
@@ -49,14 +46,24 @@ has not yet been thoroughly tested or vetted in a production environment.**
|
|
49
46
|
- [`clear {..}` => TrueClass?](#clear---trueclass)
|
50
47
|
- [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
|
51
48
|
- [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
|
49
|
+
- [Action Helper Hooks & Utilities](#action-helper-hooks-amp-utilities)
|
52
50
|
- [Authorization](#authorization)
|
53
|
-
- [`default_roles`
|
51
|
+
- [`default_roles` configurables](#default_roles-configurables)
|
54
52
|
- [`:roles` Action Helper option](#roles-action-helper-option)
|
55
53
|
- [`role` helper](#role-helper)
|
56
54
|
- [Conflicts](#conflicts)
|
55
|
+
- [Validations](#validations)
|
56
|
+
- [Missing Records](#missing-records)
|
57
57
|
- [Transactions](#transactions)
|
58
|
+
- [Side-Unloading Related Resources](#side-unloading-related-resources)
|
59
|
+
- [Side-Loading Relationships](#side-loading-relationships)
|
60
|
+
- [Avoiding Null Foreign Keys](#avoiding-null-foreign-keys)
|
61
|
+
- [Many-to-One](#many-to-one)
|
62
|
+
- [One-to-Many](#one-to-many)
|
63
|
+
- [Many-to-Many](#many-to-many)
|
58
64
|
- [Coalesced Find Requests](#coalesced-find-requests)
|
59
|
-
- [
|
65
|
+
- [Patchless Clients](#patchless-clients)
|
66
|
+
- [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)
|
60
67
|
- [Code Organization](#code-organization)
|
61
68
|
- [Development](#development)
|
62
69
|
- [Contributing](#contributing)
|
@@ -71,20 +78,17 @@ require 'sinatra'
|
|
71
78
|
require 'sinatra/jsonapi'
|
72
79
|
|
73
80
|
resource :posts do
|
74
|
-
index do
|
75
|
-
Post.all
|
76
|
-
end
|
77
|
-
|
78
81
|
show do |id|
|
79
82
|
Post[id.to_i]
|
80
83
|
end
|
81
84
|
|
85
|
+
index do
|
86
|
+
Post.all
|
87
|
+
end
|
88
|
+
|
82
89
|
create do |attr|
|
83
90
|
Post.create(attr)
|
84
91
|
end
|
85
|
-
|
86
|
-
has_one :author
|
87
|
-
has_many :comments
|
88
92
|
end
|
89
93
|
|
90
94
|
freeze_jsonapi
|
@@ -94,14 +98,13 @@ Assuming the presence of a `Post` model and serializer, running the above
|
|
94
98
|
"classic"-style Sinatra application would enable the following endpoints (with
|
95
99
|
all other JSON:API endpoints returning 404 or 405):
|
96
100
|
|
97
|
-
* `GET /posts`
|
98
101
|
* `GET /posts/<id>`
|
99
|
-
* `GET /posts
|
100
|
-
* `GET /posts/<id>/comments`
|
101
|
-
* `GET /posts/<id>/relationships/author`
|
102
|
-
* `GET /posts/<id>/relationships/comments`
|
102
|
+
* `GET /posts`
|
103
103
|
* `POST /posts`
|
104
104
|
|
105
|
+
The resource locator and other action helpers, documented below, enable other
|
106
|
+
endpoints.
|
107
|
+
|
105
108
|
Of course, "modular"-style Sinatra aplications require you to register the
|
106
109
|
extension:
|
107
110
|
|
@@ -144,9 +147,12 @@ $ gem install sinja
|
|
144
147
|
|
145
148
|
* ORM-agnostic
|
146
149
|
* Role-based authorization
|
147
|
-
* To-one and to-many relationships
|
148
|
-
* Side-loaded relationships on resource creation
|
149
|
-
*
|
150
|
+
* To-one and to-many relationships and related resources
|
151
|
+
* Side-loaded relationships on resource creation and update
|
152
|
+
* Error-handling
|
153
|
+
* Conflicts (constraint violations)
|
154
|
+
* Missing records
|
155
|
+
* Validation failures
|
150
156
|
* Plus all the features of JSONAPI::Serializers!
|
151
157
|
|
152
158
|
Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
|
@@ -164,9 +170,10 @@ on the resource definitions.
|
|
164
170
|
The "power" of implementing this functionality as a Sinatra extension is that
|
165
171
|
all of Sinatra's usual features are available within your resource definitions.
|
166
172
|
The action helpers blocks get compiled into Sinatra helpers, and the
|
167
|
-
`resource`, `has_one`, and `has_many` keywords
|
168
|
-
|
169
|
-
|
173
|
+
`resource`, `has_one`, and `has_many` keywords build [Sinatra::Namespace][21]
|
174
|
+
blocks. You can manage caching directives, set headers, and even `halt` (or
|
175
|
+
`not_found`, although such cases are usually handled transparently by returning
|
176
|
+
`nil` values or empty collections from action helpers) as desired.
|
170
177
|
|
171
178
|
```ruby
|
172
179
|
class App < Sinatra::Base
|
@@ -178,14 +185,14 @@ class App < Sinatra::Base
|
|
178
185
|
# <- This is a Sinatra::Namespace block.
|
179
186
|
|
180
187
|
show do |id|
|
181
|
-
# <- This is a Sinatra helper, scoped to the resource namespace.
|
188
|
+
# <- This is a "special" Sinatra helper, scoped to the resource namespace.
|
182
189
|
end
|
183
190
|
|
184
191
|
has_one :author do
|
185
192
|
# <- This is a Sinatra::Namespace block, nested under the resource namespace.
|
186
193
|
|
187
194
|
pluck do
|
188
|
-
# <- This is a Sinatra helper, scoped to the nested namespace.
|
195
|
+
# <- This is a "special" Sinatra helper, scoped to the nested namespace.
|
189
196
|
end
|
190
197
|
end
|
191
198
|
end
|
@@ -217,8 +224,14 @@ class App < Sinatra::Base
|
|
217
224
|
get('/status', provides: :json) { 'OK' }
|
218
225
|
|
219
226
|
resource :books do
|
227
|
+
helpers do
|
228
|
+
def find(id)
|
229
|
+
Book[id.to_i]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
220
233
|
show do |id|
|
221
|
-
book =
|
234
|
+
book = find(id)
|
222
235
|
not_found "Book #{id} not found!" unless book
|
223
236
|
headers 'X-ISBN'=>book.isbn
|
224
237
|
last_modified book.updated_at
|
@@ -243,6 +256,8 @@ class App < Sinatra::Base
|
|
243
256
|
|
244
257
|
# define a custom /books/top10 route
|
245
258
|
get '/top10' do
|
259
|
+
halt 403 unless can?(:index) # restrict access to those with index rights
|
260
|
+
|
246
261
|
serialize_models Book.where{}.reverse_order(:recent_sales).limit(10).all
|
247
262
|
end
|
248
263
|
end
|
@@ -253,6 +268,11 @@ end
|
|
253
268
|
|
254
269
|
#### Public APIs
|
255
270
|
|
271
|
+
**can?**
|
272
|
+
: Takes the symbol of an action helper and returns true if the current user has
|
273
|
+
access to call that action helper for the current resource using the `role`
|
274
|
+
helper and role definitions detailed under "Authorization" below.
|
275
|
+
|
256
276
|
**data**
|
257
277
|
: Returns the `data` key of the deserialized request payload (with symbolized
|
258
278
|
names).
|
@@ -282,6 +302,9 @@ end
|
|
282
302
|
**dedasherize_names**
|
283
303
|
: Takes a hash and returns the hash with its keys dedasherized (deeply).
|
284
304
|
|
305
|
+
**sideloaded?**
|
306
|
+
: Returns true if the request was invoked from another action helper.
|
307
|
+
|
285
308
|
### Performance
|
286
309
|
|
287
310
|
Although there is some heavy metaprogramming happening at boot time, the end
|
@@ -333,11 +356,11 @@ You'll need a database schema and models (using the engine and ORM of your
|
|
333
356
|
choice) and [serializers][3] to get started. Create a new Sinatra application
|
334
357
|
(classic or modular) to hold all your JSON:API endpoints and (if modular)
|
335
358
|
register this extension. Instead of defining routes with `get`, `post`, etc. as
|
336
|
-
you normally would,
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
359
|
+
you normally would, define `resource` blocks with action helpers and `has_one`
|
360
|
+
and `has_many` relationship blocks (with their own action helpers). Sinja will
|
361
|
+
draw and enable the appropriate routes based on the defined resources,
|
362
|
+
relationships, and action helpers. Other routes will return the appropriate
|
363
|
+
HTTP status codes: 403, 404, or 405.
|
341
364
|
|
342
365
|
### Configuration
|
343
366
|
|
@@ -350,11 +373,11 @@ elsewhere (e.g. with [Rack::URLMap][4]), or host them as a completely separate
|
|
350
373
|
web service. It may not be feasible to have custom routes that don't conform to
|
351
374
|
these settings.
|
352
375
|
|
353
|
-
* Registers [Sinatra::Namespace][21]
|
376
|
+
* Registers [Sinatra::Namespace][21] and [Mustermann][25]
|
354
377
|
* Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
|
355
378
|
by manually `use`-ing the Rack::Protection middleware)
|
356
379
|
* Disables static file routes (can be reenabled with `enable :static`)
|
357
|
-
*
|
380
|
+
* Disables "classy" error pages (in favor of "classy" JSON:API error documents)
|
358
381
|
* Adds an `:api_json` MIME-type (`Sinja::MIME_TYPE`)
|
359
382
|
* Enforces strict checking of the `Accept` and `Content-Type` request headers
|
360
383
|
* Sets the `Content-Type` response header to `:api_json` (can be overriden with
|
@@ -363,6 +386,8 @@ these settings.
|
|
363
386
|
(this may be strictly enforced in future versions of Sinja)
|
364
387
|
* Formats all errors to the proper JSON:API structure
|
365
388
|
* Serializes all response bodies (including errors) to JSON
|
389
|
+
* Modifies `halt` and `not_found` to raise exceptions instead of just setting
|
390
|
+
the status code and body of the response
|
366
391
|
|
367
392
|
#### Sinja
|
368
393
|
|
@@ -374,22 +399,84 @@ their defaults shown):
|
|
374
399
|
configure_jsonapi do |c|
|
375
400
|
#c.conflict_exceptions = [] # see "Conflicts" below
|
376
401
|
|
377
|
-
#
|
402
|
+
# see "Validations" below
|
403
|
+
#c.validation_exceptions = []
|
404
|
+
#c.validation_formatter = ->{ [] }
|
405
|
+
|
406
|
+
#c.not_found_exceptions = [] # see "Missing Records" below
|
378
407
|
|
379
|
-
#
|
380
|
-
#c.
|
408
|
+
# see "Authorization" below
|
409
|
+
#c.default_roles = {}
|
410
|
+
#c.default_has_one_roles = {}
|
411
|
+
#c.default_has_many_roles = {}
|
412
|
+
|
413
|
+
# Set the error logger used by Sinja
|
414
|
+
#c.error_logger = ->(error_hash) { logger.error('sinja') { error_hash } }
|
381
415
|
|
382
416
|
# A hash of options to pass to JSONAPI::Serializer.serialize
|
383
417
|
#c.serializer_opts = {}
|
384
418
|
|
385
419
|
# JSON methods to use when serializing response bodies and errors
|
386
420
|
#c.json_generator = development? ? :pretty_generate : :generate
|
387
|
-
#c.json_error_generator = development? ? :pretty_generate : :
|
421
|
+
#c.json_error_generator = development? ? :pretty_generate : :generate
|
388
422
|
end
|
389
423
|
```
|
390
424
|
|
391
|
-
|
392
|
-
`
|
425
|
+
The above structures are mutable (e.g. you can do `c.conflict_exceptions <<
|
426
|
+
FooError` and `c.serializer_opts[:meta] = { foo: 'bar' }`) until you call
|
427
|
+
`freeze_jsonapi` to freeze the configuration store. You should always freeze
|
428
|
+
the store after Sinja is configured and all your resources are defined.
|
429
|
+
|
430
|
+
### Resource Locator
|
431
|
+
|
432
|
+
Much of Sinja's advanced functionality (e.g. updating and destroying resources,
|
433
|
+
relationship routes) is dependent upon its ability to locate the corresponding
|
434
|
+
resource for a request. To enable these features, define an ordinary helper
|
435
|
+
method named `find` in your resource definition that takes a single ID argument
|
436
|
+
and returns the corresponding object. You can, of course, use this helper
|
437
|
+
method elsewhere in your application, such as in your `show` action helper.
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
resource :posts
|
441
|
+
helpers do
|
442
|
+
def find(id)
|
443
|
+
Post[id.to_i]
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
show do |id|
|
448
|
+
next find(id), include: 'comments'
|
449
|
+
end
|
450
|
+
end
|
451
|
+
```
|
452
|
+
|
453
|
+
* What's the difference between `find` and `show`?
|
454
|
+
|
455
|
+
You can think of it as the difference between a Model and a View: `find`
|
456
|
+
retrieves the record, `show` presents it.
|
457
|
+
|
458
|
+
* Why separate the two? Why not use `show` as the resource locator?
|
459
|
+
|
460
|
+
For a variety of reasons, but primarily because the access rights for viewing
|
461
|
+
a resource are not always the same as those for updating and/or destroying a
|
462
|
+
resource, and vice-versa. For example, a user may be able to delete a
|
463
|
+
resource or subtract a relationship link without being able to see the
|
464
|
+
resource or its relationship linkage.
|
465
|
+
|
466
|
+
* How do I control access to the resource locator?
|
467
|
+
|
468
|
+
You don't. Instead, control access to the action helpers that use it:
|
469
|
+
`update`, `destroy`, and all of the relationship action helpers such as
|
470
|
+
`pluck` and `fetch`.
|
471
|
+
|
472
|
+
* What happens if I define an action helper that requires a resource locator,
|
473
|
+
but no resource locator?
|
474
|
+
|
475
|
+
Sinja will act as if you had not defined the action helper.
|
476
|
+
|
477
|
+
As a bit of syntactic sugar, if you define a `find` helper and subsequently
|
478
|
+
call `show` without a block, Sinja will generate a `show` action helper that
|
479
|
+
delegates to `find`.
|
393
480
|
|
394
481
|
### Action Helpers
|
395
482
|
|
@@ -402,27 +489,22 @@ omitted entirely. Any helper may additionally return an options hash to pass
|
|
402
489
|
along to JSONAPI::Serializer.serialize (will be merged into the global
|
403
490
|
`serializer_opts` described above).
|
404
491
|
|
405
|
-
The `:include`
|
406
|
-
|
407
|
-
prevent specific relationships from being included in the response. This
|
408
|
-
accepts the same formats as JSONAPI::Serializers does for `:include`. If you
|
409
|
-
exclude a relationship, any sub-relationships will also be excluded. The
|
492
|
+
The `:include` (see "Side-Unloading Related Resources" below) and `:fields`
|
493
|
+
query parameters are automatically passed through to JSONAPI::Serializers. The
|
410
494
|
`:sort`, `:page`, and `:filter` query parameters must be handled manually (with
|
411
|
-
|
412
|
-
below).
|
495
|
+
one exception, discussed under "Coalesced Find Requests" below).
|
413
496
|
|
414
497
|
All arguments to action helpers are "tainted" and should be treated as
|
415
|
-
potentially dangerous: IDs, attribute hashes, and [resource
|
416
|
-
|
417
|
-
|
418
|
-
Finally, some routes will automatically invoke the
|
419
|
-
behalf and make the selected resource available to
|
420
|
-
`resource`.
|
421
|
-
|
422
|
-
|
423
|
-
the `
|
424
|
-
|
425
|
-
and `has_many` action helpers.
|
498
|
+
potentially dangerous: IDs, attribute hashes, and (arrays of) [resource
|
499
|
+
identifier object][22] hashes.
|
500
|
+
|
501
|
+
Finally, some routes will automatically invoke the resource locator on your
|
502
|
+
behalf and make the selected resource available to the corresponding action
|
503
|
+
helper(s) as `resource`. For example, the `PATCH /<name>/:id` route looks up
|
504
|
+
the resource with that ID using the `find` resource locator and makes it
|
505
|
+
available to the `update` action helper as `resource`. The same goes for the
|
506
|
+
`DELETE /<name>/:id` route and the `destroy` action helper, and all of the
|
507
|
+
`has_one` and `has_many` action helpers.
|
426
508
|
|
427
509
|
#### `resource`
|
428
510
|
|
@@ -452,14 +534,16 @@ given resource block.)
|
|
452
534
|
##### `update {|attr| ..}` => Object?
|
453
535
|
|
454
536
|
Take a hash of (dedasherized) attributes, update `resource`, and optionally
|
455
|
-
return the updated resource.
|
537
|
+
return the updated resource. **Requires a resource locator.**
|
456
538
|
|
457
539
|
##### `destroy {..}`
|
458
540
|
|
459
|
-
Delete or destroy `resource`.
|
541
|
+
Delete or destroy `resource`. **Requires a resource locator.**
|
460
542
|
|
461
543
|
#### `has_one`
|
462
544
|
|
545
|
+
**Requires a resource locator.**
|
546
|
+
|
463
547
|
##### `pluck {..}` => Object
|
464
548
|
|
465
549
|
Return the related object vis-à-vis `resource` to serialize on the
|
@@ -471,7 +555,7 @@ Remove the relationship from `resource`. To serialize the updated linkage on
|
|
471
555
|
the response, refresh or reload `resource` (if necessary) and return a truthy
|
472
556
|
value.
|
473
557
|
|
474
|
-
For example, using Sequel:
|
558
|
+
For example, using [Sequel][13]:
|
475
559
|
|
476
560
|
```ruby
|
477
561
|
has_one :qux do
|
@@ -484,12 +568,14 @@ end
|
|
484
568
|
|
485
569
|
##### `graft {|rio| ..}` => TrueClass?
|
486
570
|
|
487
|
-
Take a [resource identifier object][22] and update the relationship on
|
571
|
+
Take a [resource identifier object][22] hash and update the relationship on
|
488
572
|
`resource`. To serialize the updated linkage on the response, refresh or reload
|
489
573
|
`resource` (if necessary) and return a truthy value.
|
490
574
|
|
491
575
|
#### `has_many`
|
492
576
|
|
577
|
+
**Requires a resource locator.**
|
578
|
+
|
493
579
|
##### `fetch {..}` => Array
|
494
580
|
|
495
581
|
Return an array of related objects vis-à-vis `resource` to serialize on
|
@@ -501,7 +587,7 @@ Remove all relationships from `resource`. To serialize the updated linkage on
|
|
501
587
|
the response, refresh or reload `resource` (if necessary) and return a truthy
|
502
588
|
value.
|
503
589
|
|
504
|
-
For example, using Sequel:
|
590
|
+
For example, using [Sequel][13]:
|
505
591
|
|
506
592
|
```ruby
|
507
593
|
has_many :bars do
|
@@ -513,17 +599,66 @@ end
|
|
513
599
|
|
514
600
|
##### `merge {|rios| ..}` => TrueClass?
|
515
601
|
|
516
|
-
Take an array of [resource identifier
|
602
|
+
Take an array of [resource identifier object][22] hashes and update (add unless
|
517
603
|
already present) the relationships on `resource`. To serialize the updated
|
518
604
|
linkage on the response, refresh or reload `resource` (if necessary) and return
|
519
605
|
a truthy value.
|
520
606
|
|
521
607
|
##### `subtract {|rios| ..}` => TrueClass?
|
522
608
|
|
523
|
-
Take an array of [resource identifier
|
524
|
-
already missing) the relationships on `resource`. To serialize the
|
525
|
-
linkage on the response, refresh or reload `resource` (if necessary)
|
526
|
-
a truthy value.
|
609
|
+
Take an array of [resource identifier object][22] hashes and update (remove
|
610
|
+
unless already missing) the relationships on `resource`. To serialize the
|
611
|
+
updated linkage on the response, refresh or reload `resource` (if necessary)
|
612
|
+
and return a truthy value.
|
613
|
+
|
614
|
+
### Action Helper Hooks & Utilities
|
615
|
+
|
616
|
+
You may remove a previously-registered action helper with `remove_<action>`:
|
617
|
+
|
618
|
+
```ruby
|
619
|
+
resource :foos do
|
620
|
+
index do
|
621
|
+
# ..
|
622
|
+
end
|
623
|
+
|
624
|
+
remove_index
|
625
|
+
end
|
626
|
+
```
|
627
|
+
|
628
|
+
You may invoke an action helper keyword without a block to modify the options
|
629
|
+
(i.e. roles) of a previously-registered action helper while preseving the
|
630
|
+
existing behavior:
|
631
|
+
|
632
|
+
```ruby
|
633
|
+
resource :bars do
|
634
|
+
show do |id|
|
635
|
+
# ..
|
636
|
+
end
|
637
|
+
|
638
|
+
show(roles: :admin) # restrict the above action helper to the `admin' role
|
639
|
+
end
|
640
|
+
```
|
641
|
+
|
642
|
+
You may define an ordinary helper method named `before_<action>` (in the
|
643
|
+
resource or relationship scope or any parent scopes) that takes the same
|
644
|
+
arguments as the corresponding block:
|
645
|
+
|
646
|
+
```ruby
|
647
|
+
helpers do
|
648
|
+
def before_create(attr)
|
649
|
+
halt 400 unless valid_key?(attr.delete(:special_key))
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
resource :quxes do
|
654
|
+
create do |attr|
|
655
|
+
attr.key?(:special_key) # => false
|
656
|
+
end
|
657
|
+
end
|
658
|
+
```
|
659
|
+
|
660
|
+
Any changes made to attribute hashes or (arrays of) resource identifier object
|
661
|
+
hashes in a `before` hook will be persisted to the action helper.
|
527
662
|
|
528
663
|
### Authorization
|
529
664
|
|
@@ -537,7 +672,7 @@ Users can be in one or more roles, and action helpers can be restricted to one
|
|
537
672
|
or more roles for maximum flexibility. There are three main components to the
|
538
673
|
scheme:
|
539
674
|
|
540
|
-
#### `default_roles`
|
675
|
+
#### `default_roles` configurables
|
541
676
|
|
542
677
|
You set the default roles for the entire Sinja application in the top-level
|
543
678
|
configuration. Action helpers without any default roles are unrestricted by
|
@@ -545,20 +680,24 @@ default.
|
|
545
680
|
|
546
681
|
```ruby
|
547
682
|
configure_jsonapi do |c|
|
683
|
+
# Resource roles
|
548
684
|
c.default_roles = {
|
549
|
-
# Resource roles
|
550
685
|
index: :user,
|
551
686
|
show: :user,
|
552
687
|
create: :admin,
|
553
688
|
update: :admin,
|
554
|
-
destroy: :super
|
689
|
+
destroy: :super
|
690
|
+
}
|
555
691
|
|
556
|
-
|
692
|
+
# To-one relationship roles
|
693
|
+
c.default_has_one_roles = {
|
557
694
|
pluck: :user,
|
558
695
|
prune: :admin,
|
559
|
-
graft: :admin
|
696
|
+
graft: :admin
|
697
|
+
}
|
560
698
|
|
561
|
-
|
699
|
+
# To-many relationship roles
|
700
|
+
c.default_has_many_roles = {
|
562
701
|
fetch: :user,
|
563
702
|
clear: :admin,
|
564
703
|
merge: :admin,
|
@@ -569,21 +708,21 @@ end
|
|
569
708
|
|
570
709
|
#### `:roles` Action Helper option
|
571
710
|
|
572
|
-
To override the default roles for any given action helper,
|
573
|
-
|
574
|
-
|
575
|
-
|
711
|
+
To override the default roles for any given action helper, specify a `:roles`
|
712
|
+
option when defining it. To remove all restrictions from an action helper, set
|
713
|
+
`:roles` to an empty array. For example, to manage access to `show` at
|
714
|
+
different levels of granularity (with the above default roles):
|
576
715
|
|
577
716
|
```ruby
|
578
717
|
resource :foos do
|
579
718
|
show do
|
580
|
-
# any logged-in user (with the
|
719
|
+
# any logged-in user (with the `user' role) can access /foos/:id
|
581
720
|
end
|
582
721
|
end
|
583
722
|
|
584
723
|
resource :bars do
|
585
724
|
show(roles: :admin) do
|
586
|
-
# only logged-in users with the
|
725
|
+
# only logged-in users with the `admin' role can access /bars/:id
|
587
726
|
end
|
588
727
|
end
|
589
728
|
|
@@ -599,8 +738,8 @@ end
|
|
599
738
|
Finally, define a `role` helper in your application that returns the user's
|
600
739
|
role(s) (if any). You can handle login failures in your middleware, elsewhere
|
601
740
|
in the application (i.e. a `before` filter), or within the helper, either by
|
602
|
-
|
603
|
-
|
741
|
+
raising an error or by letting Sinja raise an error on restricted action
|
742
|
+
helpers when `role` returns `nil` (the default behavior).
|
604
743
|
|
605
744
|
```ruby
|
606
745
|
helpers do
|
@@ -614,33 +753,101 @@ end
|
|
614
753
|
```
|
615
754
|
|
616
755
|
If you need more fine-grained control, for example if your action helper logic
|
617
|
-
varies by the user's role, you can use a
|
618
|
-
`
|
756
|
+
varies by the user's role, you can use a switch statement on `role` along with
|
757
|
+
the `Sinja::Roles` utility class:
|
619
758
|
|
620
759
|
```ruby
|
621
|
-
index(roles: []) do
|
760
|
+
index(roles: [:user, :admin, :super]) do
|
622
761
|
case role
|
623
|
-
when
|
624
|
-
# logic specific to user role
|
625
|
-
when
|
762
|
+
when Sinja::Roles[:user]
|
763
|
+
# logic specific to the `user' role
|
764
|
+
when Sinja::Roles[:admin, :super]
|
626
765
|
# logic specific to administrative roles
|
627
|
-
else
|
628
|
-
halt 403, 'Access denied!'
|
629
766
|
end
|
630
767
|
end
|
631
768
|
```
|
632
769
|
|
770
|
+
Or use the `role?` helper:
|
771
|
+
|
772
|
+
```ruby
|
773
|
+
show do |id|
|
774
|
+
exclude = []
|
775
|
+
exclude << 'secrets' unless role?(:admin)
|
776
|
+
|
777
|
+
next find(id), exclude: exclude
|
778
|
+
end
|
779
|
+
```
|
780
|
+
|
781
|
+
You can append resource- or even relationship-specific roles by defining a
|
782
|
+
nested helper and calling `super` (keeping in mind that `resource` may be
|
783
|
+
`nil`).
|
784
|
+
|
785
|
+
```ruby
|
786
|
+
helpers do
|
787
|
+
def role
|
788
|
+
[:user] if logged_in_user
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
resource :foos do
|
793
|
+
helpers do
|
794
|
+
def role
|
795
|
+
if resource&.owner == logged_in_user
|
796
|
+
[*super].push(:owner)
|
797
|
+
else
|
798
|
+
super
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
create(roles: :user) { |attr| .. }
|
804
|
+
update(roles: :owner) { |attr| .. }
|
805
|
+
end
|
806
|
+
```
|
807
|
+
|
633
808
|
### Conflicts
|
634
809
|
|
635
810
|
If your database driver raises exceptions on constraint violations, you should
|
636
811
|
specify which exception class(es) should be handled and return HTTP status code
|
637
812
|
409.
|
638
813
|
|
639
|
-
For example, using Sequel:
|
814
|
+
For example, using [Sequel][13]:
|
815
|
+
|
816
|
+
```ruby
|
817
|
+
configure_jsonapi do |c|
|
818
|
+
c.conflict_exceptions << Sequel::ConstraintViolation
|
819
|
+
end
|
820
|
+
```
|
821
|
+
|
822
|
+
### Validations
|
823
|
+
|
824
|
+
If your ORM raises exceptions on validation errors, you should specify which
|
825
|
+
exception class(es) should be handled and return HTTP status code 422, along
|
826
|
+
with a formatter proc that transforms the exception object into an array of
|
827
|
+
two-element arrays containing the name or symbol of the attribute that failed
|
828
|
+
validation and the detailed errror message for that attribute.
|
829
|
+
|
830
|
+
For example, using [Sequel][13]:
|
831
|
+
|
832
|
+
```ruby
|
833
|
+
configure_jsonapi do |c|
|
834
|
+
c.validation_exceptions << Sequel::ValidationFailed
|
835
|
+
c.validation_formatter = ->(e) { e.errors.keys.zip(e.errors.full_messages) }
|
836
|
+
end
|
837
|
+
```
|
838
|
+
|
839
|
+
### Missing Records
|
840
|
+
|
841
|
+
If your database driver raises exceptions on missing records, you should
|
842
|
+
specify which exception class(es) should be handled and return HTTP status code
|
843
|
+
404. This is particularly useful for relationship action helpers, which don't
|
844
|
+
have access to a dedicated subresource locator.
|
845
|
+
|
846
|
+
For example, using [Sequel][13]:
|
640
847
|
|
641
848
|
```ruby
|
642
849
|
configure_jsonapi do |c|
|
643
|
-
c.
|
850
|
+
c.not_found_exceptions << Sequel::NoMatchingRow
|
644
851
|
end
|
645
852
|
```
|
646
853
|
|
@@ -655,7 +862,8 @@ If any step in that process fails, ideally the parent resource and any
|
|
655
862
|
relationships would be rolled back before returning an error message to the
|
656
863
|
requester.
|
657
864
|
|
658
|
-
For example, using Sequel with the database handle stored in the constant
|
865
|
+
For example, using [Sequel][13] with the database handle stored in the constant
|
866
|
+
`DB`:
|
659
867
|
|
660
868
|
```ruby
|
661
869
|
helpers do
|
@@ -665,6 +873,201 @@ helpers do
|
|
665
873
|
end
|
666
874
|
```
|
667
875
|
|
876
|
+
### Side-Unloading Related Resources
|
877
|
+
|
878
|
+
You may pass an `:include` serializer option (which can be either a
|
879
|
+
comma-delimited string or array of strings) when returning resources from
|
880
|
+
action helpers. This instructs JSONAPI::Serializers to include a default set of
|
881
|
+
related resources along with the primary resource. If the client specifies an
|
882
|
+
`include` query parameter, Sinja will automatically pass it to
|
883
|
+
JSONAPI::Serializer.serialize, replacing any default value. You may also pass a
|
884
|
+
Sinja-specific `:exclude` option to prevent certain related resources from
|
885
|
+
being included in the response. If you exclude a resource, its descendents will
|
886
|
+
be automatically excluded as well. Feedback welcome.
|
887
|
+
|
888
|
+
Sinja will attempt to automatically exclude related resources based on the
|
889
|
+
current user's role(s) and any available `pluck` and `fetch` action helper
|
890
|
+
roles. For example, if resource Foo has many Bars and the current user does not
|
891
|
+
have access to Foo.Bars#fetch, the user will not be able to include Bars. It
|
892
|
+
will traverse the roles configuration, so if the current user has access to
|
893
|
+
Foo.Bars#fetch but not Bars.Qux#pluck, the user will be able to include Bars
|
894
|
+
but not Bars.Qux. This feature is experimental. Note that in contrast to the
|
895
|
+
`:exclude` option, if a related resource is excluded by this mechanism, its
|
896
|
+
descendents will _not_ be automatically excluded.
|
897
|
+
|
898
|
+
### Side-Loading Relationships
|
899
|
+
|
900
|
+
Sinja works hard to DRY up your business logic. As mentioned above, when a
|
901
|
+
request comes in to create or update a resource and that request includes
|
902
|
+
relationships, Sinja will try to farm out the work to your defined relationship
|
903
|
+
routes. Let's look at this example request from the JSON:API specification:
|
904
|
+
|
905
|
+
```
|
906
|
+
POST /photos HTTP/1.1
|
907
|
+
Content-Type: application/vnd.api+json
|
908
|
+
Accept: application/vnd.api+json
|
909
|
+
```
|
910
|
+
|
911
|
+
```json
|
912
|
+
{
|
913
|
+
"data": {
|
914
|
+
"type": "photos",
|
915
|
+
"attributes": {
|
916
|
+
"title": "Ember Hamster",
|
917
|
+
"src": "http://example.com/images/productivity.png"
|
918
|
+
},
|
919
|
+
"relationships": {
|
920
|
+
"photographer": {
|
921
|
+
"data": { "type": "people", "id": "9" }
|
922
|
+
}
|
923
|
+
}
|
924
|
+
}
|
925
|
+
}
|
926
|
+
```
|
927
|
+
|
928
|
+
Assuming a `:photos` resource with a `has_one :photographer` relationship in
|
929
|
+
the application, and `graft` is configured to sideload on `create` (more on
|
930
|
+
this in a moment), Sinja will invoke the following action helpers in turn:
|
931
|
+
|
932
|
+
1. `create` on the Photos resource (with `data.attributes`)
|
933
|
+
1. `graft` on the Photographer relationship (with
|
934
|
+
`data.relationships.photographer.data`)
|
935
|
+
|
936
|
+
If any step of the process fails—for example, if the `graft` action
|
937
|
+
helper is not defined in the Photographer relationship, or if it does not
|
938
|
+
permit sideloading from `create`, or if it raises an error—the entire
|
939
|
+
request will fail and any database changes will be rolled back (given a
|
940
|
+
`transaction` helper). Note that the user's role must grant them access to call
|
941
|
+
either `graft` or `create`.
|
942
|
+
|
943
|
+
`create` and `update` are the only two action helpers that trigger sideloading;
|
944
|
+
`graft`, `merge`, and `clear` are the only action helpers invoked by
|
945
|
+
sideloading. You must indicate which combinations are valid using the
|
946
|
+
`:sideload_on` action helper option. (Note that if you want to sideload `merge`
|
947
|
+
on `update`, you must define a `clear` action helper as well.) For example:
|
948
|
+
|
949
|
+
```ruby
|
950
|
+
resource :photos do
|
951
|
+
helpers do
|
952
|
+
def find(id) ..; end
|
953
|
+
end
|
954
|
+
|
955
|
+
create { |attr| .. }
|
956
|
+
update { |attr| .. }
|
957
|
+
|
958
|
+
has_one :photographer do
|
959
|
+
# Allow `create' to sideload the Photographer
|
960
|
+
graft(sideload_on: :create) { |rio| .. }
|
961
|
+
end
|
962
|
+
|
963
|
+
has_many :tags do
|
964
|
+
# Allow `create' and `update' to sideload Tags
|
965
|
+
merge(sideload_on: [:create, :update]) { |rios| .. }
|
966
|
+
|
967
|
+
# Allow `update' to clear Tags before sideloading them
|
968
|
+
clear(sideload_on: :update) { .. }
|
969
|
+
end
|
970
|
+
end
|
971
|
+
```
|
972
|
+
|
973
|
+
#### Avoiding Null Foreign Keys
|
974
|
+
|
975
|
+
Now, let's say our DBA is forward-thinking and wants to make the foreign key
|
976
|
+
constraint between the `photographer_id` column on the Photos table and the
|
977
|
+
People table non-nullable. Unfortunately, that will break Sinja, because the
|
978
|
+
Photo will be inserted first, with a null Photographer. (Deferrable constraints
|
979
|
+
would be a perfect solution to this problem, but `NOT NULL` constraints are not
|
980
|
+
deferrable in Postgres, and constraints in general are not deferrable in
|
981
|
+
MySQL.)
|
982
|
+
|
983
|
+
Instead, we'll need to enforce our non-nullable relationships at the
|
984
|
+
application level. To accomplish this, define an ordinary helper named
|
985
|
+
`validate!` (in the resource scope or any parent scopes). This method, if
|
986
|
+
present, is invoked from within the transaction after the entire request has
|
987
|
+
been processed, and so can abort the transaction (following your ORM's
|
988
|
+
semantics). For example:
|
989
|
+
|
990
|
+
```ruby
|
991
|
+
resource :photos do
|
992
|
+
helpers do
|
993
|
+
def validate!
|
994
|
+
fail 'Invalid Photographer for Photo' if resource.photographer.nil?
|
995
|
+
end
|
996
|
+
end
|
997
|
+
end
|
998
|
+
```
|
999
|
+
|
1000
|
+
If your ORM supports validation—and "deferred validation"—you can
|
1001
|
+
easily handle all such situations (as well as other types of validations) at
|
1002
|
+
the top-level of your application. (Make sure to define your validation
|
1003
|
+
exceptions and formatter as described above.) For example, using [Sequel][13]:
|
1004
|
+
|
1005
|
+
```ruby
|
1006
|
+
class Photo < Sequel::Model
|
1007
|
+
many_to_one :photographer
|
1008
|
+
|
1009
|
+
# http://sequel.jeremyevans.net/rdoc/files/doc/validations_rdoc.html
|
1010
|
+
def validate
|
1011
|
+
super
|
1012
|
+
errors.add(:photographer, 'cannot be null') if photographer.nil?
|
1013
|
+
end
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
helpers do
|
1017
|
+
def validate!
|
1018
|
+
raise Sequel::ValidationFailed, resource.errors unless resource.valid?
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
resource :photos do
|
1023
|
+
create do |attr|
|
1024
|
+
photo = Photo.new
|
1025
|
+
photo.set(attr)
|
1026
|
+
photo.save(validate: false) # defer validation
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
has_one :photographer do
|
1030
|
+
graft(sideload_on: :create) do |rio|
|
1031
|
+
resource.photographer = People.with_pk!(rio[:id].to_i)
|
1032
|
+
resource.save_changes(validate: !sideloaded?) # defer validation if sideloaded
|
1033
|
+
end
|
1034
|
+
end
|
1035
|
+
end
|
1036
|
+
```
|
1037
|
+
|
1038
|
+
Note that the `validate!` hook is _only_ invoked from within transactions
|
1039
|
+
involving the `create` and `update` action helpers (and any dependent `graft`
|
1040
|
+
and `merge` action helpers), so this deferred validation pattern is only
|
1041
|
+
appropriate in those cases. You must use immedate validation in all other
|
1042
|
+
cases. The `sideloaded?` helper is provided to help disambiguate edge cases.
|
1043
|
+
|
1044
|
+
> TODO: The following three sections are a little confusing. Rewrite them.
|
1045
|
+
|
1046
|
+
##### Many-to-One
|
1047
|
+
|
1048
|
+
Example: Photo belongs to (has one) Photographer; Photo.Photographer cannot be
|
1049
|
+
null.
|
1050
|
+
|
1051
|
+
* Don't define `prune` relationship action helper
|
1052
|
+
* Define `graft` relationship action helper to enable reassigning the Photographer
|
1053
|
+
* Define `destroy` resource action helper to enable removing the Photo
|
1054
|
+
* Use `validate!` helper to check for nulls
|
1055
|
+
|
1056
|
+
##### One-to-Many
|
1057
|
+
|
1058
|
+
Example: Photographer has many Photos; Photo.Photographer cannot be null.
|
1059
|
+
|
1060
|
+
* Don't define `clear` relationship action helper
|
1061
|
+
* Don't define `subtract` relationship action helper
|
1062
|
+
* Delegate removing Photos and reassigning Photographers to Photo resource
|
1063
|
+
|
1064
|
+
##### Many-to-Many
|
1065
|
+
|
1066
|
+
Example: Photo has many Tags.
|
1067
|
+
|
1068
|
+
Nothing to worry about here! Feel free to use `NOT NULL` foreign key
|
1069
|
+
constraints on the join table.
|
1070
|
+
|
668
1071
|
### Coalesced Find Requests
|
669
1072
|
|
670
1073
|
If your JSON:API client coalesces find requests, the `show` action helper will
|
@@ -674,7 +1077,28 @@ are supported: `?filter[id]=1,2` and `?filter[id][]=1&filter[id][]=2`. If any
|
|
674
1077
|
ID is not found (i.e. `show` returns `nil`), the route will halt with HTTP
|
675
1078
|
status code 404.
|
676
1079
|
|
677
|
-
###
|
1080
|
+
### Patchless Clients
|
1081
|
+
|
1082
|
+
JSON:API [recommends][23] supporting patchless clients by using the
|
1083
|
+
`X-HTTP-Method-Override` request header to coerce a `POST` into a `PATCH`. To
|
1084
|
+
support this in Sinja, add the Sinja::MethodOverride middleware (which is a
|
1085
|
+
stripped-down version of [Rack::MethodOverride][24]) into your application (or
|
1086
|
+
Rackup configuration):
|
1087
|
+
|
1088
|
+
```ruby
|
1089
|
+
require 'sinja'
|
1090
|
+
require 'sinja/method_override'
|
1091
|
+
|
1092
|
+
class MyApp < Sinatra::Base
|
1093
|
+
use Sinja::MethodOverride
|
1094
|
+
|
1095
|
+
register Sinja
|
1096
|
+
|
1097
|
+
# ..
|
1098
|
+
end
|
1099
|
+
```
|
1100
|
+
|
1101
|
+
### Sinja or Sinatra::JSONAPI
|
678
1102
|
|
679
1103
|
Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
|
680
1104
|
requires Sinatra::Base, so this:
|
@@ -725,14 +1149,14 @@ should be relatively painless. For example:
|
|
725
1149
|
```ruby
|
726
1150
|
# controllers/foo_controller.rb
|
727
1151
|
FooController = proc do
|
728
|
-
index do
|
729
|
-
Foo.all
|
730
|
-
end
|
731
|
-
|
732
1152
|
show do |id|
|
733
1153
|
Foo[id.to_i]
|
734
1154
|
end
|
735
1155
|
|
1156
|
+
index do
|
1157
|
+
Foo.all
|
1158
|
+
end
|
1159
|
+
|
736
1160
|
# ..
|
737
1161
|
end
|
738
1162
|
|
@@ -795,3 +1219,6 @@ License](http://opensource.org/licenses/MIT).
|
|
795
1219
|
[20]: http://roda.jeremyevans.net
|
796
1220
|
[21]: http://www.sinatrarb.com/contrib/namespace.html
|
797
1221
|
[22]: http://jsonapi.org/format/#document-resource-identifier-objects
|
1222
|
+
[23]: http://jsonapi.org/recommendations/#patchless-clients
|
1223
|
+
[24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride
|
1224
|
+
[25]: http://www.sinatrarb.com/mustermann/
|