From d23a65ba2eea6c25782eaeb8b147b154d80d955f Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 14 Dec 2022 19:36:07 +0000 Subject: [PATCH] Strict GraphQL query error handling --- src/api/splatnet3.ts | 28 ++++++++++++++++++++++++---- src/cli/splatnet3/dump-album.ts | 6 +++--- src/cli/splatnet3/dump-fests.ts | 12 ++++++------ src/cli/splatnet3/dump-records.ts | 13 ++++++------- src/cli/splatnet3/dump-results.ts | 25 ++++++++++++++++--------- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/api/splatnet3.ts b/src/api/splatnet3.ts index a613b88..85face6 100644 --- a/src/api/splatnet3.ts +++ b/src/api/splatnet3.ts @@ -37,12 +37,25 @@ const AUTH_ERROR_CODES = { const REPLAY_CODE_REGEX = /^[A-Z0-9]{16}$/; +export const RequestIdSymbol = Symbol('RequestId'); +export const VariablesSymbol = Symbol('Variables'); + +export type PersistedQueryResult = GraphQLSuccessResponse & PersistedQueryResultData; + +export interface PersistedQueryResultData { + [ResponseSymbol]: Response; + [RequestIdSymbol]: KnownRequestId; + [VariablesSymbol]: {}; +} + export default class SplatNet3Api { onTokenShouldRenew: ((remaining: number, res: Response) => Promise) | null = null; onTokenExpired: ((res: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + graphql_strict = process.env.NXAPI_SPLATNET3_STRICT !== '0'; + protected constructor( public bullet_token: string, public version: string, @@ -121,7 +134,7 @@ export default class SplatNet3Api { /** @private */ _Variables extends (V extends object ? V : _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown) = (V extends object ? V : _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown), - >(id: _Id, variables: _Variables) { + >(id: _Id, variables: _Variables): Promise> { const req: GraphQLRequest<_Variables> = { variables, extensions: { @@ -135,12 +148,19 @@ export default class SplatNet3Api { const data = await this.fetch>('/graphql', 'POST', JSON.stringify(req), undefined, 'graphql query ' + id); - if (!('data' in data)) { - throw new ErrorResponse('[splatnet3] GraphQL error: ' + data.errors.map(e => e.message).join(', '), + if (!('data' in data) || (this.graphql_strict && data.errors?.length)) { + throw new ErrorResponse('[splatnet3] GraphQL error: ' + data.errors!.map(e => e.message).join(', '), data[ResponseSymbol], data); } - return data; + for (const error of data.errors ?? []) { + debugGraphQl('GraphQL error in query %s: %s', id, error.message, error); + } + + Object.defineProperty(data, RequestIdSymbol, {value: id}); + Object.defineProperty(data, VariablesSymbol, {value: variables}); + + return data as PersistedQueryResult<_Result>; } /** * */ diff --git a/src/cli/splatnet3/dump-album.ts b/src/cli/splatnet3/dump-album.ts index 7e06590..87aa6f9 100644 --- a/src/cli/splatnet3/dump-album.ts +++ b/src/cli/splatnet3/dump-album.ts @@ -3,12 +3,12 @@ import * as fs from 'node:fs/promises'; import createDebug from 'debug'; import mkdirp from 'mkdirp'; import fetch from 'node-fetch'; -import { PhotoAlbumResult, RequestId } from 'splatnet3-types/splatnet3'; +import { PhotoAlbumResult } from 'splatnet3-types/splatnet3'; import type { Arguments as ParentArguments } from '../splatnet3.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; import { getBulletToken } from '../../common/auth/splatnet3.js'; -import SplatNet3Api from '../../api/splatnet3.js'; +import SplatNet3Api, { RequestIdSymbol } from '../../api/splatnet3.js'; import { ResponseSymbol } from '../../api/util.js'; import { timeoutSignal } from '../../util/misc.js'; @@ -67,7 +67,7 @@ export async function dumpAlbumPhotos( debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.photoAlbum, - query: refresh ? RequestId.PhotoAlbumRefetchQuery : RequestId.PhotoAlbumQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); diff --git a/src/cli/splatnet3/dump-fests.ts b/src/cli/splatnet3/dump-fests.ts index 229a924..a90f4c7 100644 --- a/src/cli/splatnet3/dump-fests.ts +++ b/src/cli/splatnet3/dump-fests.ts @@ -7,7 +7,7 @@ import type { Arguments as ParentArguments } from '../splatnet3.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; import { getBulletToken } from '../../common/auth/splatnet3.js'; -import SplatNet3Api from '../../api/splatnet3.js'; +import SplatNet3Api, { RequestIdSymbol } from '../../api/splatnet3.js'; import { ResponseSymbol } from '../../api/util.js'; const debug = createDebug('cli:splatnet3:dump-records'); @@ -62,7 +62,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, await fs.writeFile(file, JSON.stringify({ result: records.data.festRecords, player: records.data.currentPlayer, - query: RequestId.FestRecordQuery, + query: records[RequestIdSymbol], app_version: splatnet.version, be_version: records[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -77,7 +77,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, const filename = 'splatnet3-fest-' + id + '-' + (fest_record.state !== FestState.CLOSED ? Date.now() + '-' : '') + - RequestId.DetailFestRecordDetailQuery + '.json'; + records[RequestIdSymbol] + '.json'; const file = path.join(directory, filename); let record: Fest_detail | null = null; @@ -96,7 +96,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, await fs.writeFile(file, JSON.stringify({ result: result.data.fest, player: result.data.currentPlayer, - query: RequestId.DetailFestRecordDetailQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -121,7 +121,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: result.data.fest, - query: RequestId.DetailVotingStatusQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -151,7 +151,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: result.data.fest, - query: RequestId.DetailFestRecordDetailQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); diff --git a/src/cli/splatnet3/dump-records.ts b/src/cli/splatnet3/dump-records.ts index fce62ec..2b66b7e 100644 --- a/src/cli/splatnet3/dump-records.ts +++ b/src/cli/splatnet3/dump-records.ts @@ -2,12 +2,11 @@ import * as path from 'node:path'; import * as fs from 'node:fs/promises'; import createDebug from 'debug'; import mkdirp from 'mkdirp'; -import { RequestId } from 'splatnet3-types/splatnet3'; import type { Arguments as ParentArguments } from '../splatnet3.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; import { getBulletToken } from '../../common/auth/splatnet3.js'; -import SplatNet3Api from '../../api/splatnet3.js'; +import SplatNet3Api, { RequestIdSymbol } from '../../api/splatnet3.js'; import { ResponseSymbol } from '../../api/util.js'; const debug = createDebug('cli:splatnet3:dump-records'); @@ -96,7 +95,7 @@ export async function dumpHistoryRecords(splatnet: SplatNet3Api, directory: stri await fs.writeFile(file, JSON.stringify({ result: results.data.playHistory, player: results.data.currentPlayer, - query: refresh ? RequestId.HistoryRecordRefetchQuery : RequestId.HistoryRecordQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -114,7 +113,7 @@ export async function dumpHeroRecords(splatnet: SplatNet3Api, directory: string) debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.heroRecord, - query: RequestId.HeroHistoryQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -134,7 +133,7 @@ export async function dumpCatalogRecords(splatnet: SplatNet3Api, directory: stri debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.catalog, - query: refresh ? RequestId.CatalogRefetchQuery : RequestId.CatalogQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -154,7 +153,7 @@ export async function dumpStageStats(splatnet: SplatNet3Api, directory: string, debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.stageRecords, - query: refresh ? RequestId.StageRecordsRefetchQuery : RequestId.StageRecordQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -174,7 +173,7 @@ export async function dumpWeaponStats(splatnet: SplatNet3Api, directory: string, debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.weaponRecords, - query: refresh ? RequestId.WeaponRecordsRefetchQuery : RequestId.WeaponRecordQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); diff --git a/src/cli/splatnet3/dump-results.ts b/src/cli/splatnet3/dump-results.ts index 7c005e7..a7487fd 100644 --- a/src/cli/splatnet3/dump-results.ts +++ b/src/cli/splatnet3/dump-results.ts @@ -7,7 +7,7 @@ import type { Arguments as ParentArguments } from '../splatnet3.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; import { getBulletToken } from '../../common/auth/splatnet3.js'; -import SplatNet3Api from '../../api/splatnet3.js'; +import SplatNet3Api, { RequestIdSymbol } from '../../api/splatnet3.js'; import { ResponseSymbol } from '../../api/util.js'; import { dumpCatalogRecords, dumpHistoryRecords } from './dump-records.js'; @@ -101,7 +101,7 @@ export async function dumpResults( await fs.writeFile(file, JSON.stringify({ player: player ? { result: player.data.currentPlayer, - query: RequestId.BattleHistoryCurrentPlayerQuery, + query: player[RequestIdSymbol], be_version: player[ResponseSymbol].headers.get('x-be-version'), } : undefined, latestBattleHistories: { @@ -109,23 +109,30 @@ export async function dumpResults( fest: battles.data.currentFest, player: 'currentPlayer' in battles.data ? (battles.data as LatestBattleHistoriesRefetchResult).currentPlayer : undefined, - query: refresh ? RequestId.LatestBattleHistoriesRefetchQuery : RequestId.LatestBattleHistoriesQuery, + query: battles[RequestIdSymbol], be_version: battles[ResponseSymbol].headers.get('x-be-version'), }, regularBattleHistories: { result: battles_regular.data.regularBattleHistories, player: 'currentPlayer' in battles_regular.data ? (battles_regular.data as RegularBattleHistoriesRefetchResult).currentPlayer : undefined, - query: refresh ? RequestId.RegularBattleHistoriesRefetchQuery : RequestId.RegularBattleHistoriesQuery, + query: battles_regular[RequestIdSymbol], be_version: battles_regular[ResponseSymbol].headers.get('x-be-version'), }, bankaraBattleHistories: { result: battles_anarchy.data.bankaraBattleHistories, player: 'currentPlayer' in battles_anarchy.data ? (battles_anarchy.data as BankaraBattleHistoriesRefetchResult).currentPlayer : undefined, - query: refresh ? RequestId.BankaraBattleHistoriesRefetchQuery : RequestId.BankaraBattleHistoriesQuery, + query: battles_anarchy[RequestIdSymbol], be_version: battles_anarchy[ResponseSymbol].headers.get('x-be-version'), }, + xBattleHistories: { + result: battles_xmatch.data.xBattleHistories, + player: 'currentPlayer' in battles_xmatch.data ? + (battles_xmatch.data as XBattleHistoriesRefetchResult).currentPlayer : undefined, + query: battles_xmatch[RequestIdSymbol], + be_version: battles_xmatch[ResponseSymbol].headers.get('x-be-version'), + }, privateBattleHistories: { result: battles_private.data.privateBattleHistories, player: 'currentPlayer' in battles_private.data ? @@ -164,7 +171,7 @@ export async function dumpResults( debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: result.data.vsHistoryDetail, - query: RequestId.VsHistoryDetailQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -201,7 +208,7 @@ export async function dumpResults( debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: result.data.vsHistoryDetail, - query: RequestId.VsHistoryDetailQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -243,7 +250,7 @@ export async function dumpCoopResults( debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: results.data.coopResult, - query: refresh ? RequestId.RefetchableCoopHistory_CoopResultQuery : RequestId.CoopHistoryQuery, + query: results[RequestIdSymbol], app_version: splatnet.version, be_version: results[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8'); @@ -272,7 +279,7 @@ export async function dumpCoopResults( debug('Writing %s', filename); await fs.writeFile(file, JSON.stringify({ result: result.data.coopHistoryDetail, - query: RequestId.CoopHistoryDetailQuery, + query: result[RequestIdSymbol], app_version: splatnet.version, be_version: result[ResponseSymbol].headers.get('x-be-version'), }, null, 4) + '\n', 'utf-8');