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_clientcontroller helper- explicit Active Record model helpers
GraphQL::Batchloaders for CardDB records, games, datasets, and publishersGraphQL::Dataloadersources 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_recordcarddb_record!reload_carddb_recordcarddb_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_deckcarddb_deck!reload_carddb_deckcarddb_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_datasetsreload_carddb_datasetscarddb_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::DeckLoaderCardDB::Rails::GraphQL::DeckByExternalRefLoaderCardDB::Rails::GraphQL::RecordLoaderCardDB::Rails::GraphQL::GameLoaderCardDB::Rails::GraphQL::DatasetLoaderCardDB::Rails::GraphQL::PublisherLoader
If your schema uses GraphQL::Dataloader instead of GraphQL::Batch, parallel sources are also available:
CardDB::Rails::GraphQL::Dataloader::RecordSourceCardDB::Rails::GraphQL::Dataloader::DeckSourceCardDB::Rails::GraphQL::Dataloader::DeckByExternalRefSourceCardDB::Rails::GraphQL::Dataloader::GameSourceCardDB::Rails::GraphQL::Dataloader::DatasetSourceCardDB::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.