carddb 0.3.0 → 0.3.5

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.
@@ -4,6 +4,79 @@ module CardDB
4
4
  module Resources
5
5
  # Datasets resource for searching, fetching, and getting schema
6
6
  class Datasets < Base
7
+ # List datasets for a publisher-management context.
8
+ def list(publisher_id:, game_id: nil, purpose: nil, first: nil, after: nil, include_archived: nil, cache: nil)
9
+ query = QueryBuilder.list_datasets(
10
+ publisher_id: publisher_id,
11
+ game_id: game_id,
12
+ purpose: purpose,
13
+ first: first,
14
+ after: after,
15
+ include_archived: include_archived
16
+ )
17
+ variables = build_variables(
18
+ publisherId: publisher_id,
19
+ gameId: game_id,
20
+ purpose: purpose,
21
+ first: first,
22
+ after: after,
23
+ includeArchived: include_archived
24
+ )
25
+ key = cache_key('datasets', 'list', **variables)
26
+
27
+ data = with_cache(key, resource: :datasets, cache: cache) { connection.execute(query, variables) }
28
+
29
+ Collection.new(
30
+ data['datasets'],
31
+ item_class: Dataset,
32
+ next_page_loader: lambda { |cursor|
33
+ list(
34
+ publisher_id: publisher_id,
35
+ game_id: game_id,
36
+ purpose: purpose,
37
+ first: first,
38
+ after: cursor,
39
+ include_archived: include_archived,
40
+ cache: cache
41
+ )
42
+ },
43
+ client: client
44
+ )
45
+ end
46
+
47
+ # Get a dataset by game ID and stable dataset key.
48
+ def get_by_key(game_id:, dataset_key:, publisher_id: nil, cache: nil)
49
+ key = cache_key(
50
+ 'datasets',
51
+ 'get_by_key',
52
+ publisher_id: publisher_id,
53
+ game_id: game_id,
54
+ dataset_key: dataset_key
55
+ )
56
+ with_cache(key, resource: :datasets, cache: cache) do
57
+ query = QueryBuilder.dataset(publisher_id: publisher_id, game_id: game_id, dataset_key: dataset_key)
58
+ variables = build_variables(publisherId: publisher_id, gameId: game_id, datasetKey: dataset_key)
59
+ data = connection.execute(query, variables)
60
+
61
+ data['dataset'] ? Dataset.new(data['dataset'], client: client) : nil
62
+ end
63
+ end
64
+
65
+ # Fetch only the schema portion for a dataset.
66
+ def get_schema(id: nil, publisher_id: nil, game_id: nil, dataset_key: nil, cache: nil)
67
+ dataset = if id
68
+ get_by_id_for_management(id, cache: cache)
69
+ else
70
+ get_by_key(
71
+ publisher_id: publisher_id,
72
+ game_id: game_id,
73
+ dataset_key: dataset_key,
74
+ cache: cache
75
+ )
76
+ end
77
+ dataset&.schema
78
+ end
79
+
7
80
  # Search for datasets
8
81
  #
9
82
  # @param publisher_slug [String, nil] Filter by publisher slug
@@ -127,6 +200,18 @@ module CardDB
127
200
  )
128
201
  dataset&.schema
129
202
  end
203
+
204
+ private
205
+
206
+ def get_by_id_for_management(id, cache: nil)
207
+ key = cache_key('datasets', 'get_schema', id: id)
208
+ with_cache(key, resource: :datasets, cache: cache) do
209
+ query = QueryBuilder.dataset(id: id)
210
+ data = connection.execute(query, { id: id })
211
+
212
+ data['dataset'] ? Dataset.new(data['dataset'], client: client) : nil
213
+ end
214
+ end
130
215
  end
131
216
  end
132
217
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Publisher dataset export jobs resource.
6
+ class Exports < Base
7
+ # Start an async dataset export. Requires a server-side secret credential.
8
+ def run(input:)
9
+ config.require_secret_credential!('exports.run')
10
+
11
+ data = connection.execute(QueryBuilder.dataset_export, { input: input })
12
+ ExportJob.new(data['datasetExport'], client: client)
13
+ end
14
+ alias create run
15
+
16
+ # Fetch one export job.
17
+ def get_job(id, cache: nil)
18
+ key = cache_key('exports', 'get_job', id: id)
19
+ with_cache(key, resource: :exports, cache: cache) do
20
+ data = connection.execute(QueryBuilder.export_job, { id: id })
21
+ data['exportJob'] ? ExportJob.new(data['exportJob'], client: client) : nil
22
+ end
23
+ end
24
+
25
+ # List export jobs for a publisher, optionally filtered by game, dataset, or status.
26
+ def list_jobs(publisher_id:, game_id: nil, dataset_id: nil, status: nil, first: nil, after: nil, cache: nil)
27
+ query = QueryBuilder.export_jobs(
28
+ publisher_id: publisher_id,
29
+ game_id: game_id,
30
+ dataset_id: dataset_id,
31
+ status: status,
32
+ first: first,
33
+ after: after
34
+ )
35
+ variables = build_variables(
36
+ publisherId: publisher_id,
37
+ gameId: game_id,
38
+ datasetId: dataset_id,
39
+ status: status,
40
+ first: first,
41
+ after: after
42
+ )
43
+ key = cache_key('exports', 'list_jobs', **variables)
44
+
45
+ data = with_cache(key, resource: :exports, cache: cache) { connection.execute(query, variables) }
46
+
47
+ Collection.new(
48
+ data['exportJobs'],
49
+ item_class: ExportJob,
50
+ next_page_loader: lambda { |cursor|
51
+ list_jobs(
52
+ publisher_id: publisher_id,
53
+ game_id: game_id,
54
+ dataset_id: dataset_id,
55
+ status: status,
56
+ first: first,
57
+ after: cursor,
58
+ cache: cache
59
+ )
60
+ },
61
+ client: client
62
+ )
63
+ end
64
+
65
+ # Refresh the signed download URL for a completed export job.
66
+ def refresh_url(id:)
67
+ data = connection.execute(QueryBuilder.export_job_refresh_url, { id: id })
68
+ ExportJob.new(data['exportJobRefreshUrl'], client: client)
69
+ end
70
+
71
+ # Cancel a pending or processing export job. Requires a server-side secret credential.
72
+ def cancel(id:)
73
+ config.require_secret_credential!('exports.cancel')
74
+
75
+ data = connection.execute(QueryBuilder.export_job_cancel, { id: id })
76
+ ExportJob.new(data['exportJobCancel'], client: client)
77
+ end
78
+
79
+ # Poll an export job until completion or failure.
80
+ def wait_for_job(id, interval: 1, timeout: 300)
81
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+
83
+ loop do
84
+ job = get_job(id, cache: false)
85
+ raise Error, "Export job '#{id}' was not found" unless job
86
+ return job if job.completed? || job.failed?
87
+
88
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
89
+ raise Error, "Timed out waiting for export job '#{id}'" if elapsed >= timeout
90
+
91
+ sleep interval
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # File upload helper resource.
6
+ class Files < Base
7
+ # Get one file by ID.
8
+ def get(id, cache: nil)
9
+ key = cache_key('files', 'get', id: id)
10
+ with_cache(key, resource: :files, cache: cache) do
11
+ data = connection.execute(QueryBuilder.file, { id: id })
12
+ data['file'] ? File.new(data['file'], client: client) : nil
13
+ end
14
+ end
15
+
16
+ # Request a presigned URL for direct upload. Requires a server-side secret credential.
17
+ def request_upload(input:)
18
+ config.require_secret_credential!('files.request_upload')
19
+
20
+ data = connection.execute(QueryBuilder.file_upload_request, { input: input })
21
+ PresignedUpload.new(data['fileUploadRequest'], client: client)
22
+ end
23
+
24
+ # Confirm that a presigned upload completed. Requires a server-side secret credential.
25
+ def confirm_upload(id:)
26
+ config.require_secret_credential!('files.confirm_upload')
27
+
28
+ data = connection.execute(QueryBuilder.file_upload_confirm, { id: id })
29
+ File.new(data['fileUploadConfirm'], client: client)
30
+ end
31
+
32
+ # Delete a file. Requires a server-side secret credential.
33
+ # rubocop:disable Naming/PredicateMethod
34
+ def delete(id:)
35
+ config.require_secret_credential!('files.delete')
36
+
37
+ data = connection.execute(QueryBuilder.file_delete, { id: id })
38
+ !!data['fileDelete']
39
+ end
40
+ # rubocop:enable Naming/PredicateMethod
41
+ end
42
+ end
43
+ end
@@ -4,6 +4,76 @@ module CardDB
4
4
  module Resources
5
5
  # Games resource for searching and fetching games
6
6
  class Games < Base
7
+ # List games for a publisher-management context.
8
+ def list(publisher_id:, first: nil, after: nil, include_archived: nil, cache: nil)
9
+ query = QueryBuilder.list_games(
10
+ publisher_id: publisher_id,
11
+ first: first,
12
+ after: after,
13
+ include_archived: include_archived
14
+ )
15
+ variables = build_variables(
16
+ publisherId: publisher_id,
17
+ first: first,
18
+ after: after,
19
+ includeArchived: include_archived
20
+ )
21
+ key = cache_key('games', 'list', **variables)
22
+
23
+ data = with_cache(key, resource: :games, cache: cache) { connection.execute(query, variables) }
24
+
25
+ Collection.new(
26
+ data['games'],
27
+ item_class: Game,
28
+ next_page_loader: lambda { |cursor|
29
+ list(publisher_id: publisher_id, first: first, after: cursor, include_archived: include_archived, cache: cache)
30
+ },
31
+ client: client
32
+ )
33
+ end
34
+
35
+ # Get a publisher-managed game by stable key.
36
+ def get_by_key(game_key:, publisher_id: nil, publisher_slug: nil, cache: nil)
37
+ key = cache_key('games', 'get_by_key', publisher_id: publisher_id, publisher_slug: publisher_slug,
38
+ game_key: game_key)
39
+ with_cache(key, resource: :games, cache: cache) do
40
+ query = QueryBuilder.game(publisher_id: publisher_id, publisher_slug: publisher_slug, game_key: game_key)
41
+ variables = build_variables(publisherId: publisher_id, publisherSlug: publisher_slug, gameKey: game_key)
42
+ data = connection.execute(query, variables)
43
+
44
+ data['game'] ? Game.new(data['game'], client: client) : nil
45
+ end
46
+ end
47
+
48
+ # Get a publisher-managed game by slug.
49
+ def get_by_slug(game_slug:, publisher_id: nil, publisher_slug: nil, cache: nil)
50
+ key = cache_key('games', 'get_by_slug', publisher_id: publisher_id, publisher_slug: publisher_slug,
51
+ game_slug: game_slug)
52
+ with_cache(key, resource: :games, cache: cache) do
53
+ query = QueryBuilder.game(publisher_id: publisher_id, publisher_slug: publisher_slug, game_slug: game_slug)
54
+ variables = build_variables(publisherId: publisher_id, publisherSlug: publisher_slug, gameSlug: game_slug)
55
+ data = connection.execute(query, variables)
56
+
57
+ data['game'] ? Game.new(data['game'], client: client) : nil
58
+ end
59
+ end
60
+
61
+ # Create a publisher-managed game. Requires a server-side secret credential.
62
+ def create(input:)
63
+ config.require_secret_credential!('games.create')
64
+
65
+ data = connection.execute(QueryBuilder.create_game, { input: input })
66
+ Game.new(data['gameCreate'], client: client)
67
+ end
68
+
69
+ # Update a publisher-managed game. Requires a server-side secret credential.
70
+ def update(id:, input:)
71
+ config.require_secret_credential!('games.update')
72
+
73
+ data = connection.execute(QueryBuilder.update_game, { id: id, input: input })
74
+ Game.new(data['gameUpdate'], client: client)
75
+ end
76
+
7
77
  # Search for games
8
78
  #
9
79
  # @param publisher_slug [String, nil] Filter by publisher slug
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Publisher-managed import format resource.
6
+ class ImportFormats < Base
7
+ # List import formats scoped by publisher or game.
8
+ def list(publisher_id: nil, game_id: nil, include_archived: nil, first: nil, after: nil, cache: nil)
9
+ query = QueryBuilder.deck_import_formats(
10
+ publisher_id: publisher_id,
11
+ game_id: game_id,
12
+ include_archived: include_archived,
13
+ first: first,
14
+ after: after
15
+ )
16
+ variables = build_variables(
17
+ publisherId: publisher_id,
18
+ gameId: game_id,
19
+ includeArchived: include_archived,
20
+ first: first,
21
+ after: after
22
+ )
23
+ key = cache_key('import_formats', 'list', **variables)
24
+
25
+ data = with_cache(key, resource: :import_formats, cache: cache) do
26
+ connection.execute(query, variables)
27
+ end
28
+
29
+ Collection.new(
30
+ data['deckImportFormats'],
31
+ item_class: DeckImportFormatDefinition,
32
+ next_page_loader: lambda { |cursor|
33
+ list(
34
+ publisher_id: publisher_id,
35
+ game_id: game_id,
36
+ include_archived: include_archived,
37
+ first: first,
38
+ after: cursor,
39
+ cache: cache
40
+ )
41
+ },
42
+ client: client
43
+ )
44
+ end
45
+
46
+ # Get one import format by UUID.
47
+ def get(id, cache: nil)
48
+ key = cache_key('import_formats', 'get', id: id)
49
+ with_cache(key, resource: :import_formats, cache: cache) do
50
+ data = connection.execute(QueryBuilder.deck_import_format, { id: id })
51
+ data['deckImportFormat'] ? DeckImportFormatDefinition.new(data['deckImportFormat'], client: client) : nil
52
+ end
53
+ end
54
+
55
+ # Get one import format by game ID and stable key.
56
+ def get_by_key(game_id:, key:, cache: nil)
57
+ cache_key_value = cache_key('import_formats', 'get_by_key', game_id: game_id, key: key)
58
+ with_cache(cache_key_value, resource: :import_formats, cache: cache) do
59
+ data = connection.execute(QueryBuilder.deck_import_format, { gameId: game_id, key: key })
60
+ data['deckImportFormat'] ? DeckImportFormatDefinition.new(data['deckImportFormat'], client: client) : nil
61
+ end
62
+ end
63
+
64
+ # Preview parsing and record resolution for a configured import format.
65
+ def test(input:)
66
+ data = connection.execute(QueryBuilder.deck_import_format_test, { input: input })
67
+ DeckImportFormatTestPayload.new(data['deckImportFormatTest'], client: client)
68
+ end
69
+
70
+ # Create an import format. Requires a server-side secret credential.
71
+ def create(input:)
72
+ config.require_secret_credential!('import_formats.create')
73
+
74
+ data = connection.execute(QueryBuilder.create_deck_import_format, { input: input })
75
+ DeckImportFormatDefinition.new(data['deckImportFormatCreate'], client: client)
76
+ end
77
+
78
+ # Update an import format. Requires a server-side secret credential.
79
+ def update(id:, input:)
80
+ config.require_secret_credential!('import_formats.update')
81
+
82
+ data = connection.execute(QueryBuilder.update_deck_import_format, { id: id, input: input })
83
+ DeckImportFormatDefinition.new(data['deckImportFormatUpdate'], client: client)
84
+ end
85
+
86
+ # Archive an import format. Requires a server-side secret credential.
87
+ def archive(id:)
88
+ config.require_secret_credential!('import_formats.archive')
89
+
90
+ data = connection.execute(QueryBuilder.archive_deck_import_format, { id: id })
91
+ DeckImportFormatDefinition.new(data['deckImportFormatArchive'], client: client)
92
+ end
93
+
94
+ # Restore an archived import format. Requires a server-side secret credential.
95
+ def unarchive(id:)
96
+ config.require_secret_credential!('import_formats.unarchive')
97
+
98
+ data = connection.execute(QueryBuilder.unarchive_deck_import_format, { id: id })
99
+ DeckImportFormatDefinition.new(data['deckImportFormatUnarchive'], client: client)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Publisher import job resource for single-dataset and game-level imports.
6
+ class Imports < Base
7
+ EMPTY_CONNECTION = {
8
+ 'totalCount' => 0,
9
+ 'pageInfo' => {
10
+ 'hasNextPage' => false,
11
+ 'hasPreviousPage' => false,
12
+ 'startCursor' => nil,
13
+ 'endCursor' => nil
14
+ },
15
+ 'edges' => []
16
+ }.freeze
17
+
18
+ # Preview a single-dataset import source without writing records or schema.
19
+ def preview(input:)
20
+ data = connection.execute(QueryBuilder.dataset_import_preview, { input: input })
21
+ DatasetImportPreviewResult.new(data['datasetImportPreview'], client: client)
22
+ end
23
+
24
+ # Validate a single-dataset import source with dry-run enforced.
25
+ def validate(input:)
26
+ data = connection.execute(QueryBuilder.dataset_import_validate, { input: input })
27
+ BulkImportResult.new(data['datasetImportValidate'], client: client)
28
+ end
29
+
30
+ # Start an async single-dataset import from fileId, data, or sourceUrl.
31
+ def run(input:)
32
+ config.require_secret_credential!('imports.run')
33
+
34
+ query, response_key = import_query_for(input)
35
+ data = connection.execute(query, { input: input })
36
+ ImportJob.new(data[response_key], client: client)
37
+ end
38
+
39
+ # Fetch one single-dataset import job.
40
+ def get_job(id, cache: nil)
41
+ key = cache_key('imports', 'get_job', id: id)
42
+ with_cache(key, resource: :imports, cache: cache) do
43
+ data = connection.execute(QueryBuilder.import_job, { id: id })
44
+ data['importJob'] ? ImportJob.new(data['importJob'], client: client) : nil
45
+ end
46
+ end
47
+
48
+ # List single-dataset import jobs for a publisher.
49
+ def list_jobs(publisher_id:, dataset_id: nil, status: nil, first: nil, after: nil, cache: nil)
50
+ query = QueryBuilder.import_jobs(
51
+ publisher_id: publisher_id,
52
+ dataset_id: dataset_id,
53
+ status: status,
54
+ first: first,
55
+ after: after
56
+ )
57
+ variables = build_variables(
58
+ publisherId: publisher_id,
59
+ datasetId: dataset_id,
60
+ status: status,
61
+ first: first,
62
+ after: after
63
+ )
64
+ key = cache_key('imports', 'list_jobs', **variables)
65
+
66
+ data = with_cache(key, resource: :imports, cache: cache) { connection.execute(query, variables) }
67
+
68
+ Collection.new(
69
+ data['importJobs'],
70
+ item_class: ImportJob,
71
+ next_page_loader: lambda { |cursor|
72
+ list_jobs(
73
+ publisher_id: publisher_id,
74
+ dataset_id: dataset_id,
75
+ status: status,
76
+ first: first,
77
+ after: cursor,
78
+ cache: cache
79
+ )
80
+ },
81
+ client: client
82
+ )
83
+ end
84
+
85
+ # Cancel a pending or processing single-dataset import job.
86
+ def cancel_job(id:)
87
+ config.require_secret_credential!('imports.cancel_job')
88
+
89
+ data = connection.execute(QueryBuilder.import_job_cancel, { id: id })
90
+ ImportJob.new(data['importJobCancel'], client: client)
91
+ end
92
+
93
+ # List structured logs for one single-dataset import job.
94
+ def list_job_logs(import_job_id:, level: nil, dataset_key: nil, first: nil, after: nil, cache: nil)
95
+ query = QueryBuilder.import_job_logs(level: level, dataset_key: dataset_key, first: first, after: after)
96
+ variables = build_variables(
97
+ importJobId: import_job_id,
98
+ level: level,
99
+ datasetKey: dataset_key,
100
+ first: first,
101
+ after: after
102
+ )
103
+ key = cache_key('imports', 'list_job_logs', **variables)
104
+
105
+ data = with_cache(key, resource: :imports, cache: cache) { connection.execute(query, variables) }
106
+ logs = data.dig('importJob', 'logs') || EMPTY_CONNECTION
107
+
108
+ Collection.new(
109
+ logs,
110
+ item_class: ImportJobLog,
111
+ next_page_loader: lambda { |cursor|
112
+ list_job_logs(
113
+ import_job_id: import_job_id,
114
+ level: level,
115
+ dataset_key: dataset_key,
116
+ first: first,
117
+ after: cursor,
118
+ cache: cache
119
+ )
120
+ },
121
+ client: client
122
+ )
123
+ end
124
+
125
+ # Poll a single-dataset import job until completion or failure.
126
+ def wait_for_job(id, interval: 1, timeout: 300)
127
+ wait_until_terminal(id, interval: interval, timeout: timeout) do
128
+ get_job(id, cache: false)
129
+ end
130
+ end
131
+
132
+ # Preview an advanced game-level import source without writing records or schema.
133
+ def preview_game(input:)
134
+ data = connection.execute(QueryBuilder.game_import_preview, { input: input })
135
+ GameImportPreview.new(data['gameImportPreview'], client: client)
136
+ end
137
+
138
+ # Start an async advanced game-level import from fileId, data, or sourceUrl.
139
+ def run_game(input:)
140
+ config.require_secret_credential!('imports.run_game')
141
+
142
+ query, response_key = game_import_query_for(input)
143
+ data = connection.execute(query, { input: input })
144
+ GameImportJob.new(data[response_key], client: client)
145
+ end
146
+
147
+ # Fetch one advanced game-level import job.
148
+ def get_game_job(id, cache: nil)
149
+ key = cache_key('imports', 'get_game_job', id: id)
150
+ with_cache(key, resource: :imports, cache: cache) do
151
+ data = connection.execute(QueryBuilder.game_import_job, { id: id })
152
+ data['gameImportJob'] ? GameImportJob.new(data['gameImportJob'], client: client) : nil
153
+ end
154
+ end
155
+
156
+ # List advanced game-level import jobs.
157
+ def list_game_jobs(game_id:, status: nil, first: nil, after: nil, cache: nil)
158
+ query = QueryBuilder.game_import_jobs(game_id: game_id, status: status, first: first, after: after)
159
+ variables = build_variables(gameId: game_id, status: status, first: first, after: after)
160
+ key = cache_key('imports', 'list_game_jobs', **variables)
161
+
162
+ data = with_cache(key, resource: :imports, cache: cache) { connection.execute(query, variables) }
163
+
164
+ Collection.new(
165
+ data['gameImportJobs'],
166
+ item_class: GameImportJob,
167
+ next_page_loader: lambda { |cursor|
168
+ list_game_jobs(game_id: game_id, status: status, first: first, after: cursor, cache: cache)
169
+ },
170
+ client: client
171
+ )
172
+ end
173
+
174
+ # Cancel a pending or processing advanced game-level import job.
175
+ def cancel_game_job(id:)
176
+ config.require_secret_credential!('imports.cancel_game_job')
177
+
178
+ data = connection.execute(QueryBuilder.game_import_job_cancel, { id: id })
179
+ GameImportJob.new(data['gameImportJobCancel'], client: client)
180
+ end
181
+
182
+ # Poll an advanced game-level import job until completion or failure.
183
+ def wait_for_game_job(id, interval: 1, timeout: 300)
184
+ wait_until_terminal(id, interval: interval, timeout: timeout) do
185
+ get_game_job(id, cache: false)
186
+ end
187
+ end
188
+
189
+ private
190
+
191
+ def import_query_for(input)
192
+ return [QueryBuilder.dataset_import_from_file, 'datasetImportFromFile'] if input_value(input, :fileId, 'fileId')
193
+ return [QueryBuilder.dataset_import_from_url, 'datasetImportFromUrl'] if input_value(input, :sourceUrl, 'sourceUrl')
194
+
195
+ [QueryBuilder.dataset_import_from_payload, 'datasetImportFromPayload']
196
+ end
197
+
198
+ def game_import_query_for(input)
199
+ return [QueryBuilder.game_import_from_file, 'gameImportFromFile'] if input_value(input, :fileId, 'fileId')
200
+ return [QueryBuilder.game_import_from_url, 'gameImportFromUrl'] if input_value(input, :sourceUrl, 'sourceUrl')
201
+
202
+ [QueryBuilder.game_import_from_payload, 'gameImportFromPayload']
203
+ end
204
+
205
+ def input_value(input, *keys)
206
+ keys.find { |key| input.key?(key) && input[key] }
207
+ end
208
+
209
+ def wait_until_terminal(id, interval:, timeout:)
210
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
211
+
212
+ loop do
213
+ job = yield
214
+ raise Error, "Job '#{id}' was not found" unless job
215
+ return job if job.completed? || job.failed?
216
+
217
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
218
+ raise Error, "Timed out waiting for job '#{id}'" if elapsed >= timeout
219
+
220
+ sleep interval
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end