🎨 Improve code readability by reformatting and updating function definitions and comments

This commit is contained in:
2026-01-28 18:18:55 +00:00
parent 1944b43af8
commit a24f766e37
154 changed files with 7261 additions and 117 deletions

View File

@@ -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
};
}