sinja 1.1.0.pre4 → 1.2.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|