sinja 0.2.0.beta2 → 1.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](https://travis-ci.org/mwpastore/sinja)
|
4
4
|
[](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/
|