# File lib/active_model_serializers/adapter/json_api_pg.rb, line 181 def primary_key_attr pk = primary_key if has_sql_method?(pk) sql_method(pk) else pk end end
class JsonThing
Each JsonThing
is a struct collecting all the stuff we need to know about a model you want in the JSONAPI output.
It has the ActiveRecord
class, the name of the thing, and how to reach it from its parent.
The full_name
param should be a dotted path like you'd pass to the `includes` option of ActiveModelSerializers
, except it should also start with the name of the top-level entity.
The reflection should be from the perspective of the parent, i.e. how you got here, not how you'd leave: “Reflection” seems to be the internal ActiveRecord
lingo for a belongs_to or has_many relationship. (The public documentation calls these “associations”. I think older versions of Rails even used that internally, but nowadays the method names use “reflection”.)
Attributes
Public Class Methods
TODO: tests
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 158 def self.json_key(k) # TODO: technically the serializer could have an option overriding the default: case ActiveModelSerializers.config.key_transform when :dash k.to_s.gsub('_', '-') else k.to_s end end
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 116 def initialize(ar_class, full_name, serializer=nil, serializer_options={}, reflection=nil, parent_json_thing=nil) @ar_class = ar_class @full_name = full_name @name = full_name.split('.').last @serializer = serializer || ActiveModel::Serializer.serializer_for(ar_class.new, {}) @serializer_options = serializer_options # json_key and json_type might be the same thing, but not always. # json_key is the name of the belongs_to/has_many association, # and json_type is the name of the thing's class. @json_key = JsonThing.json_key(name) @json_type = JsonThing.json_key(ar_class.name.underscore.pluralize) @reflection = reflection @parent = parent_json_thing @cte_name = _cte_name @jbs_name = _jbs_name @sql_methods = {} end
Public Instance Methods
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 144 def enum?(field) @ar_class.attribute_types[field.to_s].is_a? ActiveRecord::Enum::EnumType end
Constructs another JsonThing
with this one as the parent, via `reflection_name`. TODO: tests
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 139 def from_reflection(reflection_name) refl = JsonApiReflection.new(reflection_name, ar_class, serializer) JsonThing.new(refl.klass, "#{full_name}.#{reflection_name}", nil, serializer_options, refl, self) end
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 168 def has_sql_method?(field) sql_method(field).present? end
Returns the primary key column as a string, but if there is a “#{primary_key}__sql” method, then call that and return it instead. We use this for the id reported in the jsonapi output, but not for foreign key relationships.
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 172 def sql_method(field) (@sql_methods[field] ||= _sql_method(field))[0] end
Checks for alias_attribute and gets to the real attribute name.
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 149 def unaliased(field_name) ret = field_name while field_name = @ar_class.attribute_aliases[field_name.to_s] ret = field_name end ret end
Private Instance Methods
This needs to be globally unique within the SQL query, even if the same model class appears in different places (e.g. a Book has_many :authors and has_many :reviewers, but those are both of class User). So we use the full_name
to prevent conflicts. But since Postgres table names have limited length, we also hash that name to guarantee something short (like how Rails migrations generate foreign key names). TODO: tests
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 201 def _cte_name if parent.nil? 't' else "cte_#{_cte_name_human_part}_#{Digest::SHA256.hexdigest(full_name).first(10)}" end end
Gets a more informative name for the CTE based on the include key. This makes reading the big SQL query easier, especially for debugging. Note that Postgres's max identifier length is 63 chars (unless you compile yourself), and we are spending 4+4+11=19 chars elsewhere on `rel_cte_XXX_1234567890`. So this method can't return more than 63-19=44 chars.
Since we quote the CTE names, we don't actually need to remove dots in the name!
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 233 def _cte_name_human_part @cte_name_human_part ||= full_name[0, 44] end
Each thing has both a `cte_foo` CTE and a `jbs_foo` CTE. (jbs stands for “JSONBs” and is meant to take 3 chars like `cte`.) The former is just the relevant records, and the second builds the JSON object for each record. We need to split things into phases like this because of the JSON:API `relationships` item, which can contain references in *both directions*. In that case Postgres will object to our circular dependency. But with two phases, every jbs_* CTE only depends on cte_* CTEs.
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 218 def _jbs_name if parent.nil? 't' else "jbs_#{_cte_name_human_part}_#{Digest::SHA256.hexdigest(full_name).first(10)}" end end
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 237 def _sql_method(field) m = "#{field}__sql".to_sym if ar_class.respond_to?(m) # We return an array so our caller can cache a negative result too: [ar_class.send(m)] elsif serializer.instance_methods.include? m ser = serializer.new(ar_class.new, serializer_options) [ser.send(m)] else [nil] end end