Last active
February 29, 2024 19:17
-
-
Save arturopuente/0dbfcf6b309b9ca8d76d8e8ce98e5712 to your computer and use it in GitHub Desktop.
JSON-API Preloadable concern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Preloadable | |
extend ActiveSupport::Concern | |
# A very common issue with APIs is that we want to preload relationships to | |
# avoid N+1 queries, something like this: /api/projects?include=users | |
# and this works well for attributes like the user name or their email, but | |
# what happens when you try to access their avatar? Yup. N+1. | |
# JSON:API, being a more generic standard, doesn't really know or give you | |
# tools to deal with Rails relationships and preloads easily. While this makes | |
# sense from their (and the library authors) perspective, we can do better. | |
# What about self.records, you ask? Ah, good question, yes, self.records | |
# allows us to specify the relationships we want, but by default it works on | |
# _every_ request, we want to have the ability to customize what each resource | |
# included relationship looks like for every request. | |
# The goal of this concern is to allow us to ask the resource for a specific | |
# set of plain or nested relationships to be included via a URL param. | |
# We'll work with two elements, a k/v object named PRELOAD_ATTRIBUTES that | |
# holds the relationships we are able of preloading, and a list of default | |
# preloads named DEFAULT_PRELOAD_ATTRIBUTES (ideally this should be empty/not | |
# exist for new resources, but it's not because we were overriding the | |
# self.records methods on previous resources). It looks like this: | |
# USER_PRELOAD_ATTRIBUTES = [ | |
# { avatar_attachment: :blob } | |
# ] | |
# | |
# PRELOAD_ATTRIBUTES = { | |
# centers: [:centers], | |
# institutions: [:institutions], | |
# organizations: [:centers, :institutions], | |
# memberships: [ | |
# :project_memberships, | |
# :project_external_memberships, | |
# :external_user_members, | |
# members: USER_PRELOAD_ATTRIBUTES | |
# ], | |
# users: [ | |
# :external_user_members, | |
# members: USER_PRELOAD_ATTRIBUTES | |
# ], | |
# } | |
# | |
# DEFAULT_PRELOAD_ATTRIBUTES = [ | |
# :organizations, | |
# :memberships, | |
# ] | |
# Notice a couple of important things here: | |
# The value of the preload k/v pairs must always be an array. | |
# You can define auxiliary lists of properties/objects to deal with nested | |
# relationships in multiple places, e.g.: USER_PRELOAD_ATTRIBUTES. | |
# This allows you to nest attributes as much as neeed to avoid N+1 on | |
# associated resources, e.g.: retrieve the ActiveStorage blob of the avatar | |
# attachment of a list of users | |
# How does this work on the request level? Instead of using the include URL | |
# param we'll use the preload param: | |
# /api/projects?include=users,centers => /api/projects?preload=users,centers | |
# Our api/application_controller will parse it and include it in the context | |
# attribute, and this makes it available in all the resources methods that | |
# receive an options hash from the library (self.records, custom_links). | |
# Don't forget you still have to declare the relationships you want on the k/v | |
# hash on the resource! | |
class_methods do | |
def records(options = {}) | |
default_attrs = :DEFAULT_PRELOAD_ATTRIBUTES | |
attrs = const_defined?(default_attrs) ? const_get(default_attrs) : {} | |
if options[:context][:preload].present? | |
attrs = options[:context][:preload].split(",").map(&:to_sym) | |
end | |
return super if attrs.empty? | |
preload_attrs = const_get(:PRELOAD_ATTRIBUTES) | |
includable = preload_attrs.keys.flat_map do |key| | |
preload_attrs[key] if attrs.include?(key) | |
end.compact | |
super.includes(includable) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment