carddb 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rspec_status +96 -0
- data/.rubocop.yml +72 -0
- data/AGENTS.md +27 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +732 -0
- data/Rakefile +10 -0
- data/examples/basic_usage.rb +60 -0
- data/examples/filtering.rb +93 -0
- data/examples/pagination.rb +58 -0
- data/lib/carddb/batch.rb +287 -0
- data/lib/carddb/cache.rb +120 -0
- data/lib/carddb/client.rb +139 -0
- data/lib/carddb/collection.rb +919 -0
- data/lib/carddb/configuration.rb +185 -0
- data/lib/carddb/connection.rb +224 -0
- data/lib/carddb/errors.rb +85 -0
- data/lib/carddb/filter_builder.rb +214 -0
- data/lib/carddb/query_builder.rb +658 -0
- data/lib/carddb/resources/base.rb +86 -0
- data/lib/carddb/resources/datasets.rb +132 -0
- data/lib/carddb/resources/decks.rb +125 -0
- data/lib/carddb/resources/games.rb +111 -0
- data/lib/carddb/resources/publishers.rb +86 -0
- data/lib/carddb/resources/records.rb +239 -0
- data/lib/carddb/resources/rules.rb +49 -0
- data/lib/carddb/version.rb +5 -0
- data/lib/carddb.rb +160 -0
- metadata +102 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'carddb'
|
|
5
|
+
|
|
6
|
+
# Configure the client
|
|
7
|
+
CardDB.configure do |config|
|
|
8
|
+
# Optional: Set API key for higher rate limits
|
|
9
|
+
config.api_key = ENV.fetch('CARDDB_API_KEY', nil)
|
|
10
|
+
|
|
11
|
+
# Set defaults for convenience
|
|
12
|
+
config.default_publisher = 'pokemon-company'
|
|
13
|
+
config.default_game = 'pokemon-tcg'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Search for games
|
|
17
|
+
puts '=== Searching for Games ==='
|
|
18
|
+
games = CardDB.games.search
|
|
19
|
+
puts "Found #{games.total_count} games"
|
|
20
|
+
games.each do |game|
|
|
21
|
+
puts " - #{game.name} (#{game.key})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Search for datasets
|
|
25
|
+
puts "\n=== Searching for Datasets ==="
|
|
26
|
+
datasets = CardDB.datasets.search
|
|
27
|
+
puts "Found #{datasets.total_count} datasets"
|
|
28
|
+
datasets.each do |dataset|
|
|
29
|
+
puts " - #{dataset.name} (#{dataset.key})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get dataset schema
|
|
33
|
+
puts "\n=== Dataset Schema ==="
|
|
34
|
+
schema = CardDB.datasets.schema(dataset_key: 'cards')
|
|
35
|
+
if schema
|
|
36
|
+
puts 'Fields:'
|
|
37
|
+
schema.fields.each do |field|
|
|
38
|
+
filterable = field.filterable? ? '[filterable]' : ''
|
|
39
|
+
searchable = field.searchable? ? '[searchable]' : ''
|
|
40
|
+
puts " - #{field.key}: #{field.type} #{filterable} #{searchable}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Search for records
|
|
45
|
+
puts "\n=== Searching for Records ==="
|
|
46
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 5)
|
|
47
|
+
puts "Found #{records.total_count} records (showing first #{records.size})"
|
|
48
|
+
records.each do |record|
|
|
49
|
+
puts " - #{record['name']}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Search with filter DSL
|
|
53
|
+
puts "\n=== Filtered Search (HP >= 100) ==="
|
|
54
|
+
filtered = CardDB.records.search(dataset_key: 'cards', first: 5) do
|
|
55
|
+
where(hp: gte(100))
|
|
56
|
+
end
|
|
57
|
+
puts "Found #{filtered.total_count} records with HP >= 100"
|
|
58
|
+
filtered.each do |record|
|
|
59
|
+
puts " - #{record['name']} (HP: #{record['hp']})"
|
|
60
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'carddb'
|
|
5
|
+
|
|
6
|
+
# Configure the client
|
|
7
|
+
CardDB.configure do |config|
|
|
8
|
+
config.api_key = ENV.fetch('CARDDB_API_KEY', nil)
|
|
9
|
+
config.default_publisher = 'pokemon-company'
|
|
10
|
+
config.default_game = 'pokemon-tcg'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Example 1: Simple equality
|
|
14
|
+
puts '=== Simple Equality ==='
|
|
15
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
16
|
+
where(rarity: 'rare')
|
|
17
|
+
end
|
|
18
|
+
puts "Rare cards: #{records.total_count}"
|
|
19
|
+
|
|
20
|
+
# Example 2: Comparison operators
|
|
21
|
+
puts "\n=== Comparison Operators ==="
|
|
22
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
23
|
+
where(hp: gte(100))
|
|
24
|
+
where(hp: lte(150))
|
|
25
|
+
end
|
|
26
|
+
puts "Cards with HP 100-150: #{records.total_count}"
|
|
27
|
+
|
|
28
|
+
# Example 3: Pattern matching
|
|
29
|
+
puts "\n=== Pattern Matching ==="
|
|
30
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
31
|
+
where(name: ilike('%pikachu%'))
|
|
32
|
+
end
|
|
33
|
+
puts "Cards matching 'pikachu': #{records.total_count}"
|
|
34
|
+
records.each { |r| puts " - #{r['name']}" }
|
|
35
|
+
|
|
36
|
+
# Example 4: Array contains
|
|
37
|
+
puts "\n=== Array Contains ==="
|
|
38
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
39
|
+
where(types: contains('Pokemon'))
|
|
40
|
+
end
|
|
41
|
+
puts "Pokemon cards: #{records.total_count}"
|
|
42
|
+
|
|
43
|
+
# Example 5: Value in list
|
|
44
|
+
puts "\n=== Value in List ==="
|
|
45
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
46
|
+
where(rarity: within(%w[rare ultra-rare secret-rare]))
|
|
47
|
+
end
|
|
48
|
+
puts "Rare+ cards: #{records.total_count}"
|
|
49
|
+
|
|
50
|
+
# Example 6: OR conditions
|
|
51
|
+
puts "\n=== OR Conditions ==="
|
|
52
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
53
|
+
any do
|
|
54
|
+
where(hp: gte(200))
|
|
55
|
+
where(rarity: 'ultra-rare')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
puts "High HP or Ultra Rare: #{records.total_count}"
|
|
59
|
+
|
|
60
|
+
# Example 7: Complex filter
|
|
61
|
+
puts "\n=== Complex Filter ==="
|
|
62
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
63
|
+
where(types: contains('Pokemon'))
|
|
64
|
+
where(hp: gte(100))
|
|
65
|
+
any do
|
|
66
|
+
where(rarity: 'rare')
|
|
67
|
+
where(rarity: 'ultra-rare')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
puts "High HP rare Pokemon: #{records.total_count}"
|
|
71
|
+
records.each { |r| puts " - #{r['name']} (#{r['rarity']}, HP: #{r['hp']})" }
|
|
72
|
+
|
|
73
|
+
# Example 8: Nested field filtering
|
|
74
|
+
puts "\n=== Nested Fields ==="
|
|
75
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 3) do
|
|
76
|
+
where_any(:attacks, damage: gte(50))
|
|
77
|
+
end
|
|
78
|
+
puts "Cards with 50+ damage attacks: #{records.total_count}"
|
|
79
|
+
|
|
80
|
+
# Example 9: Link filtering
|
|
81
|
+
puts "\n=== Link Filtering ==="
|
|
82
|
+
records = CardDB.records.search(
|
|
83
|
+
dataset_key: 'cards',
|
|
84
|
+
first: 3,
|
|
85
|
+
resolve_links: ['set_id']
|
|
86
|
+
) do
|
|
87
|
+
where_link(:set_id, name: ilike('%scarlet%'))
|
|
88
|
+
end
|
|
89
|
+
puts "Cards from Scarlet sets: #{records.total_count}"
|
|
90
|
+
records.each do |r|
|
|
91
|
+
set_name = r.resolved_links['set_id']&.record&.[]('name')
|
|
92
|
+
puts " - #{r['name']} (Set: #{set_name})"
|
|
93
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'carddb'
|
|
5
|
+
|
|
6
|
+
# Configure the client
|
|
7
|
+
CardDB.configure do |config|
|
|
8
|
+
config.api_key = ENV.fetch('CARDDB_API_KEY', nil)
|
|
9
|
+
config.default_publisher = 'pokemon-company'
|
|
10
|
+
config.default_game = 'pokemon-tcg'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Example 1: Manual pagination
|
|
14
|
+
puts '=== Manual Pagination ==='
|
|
15
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 10)
|
|
16
|
+
puts "Total records: #{records.total_count}"
|
|
17
|
+
puts "Page 1: #{records.size} records"
|
|
18
|
+
|
|
19
|
+
page = 2
|
|
20
|
+
while records.next_page?
|
|
21
|
+
records = records.next_page
|
|
22
|
+
puts "Page #{page}: #{records.size} records"
|
|
23
|
+
page += 1
|
|
24
|
+
break if page > 5 # Limit for demo
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Example 2: Auto-pagination with limit
|
|
28
|
+
puts "\n=== Auto-Pagination (first 50) ==="
|
|
29
|
+
count = 0
|
|
30
|
+
CardDB.records.search(dataset_key: 'cards', first: 10)
|
|
31
|
+
.auto_paginate
|
|
32
|
+
.take(50)
|
|
33
|
+
.each do |record|
|
|
34
|
+
count += 1
|
|
35
|
+
puts "#{count}. #{record['name']}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Example 3: Collection metadata
|
|
39
|
+
puts "\n=== Collection Metadata ==="
|
|
40
|
+
records = CardDB.records.search(dataset_key: 'cards', first: 5)
|
|
41
|
+
puts "Total count: #{records.total_count}"
|
|
42
|
+
puts "Page size: #{records.size}"
|
|
43
|
+
puts "Has next page: #{records.next_page?}"
|
|
44
|
+
puts "Has previous page: #{records.previous_page?}"
|
|
45
|
+
puts "End cursor: #{records.end_cursor}"
|
|
46
|
+
|
|
47
|
+
# Example 4: Processing all records efficiently
|
|
48
|
+
puts "\n=== Processing All Records ==="
|
|
49
|
+
processed = 0
|
|
50
|
+
CardDB.records.search(dataset_key: 'cards', first: 100)
|
|
51
|
+
.auto_paginate
|
|
52
|
+
.each do |_record|
|
|
53
|
+
# Process record
|
|
54
|
+
processed += 1
|
|
55
|
+
print "\rProcessed: #{processed} records" if (processed % 100).zero?
|
|
56
|
+
break if processed >= 500 # Limit for demo
|
|
57
|
+
end
|
|
58
|
+
puts "\nDone! Processed #{processed} records"
|
data/lib/carddb/batch.rb
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CardDB
|
|
4
|
+
# Batch query builder for combining multiple queries into a single request.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# results = CardDB.batch do |b|
|
|
8
|
+
# b.games.fetch("uuid-1")
|
|
9
|
+
# b.games.fetch("uuid-2")
|
|
10
|
+
# b.publishers.fetch(slug: "pokemon-company")
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# results[0] # => Game
|
|
14
|
+
# results[1] # => Game
|
|
15
|
+
# results[2] # => Publisher
|
|
16
|
+
class Batch
|
|
17
|
+
# @return [Array<Hash>] The queued operations
|
|
18
|
+
attr_reader :operations
|
|
19
|
+
|
|
20
|
+
# @param client [Client] The client instance
|
|
21
|
+
def initialize(client)
|
|
22
|
+
@client = client
|
|
23
|
+
@operations = []
|
|
24
|
+
@counter = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Access the Publishers resource for batching
|
|
28
|
+
#
|
|
29
|
+
# @return [BatchProxy]
|
|
30
|
+
def publishers
|
|
31
|
+
BatchProxy.new(self, :publishers)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Access the Games resource for batching
|
|
35
|
+
#
|
|
36
|
+
# @return [BatchProxy]
|
|
37
|
+
def games
|
|
38
|
+
BatchProxy.new(self, :games)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Access the Datasets resource for batching
|
|
42
|
+
#
|
|
43
|
+
# @return [BatchProxy]
|
|
44
|
+
def datasets
|
|
45
|
+
BatchProxy.new(self, :datasets)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Access the Records resource for batching
|
|
49
|
+
#
|
|
50
|
+
# @return [BatchProxy]
|
|
51
|
+
def records
|
|
52
|
+
BatchProxy.new(self, :records)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Add an operation to the batch
|
|
56
|
+
#
|
|
57
|
+
# @param resource [Symbol] The resource type
|
|
58
|
+
# @param method [Symbol] The method to call
|
|
59
|
+
# @param args [Array] Method arguments
|
|
60
|
+
# @param kwargs [Hash] Method keyword arguments
|
|
61
|
+
# @return [Integer] The index of this operation in the results
|
|
62
|
+
def add_operation(resource:, method:, args: [], kwargs: {})
|
|
63
|
+
@counter += 1
|
|
64
|
+
alias_name = "op#{@counter}"
|
|
65
|
+
|
|
66
|
+
@operations << {
|
|
67
|
+
alias: alias_name,
|
|
68
|
+
resource: resource,
|
|
69
|
+
method: method,
|
|
70
|
+
args: args,
|
|
71
|
+
kwargs: kwargs
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@counter - 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Execute all batched operations
|
|
78
|
+
#
|
|
79
|
+
# @return [Array] Results in the same order as operations were added
|
|
80
|
+
def execute
|
|
81
|
+
return [] if @operations.empty?
|
|
82
|
+
|
|
83
|
+
# Build combined GraphQL query
|
|
84
|
+
query_parts = []
|
|
85
|
+
variables = {}
|
|
86
|
+
|
|
87
|
+
@operations.each_with_index do |op, index|
|
|
88
|
+
query_part, op_vars = build_operation_query(op, index)
|
|
89
|
+
query_parts << query_part
|
|
90
|
+
variables.merge!(op_vars)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Combine into single query
|
|
94
|
+
var_definitions = build_variable_definitions(variables)
|
|
95
|
+
combined_query = <<~GRAPHQL
|
|
96
|
+
query BatchQuery#{var_definitions} {
|
|
97
|
+
#{query_parts.join("\n ")}
|
|
98
|
+
}
|
|
99
|
+
GRAPHQL
|
|
100
|
+
|
|
101
|
+
# Execute the combined query
|
|
102
|
+
data = @client.connection.execute(combined_query, variables)
|
|
103
|
+
|
|
104
|
+
# Parse results
|
|
105
|
+
@operations.map do |op|
|
|
106
|
+
result = data[op[:alias]]
|
|
107
|
+
wrap_result(result, op[:resource], op[:method])
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def build_operation_query(op, index)
|
|
114
|
+
case op[:resource]
|
|
115
|
+
when :publishers
|
|
116
|
+
build_publisher_query(op, index)
|
|
117
|
+
when :games
|
|
118
|
+
build_game_query(op, index)
|
|
119
|
+
when :datasets
|
|
120
|
+
build_dataset_query(op, index)
|
|
121
|
+
when :records
|
|
122
|
+
build_record_query(op, index)
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "Unknown resource: #{op[:resource]}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_publisher_query(op, index)
|
|
129
|
+
case op[:method]
|
|
130
|
+
when :fetch
|
|
131
|
+
if op[:kwargs][:id]
|
|
132
|
+
var_name = "publisherId#{index}"
|
|
133
|
+
query = "#{op[:alias]}: fetchPublisher(id: $#{var_name}) { #{publisher_fields} }"
|
|
134
|
+
[query, { var_name => op[:kwargs][:id] }, { var_name => 'UUID' }]
|
|
135
|
+
else
|
|
136
|
+
var_name = "publisherSlug#{index}"
|
|
137
|
+
query = "#{op[:alias]}: fetchPublisher(slug: $#{var_name}) { #{publisher_fields} }"
|
|
138
|
+
[query, { var_name => op[:kwargs][:slug] }, { var_name => 'String' }]
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
raise ArgumentError, "Unsupported batch method for publishers: #{op[:method]}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_game_query(op, index)
|
|
146
|
+
case op[:method]
|
|
147
|
+
when :fetch
|
|
148
|
+
var_name = "gameId#{index}"
|
|
149
|
+
query = "#{op[:alias]}: fetchGame(id: $#{var_name}) { #{game_fields} }"
|
|
150
|
+
[query, { var_name => op[:args][0] }, { var_name => 'UUID' }]
|
|
151
|
+
when :get
|
|
152
|
+
slug_var = "gamePublisherSlug#{index}"
|
|
153
|
+
key_var = "gameKey#{index}"
|
|
154
|
+
query = "#{op[:alias]}: fetchGame(publisherSlug: $#{slug_var}, gameKey: $#{key_var}) { #{game_fields} }"
|
|
155
|
+
resolved_publisher = @client.config.resolve_publisher(op[:kwargs][:publisher_slug])
|
|
156
|
+
[query, { slug_var => resolved_publisher, key_var => op[:kwargs][:game_key] },
|
|
157
|
+
{ slug_var => 'String', key_var => 'String' }]
|
|
158
|
+
else
|
|
159
|
+
raise ArgumentError, "Unsupported batch method for games: #{op[:method]}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_dataset_query(op, index)
|
|
164
|
+
case op[:method]
|
|
165
|
+
when :fetch
|
|
166
|
+
var_name = "datasetId#{index}"
|
|
167
|
+
query = "#{op[:alias]}: fetchDataset(id: $#{var_name}) { #{dataset_fields} }"
|
|
168
|
+
[query, { var_name => op[:args][0] }, { var_name => 'UUID' }]
|
|
169
|
+
when :get
|
|
170
|
+
slug_var = "datasetPublisherSlug#{index}"
|
|
171
|
+
game_var = "datasetGameKey#{index}"
|
|
172
|
+
key_var = "datasetKey#{index}"
|
|
173
|
+
query = "#{op[:alias]}: fetchDataset(publisherSlug: $#{slug_var}, gameKey: $#{game_var}, datasetKey: $#{key_var}) { #{dataset_fields} #{schema_fields} }"
|
|
174
|
+
resolved_publisher = @client.config.resolve_publisher(op[:kwargs][:publisher_slug])
|
|
175
|
+
resolved_game = @client.config.resolve_game(op[:kwargs][:game_key])
|
|
176
|
+
[query, { slug_var => resolved_publisher, game_var => resolved_game, key_var => op[:kwargs][:dataset_key] },
|
|
177
|
+
{ slug_var => 'String', game_var => 'String', key_var => 'String' }]
|
|
178
|
+
else
|
|
179
|
+
raise ArgumentError, "Unsupported batch method for datasets: #{op[:method]}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def build_record_query(op, index)
|
|
184
|
+
case op[:method]
|
|
185
|
+
when :fetch
|
|
186
|
+
var_name = "recordId#{index}"
|
|
187
|
+
query = "#{op[:alias]}: fetchRecord(id: $#{var_name}) { #{record_fields} }"
|
|
188
|
+
[query, { var_name => op[:args][0] }, { var_name => 'UUID' }]
|
|
189
|
+
when :get
|
|
190
|
+
slug_var = "recordPublisherSlug#{index}"
|
|
191
|
+
game_var = "recordGameKey#{index}"
|
|
192
|
+
dataset_var = "recordDatasetKey#{index}"
|
|
193
|
+
id_var = "recordIdentifier#{index}"
|
|
194
|
+
query = "#{op[:alias]}: fetchRecordByIdentifier(publisherSlug: $#{slug_var}, gameKey: $#{game_var}, datasetKey: $#{dataset_var}, identifier: $#{id_var}) { #{record_fields} }"
|
|
195
|
+
resolved_publisher = @client.config.resolve_publisher(op[:kwargs][:publisher_slug])
|
|
196
|
+
resolved_game = @client.config.resolve_game(op[:kwargs][:game_key])
|
|
197
|
+
[query,
|
|
198
|
+
{ slug_var => resolved_publisher, game_var => resolved_game, dataset_var => op[:kwargs][:dataset_key], id_var => op[:kwargs][:identifier] }, { slug_var => 'String', game_var => 'String', dataset_var => 'String', id_var => 'String' }]
|
|
199
|
+
else
|
|
200
|
+
raise ArgumentError, "Unsupported batch method for records: #{op[:method]}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def build_variable_definitions(variables)
|
|
205
|
+
return '' if variables.empty?
|
|
206
|
+
|
|
207
|
+
# Build type map from the operations
|
|
208
|
+
type_map = {}
|
|
209
|
+
@operations.each_with_index do |op, index|
|
|
210
|
+
_, _, types = build_operation_query(op, index)
|
|
211
|
+
type_map.merge!(types) if types
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
defs = variables.keys.map do |key|
|
|
215
|
+
type = type_map[key] || 'String'
|
|
216
|
+
"$#{key}: #{type}!"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
"(#{defs.join(', ')})"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def wrap_result(result, resource, _method)
|
|
223
|
+
return nil if result.nil?
|
|
224
|
+
|
|
225
|
+
case resource
|
|
226
|
+
when :publishers
|
|
227
|
+
Publisher.new(result, client: @client)
|
|
228
|
+
when :games
|
|
229
|
+
Game.new(result, client: @client)
|
|
230
|
+
when :datasets
|
|
231
|
+
Dataset.new(result, client: @client)
|
|
232
|
+
when :records
|
|
233
|
+
Record.new(result, client: @client)
|
|
234
|
+
else
|
|
235
|
+
result
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Field definitions (simplified versions)
|
|
240
|
+
def publisher_fields
|
|
241
|
+
'id name slug status description website createdAt updatedAt logoFile { id url } bannerFile { id url }'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def game_fields
|
|
245
|
+
'id publisherId key name description website visibility isArchived createdAt updatedAt publisher { id name slug } logoFile { id url } coverFile { id url }'
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def dataset_fields
|
|
249
|
+
'id publisherId gameId key name description visibility isArchived createdAt updatedAt publisher { id name slug } game { id key name }'
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def schema_fields
|
|
253
|
+
[
|
|
254
|
+
'schema { fields {',
|
|
255
|
+
'key label description type isRequired filterable searchable isIdentifier',
|
|
256
|
+
'itemType displayFormat semanticType allowedValues',
|
|
257
|
+
'} filterableFields searchableFields linkFields {',
|
|
258
|
+
'key label targetDatasetKey targetDatasetName targetDatasetId',
|
|
259
|
+
'} }'
|
|
260
|
+
].join(' ')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def record_fields
|
|
264
|
+
'id datasetId data createdAt updatedAt dataset { id key name gameId publisherId }'
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Proxy class for batching operations on a specific resource
|
|
269
|
+
class BatchProxy
|
|
270
|
+
def initialize(batch, resource)
|
|
271
|
+
@batch = batch
|
|
272
|
+
@resource = resource
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Proxy fetch calls to the batch
|
|
276
|
+
def fetch(*args, **kwargs)
|
|
277
|
+
@batch.add_operation(resource: @resource, method: :fetch, args: args, kwargs: kwargs)
|
|
278
|
+
nil # Return nil since we're batching
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Proxy get calls to the batch
|
|
282
|
+
def get(**kwargs)
|
|
283
|
+
@batch.add_operation(resource: @resource, method: :get, kwargs: kwargs)
|
|
284
|
+
nil # Return nil since we're batching
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
data/lib/carddb/cache.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CardDB
|
|
4
|
+
# Simple in-memory cache with TTL support.
|
|
5
|
+
# Compatible with Rails.cache interface (read/write/delete).
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# cache = CardDB::MemoryCache.new
|
|
9
|
+
# cache.write("key", "value", expires_in: 60)
|
|
10
|
+
# cache.read("key") # => "value"
|
|
11
|
+
# # After 60 seconds...
|
|
12
|
+
# cache.read("key") # => nil
|
|
13
|
+
class MemoryCache
|
|
14
|
+
def initialize
|
|
15
|
+
@store = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Read a value from the cache.
|
|
20
|
+
#
|
|
21
|
+
# @param key [String] The cache key
|
|
22
|
+
# @return [Object, nil] The cached value or nil if not found/expired
|
|
23
|
+
def read(key)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
entry = @store[key]
|
|
26
|
+
return nil unless entry
|
|
27
|
+
|
|
28
|
+
if entry[:expires_at] && Time.now > entry[:expires_at]
|
|
29
|
+
@store.delete(key)
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
entry[:value]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Write a value to the cache.
|
|
38
|
+
#
|
|
39
|
+
# @param key [String] The cache key
|
|
40
|
+
# @param value [Object] The value to cache
|
|
41
|
+
# @param expires_in [Integer, nil] TTL in seconds (nil = no expiry)
|
|
42
|
+
# @return [Object] The cached value
|
|
43
|
+
def write(key, value, expires_in: nil)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
expires_at = expires_in ? Time.now + expires_in : nil
|
|
46
|
+
@store[key] = { value: value, expires_at: expires_at }
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Delete a value from the cache.
|
|
52
|
+
#
|
|
53
|
+
# @param key [String] The cache key
|
|
54
|
+
# @return [Boolean] true if the key was deleted
|
|
55
|
+
def delete(key)
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
!!@store.delete(key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if a key exists and is not expired.
|
|
62
|
+
#
|
|
63
|
+
# @param key [String] The cache key
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def exist?(key)
|
|
66
|
+
!read(key).nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Clear all cached values.
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
def clear
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
@store.clear
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get the number of cached entries (for debugging).
|
|
79
|
+
#
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def size
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
# Clean up expired entries first
|
|
84
|
+
now = Time.now
|
|
85
|
+
@store.delete_if { |_, entry| entry[:expires_at] && now > entry[:expires_at] }
|
|
86
|
+
@store.size
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Cache wrapper that provides a unified interface for caching.
|
|
92
|
+
# Supports any cache that responds to read/write (like Rails.cache).
|
|
93
|
+
#
|
|
94
|
+
# @example With built-in memory cache
|
|
95
|
+
# CardDB.configure do |config|
|
|
96
|
+
# config.cache = CardDB::MemoryCache.new
|
|
97
|
+
# config.cache_ttl = 300 # 5 minutes
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# @example With Rails.cache
|
|
101
|
+
# CardDB.configure do |config|
|
|
102
|
+
# config.cache = Rails.cache
|
|
103
|
+
# config.cache_ttl = 300
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# @example Disabling cache for a specific call
|
|
107
|
+
# CardDB.games.fetch(id, cache: false)
|
|
108
|
+
module CacheSupport
|
|
109
|
+
# Generate a cache key for a query.
|
|
110
|
+
#
|
|
111
|
+
# @param resource [String] Resource type (publishers, games, etc.)
|
|
112
|
+
# @param method [String] Method name (fetch, get, search, etc.)
|
|
113
|
+
# @param params [Hash] Query parameters
|
|
114
|
+
# @return [String] The cache key
|
|
115
|
+
def self.cache_key(resource, method, params)
|
|
116
|
+
sorted_params = params.sort.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
117
|
+
"carddb:#{resource}:#{method}:#{sorted_params}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|