Skip to content

Commit

Permalink
misc: Split graphql definitions into two schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
rsempe committed Sep 19, 2024
1 parent 3c8e9d4 commit e7db418
Show file tree
Hide file tree
Showing 39 changed files with 69,510 additions and 102 deletions.
19 changes: 16 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative 'config/application'
require 'graphql/rake_task'
require_relative "config/application"
require "graphql/rake_task"

Rails.application.load_tasks

GraphQL::RakeTask.new(schema_name: 'LagoApiSchema')
# TODO(graphql_schema): This schema is deprecated and should be removed.
GraphQL::RakeTask.new(schema_name: "LagoApiSchema")

GraphQL::RakeTask.new(
schema_name: "Schemas::ApiSchema",
idl_outfile: "graphql_schemas/api.graphql",
json_outfile: "graphql_schemas/api.json"
)

GraphQL::RakeTask.new(
schema_name: "Schemas::CustomerPortalSchema",
idl_outfile: "graphql_schemas/customer_portal.graphql",
json_outfile: "graphql_schemas/customer_portal.json"
)
36 changes: 36 additions & 0 deletions app/controllers/graphql/api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Graphql
class ApiController < BaseController
include AuthenticableUser
include OrganizationHeader
include Trackable

def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user:,
current_organization:,
request:,
permissions:
current_user&.memberships&.find_by(organization: current_organization)&.permissions_hash ||
Permission::EMPTY_PERMISSIONS_HASH
}

OpenTelemetry::Trace.current_span.add_attributes({"query" => query, "operation_name" => operation_name})
result = LagoTracer.in_span("Schemas::ApiSchema.execute") do
Schemas::ApiSchema.execute(query, variables:, context:, operation_name:)
end

render(json: result)
rescue JWT::ExpiredSignature
render_graphql_error(code: "expired_jwt_token", status: 401)
rescue => e
raise e unless Rails.env.development?

handle_error_in_development(e)
end
end
end
67 changes: 67 additions & 0 deletions app/controllers/graphql/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Graphql
class BaseController < ApplicationController
before_action :set_context_source

rescue_from JWT::ExpiredSignature do
render_graphql_error(code: "expired_jwt_token", status: 401)
end

# If accessing from outside this domain, nullify the session
# This allows for outside API access while preventing CSRF attacks,
# but you'll have to authenticate your user separately
# protect_from_forgery with: :null_session

def execute
raise NotImplementedError
end

private

# Handle variables in form data, JSON body, or a blank value
def prepare_variables(variables_param)
case variables_param
when String
if variables_param.present?
JSON.parse(variables_param) || {}
else
{}
end
when Hash
variables_param
when ActionController::Parameters
variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{variables_param}"
end
end

def handle_error_in_development(error)
logger.error(error.message)
logger.error(error.backtrace.join("\n"))

render(json: {errors: [{message: error.message, backtrace: error.backtrace}], data: {}}, status: 500)
end

def render_graphql_error(code:, status:, message: nil)
render(
json: {
data: {},
errors: [
{
message: message || code,
extensions: {status:, code:}
}
]
}
)
end

def set_context_source
CurrentContext.source = 'graphql'
end
end
end
30 changes: 30 additions & 0 deletions app/controllers/graphql/customer_portal_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Graphql
class CustomerPortalController < BaseController
include CustomerPortalUser

def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
customer_portal_user:,
request:
}

OpenTelemetry::Trace.current_span.add_attributes({"query" => query, "operation_name" => operation_name})
result = LagoTracer.in_span("Schemas::CustomerPortalSchema.execute") do
Schemas::CustomerPortalSchema.execute(query, variables:, context:, operation_name:)
end

render(json: result)
rescue JWT::ExpiredSignature
render_graphql_error(code: "expired_jwt_token", status: 401)
rescue => e
raise e unless Rails.env.development?

handle_error_in_development(e)
end
end
end
1 change: 1 addition & 0 deletions app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

# TODO(graphql_schema): This controller is deprecated and should be removed.
class GraphqlController < ApplicationController
include AuthenticableUser
include CustomerPortalUser
Expand Down
1 change: 1 addition & 0 deletions app/graphql/lago_api_schema.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

# TODO(graphql_schema): This schema is deprecated and should be removed.
class LagoApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
Expand Down
53 changes: 53 additions & 0 deletions app/graphql/schemas/api_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Schemas
class ApiSchema < GraphQL::Schema
mutation(Types::Api::MutationType)
query(Types::Api::QueryType)

# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
use GraphQL::Dataloader

max_depth 15
max_complexity 350

# GraphQL-Ruby calls this when something goes wrong while running a query:

# Union and Interface Resolution
def self.resolve_type(_abstract_type, _obj, _ctx)
# TODO: Implement this method
# to return the correct GraphQL object type for `obj`
raise(GraphQL::RequiredImplementationMissingError)
end

# Relay-style Object Identification:

# Return a string UUID for `object`
def self.id_from_object(object, type_definition, _query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
object_id = object.to_global_id.to_s
# Remove this redundant prefix to make IDs shorter:
object_id = object_id.sub("gid://#{GlobalID.app}/", '')
encoded_id = Base64.urlsafe_encode64(object_id)
# Remove the "=" padding
encoded_id = encoded_id.sub(/=+/, '')
# Add a type hint
type_hint = type_definition.graphql_name.first
"#{type_hint}_#{encoded_id}"
end

# Given a string UUID, find the object
def self.object_from_id(encoded_id_with_hint, _query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
# Split off the type hint
_type_hint, encoded_id = encoded_id_with_hint.split('_', 2)
# Decode the ID
id = Base64.urlsafe_decode64(encoded_id)
# Rebuild it for Rails then find the object:
full_global_id = "gid://#{GlobalID.app}/#{id}"
GlobalID::Locator.locate(full_global_id)
end

default_max_page_size 25
end
end
53 changes: 53 additions & 0 deletions app/graphql/schemas/customer_portal_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module Schemas
class CustomerPortalSchema < GraphQL::Schema
mutation(Types::CustomerPortal::MutationType)
query(Types::CustomerPortal::QueryType)

# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
use GraphQL::Dataloader

max_depth 15
max_complexity 350

# GraphQL-Ruby calls this when something goes wrong while running a query:

# Union and Interface Resolution
def self.resolve_type(_abstract_type, _obj, _ctx)
# TODO: Implement this method
# to return the correct GraphQL object type for `obj`
raise(GraphQL::RequiredImplementationMissingError)
end

# Relay-style Object Identification:

# Return a string UUID for `object`
def self.id_from_object(object, type_definition, _query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
object_id = object.to_global_id.to_s
# Remove this redundant prefix to make IDs shorter:
object_id = object_id.sub("gid://#{GlobalID.app}/", '')
encoded_id = Base64.urlsafe_encode64(object_id)
# Remove the "=" padding
encoded_id = encoded_id.sub(/=+/, '')
# Add a type hint
type_hint = type_definition.graphql_name.first
"#{type_hint}_#{encoded_id}"
end

# Given a string UUID, find the object
def self.object_from_id(encoded_id_with_hint, _query_ctx)
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
# Split off the type hint
_type_hint, encoded_id = encoded_id_with_hint.split('_', 2)
# Decode the ID
id = Base64.urlsafe_decode64(encoded_id)
# Rebuild it for Rails then find the object:
full_global_id = "gid://#{GlobalID.app}/#{id}"
GlobalID::Locator.locate(full_global_id)
end

default_max_page_size 25
end
end
Loading

0 comments on commit e7db418

Please sign in to comment.