sinja 1.1.0.pre4 → 1.2.0.pre2
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/Gemfile +2 -1
- data/README.md +96 -29
- data/demo-app/.dockerignore +1 -0
- data/demo-app/Dockerfile +26 -0
- data/demo-app/Gemfile +12 -1
- data/demo-app/README.md +39 -24
- data/demo-app/Rakefile +36 -0
- data/demo-app/app.rb +3 -10
- data/demo-app/boot.rb +0 -4
- data/demo-app/classes/author.rb +3 -4
- data/demo-app/{base.rb → classes/base.rb} +2 -1
- data/demo-app/classes/comment.rb +1 -2
- data/demo-app/classes/post.rb +17 -10
- data/demo-app/classes/tag.rb +7 -3
- data/extensions/sequel/Gemfile +4 -0
- data/extensions/sequel/LICENSE.txt +21 -0
- data/extensions/sequel/README.md +274 -0
- data/extensions/sequel/Rakefile +10 -0
- data/extensions/sequel/bin/console +14 -0
- data/extensions/sequel/bin/setup +8 -0
- data/extensions/sequel/lib/sinatra/jsonapi/sequel.rb +7 -0
- data/extensions/sequel/lib/sinja-sequel.rb +2 -0
- data/extensions/sequel/lib/sinja/sequel.rb +110 -0
- data/extensions/sequel/lib/sinja/sequel/core.rb +72 -0
- data/extensions/sequel/lib/sinja/sequel/helpers.rb +78 -0
- data/extensions/sequel/lib/sinja/sequel/version.rb +6 -0
- data/extensions/sequel/sinja-sequel.gemspec +29 -0
- data/extensions/sequel/test/test_helper.rb +3 -0
- data/lib/sinja.rb +40 -18
- data/lib/sinja/config.rb +4 -3
- data/lib/sinja/helpers/serializers.rb +2 -1
- data/lib/sinja/relationship_routes/has_many.rb +8 -9
- data/lib/sinja/relationship_routes/has_one.rb +1 -1
- data/lib/sinja/resource.rb +2 -1
- data/lib/sinja/resource_routes.rb +2 -2
- data/lib/sinja/version.rb +1 -1
- data/sinja.gemspec +5 -7
- metadata +39 -11
- data/.rspec +0 -2
- data/lib/sinja/extensions/sequel.rb +0 -53
- data/lib/sinja/helpers/sequel.rb +0 -101
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'sinja-sequel'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require 'pry'
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sinja/sequel/helpers'
|
3
|
+
require 'sinja/sequel/version'
|
4
|
+
|
5
|
+
module Sinja
|
6
|
+
module Sequel
|
7
|
+
def self.registered(app)
|
8
|
+
app.helpers Helpers
|
9
|
+
end
|
10
|
+
|
11
|
+
def resource(res, try_convert=:to_i, &block)
|
12
|
+
klass = res.to_s.classify.constantize
|
13
|
+
|
14
|
+
super(res) do
|
15
|
+
register Resource
|
16
|
+
|
17
|
+
helpers do
|
18
|
+
def find(id)
|
19
|
+
klass[id.send(try_convert)]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
show
|
24
|
+
|
25
|
+
show_many do |ids|
|
26
|
+
klass.where(klass.primary_key=>ids.map(&try_convert)).all
|
27
|
+
end
|
28
|
+
|
29
|
+
index do
|
30
|
+
klass.dataset
|
31
|
+
end
|
32
|
+
|
33
|
+
create do |attr|
|
34
|
+
tmp = klass.new
|
35
|
+
if respond_to?(:settable_fields)
|
36
|
+
tmp.set_fields(attr, settable_fields)
|
37
|
+
else
|
38
|
+
tmp.set(attr)
|
39
|
+
end
|
40
|
+
tmp.save(:validate=>false)
|
41
|
+
next_pk tmp
|
42
|
+
end
|
43
|
+
|
44
|
+
update do |attr|
|
45
|
+
if respond_to?(:settable_fields)
|
46
|
+
resource.update_fields(attr, settable_fields, :validate=>false, :missing=>:skip)
|
47
|
+
else
|
48
|
+
resource.set(attr)
|
49
|
+
resource.save_changes(:validate=>false)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
destroy do
|
54
|
+
resource.destroy
|
55
|
+
end
|
56
|
+
|
57
|
+
instance_eval(&block) if block
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module Resource
|
62
|
+
def has_one(rel, &block)
|
63
|
+
super(rel) do
|
64
|
+
pluck do
|
65
|
+
resource.send(rel)
|
66
|
+
end
|
67
|
+
|
68
|
+
prune(:sideload_on=>:update) do
|
69
|
+
resource.send("#{rel}=", nil)
|
70
|
+
resource.save_changes
|
71
|
+
end
|
72
|
+
|
73
|
+
graft(:sideload_on=>%i[create update]) do |rio|
|
74
|
+
klass = resource.class.association_reflection(rel).associated_class
|
75
|
+
resource.send("#{rel}=", klass.with_pk!(rio[:id]))
|
76
|
+
resource.save_changes(:validate=>!sideloaded?)
|
77
|
+
end
|
78
|
+
|
79
|
+
instance_eval(&block) if block
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_many(rel, &block)
|
84
|
+
super(rel) do
|
85
|
+
fetch do
|
86
|
+
resource.send("#{rel}_dataset")
|
87
|
+
end
|
88
|
+
|
89
|
+
clear(:sideload_on=>:update) do
|
90
|
+
resource.send("remove_all_#{rel}")
|
91
|
+
end
|
92
|
+
|
93
|
+
replace(:sideload_on=>:update) do |rios|
|
94
|
+
add_remove(rel, rios)
|
95
|
+
end
|
96
|
+
|
97
|
+
merge(:sideload_on=>:create) do |rios|
|
98
|
+
add_missing(rel, rios)
|
99
|
+
end
|
100
|
+
|
101
|
+
subtract do |rios|
|
102
|
+
remove_present(rel, rios)
|
103
|
+
end
|
104
|
+
|
105
|
+
instance_eval(&block) if block
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'forwardable'
|
3
|
+
require 'sequel'
|
4
|
+
|
5
|
+
module Sinja
|
6
|
+
module Sequel
|
7
|
+
module Core
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def self.prepended(base)
|
11
|
+
base.sinja do |c|
|
12
|
+
c.conflict_exceptions << ::Sequel::ConstraintViolation
|
13
|
+
c.not_found_exceptions << ::Sequel::NoMatchingRow
|
14
|
+
c.validation_exceptions << ::Sequel::ValidationFailed
|
15
|
+
c.validation_formatter = ->(e) { e.errors.keys.zip(e.errors.full_messages) }
|
16
|
+
end
|
17
|
+
|
18
|
+
base.include Pagination if ::Sequel::Database::EXTENSIONS.key?(:pagination)
|
19
|
+
end
|
20
|
+
|
21
|
+
def_delegator ::Sequel::Model, :db, :database
|
22
|
+
|
23
|
+
def_delegator :database, :transaction
|
24
|
+
|
25
|
+
define_method :filter, proc(&:where)
|
26
|
+
|
27
|
+
def sort(collection, fields)
|
28
|
+
collection.order(*fields.map { |k, v| ::Sequel.send(v, k) })
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method :finalize, proc(&:all)
|
32
|
+
|
33
|
+
def validate!
|
34
|
+
raise ::Sequel::ValidationFailed, resource unless resource.valid?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module Pagination
|
39
|
+
def self.included(base)
|
40
|
+
base.sinja { |c| c.page_using = {
|
41
|
+
:number=>1,
|
42
|
+
:size=>10,
|
43
|
+
:record_count=>nil
|
44
|
+
}}
|
45
|
+
end
|
46
|
+
|
47
|
+
def page(collection, opts)
|
48
|
+
collection = collection.dataset unless collection.respond_to?(:paginate)
|
49
|
+
|
50
|
+
opts = settings._sinja.page_using.merge(opts)
|
51
|
+
collection = collection.paginate opts[:number].to_i, opts[:size].to_i,
|
52
|
+
(opts[:record_count].to_i if opts[:record_count])
|
53
|
+
|
54
|
+
# Attributes common to all pagination links
|
55
|
+
base = {
|
56
|
+
:size=>collection.page_size,
|
57
|
+
:record_count=>collection.pagination_record_count
|
58
|
+
}
|
59
|
+
|
60
|
+
pagination = {
|
61
|
+
:first=>base.merge(:number=>1),
|
62
|
+
:self=>base.merge(:number=>collection.current_page),
|
63
|
+
:last=>base.merge(:number=>collection.page_count)
|
64
|
+
}
|
65
|
+
pagination[:next] = base.merge(:number=>collection.next_page) if collection.next_page
|
66
|
+
pagination[:prev] = base.merge(:number=>collection.prev_page) if collection.prev_page
|
67
|
+
|
68
|
+
return collection, pagination
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sinja/sequel/core'
|
3
|
+
|
4
|
+
module Sinja
|
5
|
+
module Sequel
|
6
|
+
module Helpers
|
7
|
+
def self.included(base)
|
8
|
+
base.prepend Core
|
9
|
+
end
|
10
|
+
|
11
|
+
def next_pk(resource, opts={})
|
12
|
+
[resource.pk, resource, opts]
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_remove(association, rios, try_convert=:to_i)
|
16
|
+
meth_suffix = association.to_s.singularize
|
17
|
+
add_meth = "add_#{meth_suffix}".to_sym
|
18
|
+
remove_meth = "remove_#{meth_suffix}".to_sym
|
19
|
+
|
20
|
+
dataset = resource.send("#{association}_dataset")
|
21
|
+
klass = dataset.association_reflection.associated_class
|
22
|
+
|
23
|
+
# does not / will not work with composite primary keys
|
24
|
+
new_ids = rios.map { |rio| rio[:id].send(try_convert) }
|
25
|
+
transaction do
|
26
|
+
resource.lock!
|
27
|
+
old_ids = dataset.select_map(klass.primary_key)
|
28
|
+
in_common = old_ids & new_ids
|
29
|
+
|
30
|
+
(new_ids - in_common).each do |id|
|
31
|
+
subresource = klass.with_pk!(id)
|
32
|
+
resource.send(add_meth, subresource) \
|
33
|
+
unless block_given? && !yield(subresource)
|
34
|
+
end
|
35
|
+
|
36
|
+
(old_ids - in_common).each do |id|
|
37
|
+
subresource = klass.with_pk!(id)
|
38
|
+
resource.send(remove_meth, subresource) \
|
39
|
+
unless block_given? && !yield(subresource)
|
40
|
+
end
|
41
|
+
|
42
|
+
resource.reload
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_missing(*args, &block)
|
47
|
+
add_or_remove(:add, :-, *args, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_present(*args, &block)
|
51
|
+
add_or_remove(:remove, :&, *args, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def add_or_remove(meth_prefix, operator, association, rios, try_convert=:to_i)
|
57
|
+
meth = "#{meth_prefix}_#{association.to_s.singularize}".to_sym
|
58
|
+
transaction do
|
59
|
+
resource.lock!
|
60
|
+
venn(operator, association, rios, try_convert) do |subresource|
|
61
|
+
resource.send(meth, subresource) \
|
62
|
+
unless block_given? && !yield(subresource)
|
63
|
+
end
|
64
|
+
resource.reload
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def venn(operator, association, rios, try_convert)
|
69
|
+
dataset = resource.send("#{association}_dataset")
|
70
|
+
klass = dataset.association_reflection.associated_class
|
71
|
+
# does not / will not work with composite primary keys
|
72
|
+
rios.map { |rio| rio[:id].send(try_convert) }
|
73
|
+
.send(operator, dataset.select_map(klass.primary_key))
|
74
|
+
.each { |id| yield klass.with_pk!(id) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sinja/sequel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'sinja-sequel'
|
8
|
+
spec.version = Sinja::Sequel::VERSION
|
9
|
+
spec.authors = ['Mike Pastore']
|
10
|
+
spec.email = ['mike@oobak.org']
|
11
|
+
|
12
|
+
spec.summary = 'Sequel-specific Helpers and DSL for Sinja'
|
13
|
+
spec.homepage = 'https://github.com/mwpastore/sinja/tree/master/extensions/sequel'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.require_paths = %w[lib]
|
20
|
+
|
21
|
+
spec.required_ruby_version = '>= 2.3.0'
|
22
|
+
|
23
|
+
spec.add_dependency 'sequel', '~> 4.0'
|
24
|
+
spec.add_dependency 'sinja', '>= 1.2.0.pre2', '< 2'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
27
|
+
spec.add_development_dependency 'minitest', '~> 5.9'
|
28
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
29
|
+
end
|
data/lib/sinja.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'set'
|
3
|
+
|
2
4
|
require 'active_support/inflector'
|
3
5
|
require 'mustermann'
|
4
6
|
require 'sinatra/base'
|
5
7
|
require 'sinatra/namespace'
|
6
8
|
|
7
|
-
require 'set'
|
8
9
|
require 'sinja/config'
|
9
10
|
require 'sinja/errors'
|
10
11
|
require 'sinja/helpers/serializers'
|
@@ -220,8 +221,11 @@ module Sinja
|
|
220
221
|
end
|
221
222
|
|
222
223
|
def filter_by?(action)
|
223
|
-
return
|
224
|
-
|
224
|
+
return if params[:filter].empty?
|
225
|
+
|
226
|
+
return params[:filter] \
|
227
|
+
if settings.resource_config[action][:filter_by].empty? ||
|
228
|
+
params[:filter].keys.to_set.subset?(settings.resource_config[action][:filter_by])
|
225
229
|
|
226
230
|
raise BadRequestError, "Invalid `filter' query parameter(s)"
|
227
231
|
end
|
@@ -239,8 +243,11 @@ module Sinja
|
|
239
243
|
end
|
240
244
|
|
241
245
|
def sort_by?(action)
|
242
|
-
return
|
243
|
-
|
246
|
+
return if params[:sort].empty?
|
247
|
+
|
248
|
+
return params[:sort] \
|
249
|
+
if settings.resource_config[action][:sort_by].empty? ||
|
250
|
+
params[:sort].keys.to_set.subset?(settings.resource_config[action][:sort_by])
|
244
251
|
|
245
252
|
raise BadRequestError, "Invalid `sort' query parameter(s)"
|
246
253
|
end
|
@@ -257,25 +264,36 @@ module Sinja
|
|
257
264
|
end
|
258
265
|
|
259
266
|
def page_using?
|
260
|
-
return
|
267
|
+
return if params[:page].empty?
|
268
|
+
|
269
|
+
return params[:page] \
|
270
|
+
if params[:page].keys.to_set.subset?(settings._sinja.page_using.keys.to_set)
|
261
271
|
|
262
272
|
raise BadRequestError, "Invalid `page' query parameter(s)"
|
263
273
|
end
|
264
274
|
|
265
275
|
def filter_sort_page?(action)
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
276
|
+
return enum_for(__callee__, action).to_h unless block_given?
|
277
|
+
|
278
|
+
if filter = filter_by?(action)
|
279
|
+
yield :filter, filter
|
280
|
+
end
|
281
|
+
|
282
|
+
if sort = sort_by?(action)
|
283
|
+
yield :sort, sort
|
284
|
+
end
|
270
285
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
286
|
+
if page = page_using?
|
287
|
+
yield :page, page
|
288
|
+
end
|
289
|
+
end
|
275
290
|
|
276
|
-
|
291
|
+
def filter_sort_page(collection, opts)
|
292
|
+
collection = filter(collection, opts[:filter]) if opts.key?(:filter)
|
293
|
+
collection = sort(collection, opts[:sort]) if opts.key?(:sort)
|
294
|
+
collection, pagination = page(collection, opts[:page]) if opts.key?(:page)
|
277
295
|
|
278
|
-
return collection, pagination
|
296
|
+
return respond_to?(:finalize) ? finalize(collection) : collection, pagination
|
279
297
|
end
|
280
298
|
|
281
299
|
def halt(code, body=nil)
|
@@ -306,10 +324,10 @@ module Sinja
|
|
306
324
|
|
307
325
|
def sanity_check!(resource_name, id=nil)
|
308
326
|
raise ConflictError, 'Resource type in payload does not match endpoint' \
|
309
|
-
|
327
|
+
unless data[:type] && data[:type].to_sym == resource_name
|
310
328
|
|
311
329
|
raise ConflictError, 'Resource ID in payload does not match endpoint' \
|
312
|
-
|
330
|
+
unless id.nil? || data[:id] && data[:id].to_s == id.to_s
|
313
331
|
end
|
314
332
|
|
315
333
|
def transaction
|
@@ -332,6 +350,10 @@ module Sinja
|
|
332
350
|
body serialize_response_body if response.ok? || response.created?
|
333
351
|
end
|
334
352
|
|
353
|
+
app.not_found do
|
354
|
+
serialize_errors(&settings._sinja.error_logger)
|
355
|
+
end
|
356
|
+
|
335
357
|
app.error 400...600 do
|
336
358
|
serialize_errors(&settings._sinja.error_logger)
|
337
359
|
end
|