CardDB Rails

Rails integration for the carddb gem.

This gem keeps the base CardDB client framework-agnostic and adds Rails-specific conveniences:

  • initializer and Railtie wiring
  • carddb_client controller helper
  • explicit Active Record model helpers
  • GraphQL::Batch loaders for CardDB records, games, datasets, and publishers
  • GraphQL::Dataloader sources for the same resource lookups

Installation

Add this line to your application's Gemfile:

gem 'carddb'
gem 'carddb-rails'

Then run:

bundle install
bin/rails generate carddb:install

Configuration

The generated initializer configures CardDB from Rails.application.credentials[:carddb] and wires Rails.cache and Rails.logger by default.

Expected credentials shape:

carddb:
  publishable_key: carddb_pk_xxx
  secret_key: carddb_sk_xxx
  access_token: carddb_oat_xxx
  api_key: carddb_legacy_xxx
  endpoint: https://carddb.xtda.org/query

Controller Helper

Controllers get a carddb_client helper:

class CardsController < ApplicationController
  def show
    @card = carddb_client.records.get(
      publisher_slug: 'pokemon-company',
      game_key: 'pokemon-tcg',
      dataset_key: 'cards',
      identifier: params[:id]
    )
  end
end

Model Helper

Include CardDB::Rails::HasCardDBRecord in models that hold a CardDB identifier locally.

class DeckEntry < ApplicationRecord
  include CardDB::Rails::HasCardDBRecord

  has_carddb_record \
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: 'pokemon-tcg',
    identifier: :card_identifier
end

This defines:

  • carddb_record
  • carddb_record!
  • reload_carddb_record
  • carddb_record_loaded?

Additional helpers are available for other CardDB resources:

class DeckTemplate < ApplicationRecord
  include CardDB::Rails::HasCardDBGame
  include CardDB::Rails::HasCardDBDataset

  has_carddb_game game_key: :game_key, publisher_slug: 'pokemon-company'
  has_carddb_dataset dataset_key: :dataset_key, publisher_slug: 'pokemon-company', game_key: :game_key
end

For deck-backed models, use HasCardDBDeck:

class TournamentDeck < ApplicationRecord
  include CardDB::Rails::HasCardDBDeck

  has_carddb_deck id: :carddb_deck_id
end

Or map a local record to an app-owned CardDB deck by external reference:

class TournamentDeck < ApplicationRecord
  include CardDB::Rails::HasCardDBDeck

  has_carddb_deck external_ref: :external_ref
end

That gives you:

  • carddb_deck
  • carddb_deck!
  • reload_carddb_deck
  • carddb_deck_loaded?

To batch-preload deck helpers for many records:

TournamentDeck.preload_carddb_decks(records)

If a model should expose the available datasets for its game, use HasCardDBDatasets:

class DeckTemplate < ApplicationRecord
  include CardDB::Rails::HasCardDBGame
  include CardDB::Rails::HasCardDBDatasets

  has_carddb_game game_key: :game_key, publisher_slug: 'pokemon-company'
  has_carddb_datasets publisher_slug: 'pokemon-company', game_key: :game_key
end

That gives you:

  • carddb_datasets
  • reload_carddb_datasets
  • carddb_datasets_loaded?
  • carddb_dataset('cards')

Example:

template.carddb_datasets
template.carddb_dataset('cards')
template.carddb_datasets(purpose: 'RULES', search: 'rotation')

You can also use dynamic values:

class CollectionItem < ApplicationRecord
  include CardDB::Rails::HasCardDBRecord

  has_carddb_record \
    dataset_key: :carddb_dataset_key,
    publisher_slug: :carddb_publisher_slug,
    game_key: :carddb_game_key,
    identifier: :carddb_identifier
end

Or define multiple named mappings:

class MatchResult < ApplicationRecord
  include CardDB::Rails::HasCardDBRecord

  has_carddb_record :home_card,
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: 'pokemon-tcg',
    identifier: :home_card_identifier

  has_carddb_record :away_card,
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: 'pokemon-tcg',
    identifier: :away_card_identifier
end

To batch-preload a CardDB helper for many records:

DeckEntry.preload_carddb_records(entries)

This uses CardDB's batch API and memoizes the loaded records back onto each model instance.

GraphQL::Batch Loader

This gem includes a GraphQL::Batch loader that fetches CardDB records in one remote batch request:

class Types::DeckEntry < Types::BaseObject
  field :carddb_record, Types::CarddbRecord, null: true

  def carddb_record
    CardDB::Rails::GraphQL::RecordLoader.for(
      dataset_key: 'cards',
      publisher_slug: 'pokemon-company',
      game_key: 'pokemon-tcg'
    ).load(object.card_identifier)
  end
end

If your Rails app already uses GraphQL::Batch, this prevents one CardDB HTTP request per node.

Available loaders:

  • CardDB::Rails::GraphQL::DeckLoader
  • CardDB::Rails::GraphQL::DeckByExternalRefLoader
  • CardDB::Rails::GraphQL::RecordLoader
  • CardDB::Rails::GraphQL::GameLoader
  • CardDB::Rails::GraphQL::DatasetLoader
  • CardDB::Rails::GraphQL::PublisherLoader

If your schema uses GraphQL::Dataloader instead of GraphQL::Batch, parallel sources are also available:

  • CardDB::Rails::GraphQL::Dataloader::RecordSource
  • CardDB::Rails::GraphQL::Dataloader::DeckSource
  • CardDB::Rails::GraphQL::Dataloader::DeckByExternalRefSource
  • CardDB::Rails::GraphQL::Dataloader::GameSource
  • CardDB::Rails::GraphQL::Dataloader::DatasetSource
  • CardDB::Rails::GraphQL::Dataloader::PublisherSource

Example:

class Types::DeckEntry < Types::BaseObject
  field :carddb_record, Types::CarddbRecord, null: true

  def carddb_record
    dataloader.with(
      CardDB::Rails::GraphQL::Dataloader::RecordSource,
      dataset_key: 'cards',
      publisher_slug: 'pokemon-company',
      game_key: object.game_key
    ).load(object.card_identifier)
  end
end

You can also use dataloader-backed field macros directly:

class Types::DeckEntry < Types::BaseObject
  carddb_record_dataloader_field :carddb_record,
    type: Types::CarddbRecord,
    identifier: :card_identifier,
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: :game_key
end

Deck fields are also available in both styles:

class Types::TournamentDeck < Types::BaseObject
  carddb_deck_field :carddb_deck,
    type: Types::CarddbDeck,
    id: :carddb_deck_id

  carddb_deck_dataloader_field :carddb_deck_async,
    type: Types::CarddbDeck,
    id: :carddb_deck_id
end

External-ref lookups use the same macros:

class Types::TournamentDeck < Types::BaseObject
  carddb_deck_field :carddb_deck,
    type: Types::CarddbDeck,
    external_ref: :external_ref

  carddb_deck_dataloader_field :carddb_deck_async,
    type: Types::CarddbDeck,
    external_ref: :external_ref
end

Deck Sync Helpers

For write flows, keep sync explicit with the provided service and job:

CardDB::Rails::DeckSyncService.call(
  external_ref: 'deck-ext-1',
  input: {
    title: 'Pikachu League Cup',
    visibility: 'PRIVATE'
  }
)
CardDB::Rails::DeckSyncJob.perform_later(
  external_ref: 'deck-ext-1',
  input: {
    title: 'Pikachu League Cup',
    visibility: 'PRIVATE'
  }
)

These helpers call client.decks.upsert_by_external_ref(...) and do not hide writes behind model callbacks.

Deck Import / Export Helpers

For explicit service-style import and export flows:

CardDB::Rails::DeckImportService.call(
  input: {
    deckId: 'deck_123',
    text: "4 Pikachu\n4 Rare Candy",
    format: 'SIMPLE_TEXT'
  }
)
CardDB::Rails::DeckExportService.call(
  deck_id: 'deck_123',
  format: 'JSON'
)

Deck Token Controller Helpers

Controllers automatically get token-aware helpers for deck preview/embed/access flows:

class DeckPreviewsController < ApplicationController
  def show
    @deck = carddb_preview_deck(params[:token])
  end
end

Available helpers:

  • carddb_preview_deck(token)
  • carddb_embed_deck(token)
  • carddb_access_deck(token)
  • carddb_exchange_deck_access_token(input)

GraphQL Field Helper

If you want a thin schema macro on top of the loader, extend CardDB::Rails::GraphQL::Helpers in your base object:

class Types::BaseObject < GraphQL::Schema::Object
  extend CardDB::Rails::GraphQL::Helpers
end

Then define CardDB-backed fields declaratively:

class Types::DeckEntry < Types::BaseObject
  carddb_record_field :carddb_record,
    type: Types::CarddbRecord,
    identifier: :card_identifier,
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: 'pokemon-tcg'
end

For a list relationship, use carddb_datasets_field:

class Types::DeckTemplate < Types::BaseObject
  carddb_datasets_field :carddb_datasets,
    type: [Types::CarddbDataset],
    publisher_slug: 'pokemon-company',
    game_key: :game_key
end

If the underlying model includes HasCardDBDatasets, this field uses the model helper directly.

If you prefer dataloader-oriented naming consistency, carddb_datasets_dataloader_field is also available.

Integration Coverage

The gem includes coverage for:

  • Railtie configuration wiring via CardDB::Rails.apply_rails_configuration!
  • install generator output for config/initializers/carddb.rb

You can also expose related CardDB resources directly:

class Types::DeckTemplate < Types::BaseObject
  carddb_game_field :carddb_game,
    type: Types::CarddbGame,
    game_key: :game_key,
    publisher_slug: 'pokemon-company'

  carddb_dataset_field :carddb_dataset,
    type: Types::CarddbDataset,
    dataset_key: :dataset_key,
    publisher_slug: 'pokemon-company',
    game_key: :game_key

  carddb_publisher_field :carddb_publisher,
    type: Types::CarddbPublisher,
    slug: :publisher_slug
end

Rails + GraphQL Example

For a GraphQL app that already uses GraphQL::Batch, a good pattern is:

class DeckEntry < ApplicationRecord
  include CardDB::Rails::HasCardDBRecord
  include CardDB::Rails::HasCardDBGame

  has_carddb_record \
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: :game_key,
    identifier: :card_identifier

  has_carddb_game \
    game_key: :game_key,
    publisher_slug: 'pokemon-company'
end
class Types::BaseObject < GraphQL::Schema::Object
  extend CardDB::Rails::GraphQL::Helpers
end
class Types::DeckEntry < Types::BaseObject
  field :id, ID, null: false

  carddb_record_field :carddb_record,
    type: Types::CarddbRecord,
    identifier: :card_identifier,
    dataset_key: 'cards',
    publisher_slug: 'pokemon-company',
    game_key: :game_key

  carddb_game_field :carddb_game,
    type: Types::CarddbGame,
    game_key: :game_key,
    publisher_slug: 'pokemon-company'
end

That keeps the Active Record model explicit while letting GraphQL batch remote CardDB lookups across sibling nodes.

If you want to expose all datasets for a game in GraphQL, a good pattern is to call the model helper directly:

class Types::DeckTemplate < Types::BaseObject
  field :carddb_datasets, [Types::CarddbDataset], null: false

  def carddb_datasets
    object.carddb_datasets
  end
end

Boundaries

This gem intentionally does not:

  • make CardDB objects behave like ActiveRecord::Relation
  • define remote CardDB fields directly on your models
  • perform implicit remote lookups in callbacks or validations
  • sync CardDB data into local tables

The intent is explicit remote access with Rails-friendly ergonomics.