🎨 Improve code readability by reformatting and updating function definitions and comments
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* Challonge API v2.1 Service
|
||||
* Client for interacting with Challonge API v2.1 (current version)
|
||||
*
|
||||
* Features:
|
||||
* - OAuth 2.0 support (Bearer tokens)
|
||||
* - API v1 key compatibility
|
||||
* - JSON:API specification compliant
|
||||
* - Tournament, Participant, Match, Race endpoints
|
||||
* - Community and Application scoping
|
||||
*
|
||||
* @see https://challonge.apidog.io/getting-started-1726706m0
|
||||
* @see https://challonge.apidog.io/llms.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the appropriate base URL based on environment
|
||||
*/
|
||||
function getBaseURL() {
|
||||
if (import.meta.env.DEV) {
|
||||
return '/api/challonge/v2.1';
|
||||
}
|
||||
return 'https://api.challonge.com/v2.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication types for Challonge API v2.1
|
||||
*/
|
||||
export const AuthType = {
|
||||
OAUTH: 'v2', // Bearer token
|
||||
API_KEY: 'v1' // Legacy API key
|
||||
};
|
||||
|
||||
/**
|
||||
* Resource scoping options
|
||||
*/
|
||||
export const ScopeType = {
|
||||
USER: 'user', // /v2.1/tournaments (default)
|
||||
COMMUNITY: 'community', // /v2.1/communities/{id}/tournaments
|
||||
APPLICATION: 'app' // /v2.1/application/tournaments
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Challonge API v2.1 client
|
||||
*
|
||||
* @param {Object} auth - Authentication configuration
|
||||
* @param {string} auth.token - OAuth Bearer token or API v1 key
|
||||
* @param {string} auth.type - AuthType.OAUTH or AuthType.API_KEY (default: API_KEY)
|
||||
* @param {Object} options - Client options
|
||||
* @param {string} options.communityId - Default community ID for scoping
|
||||
* @param {boolean} options.debug - Enable debug logging
|
||||
* @returns {Object} API client with methods
|
||||
*/
|
||||
export function createChallongeV2Client(auth, options = {}) {
|
||||
const { token, type = AuthType.API_KEY } = auth;
|
||||
const { communityId: defaultCommunityId, debug = false } = options;
|
||||
const baseURL = getBaseURL();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Authentication token is required');
|
||||
}
|
||||
|
||||
// Request tracking for debug mode
|
||||
let requestCount = 0;
|
||||
|
||||
/**
|
||||
* Make API request with JSON:API format
|
||||
*/
|
||||
async function makeRequest(endpoint, options = {}) {
|
||||
const {
|
||||
method = 'GET',
|
||||
body,
|
||||
params = {},
|
||||
headers = {},
|
||||
communityId = defaultCommunityId,
|
||||
scopeType = ScopeType.USER
|
||||
} = options;
|
||||
|
||||
const startTime = performance.now();
|
||||
requestCount++;
|
||||
|
||||
// Build URL with scoping
|
||||
let url = baseURL;
|
||||
|
||||
if (scopeType === ScopeType.COMMUNITY && communityId) {
|
||||
url += `/communities/${communityId}`;
|
||||
} else if (scopeType === ScopeType.APPLICATION) {
|
||||
url += '/application';
|
||||
}
|
||||
|
||||
url += `/${endpoint}`;
|
||||
|
||||
// Add query parameters
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
urlObj.searchParams.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Add communityId as query param if not in path
|
||||
if (communityId && scopeType === ScopeType.USER) {
|
||||
urlObj.searchParams.append('community_id', communityId);
|
||||
}
|
||||
|
||||
// Prepare headers (JSON:API required format)
|
||||
const requestHeaders = {
|
||||
'Content-Type': 'application/vnd.api+json',
|
||||
Accept: 'application/json',
|
||||
'Authorization-Type': type,
|
||||
...headers
|
||||
};
|
||||
|
||||
// Add authorization header
|
||||
if (type === AuthType.OAUTH) {
|
||||
requestHeaders['Authorization'] = `Bearer ${token}`;
|
||||
} else {
|
||||
requestHeaders['Authorization'] = token;
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers: requestHeaders
|
||||
};
|
||||
|
||||
if (body && method !== 'GET') {
|
||||
// Wrap in JSON:API format if not already wrapped
|
||||
const jsonApiBody = body.data ? body : { data: body };
|
||||
fetchOptions.body = JSON.stringify(jsonApiBody);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[Challonge v2.1 Request #${requestCount}]`,
|
||||
method,
|
||||
urlObj.toString()
|
||||
);
|
||||
if (body) console.log('Body:', fetchOptions.body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(urlObj.toString(), fetchOptions);
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
if (debug)
|
||||
console.log(
|
||||
`[Challonge v2.1 Response] 204 No Content (${duration.toFixed(0)}ms)`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, create an error with the status
|
||||
if (debug)
|
||||
console.error('[Challonge v2.1 JSON Parse Error]', parseError);
|
||||
const error = new Error(
|
||||
`HTTP ${response.status}: Failed to parse response`
|
||||
);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(
|
||||
`[Challonge v2.1 Response] ${response.status} (${duration.toFixed(0)}ms)`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// Handle JSON:API errors
|
||||
if (!response.ok) {
|
||||
if (data.errors && Array.isArray(data.errors)) {
|
||||
const errorDetails = data.errors.map(e => ({
|
||||
status: e.status || response.status,
|
||||
message: e.detail || e.title || response.statusText,
|
||||
field: e.source?.pointer
|
||||
}));
|
||||
|
||||
const errorMessage = errorDetails
|
||||
.map(
|
||||
e => `${e.status}: ${e.message}${e.field ? ` (${e.field})` : ''}`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const error = new Error(errorMessage);
|
||||
error.errors = errorDetails;
|
||||
error.response = data;
|
||||
throw error;
|
||||
}
|
||||
// Handle non-JSON:API error format
|
||||
const error = new Error(
|
||||
`HTTP ${response.status}: ${data.message || response.statusText}`
|
||||
);
|
||||
error.status = response.status;
|
||||
error.response = data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (debug) {
|
||||
console.error('[Challonge v2.1 Error]', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to unwrap JSON:API response and normalize structure
|
||||
*/
|
||||
function unwrapResponse(response) {
|
||||
if (!response) return null;
|
||||
|
||||
// If response has data property, it's JSON:API format
|
||||
if (response.data) {
|
||||
const data = response.data;
|
||||
|
||||
// Handle array of resources
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(item => normalizeResource(item));
|
||||
}
|
||||
|
||||
// Handle single resource
|
||||
return normalizeResource(data);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize JSON:API resource to flat structure
|
||||
*/
|
||||
function normalizeResource(resource) {
|
||||
if (!resource || !resource.attributes) return resource;
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
type: resource.type,
|
||||
...resource.attributes,
|
||||
relationships: resource.relationships,
|
||||
links: resource.links
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Tournament Methods ====================
|
||||
const tournaments = {
|
||||
/**
|
||||
* List tournaments
|
||||
* @param {Object} options - Query options
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
list: async (options = {}) => {
|
||||
const { communityId, scopeType, ...params } = options;
|
||||
const response = await makeRequest('tournaments.json', {
|
||||
params,
|
||||
communityId,
|
||||
scopeType
|
||||
});
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tournament details
|
||||
* @param {string} id - Tournament ID or URL
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
get: async (id, options = {}) => {
|
||||
const { communityId, scopeType, ifNoneMatch } = options;
|
||||
const response = await makeRequest(`tournaments/${id}.json`, {
|
||||
communityId,
|
||||
scopeType,
|
||||
headers: ifNoneMatch ? { 'If-None-Match': ifNoneMatch } : {}
|
||||
});
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create tournament
|
||||
* @param {Object} data - Tournament data
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
create: async (data, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest('tournaments.json', {
|
||||
method: 'POST',
|
||||
body: { type: 'Tournaments', attributes: data },
|
||||
communityId,
|
||||
scopeType
|
||||
});
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update tournament
|
||||
* @param {string} id - Tournament ID
|
||||
* @param {Object} data - Updated fields
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
update: async (id, data, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(`tournaments/${id}.json`, {
|
||||
method: 'PUT',
|
||||
body: { type: 'Tournaments', attributes: data },
|
||||
communityId,
|
||||
scopeType
|
||||
});
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete tournament
|
||||
* @param {string} id - Tournament ID
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
delete: async (id, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
return await makeRequest(`tournaments/${id}.json`, {
|
||||
method: 'DELETE',
|
||||
communityId,
|
||||
scopeType
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Change tournament state
|
||||
* @param {string} id - Tournament ID
|
||||
* @param {string} state - New state
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
changeState: async (id, state, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${id}/change_state.json`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: { type: 'TournamentState', attributes: { state } },
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
// Convenience methods
|
||||
start: (id, options) => tournaments.changeState(id, 'start', options),
|
||||
finalize: (id, options) => tournaments.changeState(id, 'finalize', options),
|
||||
reset: (id, options) => tournaments.changeState(id, 'reset', options),
|
||||
processCheckIn: (id, options) =>
|
||||
tournaments.changeState(id, 'process_checkin', options)
|
||||
};
|
||||
|
||||
// ==================== Participant Methods ====================
|
||||
const participants = {
|
||||
list: async (tournamentId, options = {}) => {
|
||||
const { communityId, scopeType, page, per_page, ifNoneMatch } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants.json`,
|
||||
{
|
||||
params: { page, per_page },
|
||||
communityId,
|
||||
scopeType,
|
||||
headers: ifNoneMatch ? { 'If-None-Match': ifNoneMatch } : {}
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
get: async (tournamentId, participantId, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/${participantId}.json`,
|
||||
{ communityId, scopeType }
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
create: async (tournamentId, data, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants.json`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { type: 'Participants', attributes: data },
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
update: async (tournamentId, participantId, data, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/${participantId}.json`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: { type: 'Participants', attributes: data },
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
delete: async (tournamentId, participantId, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
return await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/${participantId}.json`,
|
||||
{ method: 'DELETE', communityId, scopeType }
|
||||
);
|
||||
},
|
||||
|
||||
bulkAdd: async (tournamentId, participantsData, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/bulk_add.json`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
type: 'Participants',
|
||||
attributes: { participants: participantsData }
|
||||
},
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
clear: async (tournamentId, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
return await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/clear.json`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
randomize: async (tournamentId, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/participants/randomize.json`,
|
||||
{ method: 'PUT', communityId, scopeType }
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Match Methods ====================
|
||||
const matches = {
|
||||
list: async (tournamentId, options = {}) => {
|
||||
const {
|
||||
communityId,
|
||||
scopeType,
|
||||
state,
|
||||
participant_id,
|
||||
page,
|
||||
per_page,
|
||||
ifNoneMatch
|
||||
} = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/matches.json`,
|
||||
{
|
||||
params: { state, participant_id, page, per_page },
|
||||
communityId,
|
||||
scopeType,
|
||||
headers: ifNoneMatch ? { 'If-None-Match': ifNoneMatch } : {}
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
get: async (tournamentId, matchId, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/matches/${matchId}.json`,
|
||||
{ communityId, scopeType }
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
update: async (tournamentId, matchId, data, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/matches/${matchId}.json`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: { type: 'Match', attributes: data },
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
changeState: async (tournamentId, matchId, state, options = {}) => {
|
||||
const { communityId, scopeType } = options;
|
||||
const response = await makeRequest(
|
||||
`tournaments/${tournamentId}/matches/${matchId}/change_state.json`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: { type: 'MatchState', attributes: { state } },
|
||||
communityId,
|
||||
scopeType
|
||||
}
|
||||
);
|
||||
return unwrapResponse(response);
|
||||
},
|
||||
|
||||
reopen: (tournamentId, matchId, options) =>
|
||||
matches.changeState(tournamentId, matchId, 'reopen', options),
|
||||
markAsUnderway: (tournamentId, matchId, options) =>
|
||||
matches.changeState(tournamentId, matchId, 'mark_as_underway', options)
|
||||
};
|
||||
|
||||
// ==================== User & Community Methods ====================
|
||||
const user = {
|
||||
getMe: async () => {
|
||||
const response = await makeRequest('me.json');
|
||||
return unwrapResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
const communities = {
|
||||
list: async () => {
|
||||
const response = await makeRequest('communities.json');
|
||||
return unwrapResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tournaments,
|
||||
participants,
|
||||
matches,
|
||||
user,
|
||||
communities,
|
||||
// Expose request count for debugging
|
||||
getRequestCount: () => requestCount
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user