diff --git a/ruby/rails8-sidekiq/app/Gemfile b/ruby/rails8-sidekiq/app/Gemfile index 74bf41a74..784dff2a0 100644 --- a/ruby/rails8-sidekiq/app/Gemfile +++ b/ruby/rails8-sidekiq/app/Gemfile @@ -16,6 +16,7 @@ gem "turbo-rails" gem "stimulus-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" +gem "graphql" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" diff --git a/ruby/rails8-sidekiq/app/Gemfile.lock b/ruby/rails8-sidekiq/app/Gemfile.lock index a7f6d157b..fe9297777 100644 --- a/ruby/rails8-sidekiq/app/Gemfile.lock +++ b/ruby/rails8-sidekiq/app/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /integration specs: - appsignal (4.7.5) + appsignal (4.8.4) logger rack (>= 2.0.0) @@ -116,11 +116,16 @@ GEM erubi (1.13.1) et-orbi (1.2.11) tzinfo + fiber-storage (1.0.1) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + graphql (2.6.1) + base64 + fiber-storage + logger i18n (1.14.7) concurrent-ruby (~> 1.0) importmap-rails (2.0.3) @@ -394,6 +399,7 @@ DEPENDENCIES brakeman capybara debug + graphql importmap-rails jbuilder kamal diff --git a/ruby/rails8-sidekiq/app/app/controllers/examples_controller.rb b/ruby/rails8-sidekiq/app/app/controllers/examples_controller.rb index 402bd742d..a19994996 100644 --- a/ruby/rails8-sidekiq/app/app/controllers/examples_controller.rb +++ b/ruby/rails8-sidekiq/app/app/controllers/examples_controller.rb @@ -1,3 +1,6 @@ +require "net/http" +require "json" + class ReportError < StandardError; end class ExamplesController < ApplicationController @@ -48,6 +51,115 @@ def error_reporter render :html => "An error has been reported through the Rails error reporter" end + def graphql_demo + query = <<~GRAPHQL + query CustomerOverview($customerId: ID!, $ordersLimit: Int!, $status: OrderStatus) { + customer(id: $customerId) { + id + fullName + tier + loyaltyPoints + primaryAddress { + city + country + } + orders(limit: $ordersLimit, status: $status) { + id + orderNumber + status + total { + amount + currency + } + lineItems { + sku + quantity + totalPrice { + amount + currency + } + product { + id + name + category + supplier { + name + region + } + } + } + shipment { + carrier + trackingNumber + estimatedDeliveryOn + } + } + } + } + + query WarehouseOperations($warehouseId: ID!, $productQuery: String!, $limit: Int!) { + warehouse(id: $warehouseId) { + id + code + name + address { + city + country + } + inventorySnapshots { + productId + available + reserved + reorderThreshold + onHand + } + } + searchProducts(query: $productQuery, limit: $limit) { + id + name + category + unitPrice { + amount + currency + } + supplier { + id + name + } + } + } + GRAPHQL + + batch_payload = [ + { + query: query, + operationName: "CustomerOverview", + variables: { + customerId: "cust-001", + ordersLimit: 2, + status: "SHIPPED" + } + }, + { + query: query, + operationName: "WarehouseOperations", + variables: { + warehouseId: "wh-east-1", + productQuery: "pro", + limit: 3 + } + } + ] + + response = post_graphql_request(batch_payload) + + render json: { + description: "Batch request with multiple operationName values in one HTTP request", + request_payload: batch_payload, + graphql_response: response[:body] + }, status: response[:status] + end + def queries user_count = User.count User.destroy_all if user_count > 10_000 @@ -57,6 +169,36 @@ def queries end @users = User.all end + + private + + def post_graphql_request(payload) + uri = URI.parse("#{request.base_url}/graphql") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == "https" + + request = Net::HTTP::Post.new(uri.request_uri) + request["Content-Type"] = "application/json" + request.body = payload.to_json + + response = http.request(request) + { + status: response.code.to_i, + body: JSON.parse(response.body) + } + rescue JSON::ParserError + { + status: :bad_gateway, + body: { + errors: [ + { + message: "GraphQL endpoint returned invalid JSON", + raw_body: response&.body + } + ] + } + } + end end class CauseCauser diff --git a/ruby/rails8-sidekiq/app/app/controllers/graphql_controller.rb b/ruby/rails8-sidekiq/app/app/controllers/graphql_controller.rb new file mode 100644 index 000000000..3ffd8b2ad --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/controllers/graphql_controller.rb @@ -0,0 +1,72 @@ +class GraphqlController < ApplicationController + skip_forgery_protection + + def execute + if batch_request? + render json: ExampleAppSchema.multiplex(multiplex_payload) + else + render json: ExampleAppSchema.execute( + params[:query], + variables: prepare_variables(params[:variables]), + operation_name: params[:operationName], + context: graphql_context + ) + end + rescue StandardError => e + render json: { errors: [{ message: e.message }] }, status: :unprocessable_entity + ensure + transaction_name = params[:operationName] || "Unknown" + puts "!!! controller transaction name: #{transaction_name}" + Appsignal::Transaction.current.set_action(transaction_name) + end + + private + + def batch_request? + params[:_json].is_a?(Array) + end + + def multiplex_payload + params[:_json].map do |entry| + payload = normalize_entry(entry) + puts "!!! controller multiplex action name: #{payload["operationName"] || payload[:operationName]}" + { + query: payload["query"] || payload[:query], + variables: prepare_variables(payload["variables"] || payload[:variables]), + operation_name: payload["operationName"] || payload[:operationName], + context: graphql_context + } + end + end + + def normalize_entry(entry) + if entry.respond_to?(:to_unsafe_h) + entry.to_unsafe_h + else + entry.to_h + end + end + + def graphql_context + { + request_id: request.request_id, + remote_ip: request.remote_ip, + :set_appsignal_action_name => true + } + end + + def prepare_variables(variables_param) + case variables_param + when String + variables_param.present? ? prepare_variables(JSON.parse(variables_param)) : {} + when ActionController::Parameters + variables_param.to_unsafe_h + when Hash + variables_param + when nil + {} + else + raise ArgumentError, "Unexpected variables format: #{variables_param.class}" + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/example_app_schema.rb b/ruby/rails8-sidekiq/app/app/graphql/example_app_schema.rb new file mode 100644 index 000000000..b2093fc07 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/example_app_schema.rb @@ -0,0 +1,4 @@ +class ExampleAppSchema < GraphQL::Schema + trace_with(GraphQL::Tracing::AppsignalTrace) + query Types::QueryType +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/graphql_demo_data.rb b/ruby/rails8-sidekiq/app/app/graphql/graphql_demo_data.rb new file mode 100644 index 000000000..da87c15fe --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/graphql_demo_data.rb @@ -0,0 +1,307 @@ +module GraphqlDemoData + module_function + + SUPPLIERS = [ + { id: "sup-001", name: "Northwind Parts Co.", region: "North America", rating: 4.7 }, + { id: "sup-002", name: "Helios Fabrication", region: "Europe", rating: 4.4 }, + { id: "sup-003", name: "Pacific Components", region: "APAC", rating: 4.8 } + ].freeze + + PRODUCTS = [ + { + id: "prod-001", + sku: "PRM-CHAIR-01", + name: "Pro Ergonomic Chair", + category: "furniture", + supplier_id: "sup-001", + unit_price_cents: 42_500 + }, + { + id: "prod-002", + sku: "STM-DESK-02", + name: "Standing Desk Max", + category: "furniture", + supplier_id: "sup-002", + unit_price_cents: 75_000 + }, + { + id: "prod-003", + sku: "MKY-KEYB-03", + name: "Mechanical Keyboard Pro", + category: "accessories", + supplier_id: "sup-003", + unit_price_cents: 13_500 + }, + { + id: "prod-004", + sku: "4K-MON-04", + name: "4K Monitor 27in", + category: "displays", + supplier_id: "sup-001", + unit_price_cents: 31_999 + } + ].freeze + + CUSTOMERS = [ + { + id: "cust-001", + email: "alex.rivera@example.com", + first_name: "Alex", + last_name: "Rivera", + tier: "GOLD", + loyalty_points: 12_450, + primary_address: { + line1: "145 Spring St", + line2: nil, + city: "New York", + state: "NY", + postal_code: "10012", + country: "US" + } + }, + { + id: "cust-002", + email: "mina.cho@example.com", + first_name: "Mina", + last_name: "Cho", + tier: "SILVER", + loyalty_points: 3_880, + primary_address: { + line1: "24 Docklands Ave", + line2: "Unit 8", + city: "Melbourne", + state: "VIC", + postal_code: "3008", + country: "AU" + } + } + ].freeze + + WAREHOUSES = [ + { + id: "wh-east-1", + code: "USE1", + name: "US East Fulfillment", + timezone: "America/New_York", + address: { + line1: "200 Harbor Rd", + line2: nil, + city: "Newark", + state: "NJ", + postal_code: "07105", + country: "US" + } + }, + { + id: "wh-eu-1", + code: "EUC1", + name: "EU Central Fulfillment", + timezone: "Europe/Amsterdam", + address: { + line1: "75 Kanaalweg", + line2: nil, + city: "Rotterdam", + state: "ZH", + postal_code: "3011", + country: "NL" + } + } + ].freeze + + INVENTORY_SNAPSHOTS = [ + { warehouse_id: "wh-east-1", product_id: "prod-001", available: 72, reserved: 8, reorder_threshold: 15 }, + { warehouse_id: "wh-east-1", product_id: "prod-002", available: 19, reserved: 4, reorder_threshold: 10 }, + { warehouse_id: "wh-east-1", product_id: "prod-003", available: 205, reserved: 13, reorder_threshold: 40 }, + { warehouse_id: "wh-east-1", product_id: "prod-004", available: 34, reserved: 3, reorder_threshold: 8 }, + { warehouse_id: "wh-eu-1", product_id: "prod-001", available: 14, reserved: 2, reorder_threshold: 7 }, + { warehouse_id: "wh-eu-1", product_id: "prod-003", available: 89, reserved: 5, reorder_threshold: 20 } + ].freeze + + ORDERS = [ + { + id: "ord-1001", + order_number: "SO-2026-1001", + customer_id: "cust-001", + status: "SHIPPED", + placed_at: Time.utc(2026, 3, 10, 14, 15, 0), + billing_address: { + line1: "145 Spring St", + line2: nil, + city: "New York", + state: "NY", + postal_code: "10012", + country: "US" + }, + shipping_address: { + line1: "145 Spring St", + line2: nil, + city: "New York", + state: "NY", + postal_code: "10012", + country: "US" + }, + shipment: { + carrier: "DHL", + service_level: "EXPRESS", + tracking_number: "DHL123456789US", + estimated_delivery_on: Date.new(2026, 3, 14), + shipped_at: Time.utc(2026, 3, 11, 9, 5, 0), + status: "IN_TRANSIT" + }, + line_items: [ + { sku: "PRM-CHAIR-01", quantity: 2, product_id: "prod-001", unit_price_cents: 42_500, discount_cents: 3_000 }, + { sku: "MKY-KEYB-03", quantity: 1, product_id: "prod-003", unit_price_cents: 13_500, discount_cents: 0 } + ] + }, + { + id: "ord-1002", + order_number: "SO-2026-1027", + customer_id: "cust-001", + status: "PROCESSING", + placed_at: Time.utc(2026, 4, 1, 8, 45, 0), + billing_address: { + line1: "145 Spring St", + line2: nil, + city: "New York", + state: "NY", + postal_code: "10012", + country: "US" + }, + shipping_address: { + line1: "605 Market St", + line2: "Suite 300", + city: "San Francisco", + state: "CA", + postal_code: "94105", + country: "US" + }, + shipment: { + carrier: "UPS", + service_level: "GROUND", + tracking_number: "", + estimated_delivery_on: Date.new(2026, 4, 6), + shipped_at: nil, + status: "LABEL_CREATED" + }, + line_items: [ + { sku: "STM-DESK-02", quantity: 1, product_id: "prod-002", unit_price_cents: 75_000, discount_cents: 5_000 }, + { sku: "4K-MON-04", quantity: 2, product_id: "prod-004", unit_price_cents: 31_999, discount_cents: 2_000 } + ] + }, + { + id: "ord-1003", + order_number: "SO-2026-1055", + customer_id: "cust-002", + status: "DELIVERED", + placed_at: Time.utc(2026, 2, 20, 22, 10, 0), + billing_address: { + line1: "24 Docklands Ave", + line2: "Unit 8", + city: "Melbourne", + state: "VIC", + postal_code: "3008", + country: "AU" + }, + shipping_address: { + line1: "24 Docklands Ave", + line2: "Unit 8", + city: "Melbourne", + state: "VIC", + postal_code: "3008", + country: "AU" + }, + shipment: { + carrier: "FedEx", + service_level: "PRIORITY", + tracking_number: "FX5566778899", + estimated_delivery_on: Date.new(2026, 2, 25), + shipped_at: Time.utc(2026, 2, 21, 6, 30, 0), + status: "DELIVERED" + }, + line_items: [ + { sku: "MKY-KEYB-03", quantity: 3, product_id: "prod-003", unit_price_cents: 13_500, discount_cents: 1_500 } + ] + } + ].freeze + + def customer(id) + CUSTOMERS.find { |record| record[:id] == id } + end + + def customers(tier: nil, limit: nil) + result = CUSTOMERS + result = result.select { |record| record[:tier] == tier } if tier.present? + limit.present? ? result.first(limit) : result + end + + def order(id) + ORDERS.find { |record| record[:id] == id } + end + + def orders_for_customer(customer_id, status: nil, limit: nil) + result = ORDERS.select { |record| record[:customer_id] == customer_id } + result = result.select { |record| record[:status] == status } if status.present? + result = result.sort_by { |record| record[:placed_at] }.reverse + limit.present? ? result.first(limit) : result + end + + def warehouse(id) + WAREHOUSES.find { |record| record[:id] == id } + end + + def warehouses + WAREHOUSES + end + + def supplier(id) + SUPPLIERS.find { |record| record[:id] == id } + end + + def product(id) + PRODUCTS.find { |record| record[:id] == id } + end + + def search_products(query, limit) + normalized = query.to_s.downcase + result = PRODUCTS.select do |record| + record[:name].downcase.include?(normalized) || + record[:sku].downcase.include?(normalized) || + record[:category].downcase.include?(normalized) + end + result.first(limit) + end + + def inventory_for_product(product_id) + INVENTORY_SNAPSHOTS.select { |snapshot| snapshot[:product_id] == product_id } + end + + def inventory_for_warehouse(warehouse_id, product_ids: nil) + result = INVENTORY_SNAPSHOTS.select { |snapshot| snapshot[:warehouse_id] == warehouse_id } + result = result.select { |snapshot| product_ids.include?(snapshot[:product_id]) } if product_ids.present? + result + end + + def money(cents, currency = "USD") + { + amount: (cents.to_f / 100).round(2), + currency: currency + } + end + + def line_item_total_cents(line_item) + gross = line_item[:unit_price_cents] * line_item[:quantity] + [gross - line_item[:discount_cents], 0].max + end + + def order_subtotal_cents(order) + order[:line_items].sum { |line_item| line_item[:unit_price_cents] * line_item[:quantity] } + end + + def order_total_cents(order) + order[:line_items].sum { |line_item| line_item_total_cents(line_item) } + end + + def customer_total_spend_cents(customer_id) + orders_for_customer(customer_id).sum { |order| order_total_cents(order) } + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/address_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/address_type.rb new file mode 100644 index 000000000..b822a901a --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/address_type.rb @@ -0,0 +1,10 @@ +module Types + class AddressType < Types::BaseObject + field :line1, String, null: false + field :line2, String, null: true + field :city, String, null: false + field :state, String, null: false + field :postal_code, String, null: false + field :country, String, null: false + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/base_enum.rb b/ruby/rails8-sidekiq/app/app/graphql/types/base_enum.rb new file mode 100644 index 000000000..b45a845f7 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/base_enum.rb @@ -0,0 +1,4 @@ +module Types + class BaseEnum < GraphQL::Schema::Enum + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/base_object.rb b/ruby/rails8-sidekiq/app/app/graphql/types/base_object.rb new file mode 100644 index 000000000..40a81ccd2 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/base_object.rb @@ -0,0 +1,4 @@ +module Types + class BaseObject < GraphQL::Schema::Object + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/customer_tier_enum.rb b/ruby/rails8-sidekiq/app/app/graphql/types/customer_tier_enum.rb new file mode 100644 index 000000000..059c9830d --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/customer_tier_enum.rb @@ -0,0 +1,10 @@ +module Types + class CustomerTierEnum < Types::BaseEnum + graphql_name "CustomerTier" + + value "BRONZE" + value "SILVER" + value "GOLD" + value "PLATINUM" + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/customer_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/customer_type.rb new file mode 100644 index 000000000..c515713ab --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/customer_type.rb @@ -0,0 +1,29 @@ +module Types + class CustomerType < Types::BaseObject + field :id, ID, null: false + field :email, String, null: false + field :first_name, String, null: false + field :last_name, String, null: false + field :full_name, String, null: false + field :tier, Types::CustomerTierEnum, null: false + field :loyalty_points, Integer, null: false + field :primary_address, Types::AddressType, null: false + field :orders, [ "Types::OrderType" ], null: false do + argument :status, Types::OrderStatusEnum, required: false + argument :limit, Integer, required: false + end + field :total_spend, Types::MoneyType, null: false + + def full_name + "#{object[:first_name]} #{object[:last_name]}" + end + + def orders(status: nil, limit: nil) + GraphqlDemoData.orders_for_customer(object[:id], status:, limit:) + end + + def total_spend + GraphqlDemoData.money(GraphqlDemoData.customer_total_spend_cents(object[:id])) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/inventory_snapshot_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/inventory_snapshot_type.rb new file mode 100644 index 000000000..b628bdf5a --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/inventory_snapshot_type.rb @@ -0,0 +1,14 @@ +module Types + class InventorySnapshotType < Types::BaseObject + field :product_id, ID, null: false + field :warehouse_id, ID, null: false + field :available, Integer, null: false + field :reserved, Integer, null: false + field :reorder_threshold, Integer, null: false + field :on_hand, Integer, null: false + + def on_hand + object[:available] + object[:reserved] + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/line_item_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/line_item_type.rb new file mode 100644 index 000000000..0bdea5734 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/line_item_type.rb @@ -0,0 +1,26 @@ +module Types + class LineItemType < Types::BaseObject + field :sku, String, null: false + field :quantity, Integer, null: false + field :unit_price, Types::MoneyType, null: false + field :discount, Types::MoneyType, null: false + field :total_price, Types::MoneyType, null: false + field :product, Types::ProductType, null: false + + def unit_price + GraphqlDemoData.money(object[:unit_price_cents]) + end + + def discount + GraphqlDemoData.money(object[:discount_cents]) + end + + def total_price + GraphqlDemoData.money(GraphqlDemoData.line_item_total_cents(object)) + end + + def product + GraphqlDemoData.product(object[:product_id]) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/money_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/money_type.rb new file mode 100644 index 000000000..7b9b13a68 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/money_type.rb @@ -0,0 +1,6 @@ +module Types + class MoneyType < Types::BaseObject + field :amount, Float, null: false + field :currency, String, null: false + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/order_status_enum.rb b/ruby/rails8-sidekiq/app/app/graphql/types/order_status_enum.rb new file mode 100644 index 000000000..85354e5dc --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/order_status_enum.rb @@ -0,0 +1,11 @@ +module Types + class OrderStatusEnum < Types::BaseEnum + graphql_name "OrderStatus" + + value "PENDING" + value "PROCESSING" + value "SHIPPED" + value "DELIVERED" + value "CANCELLED" + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/order_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/order_type.rb new file mode 100644 index 000000000..124515beb --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/order_type.rb @@ -0,0 +1,27 @@ +module Types + class OrderType < Types::BaseObject + field :id, ID, null: false + field :order_number, String, null: false + field :status, Types::OrderStatusEnum, null: false + field :placed_at, GraphQL::Types::ISO8601DateTime, null: false + field :billing_address, Types::AddressType, null: false + field :shipping_address, Types::AddressType, null: false + field :shipment, Types::ShipmentType, null: false + field :line_items, [ Types::LineItemType ], null: false + field :subtotal, Types::MoneyType, null: false + field :total, Types::MoneyType, null: false + field :customer, Types::CustomerType, null: false + + def subtotal + GraphqlDemoData.money(GraphqlDemoData.order_subtotal_cents(object)) + end + + def total + GraphqlDemoData.money(GraphqlDemoData.order_total_cents(object)) + end + + def customer + GraphqlDemoData.customer(object[:customer_id]) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/product_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/product_type.rb new file mode 100644 index 000000000..114030da8 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/product_type.rb @@ -0,0 +1,23 @@ +module Types + class ProductType < Types::BaseObject + field :id, ID, null: false + field :sku, String, null: false + field :name, String, null: false + field :category, String, null: false + field :unit_price, Types::MoneyType, null: false + field :supplier, Types::SupplierType, null: false + field :inventory_snapshots, [ Types::InventorySnapshotType ], null: false + + def unit_price + GraphqlDemoData.money(object[:unit_price_cents]) + end + + def supplier + GraphqlDemoData.supplier(object[:supplier_id]) + end + + def inventory_snapshots + GraphqlDemoData.inventory_for_product(object[:id]) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/query_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/query_type.rb new file mode 100644 index 000000000..a157f1d61 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/query_type.rb @@ -0,0 +1,51 @@ +module Types + class QueryType < Types::BaseObject + field :customer, Types::CustomerType, null: true do + argument :id, ID, required: true + end + + field :customers, [ Types::CustomerType ], null: false do + argument :tier, Types::CustomerTierEnum, required: false + argument :limit, Integer, required: false, default_value: 20 + end + + field :order, Types::OrderType, null: true do + argument :id, ID, required: true + end + + field :warehouse, Types::WarehouseType, null: true do + argument :id, ID, required: true + end + + field :warehouses, [ Types::WarehouseType ], null: false + + field :search_products, [ Types::ProductType ], null: false do + argument :query, String, required: true + argument :limit, Integer, required: false, default_value: 10 + end + + def customer(id:) + GraphqlDemoData.customer(id) + end + + def customers(tier: nil, limit: 20) + GraphqlDemoData.customers(tier:, limit:) + end + + def order(id:) + GraphqlDemoData.order(id) + end + + def warehouse(id:) + GraphqlDemoData.warehouse(id) + end + + def warehouses + GraphqlDemoData.warehouses + end + + def search_products(query:, limit: 10) + GraphqlDemoData.search_products(query, limit) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/shipment_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/shipment_type.rb new file mode 100644 index 000000000..07aad6cc5 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/shipment_type.rb @@ -0,0 +1,10 @@ +module Types + class ShipmentType < Types::BaseObject + field :carrier, String, null: false + field :service_level, String, null: false + field :tracking_number, String, null: false + field :estimated_delivery_on, GraphQL::Types::ISO8601Date, null: false + field :shipped_at, GraphQL::Types::ISO8601DateTime, null: true + field :status, String, null: false + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/supplier_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/supplier_type.rb new file mode 100644 index 000000000..667a94ea3 --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/supplier_type.rb @@ -0,0 +1,8 @@ +module Types + class SupplierType < Types::BaseObject + field :id, ID, null: false + field :name, String, null: false + field :region, String, null: false + field :rating, Float, null: false + end +end diff --git a/ruby/rails8-sidekiq/app/app/graphql/types/warehouse_type.rb b/ruby/rails8-sidekiq/app/app/graphql/types/warehouse_type.rb new file mode 100644 index 000000000..c95edb18f --- /dev/null +++ b/ruby/rails8-sidekiq/app/app/graphql/types/warehouse_type.rb @@ -0,0 +1,16 @@ +module Types + class WarehouseType < Types::BaseObject + field :id, ID, null: false + field :code, String, null: false + field :name, String, null: false + field :timezone, String, null: false + field :address, Types::AddressType, null: false + field :inventory_snapshots, [ Types::InventorySnapshotType ], null: false do + argument :product_ids, [ ID ], required: false + end + + def inventory_snapshots(product_ids: nil) + GraphqlDemoData.inventory_for_warehouse(object[:id], product_ids:) + end + end +end diff --git a/ruby/rails8-sidekiq/app/app/views/examples/index.html.erb b/ruby/rails8-sidekiq/app/app/views/examples/index.html.erb index 8670288b4..96aef85a6 100644 --- a/ruby/rails8-sidekiq/app/app/views/examples/index.html.erb +++ b/ruby/rails8-sidekiq/app/app/views/examples/index.html.erb @@ -6,6 +6,7 @@