Graphiti

repository·main·Indexed 22 days ago

https://github.com/graphiti-api/graphiti

A resource-oriented framework for Ruby that provides a JSON:API-compliant interface on top of models like ActiveRecord. Graphiti abstracts API concerns including serialization, filtering, sorting, pagination, and relationship sideloading by allowing developers to define Resources via ApplicationResource.

Tokens
6.8K
Snippets
22
Records
29
Agent score
77%

What's inside graphiti

  1. What is Graphiti?

    main

    Graphiti is a resource-oriented framework designed to sit on top of your models (typically ActiveRecord) to expose them via a JSON:API-compliant interface. Instead of writing complex controllers and serializers, you define Resources.

    Graphiti abstracts the following API concerns:

    • Serialization: Converting models to JSON:API format.
    • Filtering: Allowing clients to query specific subsets of data.
    • Sorting: Enabling clients to order results.
    • Pagination: Managing large datasets.
    • Sideloading Relationships: Efficiently including related data in responses.
  2. Define a Resource with ApplicationResource

    main

    To expose a model via Graphiti, create a class that inherits from ApplicationResource. Within this class, you define attributes, relationships, filters, and sorts.

    Key features include:

    • attribute: Defines a field, its type, and whether it is writable or has specific capabilities like filterable or sortable.
    • has_many, has_one, many_to_many, and polymorphic_has_many: Define relationships between resources.
    • params: A block used within a relationship to inject specific parameters (e.g., applying a filter to a related resource).
    • filter: Custom logic to handle complex queries.
    • sort: Custom logic to handle complex ordering.
    class EmployeeResource < ApplicationResource
      attribute :first_name, :string
      attribute :last_name, :string
      attribute :age, :integer
      attribute :created_at, :datetime, writable: false
      attribute :updated_at, :datetime, writable: false
      attribute :title, :string, only: [:filterable, :sortable]
    
      has_many :positions
      has_many :tasks
      many_to_many :teams
      polymorphic_has_many :notes, as: :notable
      
      has_one :current_position, resource: PositionResource do
        params do |hash|
          hash[:filter][:current] = true
        end
      end
    
      filter :title, only: [:eq] do
        eq do |scope, value|
          scope.joins(:current_position).merge(Position.where(title: value))
        end
      end
    
      sort :title do |scope, value|
        scope.joins(:current_position).merge(Position.order(title: value))
      end
    end
  3. Implement a Controller using Graphiti Resources

    main

    Graphiti resources are designed to work with minimal boilerplate in your controllers. You use the resource class to fetch, build, or find records based on the incoming params.

    Common patterns:

    • Index: Use Resource.all(params) to retrieve a collection.
    • Show: Use Resource.find(params) to retrieve a single record.
    • Create: Use Resource.build(params) to instantiate a new record.
    • Update: Use Resource.find(params) followed by update_attributes.
    • Destroy: Use Resource.find(params) followed by destroy.

    When rendering, use render jsonapi: resource for successful operations and render jsonapi_errors: resource for handling validation errors.

    class EmployeesController < ApplicationController
      def index
        employees = EmployeeResource.all(params)
        respond_with(employees)
      end
    
      def show
        employee = EmployeeResource.find(params)
        respond_with(employee)
      end
    
      def create
        employee = EmployeeResource.build(params)
    
        if employee.save
          render jsonapi: employee, status: 201
        else
          render jsonapi_errors: employee
        end
      end
    
      def update
        employee = EmployeeResource.find(params)
    
        if employee.update_attributes
          render jsonapi: employee
        else
          render jsonapi_errors: employee
        end
      end
    
      def destroy
        employee = EmployeeResource.find(params)
    
        if employee.destroy
          render jsonapi: { meta: {} }, status: 200
        else
          render jsonapi_errors: employee
        end
      end
    end
  4. How Graphiti validates incoming request payloads

    main

    Graphiti uses the Graphiti::RequestValidators::Validator to process incoming request payloads. Even read requests currently pass through this validator.

    When a payload is validated, the validator performs the following steps:

    1. Deserialization: It uses Graphiti::Deserializer to parse the parameters.
    2. Type Checking: It ensures the data.type is present. If the resource is polymorphic, it resolves the correct resource type based on the payload's meta[:type].
    3. Attribute Typecasting: It iterates through attributes and attempts to typecast them using the resource's typecast method. It catches and records errors for UnknownAttribute, InvalidAttributeAccess (unwritable), and TypecastFailed.
    4. Relationship Processing: It validates that requested relationships are defined as sideload on the resource and checks if they are writable before proceeding with nested processing.

    Key Validation Rules:

    • ID Validation: The :id attribute is only validated if the action is :create. For other actions, it is used strictly for lookup.
    • Error Reporting: Errors are collected into a Graphiti::Util::SimpleErrors object, using fully qualified keys (e.g., data.relationships.name.attributes.key) to pinpoint the location of the error in the payload.
  5. Configure Graphiti Debugging via Environment Variables

    main

    You can enable debugging for Graphiti and its models using environment variables. This is useful for troubleshooting API requests and model behavior.

    • GRAPHITI_DEBUG: Enables general Graphiti debugging. Defaults to true.
    • GRAPHITI_DEBUG_MODELS: Enables debugging specifically for models. Defaults to false.
    export GRAPHITI_DEBUG=true
    export GRAPHITI_DEBUG_MODELS=true
  6. Define statistical calculations using allow_stat in the Resource DSL

    main

    You can define statistical calculations for a resource using the allow_stat method within your Resource DSL. This allows you to specify both predefined convenience metrics and custom arbitrary calculations.

    Using Predefined Metrics

    You can use shorthand symbols to include standard metrics like count, sum, average, maximum, and minimum.

    Defining Custom Calculations

    For metrics not covered by convenience methods, you can define a block that will be evaluated in the Stats::DSL context. These custom calculations receive a scope and an optional attr argument.

    Examples

    Using shorthand for standard metrics:

    allow_stat :rating do
      count!
      average!
    end

    Defining a custom calculation (e.g., standard deviation):

    allow_stat total: [:count] do
      standard_deviation { |scope, attr| ... }
    end
    allow_stat :rating do
      count!
      average!
    end
  7. Configure Graphiti settings

    main

    Use Graphiti.configure to set global configuration options. This is typically used during application initialization to adjust behavior such as how missing sideloads are handled.

    Common configuration options include raise_on_missing_sideload.

    Graphiti.configure do |c|
      c.raise_on_missing_sideload = false
    end
  8. Initialize Graphiti with sideloads

    main

    If you are not using Rails autoloading, or if you are eager loading your application, you must manually call Graphiti.setup! after all your Resource classes have been loaded. This ensures that sideloads are correctly applied to the Resource's serializers, even if the related Resource classes were not yet loaded when the initial configuration occurred.

    # Load your resource classes first
    # ...
    
    Graphiti.setup!
  9. Configure Graphiti Sideloading and Concurrency

    main

    Graphiti allows you to control how relationships (sideloads) are fetched and whether they are processed concurrently.

    Concurrency and Database Connection Pool

    When concurrency is set to true, Graphiti uses a global executor to fetch sideloads asynchronously. This requires careful management of your database connection pool.

    Important: Your database connection pool size (configured in database.yml) must be large enough to accommodate both your foreground threads (web server/job workers) and the background threads used by Graphiti.

    To avoid connection exhaustion, your pool size should be at least: thread_count + concurrency_max_threads + 1

    Example: If your web server has 3 threads and concurrency_max_threads is 4, your pool size should be at least 8.*

    Graphiti.configure do |config|
      config.raise_on_missing_sideload = true # Defaults to true
      config.concurrency = true             # Defaults to false
      config.concurrency_max_threads = 4     # Defaults to 4
    end
  10. Configure the schema generation context

    main

    When generating endpoints, Graphiti uses a context provider to determine metadata for specific paths and actions. You can customize this behavior by providing a proc to Graphiti.config.context_for_endpoint.

    The proc is called with (path, action), where path is the endpoint's full path and action is the specific action (e.g., :read, :create). This allows you to inject custom sideload_allowlist configurations into the generated schema for specific endpoints.

    Graphiti.configure do |config|
      config.context_for_endpoint = ->(path, action) {
        # Return an object that responds to :sideload_allowlist
        # e.g., OpenStruct.new(sideload_allowlist: { relationship_name: [:action1, :action2] })
      }
    end
  11. Enable Cache Rendering

    main

    You can enable cache_rendering to improve performance. However, if you enable this, you must provide a cache store that responds to .fetch (for example, Rails.cache).

    If cache_rendering is set to true but no valid cache is configured, Graphiti will raise an error.

    # In a Rails app
    Graphiti.configure do |config|
      config.cache_rendering = true
      Graphiti.cache = Rails.cache
    end
  12. Configure Schema Path

    main

    Graphiti requires a schema_path to be defined to save your schema. If not explicitly set, accessing the schema will raise an error.

    In a Rails environment, Graphiti looks for a .graphiticfg.yml file in the Rails root. If present, it uses the namespace key from that YAML file to construct the path: public#{namespace}/schema.json. Otherwise, it defaults to public/schema.json.

    Graphiti.configure do |config|
      config.schema_path = Rails.root.join('public/my_schema.json')
    end