active_model_serializers_pg 0.0.5 → 0.2.0
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/README.md +10 -1
- data/Rakefile +11 -1
- data/active_model_serializers_pg.gemspec +3 -1
- data/gemfiles/Gemfile.activerecord-5.0.x +1 -1
- data/gemfiles/Gemfile.activerecord-5.1.x +1 -1
- data/gemfiles/Gemfile.activerecord-5.2.x +1 -1
- data/gemfiles/Gemfile.activerecord-6.0.x +5 -0
- data/lib/active_model_serializers/adapter/json_api_pg.rb +79 -36
- data/lib/active_model_serializers_pg/version.rb +1 -1
- data/lib/generators/active_model_serializers_pg/active_model_serializers_pg_generator.rb +20 -0
- data/lib/generators/active_record/active_model_serializers_pg_generator.rb +43 -0
- data/lib/generators/active_record/templates/jsonb_dasherize.sql +32 -0
- data/lib/generators/active_record/templates/migration.rb +11 -0
- data/spec/generators/main_generator_spec.rb +23 -0
- data/spec/serializer_spec.rb +59 -0
- data/spec/spec_helper.rb +24 -2
- metadata +40 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0692a6200c2d79a0704f65112f64c150bca435c24f82409c6b7e327ecb8e5a62'
|
4
|
+
data.tar.gz: c4a9b76cb7e6f5f896ddacf01fd6d4ae2c7e0b43f14692f3877a0afaec024953
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fda8dad0da5f0b7e877e348b7b42494b74b6378e9a84977e276f3cbe1595a259eaef4a2d708747d1d6b307f24b5c4f6949c3e7aec6210670030e2b52cf2a9a9c
|
7
|
+
data.tar.gz: a81b5904a40a60a482bd3745f289c9a9c774a73b2bc2ce1c3ed26efb518e21318270ad7b401338e34dadb537793cb5ea67d845942ee936f6df25bb4d64a4e488
|
data/README.md
CHANGED
@@ -31,6 +31,13 @@ Or install it yourself as:
|
|
31
31
|
|
32
32
|
gem install active_model_serializers_pg
|
33
33
|
|
34
|
+
### Migrations
|
35
|
+
|
36
|
+
This gem depends on a SQL function to dasherize hstore/json/jsonb columns, so you must add its migration to your project and run it, like this:
|
37
|
+
|
38
|
+
rails g active_model_serializers_pg
|
39
|
+
rake db:migrate
|
40
|
+
|
34
41
|
## Usage
|
35
42
|
|
36
43
|
You can enable in-Postgres serialization for everything by putting this in a Rails initializer:
|
@@ -52,8 +59,10 @@ Here are some other details we support:
|
|
52
59
|
- `belongs_to`, `has_one`, and `has_many` associations.
|
53
60
|
- If you serialize an `enum` you get the string values, not integers.
|
54
61
|
- You can serialize an `alias`'d association.
|
62
|
+
- You can serialize an `alias_attribute`'d column.
|
55
63
|
- We preserve SQL ordering from a model's `default_scope`.
|
56
64
|
- We preserve SQL ordering attached to an association.
|
65
|
+
- When dasherizing we also dasherize json/jsonb/hstore contents (like standard AMS).
|
57
66
|
|
58
67
|
### Methods in Serializers and Models
|
59
68
|
|
@@ -110,7 +119,7 @@ To work on active\_model\_serializers\_pg locally, follow these steps:
|
|
110
119
|
4. Run `bundle exec rake db:create`, this will create the test database.
|
111
120
|
5. Run `bundle exec rake db:migrate`, this will set up the database tables required
|
112
121
|
by the test.
|
113
|
-
6. Run `bundle exec rake test:all` to run tests against all supported versions of Active Record (currently 5.0.x, 5.1.x, 5.2.x).
|
122
|
+
6. Run `bundle exec rake test:all` to run tests against all supported versions of Active Record (currently 5.0.x, 5.1.x, 5.2.x, 6.0.x).
|
114
123
|
You can also say `BUNDLE_GEMFILE=gemfiles/Gemfile.activerecord-5.2.x bundle exec rspec spec` to run against a specific version (and select specific tests).
|
115
124
|
|
116
125
|
Commands for building/releasing/installing:
|
data/Rakefile
CHANGED
@@ -72,9 +72,17 @@ namespace :db do
|
|
72
72
|
task :migrate => :load_db_settings do
|
73
73
|
ActiveRecord::Base.establish_connection
|
74
74
|
|
75
|
+
ActiveRecord::Base.connection.execute "CREATE EXTENSION hstore"
|
76
|
+
|
75
77
|
ActiveRecord::Base.connection.create_table :people, force: true do |t|
|
76
78
|
t.string "first_name"
|
77
79
|
t.string "last_name"
|
80
|
+
t.json "options"
|
81
|
+
t.jsonb "prefs"
|
82
|
+
t.hstore "settings"
|
83
|
+
t.json "selfies", array: true
|
84
|
+
t.jsonb "portraits", array: true
|
85
|
+
t.hstore "landscapes", array: true
|
78
86
|
t.datetime "created_at"
|
79
87
|
t.datetime "updated_at"
|
80
88
|
end
|
@@ -131,6 +139,8 @@ namespace :db do
|
|
131
139
|
t.datetime "updated_at"
|
132
140
|
end
|
133
141
|
|
142
|
+
ActiveRecord::Base.connection.execute File.read(File.expand_path('../lib/generators/active_record/templates/jsonb_dasherize.sql', __FILE__))
|
143
|
+
|
134
144
|
puts 'Database migrated'
|
135
145
|
end
|
136
146
|
end
|
@@ -141,7 +151,7 @@ namespace :test do
|
|
141
151
|
# Escape current bundler environment
|
142
152
|
Bundler.with_clean_env do
|
143
153
|
# Currently only supports Active Record v5.0-v5.2
|
144
|
-
%w(5.0.x 5.1.x 5.2.x).each do |version|
|
154
|
+
%w(5.0.x 5.1.x 5.2.x 6.0.x).each do |version|
|
145
155
|
sh "BUNDLE_GEMFILE='gemfiles/Gemfile.activerecord-#{version}' bundle install --quiet"
|
146
156
|
sh "BUNDLE_GEMFILE='gemfiles/Gemfile.activerecord-#{version}' bundle exec rspec spec"
|
147
157
|
end
|
@@ -19,12 +19,14 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_runtime_dependency 'active_model_serializers', '~> 0.10.8'
|
22
|
-
spec.add_runtime_dependency 'activerecord', '
|
22
|
+
spec.add_runtime_dependency 'activerecord', '>= 5.0'
|
23
23
|
|
24
24
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
25
25
|
spec.add_development_dependency 'actionpack', '> 4.0'
|
26
26
|
spec.add_development_dependency 'rake'
|
27
27
|
spec.add_development_dependency 'rspec'
|
28
|
+
spec.add_development_dependency 'rspec-rails'
|
29
|
+
spec.add_development_dependency 'generator_spec'
|
28
30
|
spec.add_development_dependency 'bourne', '~> 1.3.0'
|
29
31
|
spec.add_development_dependency 'database_cleaner'
|
30
32
|
spec.add_development_dependency 'dotenv'
|
@@ -77,7 +77,7 @@ module ActiveModelSerializers
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def self.warn_about_collection_serializer
|
80
|
-
msg = "You are using an ordinary AMS CollectionSerializer with the json_api_pg adapter, which probably means Rails is pointlessly loading all your ActiveRecord instances *and* running the
|
80
|
+
msg = "You are using an ordinary AMS CollectionSerializer with the json_api_pg adapter, which probably means Rails is pointlessly loading all your ActiveRecord instances *and* running the JSON-building query in Postgres."
|
81
81
|
if Object.const_defined? 'Rails'
|
82
82
|
Rails.logger.warn msg
|
83
83
|
else
|
@@ -105,8 +105,8 @@ end
|
|
105
105
|
# i.e. how you got here, not how you'd leave:
|
106
106
|
# "Reflection" seems to be the internal ActiveRecord lingo
|
107
107
|
# for a belongs_to or has_many relationship.
|
108
|
-
# (The public documentation calls these "associations"
|
109
|
-
# I think
|
108
|
+
# (The public documentation calls these "associations".
|
109
|
+
# I think older versions of Rails even used that internally,
|
110
110
|
# but nowadays the method names use "reflection".)
|
111
111
|
class JsonThing
|
112
112
|
attr_reader :ar_class, :full_name, :name, :serializer, :serializer_options, :json_key, :json_type, :reflection, :parent, :cte_name, :jbs_name
|
@@ -141,28 +141,10 @@ class JsonThing
|
|
141
141
|
JsonThing.new(refl.klass, "#{full_name}.#{reflection_name}", nil, serializer_options, refl, self)
|
142
142
|
end
|
143
143
|
|
144
|
-
# Gets the attributes (i.e. scalar fields) on the AR class
|
145
|
-
# as a Set of symbols.
|
146
|
-
# TODO: tests
|
147
|
-
def declared_attributes
|
148
|
-
@declared_attributes ||= Set.new(@ar_class.attribute_types.keys.map(&:to_sym))
|
149
|
-
end
|
150
|
-
|
151
144
|
def enum?(field)
|
152
145
|
@ar_class.attribute_types[field.to_s].is_a? ActiveRecord::Enum::EnumType
|
153
146
|
end
|
154
147
|
|
155
|
-
# Gets the reflections (aka associations) of the AR class
|
156
|
-
# as a Hash from symbol to a subclass of ActiveRecord::Reflection.
|
157
|
-
# TODO: tests
|
158
|
-
def declared_reflections
|
159
|
-
@declared_reflections ||= Hash[
|
160
|
-
@ar_class.reflections.map{|k, v|
|
161
|
-
[k.to_sym, v]
|
162
|
-
}
|
163
|
-
]
|
164
|
-
end
|
165
|
-
|
166
148
|
# Checks for alias_attribute and gets to the real attribute name.
|
167
149
|
def unaliased(field_name)
|
168
150
|
ret = field_name
|
@@ -191,6 +173,20 @@ class JsonThing
|
|
191
173
|
(@sql_methods[field] ||= _sql_method(field))[0]
|
192
174
|
end
|
193
175
|
|
176
|
+
# Returns the primary key column as a string,
|
177
|
+
# but if there is a "#{primary_key}__sql" method,
|
178
|
+
# then call that and return it instead.
|
179
|
+
# We use this for the id reported in the jsonapi output,
|
180
|
+
# but not for foreign key relationships.
|
181
|
+
def primary_key_attr
|
182
|
+
pk = primary_key
|
183
|
+
if has_sql_method?(pk)
|
184
|
+
sql_method(pk)
|
185
|
+
else
|
186
|
+
pk
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
194
190
|
private
|
195
191
|
|
196
192
|
# This needs to be globally unique within the SQL query,
|
@@ -210,7 +206,7 @@ class JsonThing
|
|
210
206
|
end
|
211
207
|
end
|
212
208
|
|
213
|
-
# Each thing has
|
209
|
+
# Each thing has both a `cte_foo` CTE and a `jbs_foo` CTE.
|
214
210
|
# (jbs stands for "JSONBs" and is meant to take 3 chars like `cte`.)
|
215
211
|
# The former is just the relevant records,
|
216
212
|
# and the second builds the JSON object for each record.
|
@@ -376,6 +372,15 @@ end
|
|
376
372
|
class JsonApiPgSql
|
377
373
|
attr_reader :base_serializer, :base_relation
|
378
374
|
|
375
|
+
def self.json_column_type
|
376
|
+
# These classes may not exist, depending on the Rails version:
|
377
|
+
@@json_column_type = if Rails::VERSION::STRING >= '5.2'
|
378
|
+
'ActiveRecord::Type::Json'
|
379
|
+
else
|
380
|
+
'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json'
|
381
|
+
end.constantize
|
382
|
+
end
|
383
|
+
|
379
384
|
def initialize(base_serializer, base_relation, instance_options, options)
|
380
385
|
@base_relation = base_relation
|
381
386
|
@instance_options = instance_options
|
@@ -452,14 +457,58 @@ class JsonApiPgSql
|
|
452
457
|
elsif resource.has_sql_method?(field)
|
453
458
|
resource.sql_method(field)
|
454
459
|
else
|
455
|
-
|
460
|
+
field = resource.unaliased(field)
|
461
|
+
# Standard AMS dasherizes json/jsonb/hstore columns,
|
462
|
+
# so we have to do the same:
|
463
|
+
if ActiveModelSerializers.config.key_transform == :dash
|
464
|
+
cl = resource.ar_class.attribute_types[field.to_s]
|
465
|
+
if column_is_jsonb? cl
|
466
|
+
%Q{jsonb_dasherize("#{resource.table_name}"."#{field}")}
|
467
|
+
elsif column_is_jsonb_array? cl
|
468
|
+
# TODO: Could be faster:
|
469
|
+
# If we made the jsonb_dasherize function smarter so it could handle jsonb[],
|
470
|
+
# we wouldn't have to build a json object from the array then cast to jsonb[].
|
471
|
+
%Q{jsonb_dasherize(array_to_json("#{resource.table_name}"."#{field}")::jsonb)}
|
472
|
+
elsif column_is_castable_to_jsonb? cl
|
473
|
+
# Fortunately we can cast hstore to jsonb,
|
474
|
+
# which gives us a solution that works whether or not the hstore extension is installed.
|
475
|
+
# Defining an hstore_dasherize function would work only if the extension were present.
|
476
|
+
%Q{jsonb_dasherize("#{resource.table_name}"."#{field}"::jsonb)}
|
477
|
+
elsif column_is_castable_to_jsonb_array? cl
|
478
|
+
# TODO: Could be faster (see above):
|
479
|
+
%Q{jsonb_dasherize(array_to_json("#{resource.table_name}"."#{field}"::jsonb[])::jsonb)}
|
480
|
+
else
|
481
|
+
%Q{"#{resource.table_name}"."#{field}"}
|
482
|
+
end
|
483
|
+
else
|
484
|
+
%Q{"#{resource.table_name}"."#{field}"}
|
485
|
+
end
|
456
486
|
end
|
457
487
|
end
|
458
488
|
|
489
|
+
def column_is_jsonb?(column_class)
|
490
|
+
column_class.is_a? ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
|
491
|
+
end
|
492
|
+
|
493
|
+
def column_is_jsonb_array?(column_class)
|
494
|
+
column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) and
|
495
|
+
column_class.subtype.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb)
|
496
|
+
end
|
497
|
+
|
498
|
+
def column_is_castable_to_jsonb?(column_class)
|
499
|
+
column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Hstore) or
|
500
|
+
column_class.is_a?(self.class.json_column_type)
|
501
|
+
end
|
502
|
+
|
503
|
+
def column_is_castable_to_jsonb_array?(column_class)
|
504
|
+
column_class.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array) and
|
505
|
+
column_is_castable_to_jsonb?(column_class.subtype)
|
506
|
+
end
|
507
|
+
|
459
508
|
def select_resource_relationship_links(resource, reflection)
|
460
509
|
reflection.links.map {|link_name, link_parts|
|
461
510
|
<<~EOQ
|
462
|
-
'#{link_name}', CONCAT(#{link_parts.join(%Q{, "#{resource.parent.table_name}"."#{resource.parent.
|
511
|
+
'#{link_name}', CONCAT(#{link_parts.join(%Q{, "#{resource.parent.table_name}"."#{resource.parent.primary_key_attr}", })})
|
463
512
|
EOQ
|
464
513
|
}.join(",\n")
|
465
514
|
end
|
@@ -510,12 +559,8 @@ class JsonApiPgSql
|
|
510
559
|
# or from the class's default scope.
|
511
560
|
# TODO: preserve the whole custom relation, not just ordering
|
512
561
|
p = refl.ar_class.new
|
513
|
-
ordering =
|
514
|
-
|
515
|
-
# TODO: Calling `orders` prints deprecation warnings, so find another way:
|
516
|
-
ordering = p.send(refl.name).orders
|
517
|
-
ordering = child_resource.ar_class.default_scoped.orders if ordering.empty?
|
518
|
-
end
|
562
|
+
ordering = p.send(refl.name).arel.orders
|
563
|
+
ordering = child_resource.ar_class.default_scoped.arel.orders if ordering.empty?
|
519
564
|
ordering = ordering.map{|o|
|
520
565
|
case o
|
521
566
|
# TODO: The gsub is pretty awful....
|
@@ -530,7 +575,7 @@ class JsonApiPgSql
|
|
530
575
|
ordering = "ORDER BY #{ordering}" if ordering
|
531
576
|
<<~EOQ
|
532
577
|
LEFT OUTER JOIN LATERAL (
|
533
|
-
SELECT coalesce(jsonb_agg(jsonb_build_object('id', rel."#{child_resource.
|
578
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object('id', rel."#{child_resource.primary_key_attr}"::text,
|
534
579
|
'type', '#{child_resource.json_type}') #{ordering}), '[]') AS j
|
535
580
|
FROM "#{child_resource.table_name}" rel
|
536
581
|
WHERE rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
|
@@ -543,7 +588,7 @@ class JsonApiPgSql
|
|
543
588
|
when ActiveRecord::Relation
|
544
589
|
rel = refl.reflection_sql
|
545
590
|
sql = rel.select(<<~EOQ).to_sql
|
546
|
-
coalesce(jsonb_agg(jsonb_build_object('id', "#{child_resource.table_name}"."#{child_resource.
|
591
|
+
coalesce(jsonb_agg(jsonb_build_object('id', "#{child_resource.table_name}"."#{child_resource.primary_key_attr}"::text,
|
547
592
|
'type', '#{child_resource.json_type}')), '[]') AS j
|
548
593
|
EOQ
|
549
594
|
<<~EOQ
|
@@ -556,7 +601,7 @@ class JsonApiPgSql
|
|
556
601
|
elsif refl.has_one?
|
557
602
|
<<~EOQ
|
558
603
|
LEFT OUTER JOIN LATERAL (
|
559
|
-
SELECT jsonb_build_object('id', rel."#{child_resource.
|
604
|
+
SELECT jsonb_build_object('id', rel."#{child_resource.primary_key_attr}"::text,
|
560
605
|
'type', '#{child_resource.json_type}') AS j
|
561
606
|
FROM "#{child_resource.table_name}" rel
|
562
607
|
WHERE rel."#{child_resource.foreign_key}" = "#{resource.table_name}"."#{resource.primary_key}"
|
@@ -593,8 +638,6 @@ class JsonApiPgSql
|
|
593
638
|
|
594
639
|
# See note in _jbs_name method for why we split each thing into two CTEs.
|
595
640
|
def include_cte(resource)
|
596
|
-
# Sometimes options[:fields] has plural keys and sometimes singular,
|
597
|
-
# so try both:
|
598
641
|
parent = resource.parent
|
599
642
|
<<~EOQ
|
600
643
|
SELECT DISTINCT ON ("#{resource.table_name}"."#{resource.primary_key}")
|
@@ -674,7 +717,7 @@ class JsonApiPgSql
|
|
674
717
|
def select_resource(resource)
|
675
718
|
fields = fields_for(resource)
|
676
719
|
<<~EOQ
|
677
|
-
jsonb_build_object('id', "#{resource.table_name}"."#{resource.
|
720
|
+
jsonb_build_object('id', "#{resource.table_name}"."#{resource.primary_key_attr}"::text,
|
678
721
|
'type', '#{resource.json_type}',
|
679
722
|
'attributes', #{select_resource_attributes(resource)}
|
680
723
|
#{maybe_select_resource_relationships(resource)})
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module ActiveModelSerializersPg
|
4
|
+
module Generators
|
5
|
+
class ActiveModelSerializersPgGenerator < Rails::Generators::NamedBase
|
6
|
+
Rails::Generators::ResourceHelpers
|
7
|
+
|
8
|
+
# The ORM generator assumes you're passing a name argument,
|
9
|
+
# but we don't need one, so we give it a default value:
|
10
|
+
argument :name, type: :string, default: "ignored"
|
11
|
+
source_root File.expand_path("../templates", __FILE__)
|
12
|
+
|
13
|
+
namespace :active_model_serializers_pg
|
14
|
+
hook_for :orm, required: true, name: "ignored"
|
15
|
+
|
16
|
+
desc "Creates an active_model_serialiers_pg database migration"
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Generators
|
5
|
+
class ActiveModelSerializersPgGenerator < ActiveRecord::Generators::Base
|
6
|
+
argument :name, type: :string, default: "ignored"
|
7
|
+
source_root File.expand_path("../templates", __FILE__)
|
8
|
+
|
9
|
+
def write_migration
|
10
|
+
migration_template "migration.rb", "#{migration_path}/ams_pg_create_dasherize_functions.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def read_sql(funcname)
|
16
|
+
File.read(File.join(File.expand_path('../templates', __FILE__), "#{funcname}.sql"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def migration_exists?(table_name)
|
20
|
+
Dir.glob("#{File.join destination_root, migration_path}/[0-9]*_*.rb").grep(/\d+_ams_pg_create_dasherize_functions.rb$/).first
|
21
|
+
end
|
22
|
+
|
23
|
+
def migration_path
|
24
|
+
if Rails.version >= '5.0.3'
|
25
|
+
db_migrate_path
|
26
|
+
else
|
27
|
+
@migration_path ||= File.join "db", "migrate"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def migration_version
|
32
|
+
if Rails.version.start_with? '5'
|
33
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def jsonb_dasherize
|
38
|
+
read_sql('jsonb_dasherize')
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
CREATE FUNCTION jsonb_dasherize(j jsonb)
|
2
|
+
RETURNS jsonb
|
3
|
+
IMMUTABLE
|
4
|
+
AS
|
5
|
+
$$
|
6
|
+
DECLARE
|
7
|
+
t text;
|
8
|
+
ret jsonb;
|
9
|
+
BEGIN
|
10
|
+
t := jsonb_typeof(j);
|
11
|
+
IF t = 'object' THEN
|
12
|
+
SELECT COALESCE(jsonb_object_agg(replace(lower(regexp_replace(k, '([A-Z])', '_\1', 'g')), '_', '-'), jsonb_dasherize(v)), '{}')
|
13
|
+
INTO ret
|
14
|
+
FROM jsonb_each(j) AS t(k, v);
|
15
|
+
RETURN ret;
|
16
|
+
ELSIF t = 'array' THEN
|
17
|
+
SELECT COALESCE(jsonb_agg(jsonb_dasherize(elem)), '[]')
|
18
|
+
INTO ret
|
19
|
+
FROM jsonb_array_elements(j) AS t(elem);
|
20
|
+
RETURN ret;
|
21
|
+
ELSIF t IS NULL THEN
|
22
|
+
-- This should never happen internally
|
23
|
+
-- (thankfully, lest jsonb_set return NULL and destroy everything),
|
24
|
+
-- but only from a passed-in NULL.
|
25
|
+
RETURN NULL;
|
26
|
+
ELSE
|
27
|
+
-- string/number/null:
|
28
|
+
RETURN j;
|
29
|
+
END IF;
|
30
|
+
END;
|
31
|
+
$$
|
32
|
+
LANGUAGE plpgsql;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'generators/active_model_serializers_pg/active_model_serializers_pg_generator'
|
3
|
+
|
4
|
+
describe ActiveModelSerializersPg::Generators::ActiveModelSerializersPgGenerator, type: :generator do
|
5
|
+
destination File.expand_path "../../../tmp", __FILE__
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
prepare_destination
|
9
|
+
end
|
10
|
+
|
11
|
+
it "creates the migration file" do
|
12
|
+
run_generator
|
13
|
+
expect(destination_root).to have_structure {
|
14
|
+
directory "db" do
|
15
|
+
directory "migrate" do
|
16
|
+
migration "ams_pg_create_dasherize_functions" do
|
17
|
+
contains "class AmsPgCreateDasherizeFunctions"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
data/spec/serializer_spec.rb
CHANGED
@@ -209,6 +209,65 @@ describe 'ArraySerializer' do
|
|
209
209
|
end
|
210
210
|
end
|
211
211
|
|
212
|
+
context 'with dasherized json columns' do
|
213
|
+
let(:relation) { Person.all }
|
214
|
+
let(:controller) { PeopleController.new }
|
215
|
+
let(:person) {
|
216
|
+
Person.create first_name: 'Test',
|
217
|
+
last_name: 'User',
|
218
|
+
options: { 'foo_foo': 'baz', barBar: [{ 'jar_jar': 'binks' }] },
|
219
|
+
prefs: { 'foo_foo': 'baz', barBar: [{ 'jar_jar': 'binks' }] },
|
220
|
+
settings: { 'foo_foo': 'bar' },
|
221
|
+
selfies: [ { 'photo_resolution': '200x200' } ],
|
222
|
+
portraits: [ { 'photo_resolution': '150x200' } ],
|
223
|
+
landscapes: [ { 'photo_resolution': '200x150' } ]
|
224
|
+
}
|
225
|
+
let(:options) { { each_serializer: PersonWithJsonSerializer } }
|
226
|
+
|
227
|
+
before do
|
228
|
+
@old_key_setting = ActiveModelSerializers.config.key_transform
|
229
|
+
ActiveModelSerializers.config.key_transform = :dash
|
230
|
+
end
|
231
|
+
|
232
|
+
after do
|
233
|
+
ActiveModelSerializers.config.key_transform = @old_key_setting
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'generates the proper json output' do
|
237
|
+
json_expected = {
|
238
|
+
data: [
|
239
|
+
{
|
240
|
+
id: person.id.to_s,
|
241
|
+
type: 'people',
|
242
|
+
attributes: {
|
243
|
+
prefs: {
|
244
|
+
'bar-bar': [{ 'jar-jar' => 'binks'}],
|
245
|
+
'foo-foo' => 'baz',
|
246
|
+
},
|
247
|
+
options: {
|
248
|
+
'bar-bar': [{ 'jar-jar' => 'binks'}],
|
249
|
+
'foo-foo' => 'baz',
|
250
|
+
},
|
251
|
+
selfies: [
|
252
|
+
{ 'photo-resolution' => '200x200' },
|
253
|
+
],
|
254
|
+
settings: {
|
255
|
+
'foo-foo' => 'bar'
|
256
|
+
},
|
257
|
+
portraits: [
|
258
|
+
{ 'photo-resolution' => '150x200' },
|
259
|
+
],
|
260
|
+
landscapes: [
|
261
|
+
{ 'photo-resolution' => '200x150' },
|
262
|
+
],
|
263
|
+
},
|
264
|
+
}
|
265
|
+
]
|
266
|
+
}.to_json
|
267
|
+
expect(json_data).to eq json_expected
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
212
271
|
context 'with aliased association' do
|
213
272
|
let(:relation) { Tag.first }
|
214
273
|
let(:controller) { TagsController.new }
|
data/spec/spec_helper.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
require '
|
2
|
-
require 'action_controller'
|
1
|
+
require 'rails/all'
|
3
2
|
require 'rspec'
|
3
|
+
require 'rspec/rails'
|
4
4
|
require 'bourne'
|
5
5
|
require 'database_cleaner'
|
6
6
|
require 'active_model_serializers'
|
7
7
|
require 'action_controller/serialization'
|
8
8
|
require 'active_model_serializers_pg'
|
9
|
+
require 'generator_spec'
|
9
10
|
if ENV['TEST_UNPATCHED_AMS']
|
10
11
|
ActiveModelSerializers.config.adapter = :json_api
|
11
12
|
else
|
@@ -24,6 +25,17 @@ end
|
|
24
25
|
require 'dotenv'
|
25
26
|
Dotenv.load
|
26
27
|
|
28
|
+
# Need this or Rails.application is nil just below:
|
29
|
+
module TestApp
|
30
|
+
class Application < ::Rails::Application
|
31
|
+
config.root = File.dirname(__FILE__)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Need this line or `hook_for` in our generators is ignored,
|
36
|
+
# so our migration generator doesn't run:
|
37
|
+
Rails.application.load_generators
|
38
|
+
|
27
39
|
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
|
28
40
|
|
29
41
|
class TestController < ActionController::Base
|
@@ -56,6 +68,16 @@ class PersonSerializer < ActiveModel::Serializer
|
|
56
68
|
end
|
57
69
|
end
|
58
70
|
|
71
|
+
class PersonWithJsonSerializer < ActiveModel::Serializer
|
72
|
+
attributes :id,
|
73
|
+
:options, # json
|
74
|
+
:prefs, # jsonb
|
75
|
+
:settings, # hstore
|
76
|
+
:selfies, # json[]
|
77
|
+
:portraits, # jsonb[]
|
78
|
+
:landscapes # hstore[]
|
79
|
+
end
|
80
|
+
|
59
81
|
class Note < ActiveRecord::Base
|
60
82
|
has_many :tags
|
61
83
|
has_many :sorted_tags
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_model_serializers_pg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul A. Jungwirth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: active_model_serializers
|
@@ -28,14 +28,14 @@ dependencies:
|
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '5.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '5.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -94,6 +94,34 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: generator_spec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
126
|
name: bourne
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,10 +196,16 @@ files:
|
|
168
196
|
- gemfiles/Gemfile.activerecord-5.0.x
|
169
197
|
- gemfiles/Gemfile.activerecord-5.1.x
|
170
198
|
- gemfiles/Gemfile.activerecord-5.2.x
|
199
|
+
- gemfiles/Gemfile.activerecord-6.0.x
|
171
200
|
- lib/active_model_serializers/adapter/json_api_pg.rb
|
172
201
|
- lib/active_model_serializers_pg.rb
|
173
202
|
- lib/active_model_serializers_pg/collection_serializer.rb
|
174
203
|
- lib/active_model_serializers_pg/version.rb
|
204
|
+
- lib/generators/active_model_serializers_pg/active_model_serializers_pg_generator.rb
|
205
|
+
- lib/generators/active_record/active_model_serializers_pg_generator.rb
|
206
|
+
- lib/generators/active_record/templates/jsonb_dasherize.sql
|
207
|
+
- lib/generators/active_record/templates/migration.rb
|
208
|
+
- spec/generators/main_generator_spec.rb
|
175
209
|
- spec/serializer_spec.rb
|
176
210
|
- spec/spec_helper.rb
|
177
211
|
homepage: https://github.com/pjungwir/active_model_serializers_pg
|
@@ -193,10 +227,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
227
|
- !ruby/object:Gem::Version
|
194
228
|
version: '0'
|
195
229
|
requirements: []
|
196
|
-
rubygems_version: 3.0.
|
230
|
+
rubygems_version: 3.0.3
|
197
231
|
signing_key:
|
198
232
|
specification_version: 4
|
199
233
|
summary: Harness the power of PostgreSQL when crafting JSON reponses
|
200
234
|
test_files:
|
235
|
+
- spec/generators/main_generator_spec.rb
|
201
236
|
- spec/serializer_spec.rb
|
202
237
|
- spec/spec_helper.rb
|