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

ar_class[R]
cte_name[R]
full_name[R]
jbs_name[R]
json_key[R]
json_type[R]
name[R]
parent[R]
reflection[R]
serializer[R]
serializer_options[R]

Public Class Methods

json_key(k) click to toggle source

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
new(ar_class, full_name, serializer=nil, serializer_options={}, reflection=nil, parent_json_thing=nil) click to toggle source
# 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

enum?(field) click to toggle source
# 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
from_reflection(reflection_name) click to toggle source

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
has_sql_method?(field) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 168
def has_sql_method?(field)
  sql_method(field).present?
end
primary_key_attr() click to toggle source

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 181
def primary_key_attr
  pk = primary_key
  if has_sql_method?(pk)
    sql_method(pk)
  else
    pk
  end
end
sql_method(field) click to toggle source
# File lib/active_model_serializers/adapter/json_api_pg.rb, line 172
def sql_method(field)
  (@sql_methods[field] ||= _sql_method(field))[0]
end
unaliased(field_name) click to toggle source

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

_cte_name() click to toggle source

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
_cte_name_human_part() click to toggle source

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
_jbs_name() click to toggle source

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
_sql_method(field) click to toggle source
# 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