diff --git a/packages/apisports/api.test.ts b/packages/apisports/api.test.ts new file mode 100644 index 000000000..f473d647c --- /dev/null +++ b/packages/apisports/api.test.ts @@ -0,0 +1,126 @@ +import 'dotenv/config'; +import { + ApiSportsAPIError, + makeApiSportsRequest, + normalizeQuery, +} from './client'; +import { API_SPORTS_ROUTES } from './endpoints/routes'; +import type { ApiSportsResponse } from './endpoints/types'; +import { + ApiSportsEndpointOutputSchemas, + ApiSportsResponseSchema, +} from './endpoints/types'; + +const TEST_API_KEY = process.env.API_SPORTS_API_KEY; + +describe('normalizeQuery', () => { + it('joins array query params with hyphens', () => { + expect(normalizeQuery({ ids: [1208002, 1208003] })).toEqual({ + ids: '1208002-1208003', + }); + expect(normalizeQuery({ ids: [1, 2], live: true })).toEqual({ + ids: '1-2', + live: true, + }); + }); +}); + +describe('API-Sports auth body errors', () => { + it('throws auth body errors for invalid key', async () => { + await expect( + makeApiSportsRequest('football', '/countries', { + apiKey: 'invalid', + }), + ).rejects.toThrow(/token|application key/i); + }); +}); + +(TEST_API_KEY ? describe : describe.skip)('API-Sports API Type Tests', () => { + if (!TEST_API_KEY) { + console.warn('Skipping: API_SPORTS_API_KEY not set'); + } + + it('football countries returns correct type', async () => { + const response = await makeApiSportsRequest( + 'football', + '/countries', + { apiKey: TEST_API_KEY }, + ); + + ApiSportsEndpointOutputSchemas.getCountries.parse(response); + expect(response.response).toBeDefined(); + }); + + it('football timezone returns correct type', async () => { + const response = await makeApiSportsRequest( + 'football', + '/timezone', + { apiKey: TEST_API_KEY }, + ); + + ApiSportsEndpointOutputSchemas.getTimezone.parse(response); + expect(response.response).toBeDefined(); + }); + + it('nba status returns correct type', async () => { + const response = await makeApiSportsRequest( + 'nba', + '/status', + { apiKey: TEST_API_KEY }, + ); + + ApiSportsResponseSchema.parse(response); + expect(response.response).toBeDefined(); + }); + + it('standings stages route hits football host', async () => { + const route = API_SPORTS_ROUTES.getStandingsStages; + expect(route.sport).toBe('football'); + + const response = await makeApiSportsRequest( + route.sport, + route.path, + { apiKey: TEST_API_KEY, query: { league: 39, season: 2023 } }, + ); + + ApiSportsEndpointOutputSchemas.getStandingsStages.parse(response); + expect(Array.isArray(response.response) || response.response).toBeTruthy(); + }); + + it('standings groups route hits football host', async () => { + const route = API_SPORTS_ROUTES.getStandingsGroups; + expect(route.sport).toBe('football'); + + const response = await makeApiSportsRequest( + route.sport, + route.path, + { apiKey: TEST_API_KEY, query: { league: 39, season: 2023 } }, + ); + + ApiSportsEndpointOutputSchemas.getStandingsGroups.parse(response); + }); + + it('games events route hits nfl host', async () => { + const route = API_SPORTS_ROUTES.getGamesEvents; + expect(route.sport).toBe('nfl'); + + const response = await makeApiSportsRequest( + route.sport, + route.path, + { apiKey: TEST_API_KEY, query: { id: 1 } }, + ); + + ApiSportsEndpointOutputSchemas.getGamesEvents.parse(response); + }); + + it('throws on API-Sports body errors', async () => { + const error = await makeApiSportsRequest( + 'nba', + '/games/events', + { apiKey: TEST_API_KEY }, + ).catch((e: unknown) => e); + + expect(error).toBeInstanceOf(ApiSportsAPIError); + expect((error as Error).message).toMatch(/endpoint/i); + }); +}); diff --git a/packages/apisports/client.ts b/packages/apisports/client.ts new file mode 100644 index 000000000..8fa6de705 --- /dev/null +++ b/packages/apisports/client.ts @@ -0,0 +1,165 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; + +export class ApiSportsAPIError extends Error { + public readonly status?: number; + public readonly statusText?: string; + // API error bodies vary by sport endpoint; unknown forces callers to narrow before use. + public readonly body?: unknown; + public readonly retryAfter?: number; + + constructor( + message: string, + public readonly code?: number, + options?: { cause?: Error; body?: unknown }, + ) { + super(message, options); + this.name = 'ApiSportsAPIError'; + + if (options?.cause instanceof ApiError) { + this.status = options.cause.status; + this.statusText = options.cause.statusText; + this.body = options.cause.body; + this.retryAfter = options.cause.retryAfter; + } else if (options?.body !== undefined) { + this.body = options.body; + } + } +} + +export type ApiSport = + | 'football' + | 'basketball' + | 'nba' + | 'afl' + | 'baseball' + | 'formula1' + | 'mma' + | 'nfl'; + +export const API_SPORTS_BASE_URLS: Record = { + football: 'https://v3.football.api-sports.io', + basketball: 'https://v1.basketball.api-sports.io', + nba: 'https://v2.nba.api-sports.io', + afl: 'https://v1.afl.api-sports.io', + baseball: 'https://v1.baseball.api-sports.io', + formula1: 'https://v1.formula-1.api-sports.io', + mma: 'https://v1.mma.api-sports.io', + nfl: 'https://v1.american-football.api-sports.io', +}; + +export type ApiSportsQueryValue = + | string + | number + | boolean + | Array + | undefined; + +/** API-Sports multi-id filters expect `ids=1-2-3`, not repeated keys. */ +export function normalizeQuery( + query: Record, +): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(query)) { + if (value === undefined) continue; + if (Array.isArray(value)) { + out[key] = value.join('-'); + } else { + out[key] = value; + } + } + return out; +} + +function formatApiSportsErrors(errors: unknown): string { + if (Array.isArray(errors)) { + return errors + .map((e) => (typeof e === 'string' ? e : JSON.stringify(e))) + .join(', '); + } + if (errors && typeof errors === 'object') { + return Object.entries(errors as Record) + .map(([k, v]) => `${k}: ${String(v)}`) + .join(', '); + } + return 'API-Sports request failed'; +} + +function hasApiSportsErrors(errors: unknown): boolean { + if (errors == null) return false; + if (Array.isArray(errors)) return errors.length > 0; + if (typeof errors === 'object') + return Object.keys(errors as object).length > 0; + return true; +} + +// Catch values are untyped at runtime; narrow to ApiError/Error before rethrowing. +async function handleRequestError(error: unknown): Promise { + if (error instanceof ApiError) { + throw new ApiSportsAPIError(error.message, error.status, { + cause: error, + }); + } + if (error instanceof Error) { + throw new ApiSportsAPIError(error.message, undefined, { cause: error }); + } + throw new ApiSportsAPIError('Unknown error'); +} + +/** + * Performs a GET request to a sport-specific API-Sports REST API. + * + * Auth: API key via the `x-apisports-key` request header. + */ +export async function makeApiSportsRequest( + sport: ApiSport, + path: string, + options: { + apiKey?: string; + query?: Record; + } = {}, +): Promise { + const { apiKey, query = {} } = options; + + const config: OpenAPIConfig = { + BASE: API_SPORTS_BASE_URLS[sport], + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: undefined, + HEADERS: { + Accept: 'application/json', + ...(apiKey ? { 'x-apisports-key': apiKey } : {}), + }, + }; + + const requestOptions: ApiRequestOptions = { + method: 'GET', + url: path, + query: normalizeQuery(query), + }; + + try { + const response = await request(config, requestOptions); + // API-Sports returns HTTP 200 with a non-empty `errors` object/array on failures. + if ( + response && + typeof response === 'object' && + 'errors' in response && + hasApiSportsErrors((response as { errors?: unknown }).errors) + ) { + const body = response as { errors?: unknown }; + throw new ApiSportsAPIError( + formatApiSportsErrors(body.errors), + undefined, + { + body, + }, + ); + } + return response; + } catch (error) { + if (error instanceof ApiSportsAPIError) throw error; + return handleRequestError(error); + } +} diff --git a/packages/apisports/endpoints/afl.ts b/packages/apisports/endpoints/afl.ts new file mode 100644 index 000000000..b49ad3292 --- /dev/null +++ b/packages/apisports/endpoints/afl.ts @@ -0,0 +1,91 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get AFL Seasons */ +export const getAflSeasons: ApiSportsEndpoints['getAflSeasons'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getAflSeasons; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getAflSeasons'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.afl.getAflSeasons', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get AFL Games */ +export const getAflGames: ApiSportsEndpoints['getAflGames'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getAflGames; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getAflGames'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.afl.getAflGames', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get AFL Games Quarters */ +export const getAflGamesQuarters: ApiSportsEndpoints['getAflGamesQuarters'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getAflGamesQuarters; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getAflGamesQuarters'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.afl.getAflGamesQuarters', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get AFL Game Player Statistics */ +export const getAflGamePlayerStatistics: ApiSportsEndpoints['getAflGamePlayerStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getAflGamePlayerStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getAflGamePlayerStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.afl.getAflGamePlayerStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get AFL Standings */ +export const getAflStandings: ApiSportsEndpoints['getAflStandings'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getAflStandings; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getAflStandings'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.afl.getAflStandings', + input ?? {}, + 'completed', + ); + return response; +}; diff --git a/packages/apisports/endpoints/baseball.ts b/packages/apisports/endpoints/baseball.ts new file mode 100644 index 000000000..a231635bd --- /dev/null +++ b/packages/apisports/endpoints/baseball.ts @@ -0,0 +1,21 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Baseball Games Head-to-Head */ +export const getBaseballGamesHeadToHead: ApiSportsEndpoints['getBaseballGamesHeadToHead'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getBaseballGamesHeadToHead; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getBaseballGamesHeadToHead'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.baseball.getBaseballGamesHeadToHead', + input ?? {}, + 'completed', + ); + return response; + }; diff --git a/packages/apisports/endpoints/basketball.ts b/packages/apisports/endpoints/basketball.ts new file mode 100644 index 000000000..a62f18d19 --- /dev/null +++ b/packages/apisports/endpoints/basketball.ts @@ -0,0 +1,119 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Basketball Statistics */ +export const getBasketballStatistics: ApiSportsEndpoints['getBasketballStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getBasketballStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getBasketballStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getBasketballStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Basketball Bets */ +export const getBasketballBets: ApiSportsEndpoints['getBasketballBets'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getBasketballBets; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getBasketballBets'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getBasketballBets', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Basketball Bookmakers */ +export const getBasketballBookmakers: ApiSportsEndpoints['getBasketballBookmakers'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getBasketballBookmakers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getBasketballBookmakers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getBasketballBookmakers', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get NBA Game Statistics */ +export const getNbaGameStatistics: ApiSportsEndpoints['getNbaGameStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getNbaGameStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getNbaGameStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getNbaGameStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Player Statistics */ +export const getPlayerStatistics: ApiSportsEndpoints['getPlayerStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayerStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayerStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getPlayerStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Game Statistics by Teams */ +export const getGameStatisticsByTeams: ApiSportsEndpoints['getGameStatisticsByTeams'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getGameStatisticsByTeams; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getGameStatisticsByTeams'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getGameStatisticsByTeams', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Games Events */ +export const getGamesEvents: ApiSportsEndpoints['getGamesEvents'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getGamesEvents; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getGamesEvents'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.basketball.getGamesEvents', + input ?? {}, + 'completed', + ); + return response; +}; diff --git a/packages/apisports/endpoints/core.ts b/packages/apisports/endpoints/core.ts new file mode 100644 index 000000000..b38d6485e --- /dev/null +++ b/packages/apisports/endpoints/core.ts @@ -0,0 +1,252 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Countries */ +export const getCountries: ApiSportsEndpoints['getCountries'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getCountries; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getCountries'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getCountries', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Timezone */ +export const getTimezone: ApiSportsEndpoints['getTimezone'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getTimezone; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTimezone'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTimezone', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Leagues */ +export const getLeagues: ApiSportsEndpoints['getLeagues'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getLeagues; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getLeagues'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getLeagues', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get League Seasons */ +export const getLeagueSeasons: ApiSportsEndpoints['getLeagueSeasons'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getLeagueSeasons; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getLeagueSeasons'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getLeagueSeasons', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Teams */ +export const getTeams: ApiSportsEndpoints['getTeams'] = async (ctx, input) => { + const route = API_SPORTS_ROUTES.getTeams; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTeams'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTeams', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Team Seasons */ +export const getTeamSeasons: ApiSportsEndpoints['getTeamSeasons'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getTeamSeasons; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTeamSeasons'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTeamSeasons', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Team Statistics */ +export const getTeamStatistics: ApiSportsEndpoints['getTeamStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getTeamStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTeamStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTeamStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Venues */ +export const getVenues: ApiSportsEndpoints['getVenues'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getVenues; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getVenues'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getVenues', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Coaches */ +export const getCoaches: ApiSportsEndpoints['getCoaches'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getCoaches; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getCoaches'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getCoaches', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Injuries */ +export const getInjuries: ApiSportsEndpoints['getInjuries'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getInjuries; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getInjuries'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getInjuries', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Sidelined */ +export const getSidelined: ApiSportsEndpoints['getSidelined'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getSidelined; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getSidelined'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getSidelined', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Transfers */ +export const getTransfers: ApiSportsEndpoints['getTransfers'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getTransfers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTransfers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTransfers', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Trophies */ +export const getTrophies: ApiSportsEndpoints['getTrophies'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getTrophies; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getTrophies'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getTrophies', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Predictions */ +export const getPredictions: ApiSportsEndpoints['getPredictions'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getPredictions; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPredictions'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.core.getPredictions', + input ?? {}, + 'completed', + ); + return response; +}; diff --git a/packages/apisports/endpoints/fixtures.ts b/packages/apisports/endpoints/fixtures.ts new file mode 100644 index 000000000..191d48b81 --- /dev/null +++ b/packages/apisports/endpoints/fixtures.ts @@ -0,0 +1,119 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Fixtures */ +export const getFixtures: ApiSportsEndpoints['getFixtures'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getFixtures; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixtures'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixtures', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Fixtures Rounds */ +export const getFixturesRounds: ApiSportsEndpoints['getFixturesRounds'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFixturesRounds; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixturesRounds'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixturesRounds', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Head-to-Head Fixtures */ +export const getHeadToHeadFixtures: ApiSportsEndpoints['getHeadToHeadFixtures'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getHeadToHeadFixtures; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getHeadToHeadFixtures'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getHeadToHeadFixtures', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fixture Lineups */ +export const getFixtureLineups: ApiSportsEndpoints['getFixtureLineups'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFixtureLineups; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixtureLineups'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixtureLineups', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fixture Statistics */ +export const getFixtureStatistics: ApiSportsEndpoints['getFixtureStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFixtureStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixtureStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixtureStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fixtures Events */ +export const getFixturesEvents: ApiSportsEndpoints['getFixturesEvents'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFixturesEvents; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixturesEvents'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixturesEvents', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fixtures Players */ +export const getFixturesPlayers: ApiSportsEndpoints['getFixturesPlayers'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFixturesPlayers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFixturesPlayers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.fixtures.getFixturesPlayers', + input ?? {}, + 'completed', + ); + return response; + }; diff --git a/packages/apisports/endpoints/formula1.ts b/packages/apisports/endpoints/formula1.ts new file mode 100644 index 000000000..5d17f68d6 --- /dev/null +++ b/packages/apisports/endpoints/formula1.ts @@ -0,0 +1,137 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Formula 1 Circuits */ +export const getFormula1Circuits: ApiSportsEndpoints['getFormula1Circuits'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFormula1Circuits; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1Circuits'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1Circuits', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Formula 1 Competitions */ +export const getFormula1Competitions: ApiSportsEndpoints['getFormula1Competitions'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFormula1Competitions; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1Competitions'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1Competitions', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Formula 1 Races */ +export const getFormula1Races: ApiSportsEndpoints['getFormula1Races'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getFormula1Races; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1Races'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1Races', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Formula 1 Driver Rankings */ +export const getFormula1DriverRankings: ApiSportsEndpoints['getFormula1DriverRankings'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFormula1DriverRankings; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1DriverRankings'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1DriverRankings', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Formula 1 Team Rankings */ +export const getFormula1TeamRankings: ApiSportsEndpoints['getFormula1TeamRankings'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFormula1TeamRankings; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1TeamRankings'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1TeamRankings', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Formula 1 Starting Grid */ +export const getFormula1StartingGrid: ApiSportsEndpoints['getFormula1StartingGrid'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFormula1StartingGrid; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFormula1StartingGrid'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFormula1StartingGrid', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fastest Laps Rankings */ +export const getFastestLapsRankings: ApiSportsEndpoints['getFastestLapsRankings'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFastestLapsRankings; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFastestLapsRankings'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getFastestLapsRankings', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Race Rankings */ +export const getRaceRankings: ApiSportsEndpoints['getRaceRankings'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getRaceRankings; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getRaceRankings'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.formula1.getRaceRankings', + input ?? {}, + 'completed', + ); + return response; +}; diff --git a/packages/apisports/endpoints/index.ts b/packages/apisports/endpoints/index.ts new file mode 100644 index 000000000..64770e6f1 --- /dev/null +++ b/packages/apisports/endpoints/index.ts @@ -0,0 +1,12 @@ +export * as Afl from './afl'; +export * as Baseball from './baseball'; +export * as Basketball from './basketball'; +export * as Core from './core'; +export * as Fixtures from './fixtures'; +export * as Formula1 from './formula1'; +export * as Mma from './mma'; +export * as Odds from './odds'; +export * as Players from './players'; +export * from './routes'; +export * as Standings from './standings'; +export * from './types'; diff --git a/packages/apisports/endpoints/mma.ts b/packages/apisports/endpoints/mma.ts new file mode 100644 index 000000000..ee38d866a --- /dev/null +++ b/packages/apisports/endpoints/mma.ts @@ -0,0 +1,107 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get MMA Categories */ +export const getMmaCategories: ApiSportsEndpoints['getMmaCategories'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getMmaCategories; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getMmaCategories'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getMmaCategories', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get MMA Fighters */ +export const getMmaFighters: ApiSportsEndpoints['getMmaFighters'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getMmaFighters; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getMmaFighters'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getMmaFighters', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get MMA Fights */ +export const getMmaFights: ApiSportsEndpoints['getMmaFights'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getMmaFights; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getMmaFights'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getMmaFights', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get MMA Fight Results */ +export const getMmaFightResults: ApiSportsEndpoints['getMmaFightResults'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getMmaFightResults; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getMmaFightResults'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getMmaFightResults', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get MMA Fighter Statistics */ +export const getMmaFighterStatistics: ApiSportsEndpoints['getMmaFighterStatistics'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getMmaFighterStatistics; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getMmaFighterStatistics'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getMmaFighterStatistics', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Fighters Records */ +export const getFightersRecords: ApiSportsEndpoints['getFightersRecords'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getFightersRecords; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getFightersRecords'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.mma.getFightersRecords', + input ?? {}, + 'completed', + ); + return response; + }; diff --git a/packages/apisports/endpoints/odds.ts b/packages/apisports/endpoints/odds.ts new file mode 100644 index 000000000..63dbaece0 --- /dev/null +++ b/packages/apisports/endpoints/odds.ts @@ -0,0 +1,108 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Odds */ +export const getOdds: ApiSportsEndpoints['getOdds'] = async (ctx, input) => { + const route = API_SPORTS_ROUTES.getOdds; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getOdds'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getOdds', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Odds Bets */ +export const getOddsBets: ApiSportsEndpoints['getOddsBets'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getOddsBets; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getOddsBets'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getOddsBets', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Odds Bookmakers */ +export const getOddsBookmakers: ApiSportsEndpoints['getOddsBookmakers'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getOddsBookmakers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getOddsBookmakers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getOddsBookmakers', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Odds Mapping */ +export const getOddsMapping: ApiSportsEndpoints['getOddsMapping'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getOddsMapping; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getOddsMapping'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getOddsMapping', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get In-Play Odds */ +export const getInPlayOdds: ApiSportsEndpoints['getInPlayOdds'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getInPlayOdds; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getInPlayOdds'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getInPlayOdds', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Live Odds Bets */ +export const getLiveOddsBets: ApiSportsEndpoints['getLiveOddsBets'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getLiveOddsBets; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getLiveOddsBets'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.odds.getLiveOddsBets', + input ?? {}, + 'completed', + ); + return response; +}; diff --git a/packages/apisports/endpoints/players.ts b/packages/apisports/endpoints/players.ts new file mode 100644 index 000000000..7184d3819 --- /dev/null +++ b/packages/apisports/endpoints/players.ts @@ -0,0 +1,155 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Players */ +export const getPlayers: ApiSportsEndpoints['getPlayers'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getPlayers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayers', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Players Profiles */ +export const getPlayersProfiles: ApiSportsEndpoints['getPlayersProfiles'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersProfiles; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersProfiles'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersProfiles', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Players Seasons */ +export const getPlayersSeasons: ApiSportsEndpoints['getPlayersSeasons'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersSeasons; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersSeasons'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersSeasons', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Players Squads */ +export const getPlayersSquads: ApiSportsEndpoints['getPlayersSquads'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getPlayersSquads; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersSquads'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersSquads', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Players Teams */ +export const getPlayersTeams: ApiSportsEndpoints['getPlayersTeams'] = async ( + ctx, + input, +) => { + const route = API_SPORTS_ROUTES.getPlayersTeams; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersTeams'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersTeams', + input ?? {}, + 'completed', + ); + return response; +}; + +/** Get Players Top Scorers */ +export const getPlayersTopScorers: ApiSportsEndpoints['getPlayersTopScorers'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersTopScorers; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersTopScorers'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersTopScorers', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Players Top Assists */ +export const getPlayersTopAssists: ApiSportsEndpoints['getPlayersTopAssists'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersTopAssists; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersTopAssists'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersTopAssists', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Players Top Yellow Cards */ +export const getPlayersTopYellowCards: ApiSportsEndpoints['getPlayersTopYellowCards'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersTopYellowCards; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersTopYellowCards'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersTopYellowCards', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Players Top Red Cards */ +export const getPlayersTopRedCards: ApiSportsEndpoints['getPlayersTopRedCards'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getPlayersTopRedCards; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getPlayersTopRedCards'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.players.getPlayersTopRedCards', + input ?? {}, + 'completed', + ); + return response; + }; diff --git a/packages/apisports/endpoints/routes.ts b/packages/apisports/endpoints/routes.ts new file mode 100644 index 000000000..3d99ab5ed --- /dev/null +++ b/packages/apisports/endpoints/routes.ts @@ -0,0 +1,333 @@ +import type { ApiSport } from '../client'; + +export type ApiSportsRoute = { + sport: ApiSport; + path: string; + description: string; +}; + +export const API_SPORTS_ROUTES = { + getCountries: { + sport: 'football', + path: '/countries', + description: 'Get Countries', + }, + getTimezone: { + sport: 'football', + path: '/timezone', + description: 'Get Timezone', + }, + getLeagues: { + sport: 'football', + path: '/leagues', + description: 'Get Leagues', + }, + getLeagueSeasons: { + sport: 'football', + path: '/leagues/seasons', + description: 'Get League Seasons', + }, + getTeams: { sport: 'football', path: '/teams', description: 'Get Teams' }, + getTeamSeasons: { + sport: 'football', + path: '/teams/seasons', + description: 'Get Team Seasons', + }, + getTeamStatistics: { + sport: 'football', + path: '/teams/statistics', + description: 'Get Team Statistics', + }, + getVenues: { sport: 'football', path: '/venues', description: 'Get Venues' }, + getCoaches: { + sport: 'football', + path: '/coachs', + description: 'Get Coaches', + }, + getInjuries: { + sport: 'football', + path: '/injuries', + description: 'Get Injuries', + }, + getSidelined: { + sport: 'football', + path: '/sidelined', + description: 'Get Sidelined', + }, + getTransfers: { + sport: 'football', + path: '/transfers', + description: 'Get Transfers', + }, + getTrophies: { + sport: 'football', + path: '/trophies', + description: 'Get Trophies', + }, + getPredictions: { + sport: 'football', + path: '/predictions', + description: 'Get Predictions', + }, + getFixtures: { + sport: 'football', + path: '/fixtures', + description: 'Get Fixtures', + }, + getFixturesRounds: { + sport: 'football', + path: '/fixtures/rounds', + description: 'Get Fixtures Rounds', + }, + getHeadToHeadFixtures: { + sport: 'football', + path: '/fixtures/headtohead', + description: 'Get Head-to-Head Fixtures', + }, + getFixtureLineups: { + sport: 'football', + path: '/fixtures/lineups', + description: 'Get Fixture Lineups', + }, + getFixtureStatistics: { + sport: 'football', + path: '/fixtures/statistics', + description: 'Get Fixture Statistics', + }, + getFixturesEvents: { + sport: 'football', + path: '/fixtures/events', + description: 'Get Fixtures Events', + }, + getFixturesPlayers: { + sport: 'football', + path: '/fixtures/players', + description: 'Get Fixtures Players', + }, + // Football API documents /standings/stages + /standings/groups; NFL host rejects both. + getStandingsStages: { + sport: 'football', + path: '/standings/stages', + description: 'Get Standings Stages', + }, + getStandingsGroups: { + sport: 'football', + path: '/standings/groups', + description: 'Get Standings Groups', + }, + getStandingsDivisions: { + sport: 'nfl', + path: '/standings/divisions', + description: 'Get Standings Divisions', + }, + getNflStandingsConferences: { + sport: 'nfl', + path: '/standings/conferences', + description: 'Get NFL Standings Conferences', + }, + getPlayers: { + sport: 'football', + path: '/players', + description: 'Get Players', + }, + getPlayersProfiles: { + sport: 'football', + path: '/players/profiles', + description: 'Get Players Profiles', + }, + getPlayersSeasons: { + sport: 'football', + path: '/players/seasons', + description: 'Get Players Seasons', + }, + getPlayersSquads: { + sport: 'football', + path: '/players/squads', + description: 'Get Players Squads', + }, + getPlayersTeams: { + sport: 'football', + path: '/players/teams', + description: 'Get Players Teams', + }, + getPlayersTopScorers: { + sport: 'football', + path: '/players/topscorers', + description: 'Get Players Top Scorers', + }, + getPlayersTopAssists: { + sport: 'football', + path: '/players/topassists', + description: 'Get Players Top Assists', + }, + getPlayersTopYellowCards: { + sport: 'football', + path: '/players/topyellowcards', + description: 'Get Players Top Yellow Cards', + }, + getPlayersTopRedCards: { + sport: 'football', + path: '/players/topredcards', + description: 'Get Players Top Red Cards', + }, + getOdds: { sport: 'football', path: '/odds', description: 'Get Odds' }, + getOddsBets: { + sport: 'football', + path: '/odds/bets', + description: 'Get Odds Bets', + }, + getOddsBookmakers: { + sport: 'football', + path: '/odds/bookmakers', + description: 'Get Odds Bookmakers', + }, + getOddsMapping: { + sport: 'football', + path: '/odds/mapping', + description: 'Get Odds Mapping', + }, + getInPlayOdds: { + sport: 'football', + path: '/odds/live', + description: 'Get In-Play Odds', + }, + getLiveOddsBets: { + sport: 'football', + path: '/odds/live/bets', + description: 'Get Live Odds Bets', + }, + getBasketballStatistics: { + sport: 'basketball', + path: '/statistics', + description: 'Get Basketball Statistics', + }, + getBasketballBets: { + sport: 'basketball', + path: '/bets', + description: 'Get Basketball Bets', + }, + getBasketballBookmakers: { + sport: 'basketball', + path: '/bookmakers', + description: 'Get Basketball Bookmakers', + }, + getNbaGameStatistics: { + sport: 'nba', + path: '/games/statistics', + description: 'Get NBA Game Statistics', + }, + getPlayerStatistics: { + sport: 'nba', + path: '/players/statistics', + description: 'Get Player Statistics', + }, + getGameStatisticsByTeams: { + sport: 'basketball', + path: '/games/statistics/teams', + description: 'Get Game Statistics by Teams', + }, + // NBA has no /games/events; American football (nfl) does. + getGamesEvents: { + sport: 'nfl', + path: '/games/events', + description: 'Get Games Events', + }, + getAflSeasons: { + sport: 'afl', + path: '/seasons', + description: 'Get AFL Seasons', + }, + getAflGames: { sport: 'afl', path: '/games', description: 'Get AFL Games' }, + getAflGamesQuarters: { + sport: 'afl', + path: '/games/quarters', + description: 'Get AFL Games Quarters', + }, + getAflGamePlayerStatistics: { + sport: 'afl', + path: '/games/statistics/players', + description: 'Get AFL Game Player Statistics', + }, + getAflStandings: { + sport: 'afl', + path: '/standings', + description: 'Get AFL Standings', + }, + getBaseballGamesHeadToHead: { + sport: 'baseball', + path: '/games/h2h', + description: 'Get Baseball Games Head-to-Head', + }, + getFormula1Circuits: { + sport: 'formula1', + path: '/circuits', + description: 'Get Formula 1 Circuits', + }, + getFormula1Competitions: { + sport: 'formula1', + path: '/competitions', + description: 'Get Formula 1 Competitions', + }, + getFormula1Races: { + sport: 'formula1', + path: '/races', + description: 'Get Formula 1 Races', + }, + getFormula1DriverRankings: { + sport: 'formula1', + path: '/rankings/drivers', + description: 'Get Formula 1 Driver Rankings', + }, + getFormula1TeamRankings: { + sport: 'formula1', + path: '/rankings/teams', + description: 'Get Formula 1 Team Rankings', + }, + getFormula1StartingGrid: { + sport: 'formula1', + path: '/rankings/startinggrid', + description: 'Get Formula 1 Starting Grid', + }, + getFastestLapsRankings: { + sport: 'formula1', + path: '/rankings/fastestlaps', + description: 'Get Fastest Laps Rankings', + }, + getRaceRankings: { + sport: 'formula1', + path: '/rankings/races', + description: 'Get Race Rankings', + }, + getMmaCategories: { + sport: 'mma', + path: '/categories', + description: 'Get MMA Categories', + }, + getMmaFighters: { + sport: 'mma', + path: '/fighters', + description: 'Get MMA Fighters', + }, + getMmaFights: { + sport: 'mma', + path: '/fights', + description: 'Get MMA Fights', + }, + getMmaFightResults: { + sport: 'mma', + path: '/fights/results', + description: 'Get MMA Fight Results', + }, + getMmaFighterStatistics: { + sport: 'mma', + path: '/fights/statistics/fighters', + description: 'Get MMA Fighter Statistics', + }, + getFightersRecords: { + sport: 'mma', + path: '/fighters/records', + description: 'Get Fighters Records', + }, +} as const satisfies Record; + +export type ApiSportsRouteKey = keyof typeof API_SPORTS_ROUTES; diff --git a/packages/apisports/endpoints/shared.ts b/packages/apisports/endpoints/shared.ts new file mode 100644 index 000000000..ccf55a4fb --- /dev/null +++ b/packages/apisports/endpoints/shared.ts @@ -0,0 +1,61 @@ +import type { ApiSport, ApiSportsQueryValue } from '../client'; +import { makeApiSportsRequest } from '../client'; +import type { ApiSportsContext } from '../index'; + +function stableQueryKey( + query: Record | undefined, +) { + if (!query) return ''; + return JSON.stringify( + Object.keys(query) + .sort() + .reduce>((acc, key) => { + acc[key] = query[key]; + return acc; + }, {}), + ); +} + +export function buildQueryEntityId( + sport: ApiSport, + path: string, + query: Record | undefined, +) { + return `${sport}:${path}:${stableQueryKey(query)}`; +} + +export async function cacheApiSportsQuery( + ctx: ApiSportsContext, + sport: ApiSport, + path: string, + query: Record | undefined, +) { + if (!ctx.db.queries) return; + + try { + await ctx.db.queries.upsertByEntityId( + buildQueryEntityId(sport, path, query), + { + sport, + path, + queriedAt: new Date(), + }, + ); + } catch (error) { + console.warn('[apisports] Failed to save query to database:', error); + } +} + +export async function executeApiSportsRequest( + ctx: ApiSportsContext, + sport: ApiSport, + path: string, + options: { + apiKey?: string; + query?: Record; + } = {}, +): Promise { + const response = await makeApiSportsRequest(sport, path, options); + await cacheApiSportsQuery(ctx, sport, path, options.query); + return response; +} diff --git a/packages/apisports/endpoints/standings.ts b/packages/apisports/endpoints/standings.ts new file mode 100644 index 000000000..779a97d50 --- /dev/null +++ b/packages/apisports/endpoints/standings.ts @@ -0,0 +1,69 @@ +import { logEventFromContext } from 'corsair/core'; +import type { ApiSportsEndpoints } from '../index'; +import { API_SPORTS_ROUTES } from './routes'; +import { executeApiSportsRequest } from './shared'; +import type { ApiSportsEndpointOutputs } from './types'; + +/** Get Standings Stages */ +export const getStandingsStages: ApiSportsEndpoints['getStandingsStages'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getStandingsStages; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getStandingsStages'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.standings.getStandingsStages', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Standings Groups */ +export const getStandingsGroups: ApiSportsEndpoints['getStandingsGroups'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getStandingsGroups; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getStandingsGroups'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.standings.getStandingsGroups', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get Standings Divisions */ +export const getStandingsDivisions: ApiSportsEndpoints['getStandingsDivisions'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getStandingsDivisions; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getStandingsDivisions'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.standings.getStandingsDivisions', + input ?? {}, + 'completed', + ); + return response; + }; + +/** Get NFL Standings Conferences */ +export const getNflStandingsConferences: ApiSportsEndpoints['getNflStandingsConferences'] = + async (ctx, input) => { + const route = API_SPORTS_ROUTES.getNflStandingsConferences; + const response = await executeApiSportsRequest< + ApiSportsEndpointOutputs['getNflStandingsConferences'] + >(ctx, route.sport, route.path, { apiKey: ctx.key, query: input }); + await logEventFromContext( + ctx, + 'apisports.standings.getNflStandingsConferences', + input ?? {}, + 'completed', + ); + return response; + }; diff --git a/packages/apisports/endpoints/types.ts b/packages/apisports/endpoints/types.ts new file mode 100644 index 000000000..9fe1111bc --- /dev/null +++ b/packages/apisports/endpoints/types.ts @@ -0,0 +1,319 @@ +import { z } from 'zod'; + +/** Query params vary per sport API; callers pass documented filter fields. */ +export const ApiSportsQueryInputSchema = z + .record( + z.string(), + z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(z.union([z.string(), z.number()])), + ]), + ) + .optional(); + +export type ApiSportsQueryInput = z.infer; + +export const ApiSportsResponseSchema = z + .object({ + get: z.string().optional(), + parameters: z + .union([z.record(z.string(), z.unknown()), z.array(z.unknown())]) + .optional(), + // Live API returns [] on success and `{ token|endpoint: string }` on failure. + errors: z + .union([z.array(z.unknown()), z.record(z.string(), z.unknown())]) + .optional(), + results: z.number().optional(), + paging: z + .object({ + current: z.number().optional(), + total: z.number().optional(), + }) + .optional(), + response: z.unknown(), + }) + .loose(); + +export type ApiSportsResponse = z.infer; + +export type ApiSportsEndpointInputs = { + getCountries: ApiSportsQueryInput; + getTimezone: ApiSportsQueryInput; + getLeagues: ApiSportsQueryInput; + getLeagueSeasons: ApiSportsQueryInput; + getTeams: ApiSportsQueryInput; + getTeamSeasons: ApiSportsQueryInput; + getTeamStatistics: ApiSportsQueryInput; + getVenues: ApiSportsQueryInput; + getCoaches: ApiSportsQueryInput; + getInjuries: ApiSportsQueryInput; + getSidelined: ApiSportsQueryInput; + getTransfers: ApiSportsQueryInput; + getTrophies: ApiSportsQueryInput; + getPredictions: ApiSportsQueryInput; + getFixtures: ApiSportsQueryInput; + getFixturesRounds: ApiSportsQueryInput; + getHeadToHeadFixtures: ApiSportsQueryInput; + getFixtureLineups: ApiSportsQueryInput; + getFixtureStatistics: ApiSportsQueryInput; + getFixturesEvents: ApiSportsQueryInput; + getFixturesPlayers: ApiSportsQueryInput; + getStandingsStages: ApiSportsQueryInput; + getStandingsGroups: ApiSportsQueryInput; + getStandingsDivisions: ApiSportsQueryInput; + getNflStandingsConferences: ApiSportsQueryInput; + getPlayers: ApiSportsQueryInput; + getPlayersProfiles: ApiSportsQueryInput; + getPlayersSeasons: ApiSportsQueryInput; + getPlayersSquads: ApiSportsQueryInput; + getPlayersTeams: ApiSportsQueryInput; + getPlayersTopScorers: ApiSportsQueryInput; + getPlayersTopAssists: ApiSportsQueryInput; + getPlayersTopYellowCards: ApiSportsQueryInput; + getPlayersTopRedCards: ApiSportsQueryInput; + getOdds: ApiSportsQueryInput; + getOddsBets: ApiSportsQueryInput; + getOddsBookmakers: ApiSportsQueryInput; + getOddsMapping: ApiSportsQueryInput; + getInPlayOdds: ApiSportsQueryInput; + getLiveOddsBets: ApiSportsQueryInput; + getBasketballStatistics: ApiSportsQueryInput; + getBasketballBets: ApiSportsQueryInput; + getBasketballBookmakers: ApiSportsQueryInput; + getNbaGameStatistics: ApiSportsQueryInput; + getPlayerStatistics: ApiSportsQueryInput; + getGameStatisticsByTeams: ApiSportsQueryInput; + getGamesEvents: ApiSportsQueryInput; + getAflSeasons: ApiSportsQueryInput; + getAflGames: ApiSportsQueryInput; + getAflGamesQuarters: ApiSportsQueryInput; + getAflGamePlayerStatistics: ApiSportsQueryInput; + getAflStandings: ApiSportsQueryInput; + getBaseballGamesHeadToHead: ApiSportsQueryInput; + getFormula1Circuits: ApiSportsQueryInput; + getFormula1Competitions: ApiSportsQueryInput; + getFormula1Races: ApiSportsQueryInput; + getFormula1DriverRankings: ApiSportsQueryInput; + getFormula1TeamRankings: ApiSportsQueryInput; + getFormula1StartingGrid: ApiSportsQueryInput; + getFastestLapsRankings: ApiSportsQueryInput; + getRaceRankings: ApiSportsQueryInput; + getMmaCategories: ApiSportsQueryInput; + getMmaFighters: ApiSportsQueryInput; + getMmaFights: ApiSportsQueryInput; + getMmaFightResults: ApiSportsQueryInput; + getMmaFighterStatistics: ApiSportsQueryInput; + getFightersRecords: ApiSportsQueryInput; +}; + +export type ApiSportsEndpointOutputs = { + getCountries: ApiSportsResponse; + getTimezone: ApiSportsResponse; + getLeagues: ApiSportsResponse; + getLeagueSeasons: ApiSportsResponse; + getTeams: ApiSportsResponse; + getTeamSeasons: ApiSportsResponse; + getTeamStatistics: ApiSportsResponse; + getVenues: ApiSportsResponse; + getCoaches: ApiSportsResponse; + getInjuries: ApiSportsResponse; + getSidelined: ApiSportsResponse; + getTransfers: ApiSportsResponse; + getTrophies: ApiSportsResponse; + getPredictions: ApiSportsResponse; + getFixtures: ApiSportsResponse; + getFixturesRounds: ApiSportsResponse; + getHeadToHeadFixtures: ApiSportsResponse; + getFixtureLineups: ApiSportsResponse; + getFixtureStatistics: ApiSportsResponse; + getFixturesEvents: ApiSportsResponse; + getFixturesPlayers: ApiSportsResponse; + getStandingsStages: ApiSportsResponse; + getStandingsGroups: ApiSportsResponse; + getStandingsDivisions: ApiSportsResponse; + getNflStandingsConferences: ApiSportsResponse; + getPlayers: ApiSportsResponse; + getPlayersProfiles: ApiSportsResponse; + getPlayersSeasons: ApiSportsResponse; + getPlayersSquads: ApiSportsResponse; + getPlayersTeams: ApiSportsResponse; + getPlayersTopScorers: ApiSportsResponse; + getPlayersTopAssists: ApiSportsResponse; + getPlayersTopYellowCards: ApiSportsResponse; + getPlayersTopRedCards: ApiSportsResponse; + getOdds: ApiSportsResponse; + getOddsBets: ApiSportsResponse; + getOddsBookmakers: ApiSportsResponse; + getOddsMapping: ApiSportsResponse; + getInPlayOdds: ApiSportsResponse; + getLiveOddsBets: ApiSportsResponse; + getBasketballStatistics: ApiSportsResponse; + getBasketballBets: ApiSportsResponse; + getBasketballBookmakers: ApiSportsResponse; + getNbaGameStatistics: ApiSportsResponse; + getPlayerStatistics: ApiSportsResponse; + getGameStatisticsByTeams: ApiSportsResponse; + getGamesEvents: ApiSportsResponse; + getAflSeasons: ApiSportsResponse; + getAflGames: ApiSportsResponse; + getAflGamesQuarters: ApiSportsResponse; + getAflGamePlayerStatistics: ApiSportsResponse; + getAflStandings: ApiSportsResponse; + getBaseballGamesHeadToHead: ApiSportsResponse; + getFormula1Circuits: ApiSportsResponse; + getFormula1Competitions: ApiSportsResponse; + getFormula1Races: ApiSportsResponse; + getFormula1DriverRankings: ApiSportsResponse; + getFormula1TeamRankings: ApiSportsResponse; + getFormula1StartingGrid: ApiSportsResponse; + getFastestLapsRankings: ApiSportsResponse; + getRaceRankings: ApiSportsResponse; + getMmaCategories: ApiSportsResponse; + getMmaFighters: ApiSportsResponse; + getMmaFights: ApiSportsResponse; + getMmaFightResults: ApiSportsResponse; + getMmaFighterStatistics: ApiSportsResponse; + getFightersRecords: ApiSportsResponse; +}; + +export const ApiSportsEndpointInputSchemas = { + getCountries: ApiSportsQueryInputSchema, + getTimezone: ApiSportsQueryInputSchema, + getLeagues: ApiSportsQueryInputSchema, + getLeagueSeasons: ApiSportsQueryInputSchema, + getTeams: ApiSportsQueryInputSchema, + getTeamSeasons: ApiSportsQueryInputSchema, + getTeamStatistics: ApiSportsQueryInputSchema, + getVenues: ApiSportsQueryInputSchema, + getCoaches: ApiSportsQueryInputSchema, + getInjuries: ApiSportsQueryInputSchema, + getSidelined: ApiSportsQueryInputSchema, + getTransfers: ApiSportsQueryInputSchema, + getTrophies: ApiSportsQueryInputSchema, + getPredictions: ApiSportsQueryInputSchema, + getFixtures: ApiSportsQueryInputSchema, + getFixturesRounds: ApiSportsQueryInputSchema, + getHeadToHeadFixtures: ApiSportsQueryInputSchema, + getFixtureLineups: ApiSportsQueryInputSchema, + getFixtureStatistics: ApiSportsQueryInputSchema, + getFixturesEvents: ApiSportsQueryInputSchema, + getFixturesPlayers: ApiSportsQueryInputSchema, + getStandingsStages: ApiSportsQueryInputSchema, + getStandingsGroups: ApiSportsQueryInputSchema, + getStandingsDivisions: ApiSportsQueryInputSchema, + getNflStandingsConferences: ApiSportsQueryInputSchema, + getPlayers: ApiSportsQueryInputSchema, + getPlayersProfiles: ApiSportsQueryInputSchema, + getPlayersSeasons: ApiSportsQueryInputSchema, + getPlayersSquads: ApiSportsQueryInputSchema, + getPlayersTeams: ApiSportsQueryInputSchema, + getPlayersTopScorers: ApiSportsQueryInputSchema, + getPlayersTopAssists: ApiSportsQueryInputSchema, + getPlayersTopYellowCards: ApiSportsQueryInputSchema, + getPlayersTopRedCards: ApiSportsQueryInputSchema, + getOdds: ApiSportsQueryInputSchema, + getOddsBets: ApiSportsQueryInputSchema, + getOddsBookmakers: ApiSportsQueryInputSchema, + getOddsMapping: ApiSportsQueryInputSchema, + getInPlayOdds: ApiSportsQueryInputSchema, + getLiveOddsBets: ApiSportsQueryInputSchema, + getBasketballStatistics: ApiSportsQueryInputSchema, + getBasketballBets: ApiSportsQueryInputSchema, + getBasketballBookmakers: ApiSportsQueryInputSchema, + getNbaGameStatistics: ApiSportsQueryInputSchema, + getPlayerStatistics: ApiSportsQueryInputSchema, + getGameStatisticsByTeams: ApiSportsQueryInputSchema, + getGamesEvents: ApiSportsQueryInputSchema, + getAflSeasons: ApiSportsQueryInputSchema, + getAflGames: ApiSportsQueryInputSchema, + getAflGamesQuarters: ApiSportsQueryInputSchema, + getAflGamePlayerStatistics: ApiSportsQueryInputSchema, + getAflStandings: ApiSportsQueryInputSchema, + getBaseballGamesHeadToHead: ApiSportsQueryInputSchema, + getFormula1Circuits: ApiSportsQueryInputSchema, + getFormula1Competitions: ApiSportsQueryInputSchema, + getFormula1Races: ApiSportsQueryInputSchema, + getFormula1DriverRankings: ApiSportsQueryInputSchema, + getFormula1TeamRankings: ApiSportsQueryInputSchema, + getFormula1StartingGrid: ApiSportsQueryInputSchema, + getFastestLapsRankings: ApiSportsQueryInputSchema, + getRaceRankings: ApiSportsQueryInputSchema, + getMmaCategories: ApiSportsQueryInputSchema, + getMmaFighters: ApiSportsQueryInputSchema, + getMmaFights: ApiSportsQueryInputSchema, + getMmaFightResults: ApiSportsQueryInputSchema, + getMmaFighterStatistics: ApiSportsQueryInputSchema, + getFightersRecords: ApiSportsQueryInputSchema, +} as const; + +export const ApiSportsEndpointOutputSchemas = { + getCountries: ApiSportsResponseSchema, + getTimezone: ApiSportsResponseSchema, + getLeagues: ApiSportsResponseSchema, + getLeagueSeasons: ApiSportsResponseSchema, + getTeams: ApiSportsResponseSchema, + getTeamSeasons: ApiSportsResponseSchema, + getTeamStatistics: ApiSportsResponseSchema, + getVenues: ApiSportsResponseSchema, + getCoaches: ApiSportsResponseSchema, + getInjuries: ApiSportsResponseSchema, + getSidelined: ApiSportsResponseSchema, + getTransfers: ApiSportsResponseSchema, + getTrophies: ApiSportsResponseSchema, + getPredictions: ApiSportsResponseSchema, + getFixtures: ApiSportsResponseSchema, + getFixturesRounds: ApiSportsResponseSchema, + getHeadToHeadFixtures: ApiSportsResponseSchema, + getFixtureLineups: ApiSportsResponseSchema, + getFixtureStatistics: ApiSportsResponseSchema, + getFixturesEvents: ApiSportsResponseSchema, + getFixturesPlayers: ApiSportsResponseSchema, + getStandingsStages: ApiSportsResponseSchema, + getStandingsGroups: ApiSportsResponseSchema, + getStandingsDivisions: ApiSportsResponseSchema, + getNflStandingsConferences: ApiSportsResponseSchema, + getPlayers: ApiSportsResponseSchema, + getPlayersProfiles: ApiSportsResponseSchema, + getPlayersSeasons: ApiSportsResponseSchema, + getPlayersSquads: ApiSportsResponseSchema, + getPlayersTeams: ApiSportsResponseSchema, + getPlayersTopScorers: ApiSportsResponseSchema, + getPlayersTopAssists: ApiSportsResponseSchema, + getPlayersTopYellowCards: ApiSportsResponseSchema, + getPlayersTopRedCards: ApiSportsResponseSchema, + getOdds: ApiSportsResponseSchema, + getOddsBets: ApiSportsResponseSchema, + getOddsBookmakers: ApiSportsResponseSchema, + getOddsMapping: ApiSportsResponseSchema, + getInPlayOdds: ApiSportsResponseSchema, + getLiveOddsBets: ApiSportsResponseSchema, + getBasketballStatistics: ApiSportsResponseSchema, + getBasketballBets: ApiSportsResponseSchema, + getBasketballBookmakers: ApiSportsResponseSchema, + getNbaGameStatistics: ApiSportsResponseSchema, + getPlayerStatistics: ApiSportsResponseSchema, + getGameStatisticsByTeams: ApiSportsResponseSchema, + getGamesEvents: ApiSportsResponseSchema, + getAflSeasons: ApiSportsResponseSchema, + getAflGames: ApiSportsResponseSchema, + getAflGamesQuarters: ApiSportsResponseSchema, + getAflGamePlayerStatistics: ApiSportsResponseSchema, + getAflStandings: ApiSportsResponseSchema, + getBaseballGamesHeadToHead: ApiSportsResponseSchema, + getFormula1Circuits: ApiSportsResponseSchema, + getFormula1Competitions: ApiSportsResponseSchema, + getFormula1Races: ApiSportsResponseSchema, + getFormula1DriverRankings: ApiSportsResponseSchema, + getFormula1TeamRankings: ApiSportsResponseSchema, + getFormula1StartingGrid: ApiSportsResponseSchema, + getFastestLapsRankings: ApiSportsResponseSchema, + getRaceRankings: ApiSportsResponseSchema, + getMmaCategories: ApiSportsResponseSchema, + getMmaFighters: ApiSportsResponseSchema, + getMmaFights: ApiSportsResponseSchema, + getMmaFightResults: ApiSportsResponseSchema, + getMmaFighterStatistics: ApiSportsResponseSchema, + getFightersRecords: ApiSportsResponseSchema, +} as const; diff --git a/packages/apisports/error-handlers.ts b/packages/apisports/error-handlers.ts new file mode 100644 index 000000000..098029fa5 --- /dev/null +++ b/packages/apisports/error-handlers.ts @@ -0,0 +1,68 @@ +import type { CorsairErrorHandler } from 'corsair/core'; +import type { ApiSportsAPIError } from './client'; + +// CorsairErrorHandler receives a plain Error; duck-type ApiSports-specific fields +// without instanceof so handlers work across module boundaries. +function getStatus(error: Error): number | undefined { + return (error as Partial).status; +} + +function getRetryAfter(error: Error): number | undefined { + return (error as Partial).retryAfter; +} + +export const errorHandlers = { + RATE_LIMIT_ERROR: { + match: (error: Error) => { + if (getStatus(error) === 429) return true; + const msg = error.message.toLowerCase(); + return msg.includes('429') || msg.includes('rate limit'); + }, + handler: async (error: Error) => ({ + maxRetries: 3, + retryStrategy: 'exponential_backoff' as const, + headersRetryAfterMs: getRetryAfter(error), + }), + }, + AUTH_ERROR: { + match: (error: Error) => { + if (getStatus(error) === 401 || getStatus(error) === 403) return true; + const msg = error.message.toLowerCase(); + return ( + msg.includes('unauthorized') || + msg.includes('invalid api key') || + msg.includes('invalid key') || + msg.includes('application key') || + msg.includes('token:') || + msg.includes('401') + ); + }, + handler: async () => { + console.error( + '[API_SPORTS] Authentication failed — check your x-apisports-key.', + ); + return { maxRetries: 0 }; + }, + }, + NOT_FOUND_ERROR: { + match: (error: Error) => getStatus(error) === 404, + handler: async () => ({ maxRetries: 0 }), + }, + SERVER_ERROR: { + match: (error: Error) => { + const status = getStatus(error); + return status !== undefined && status >= 500; + }, + handler: async () => ({ + maxRetries: 2, + retryStrategy: 'exponential_backoff' as const, + }), + }, + DEFAULT: { + match: () => true, + handler: async (error: Error) => { + console.error(`[API_SPORTS] Unhandled error: ${error.message}`); + return { maxRetries: 0 }; + }, + }, +} satisfies CorsairErrorHandler; diff --git a/packages/apisports/index.ts b/packages/apisports/index.ts new file mode 100644 index 000000000..93c5213b8 --- /dev/null +++ b/packages/apisports/index.ts @@ -0,0 +1,842 @@ +import type { + AuthTypes, + BindEndpoints, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, +} from 'corsair/core'; +import { AuthMissingError } from 'corsair/core'; +import * as Afl from './endpoints/afl'; +import * as Baseball from './endpoints/baseball'; +import * as Basketball from './endpoints/basketball'; +import * as Core from './endpoints/core'; +import * as Fixtures from './endpoints/fixtures'; +import * as Formula1 from './endpoints/formula1'; +import * as Mma from './endpoints/mma'; +import * as Odds from './endpoints/odds'; +import * as Players from './endpoints/players'; +import * as Standings from './endpoints/standings'; +import type { + ApiSportsEndpointInputs, + ApiSportsEndpointOutputs, +} from './endpoints/types'; +import { + ApiSportsEndpointInputSchemas, + ApiSportsEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { ApiSportsSchema } from './schema'; + +export type ApiSportsPluginOptions = { + authType?: PickAuth<'api_key'>; + key?: string; + hooks?: InternalApiSportsPlugin['hooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type ApiSportsContext = CorsairPluginContext< + typeof ApiSportsSchema, + ApiSportsPluginOptions +>; + +export type ApiSportsKeyBuilderContext = + KeyBuilderContext; + +export type ApiSportsBoundEndpoints = BindEndpoints< + typeof apiSportsEndpointsNested +>; + +type ApiSportsEndpoint = + CorsairEndpoint< + ApiSportsContext, + ApiSportsEndpointInputs[K], + ApiSportsEndpointOutputs[K] + >; + +export type ApiSportsEndpoints = { + getCountries: ApiSportsEndpoint<'getCountries'>; + getTimezone: ApiSportsEndpoint<'getTimezone'>; + getLeagues: ApiSportsEndpoint<'getLeagues'>; + getLeagueSeasons: ApiSportsEndpoint<'getLeagueSeasons'>; + getTeams: ApiSportsEndpoint<'getTeams'>; + getTeamSeasons: ApiSportsEndpoint<'getTeamSeasons'>; + getTeamStatistics: ApiSportsEndpoint<'getTeamStatistics'>; + getVenues: ApiSportsEndpoint<'getVenues'>; + getCoaches: ApiSportsEndpoint<'getCoaches'>; + getInjuries: ApiSportsEndpoint<'getInjuries'>; + getSidelined: ApiSportsEndpoint<'getSidelined'>; + getTransfers: ApiSportsEndpoint<'getTransfers'>; + getTrophies: ApiSportsEndpoint<'getTrophies'>; + getPredictions: ApiSportsEndpoint<'getPredictions'>; + getFixtures: ApiSportsEndpoint<'getFixtures'>; + getFixturesRounds: ApiSportsEndpoint<'getFixturesRounds'>; + getHeadToHeadFixtures: ApiSportsEndpoint<'getHeadToHeadFixtures'>; + getFixtureLineups: ApiSportsEndpoint<'getFixtureLineups'>; + getFixtureStatistics: ApiSportsEndpoint<'getFixtureStatistics'>; + getFixturesEvents: ApiSportsEndpoint<'getFixturesEvents'>; + getFixturesPlayers: ApiSportsEndpoint<'getFixturesPlayers'>; + getStandingsStages: ApiSportsEndpoint<'getStandingsStages'>; + getStandingsGroups: ApiSportsEndpoint<'getStandingsGroups'>; + getStandingsDivisions: ApiSportsEndpoint<'getStandingsDivisions'>; + getNflStandingsConferences: ApiSportsEndpoint<'getNflStandingsConferences'>; + getPlayers: ApiSportsEndpoint<'getPlayers'>; + getPlayersProfiles: ApiSportsEndpoint<'getPlayersProfiles'>; + getPlayersSeasons: ApiSportsEndpoint<'getPlayersSeasons'>; + getPlayersSquads: ApiSportsEndpoint<'getPlayersSquads'>; + getPlayersTeams: ApiSportsEndpoint<'getPlayersTeams'>; + getPlayersTopScorers: ApiSportsEndpoint<'getPlayersTopScorers'>; + getPlayersTopAssists: ApiSportsEndpoint<'getPlayersTopAssists'>; + getPlayersTopYellowCards: ApiSportsEndpoint<'getPlayersTopYellowCards'>; + getPlayersTopRedCards: ApiSportsEndpoint<'getPlayersTopRedCards'>; + getOdds: ApiSportsEndpoint<'getOdds'>; + getOddsBets: ApiSportsEndpoint<'getOddsBets'>; + getOddsBookmakers: ApiSportsEndpoint<'getOddsBookmakers'>; + getOddsMapping: ApiSportsEndpoint<'getOddsMapping'>; + getInPlayOdds: ApiSportsEndpoint<'getInPlayOdds'>; + getLiveOddsBets: ApiSportsEndpoint<'getLiveOddsBets'>; + getBasketballStatistics: ApiSportsEndpoint<'getBasketballStatistics'>; + getBasketballBets: ApiSportsEndpoint<'getBasketballBets'>; + getBasketballBookmakers: ApiSportsEndpoint<'getBasketballBookmakers'>; + getNbaGameStatistics: ApiSportsEndpoint<'getNbaGameStatistics'>; + getPlayerStatistics: ApiSportsEndpoint<'getPlayerStatistics'>; + getGameStatisticsByTeams: ApiSportsEndpoint<'getGameStatisticsByTeams'>; + getGamesEvents: ApiSportsEndpoint<'getGamesEvents'>; + getAflSeasons: ApiSportsEndpoint<'getAflSeasons'>; + getAflGames: ApiSportsEndpoint<'getAflGames'>; + getAflGamesQuarters: ApiSportsEndpoint<'getAflGamesQuarters'>; + getAflGamePlayerStatistics: ApiSportsEndpoint<'getAflGamePlayerStatistics'>; + getAflStandings: ApiSportsEndpoint<'getAflStandings'>; + getBaseballGamesHeadToHead: ApiSportsEndpoint<'getBaseballGamesHeadToHead'>; + getFormula1Circuits: ApiSportsEndpoint<'getFormula1Circuits'>; + getFormula1Competitions: ApiSportsEndpoint<'getFormula1Competitions'>; + getFormula1Races: ApiSportsEndpoint<'getFormula1Races'>; + getFormula1DriverRankings: ApiSportsEndpoint<'getFormula1DriverRankings'>; + getFormula1TeamRankings: ApiSportsEndpoint<'getFormula1TeamRankings'>; + getFormula1StartingGrid: ApiSportsEndpoint<'getFormula1StartingGrid'>; + getFastestLapsRankings: ApiSportsEndpoint<'getFastestLapsRankings'>; + getRaceRankings: ApiSportsEndpoint<'getRaceRankings'>; + getMmaCategories: ApiSportsEndpoint<'getMmaCategories'>; + getMmaFighters: ApiSportsEndpoint<'getMmaFighters'>; + getMmaFights: ApiSportsEndpoint<'getMmaFights'>; + getMmaFightResults: ApiSportsEndpoint<'getMmaFightResults'>; + getMmaFighterStatistics: ApiSportsEndpoint<'getMmaFighterStatistics'>; + getFightersRecords: ApiSportsEndpoint<'getFightersRecords'>; +}; + +const apiSportsEndpointsNested = { + core: { + getCountries: Core.getCountries, + getTimezone: Core.getTimezone, + getLeagues: Core.getLeagues, + getLeagueSeasons: Core.getLeagueSeasons, + getTeams: Core.getTeams, + getTeamSeasons: Core.getTeamSeasons, + getTeamStatistics: Core.getTeamStatistics, + getVenues: Core.getVenues, + getCoaches: Core.getCoaches, + getInjuries: Core.getInjuries, + getSidelined: Core.getSidelined, + getTransfers: Core.getTransfers, + getTrophies: Core.getTrophies, + getPredictions: Core.getPredictions, + }, + fixtures: { + getFixtures: Fixtures.getFixtures, + getFixturesRounds: Fixtures.getFixturesRounds, + getHeadToHeadFixtures: Fixtures.getHeadToHeadFixtures, + getFixtureLineups: Fixtures.getFixtureLineups, + getFixtureStatistics: Fixtures.getFixtureStatistics, + getFixturesEvents: Fixtures.getFixturesEvents, + getFixturesPlayers: Fixtures.getFixturesPlayers, + }, + standings: { + getStandingsStages: Standings.getStandingsStages, + getStandingsGroups: Standings.getStandingsGroups, + getStandingsDivisions: Standings.getStandingsDivisions, + getNflStandingsConferences: Standings.getNflStandingsConferences, + }, + players: { + getPlayers: Players.getPlayers, + getPlayersProfiles: Players.getPlayersProfiles, + getPlayersSeasons: Players.getPlayersSeasons, + getPlayersSquads: Players.getPlayersSquads, + getPlayersTeams: Players.getPlayersTeams, + getPlayersTopScorers: Players.getPlayersTopScorers, + getPlayersTopAssists: Players.getPlayersTopAssists, + getPlayersTopYellowCards: Players.getPlayersTopYellowCards, + getPlayersTopRedCards: Players.getPlayersTopRedCards, + }, + odds: { + getOdds: Odds.getOdds, + getOddsBets: Odds.getOddsBets, + getOddsBookmakers: Odds.getOddsBookmakers, + getOddsMapping: Odds.getOddsMapping, + getInPlayOdds: Odds.getInPlayOdds, + getLiveOddsBets: Odds.getLiveOddsBets, + }, + basketball: { + getBasketballStatistics: Basketball.getBasketballStatistics, + getBasketballBets: Basketball.getBasketballBets, + getBasketballBookmakers: Basketball.getBasketballBookmakers, + getNbaGameStatistics: Basketball.getNbaGameStatistics, + getPlayerStatistics: Basketball.getPlayerStatistics, + getGameStatisticsByTeams: Basketball.getGameStatisticsByTeams, + getGamesEvents: Basketball.getGamesEvents, + }, + afl: { + getAflSeasons: Afl.getAflSeasons, + getAflGames: Afl.getAflGames, + getAflGamesQuarters: Afl.getAflGamesQuarters, + getAflGamePlayerStatistics: Afl.getAflGamePlayerStatistics, + getAflStandings: Afl.getAflStandings, + }, + baseball: { + getBaseballGamesHeadToHead: Baseball.getBaseballGamesHeadToHead, + }, + formula1: { + getFormula1Circuits: Formula1.getFormula1Circuits, + getFormula1Competitions: Formula1.getFormula1Competitions, + getFormula1Races: Formula1.getFormula1Races, + getFormula1DriverRankings: Formula1.getFormula1DriverRankings, + getFormula1TeamRankings: Formula1.getFormula1TeamRankings, + getFormula1StartingGrid: Formula1.getFormula1StartingGrid, + getFastestLapsRankings: Formula1.getFastestLapsRankings, + getRaceRankings: Formula1.getRaceRankings, + }, + mma: { + getMmaCategories: Mma.getMmaCategories, + getMmaFighters: Mma.getMmaFighters, + getMmaFights: Mma.getMmaFights, + getMmaFightResults: Mma.getMmaFightResults, + getMmaFighterStatistics: Mma.getMmaFighterStatistics, + getFightersRecords: Mma.getFightersRecords, + }, +} as const; + +const apiSportsWebhooksNested = {} as const; + +export const apiSportsEndpointSchemas = { + 'core.getCountries': { + input: ApiSportsEndpointInputSchemas.getCountries, + output: ApiSportsEndpointOutputSchemas.getCountries, + }, + 'core.getTimezone': { + input: ApiSportsEndpointInputSchemas.getTimezone, + output: ApiSportsEndpointOutputSchemas.getTimezone, + }, + 'core.getLeagues': { + input: ApiSportsEndpointInputSchemas.getLeagues, + output: ApiSportsEndpointOutputSchemas.getLeagues, + }, + 'core.getLeagueSeasons': { + input: ApiSportsEndpointInputSchemas.getLeagueSeasons, + output: ApiSportsEndpointOutputSchemas.getLeagueSeasons, + }, + 'core.getTeams': { + input: ApiSportsEndpointInputSchemas.getTeams, + output: ApiSportsEndpointOutputSchemas.getTeams, + }, + 'core.getTeamSeasons': { + input: ApiSportsEndpointInputSchemas.getTeamSeasons, + output: ApiSportsEndpointOutputSchemas.getTeamSeasons, + }, + 'core.getTeamStatistics': { + input: ApiSportsEndpointInputSchemas.getTeamStatistics, + output: ApiSportsEndpointOutputSchemas.getTeamStatistics, + }, + 'core.getVenues': { + input: ApiSportsEndpointInputSchemas.getVenues, + output: ApiSportsEndpointOutputSchemas.getVenues, + }, + 'core.getCoaches': { + input: ApiSportsEndpointInputSchemas.getCoaches, + output: ApiSportsEndpointOutputSchemas.getCoaches, + }, + 'core.getInjuries': { + input: ApiSportsEndpointInputSchemas.getInjuries, + output: ApiSportsEndpointOutputSchemas.getInjuries, + }, + 'core.getSidelined': { + input: ApiSportsEndpointInputSchemas.getSidelined, + output: ApiSportsEndpointOutputSchemas.getSidelined, + }, + 'core.getTransfers': { + input: ApiSportsEndpointInputSchemas.getTransfers, + output: ApiSportsEndpointOutputSchemas.getTransfers, + }, + 'core.getTrophies': { + input: ApiSportsEndpointInputSchemas.getTrophies, + output: ApiSportsEndpointOutputSchemas.getTrophies, + }, + 'core.getPredictions': { + input: ApiSportsEndpointInputSchemas.getPredictions, + output: ApiSportsEndpointOutputSchemas.getPredictions, + }, + 'fixtures.getFixtures': { + input: ApiSportsEndpointInputSchemas.getFixtures, + output: ApiSportsEndpointOutputSchemas.getFixtures, + }, + 'fixtures.getFixturesRounds': { + input: ApiSportsEndpointInputSchemas.getFixturesRounds, + output: ApiSportsEndpointOutputSchemas.getFixturesRounds, + }, + 'fixtures.getHeadToHeadFixtures': { + input: ApiSportsEndpointInputSchemas.getHeadToHeadFixtures, + output: ApiSportsEndpointOutputSchemas.getHeadToHeadFixtures, + }, + 'fixtures.getFixtureLineups': { + input: ApiSportsEndpointInputSchemas.getFixtureLineups, + output: ApiSportsEndpointOutputSchemas.getFixtureLineups, + }, + 'fixtures.getFixtureStatistics': { + input: ApiSportsEndpointInputSchemas.getFixtureStatistics, + output: ApiSportsEndpointOutputSchemas.getFixtureStatistics, + }, + 'fixtures.getFixturesEvents': { + input: ApiSportsEndpointInputSchemas.getFixturesEvents, + output: ApiSportsEndpointOutputSchemas.getFixturesEvents, + }, + 'fixtures.getFixturesPlayers': { + input: ApiSportsEndpointInputSchemas.getFixturesPlayers, + output: ApiSportsEndpointOutputSchemas.getFixturesPlayers, + }, + 'standings.getStandingsStages': { + input: ApiSportsEndpointInputSchemas.getStandingsStages, + output: ApiSportsEndpointOutputSchemas.getStandingsStages, + }, + 'standings.getStandingsGroups': { + input: ApiSportsEndpointInputSchemas.getStandingsGroups, + output: ApiSportsEndpointOutputSchemas.getStandingsGroups, + }, + 'standings.getStandingsDivisions': { + input: ApiSportsEndpointInputSchemas.getStandingsDivisions, + output: ApiSportsEndpointOutputSchemas.getStandingsDivisions, + }, + 'standings.getNflStandingsConferences': { + input: ApiSportsEndpointInputSchemas.getNflStandingsConferences, + output: ApiSportsEndpointOutputSchemas.getNflStandingsConferences, + }, + 'players.getPlayers': { + input: ApiSportsEndpointInputSchemas.getPlayers, + output: ApiSportsEndpointOutputSchemas.getPlayers, + }, + 'players.getPlayersProfiles': { + input: ApiSportsEndpointInputSchemas.getPlayersProfiles, + output: ApiSportsEndpointOutputSchemas.getPlayersProfiles, + }, + 'players.getPlayersSeasons': { + input: ApiSportsEndpointInputSchemas.getPlayersSeasons, + output: ApiSportsEndpointOutputSchemas.getPlayersSeasons, + }, + 'players.getPlayersSquads': { + input: ApiSportsEndpointInputSchemas.getPlayersSquads, + output: ApiSportsEndpointOutputSchemas.getPlayersSquads, + }, + 'players.getPlayersTeams': { + input: ApiSportsEndpointInputSchemas.getPlayersTeams, + output: ApiSportsEndpointOutputSchemas.getPlayersTeams, + }, + 'players.getPlayersTopScorers': { + input: ApiSportsEndpointInputSchemas.getPlayersTopScorers, + output: ApiSportsEndpointOutputSchemas.getPlayersTopScorers, + }, + 'players.getPlayersTopAssists': { + input: ApiSportsEndpointInputSchemas.getPlayersTopAssists, + output: ApiSportsEndpointOutputSchemas.getPlayersTopAssists, + }, + 'players.getPlayersTopYellowCards': { + input: ApiSportsEndpointInputSchemas.getPlayersTopYellowCards, + output: ApiSportsEndpointOutputSchemas.getPlayersTopYellowCards, + }, + 'players.getPlayersTopRedCards': { + input: ApiSportsEndpointInputSchemas.getPlayersTopRedCards, + output: ApiSportsEndpointOutputSchemas.getPlayersTopRedCards, + }, + 'odds.getOdds': { + input: ApiSportsEndpointInputSchemas.getOdds, + output: ApiSportsEndpointOutputSchemas.getOdds, + }, + 'odds.getOddsBets': { + input: ApiSportsEndpointInputSchemas.getOddsBets, + output: ApiSportsEndpointOutputSchemas.getOddsBets, + }, + 'odds.getOddsBookmakers': { + input: ApiSportsEndpointInputSchemas.getOddsBookmakers, + output: ApiSportsEndpointOutputSchemas.getOddsBookmakers, + }, + 'odds.getOddsMapping': { + input: ApiSportsEndpointInputSchemas.getOddsMapping, + output: ApiSportsEndpointOutputSchemas.getOddsMapping, + }, + 'odds.getInPlayOdds': { + input: ApiSportsEndpointInputSchemas.getInPlayOdds, + output: ApiSportsEndpointOutputSchemas.getInPlayOdds, + }, + 'odds.getLiveOddsBets': { + input: ApiSportsEndpointInputSchemas.getLiveOddsBets, + output: ApiSportsEndpointOutputSchemas.getLiveOddsBets, + }, + 'basketball.getBasketballStatistics': { + input: ApiSportsEndpointInputSchemas.getBasketballStatistics, + output: ApiSportsEndpointOutputSchemas.getBasketballStatistics, + }, + 'basketball.getBasketballBets': { + input: ApiSportsEndpointInputSchemas.getBasketballBets, + output: ApiSportsEndpointOutputSchemas.getBasketballBets, + }, + 'basketball.getBasketballBookmakers': { + input: ApiSportsEndpointInputSchemas.getBasketballBookmakers, + output: ApiSportsEndpointOutputSchemas.getBasketballBookmakers, + }, + 'basketball.getNbaGameStatistics': { + input: ApiSportsEndpointInputSchemas.getNbaGameStatistics, + output: ApiSportsEndpointOutputSchemas.getNbaGameStatistics, + }, + 'basketball.getPlayerStatistics': { + input: ApiSportsEndpointInputSchemas.getPlayerStatistics, + output: ApiSportsEndpointOutputSchemas.getPlayerStatistics, + }, + 'basketball.getGameStatisticsByTeams': { + input: ApiSportsEndpointInputSchemas.getGameStatisticsByTeams, + output: ApiSportsEndpointOutputSchemas.getGameStatisticsByTeams, + }, + 'basketball.getGamesEvents': { + input: ApiSportsEndpointInputSchemas.getGamesEvents, + output: ApiSportsEndpointOutputSchemas.getGamesEvents, + }, + 'afl.getAflSeasons': { + input: ApiSportsEndpointInputSchemas.getAflSeasons, + output: ApiSportsEndpointOutputSchemas.getAflSeasons, + }, + 'afl.getAflGames': { + input: ApiSportsEndpointInputSchemas.getAflGames, + output: ApiSportsEndpointOutputSchemas.getAflGames, + }, + 'afl.getAflGamesQuarters': { + input: ApiSportsEndpointInputSchemas.getAflGamesQuarters, + output: ApiSportsEndpointOutputSchemas.getAflGamesQuarters, + }, + 'afl.getAflGamePlayerStatistics': { + input: ApiSportsEndpointInputSchemas.getAflGamePlayerStatistics, + output: ApiSportsEndpointOutputSchemas.getAflGamePlayerStatistics, + }, + 'afl.getAflStandings': { + input: ApiSportsEndpointInputSchemas.getAflStandings, + output: ApiSportsEndpointOutputSchemas.getAflStandings, + }, + 'baseball.getBaseballGamesHeadToHead': { + input: ApiSportsEndpointInputSchemas.getBaseballGamesHeadToHead, + output: ApiSportsEndpointOutputSchemas.getBaseballGamesHeadToHead, + }, + 'formula1.getFormula1Circuits': { + input: ApiSportsEndpointInputSchemas.getFormula1Circuits, + output: ApiSportsEndpointOutputSchemas.getFormula1Circuits, + }, + 'formula1.getFormula1Competitions': { + input: ApiSportsEndpointInputSchemas.getFormula1Competitions, + output: ApiSportsEndpointOutputSchemas.getFormula1Competitions, + }, + 'formula1.getFormula1Races': { + input: ApiSportsEndpointInputSchemas.getFormula1Races, + output: ApiSportsEndpointOutputSchemas.getFormula1Races, + }, + 'formula1.getFormula1DriverRankings': { + input: ApiSportsEndpointInputSchemas.getFormula1DriverRankings, + output: ApiSportsEndpointOutputSchemas.getFormula1DriverRankings, + }, + 'formula1.getFormula1TeamRankings': { + input: ApiSportsEndpointInputSchemas.getFormula1TeamRankings, + output: ApiSportsEndpointOutputSchemas.getFormula1TeamRankings, + }, + 'formula1.getFormula1StartingGrid': { + input: ApiSportsEndpointInputSchemas.getFormula1StartingGrid, + output: ApiSportsEndpointOutputSchemas.getFormula1StartingGrid, + }, + 'formula1.getFastestLapsRankings': { + input: ApiSportsEndpointInputSchemas.getFastestLapsRankings, + output: ApiSportsEndpointOutputSchemas.getFastestLapsRankings, + }, + 'formula1.getRaceRankings': { + input: ApiSportsEndpointInputSchemas.getRaceRankings, + output: ApiSportsEndpointOutputSchemas.getRaceRankings, + }, + 'mma.getMmaCategories': { + input: ApiSportsEndpointInputSchemas.getMmaCategories, + output: ApiSportsEndpointOutputSchemas.getMmaCategories, + }, + 'mma.getMmaFighters': { + input: ApiSportsEndpointInputSchemas.getMmaFighters, + output: ApiSportsEndpointOutputSchemas.getMmaFighters, + }, + 'mma.getMmaFights': { + input: ApiSportsEndpointInputSchemas.getMmaFights, + output: ApiSportsEndpointOutputSchemas.getMmaFights, + }, + 'mma.getMmaFightResults': { + input: ApiSportsEndpointInputSchemas.getMmaFightResults, + output: ApiSportsEndpointOutputSchemas.getMmaFightResults, + }, + 'mma.getMmaFighterStatistics': { + input: ApiSportsEndpointInputSchemas.getMmaFighterStatistics, + output: ApiSportsEndpointOutputSchemas.getMmaFighterStatistics, + }, + 'mma.getFightersRecords': { + input: ApiSportsEndpointInputSchemas.getFightersRecords, + output: ApiSportsEndpointOutputSchemas.getFightersRecords, + }, +} satisfies RequiredPluginEndpointSchemas; + +const apiSportsEndpointMeta = { + 'core.getCountries': { + riskLevel: 'read', + description: 'Get Countries', + }, + 'core.getTimezone': { + riskLevel: 'read', + description: 'Get Timezone', + }, + 'core.getLeagues': { + riskLevel: 'read', + description: 'Get Leagues', + }, + 'core.getLeagueSeasons': { + riskLevel: 'read', + description: 'Get League Seasons', + }, + 'core.getTeams': { + riskLevel: 'read', + description: 'Get Teams', + }, + 'core.getTeamSeasons': { + riskLevel: 'read', + description: 'Get Team Seasons', + }, + 'core.getTeamStatistics': { + riskLevel: 'read', + description: 'Get Team Statistics', + }, + 'core.getVenues': { + riskLevel: 'read', + description: 'Get Venues', + }, + 'core.getCoaches': { + riskLevel: 'read', + description: 'Get Coaches', + }, + 'core.getInjuries': { + riskLevel: 'read', + description: 'Get Injuries', + }, + 'core.getSidelined': { + riskLevel: 'read', + description: 'Get Sidelined', + }, + 'core.getTransfers': { + riskLevel: 'read', + description: 'Get Transfers', + }, + 'core.getTrophies': { + riskLevel: 'read', + description: 'Get Trophies', + }, + 'core.getPredictions': { + riskLevel: 'read', + description: 'Get Predictions', + }, + 'fixtures.getFixtures': { + riskLevel: 'read', + description: 'Get Fixtures', + }, + 'fixtures.getFixturesRounds': { + riskLevel: 'read', + description: 'Get Fixtures Rounds', + }, + 'fixtures.getHeadToHeadFixtures': { + riskLevel: 'read', + description: 'Get Head-to-Head Fixtures', + }, + 'fixtures.getFixtureLineups': { + riskLevel: 'read', + description: 'Get Fixture Lineups', + }, + 'fixtures.getFixtureStatistics': { + riskLevel: 'read', + description: 'Get Fixture Statistics', + }, + 'fixtures.getFixturesEvents': { + riskLevel: 'read', + description: 'Get Fixtures Events', + }, + 'fixtures.getFixturesPlayers': { + riskLevel: 'read', + description: 'Get Fixtures Players', + }, + 'standings.getStandingsStages': { + riskLevel: 'read', + description: 'Get Standings Stages', + }, + 'standings.getStandingsGroups': { + riskLevel: 'read', + description: 'Get Standings Groups', + }, + 'standings.getStandingsDivisions': { + riskLevel: 'read', + description: 'Get Standings Divisions', + }, + 'standings.getNflStandingsConferences': { + riskLevel: 'read', + description: 'Get NFL Standings Conferences', + }, + 'players.getPlayers': { + riskLevel: 'read', + description: 'Get Players', + }, + 'players.getPlayersProfiles': { + riskLevel: 'read', + description: 'Get Players Profiles', + }, + 'players.getPlayersSeasons': { + riskLevel: 'read', + description: 'Get Players Seasons', + }, + 'players.getPlayersSquads': { + riskLevel: 'read', + description: 'Get Players Squads', + }, + 'players.getPlayersTeams': { + riskLevel: 'read', + description: 'Get Players Teams', + }, + 'players.getPlayersTopScorers': { + riskLevel: 'read', + description: 'Get Players Top Scorers', + }, + 'players.getPlayersTopAssists': { + riskLevel: 'read', + description: 'Get Players Top Assists', + }, + 'players.getPlayersTopYellowCards': { + riskLevel: 'read', + description: 'Get Players Top Yellow Cards', + }, + 'players.getPlayersTopRedCards': { + riskLevel: 'read', + description: 'Get Players Top Red Cards', + }, + 'odds.getOdds': { + riskLevel: 'read', + description: 'Get Odds', + }, + 'odds.getOddsBets': { + riskLevel: 'read', + description: 'Get Odds Bets', + }, + 'odds.getOddsBookmakers': { + riskLevel: 'read', + description: 'Get Odds Bookmakers', + }, + 'odds.getOddsMapping': { + riskLevel: 'read', + description: 'Get Odds Mapping', + }, + 'odds.getInPlayOdds': { + riskLevel: 'read', + description: 'Get In-Play Odds', + }, + 'odds.getLiveOddsBets': { + riskLevel: 'read', + description: 'Get Live Odds Bets', + }, + 'basketball.getBasketballStatistics': { + riskLevel: 'read', + description: 'Get Basketball Statistics', + }, + 'basketball.getBasketballBets': { + riskLevel: 'read', + description: 'Get Basketball Bets', + }, + 'basketball.getBasketballBookmakers': { + riskLevel: 'read', + description: 'Get Basketball Bookmakers', + }, + 'basketball.getNbaGameStatistics': { + riskLevel: 'read', + description: 'Get NBA Game Statistics', + }, + 'basketball.getPlayerStatistics': { + riskLevel: 'read', + description: 'Get Player Statistics', + }, + 'basketball.getGameStatisticsByTeams': { + riskLevel: 'read', + description: 'Get Game Statistics by Teams', + }, + 'basketball.getGamesEvents': { + riskLevel: 'read', + description: 'Get Games Events', + }, + 'afl.getAflSeasons': { + riskLevel: 'read', + description: 'Get AFL Seasons', + }, + 'afl.getAflGames': { + riskLevel: 'read', + description: 'Get AFL Games', + }, + 'afl.getAflGamesQuarters': { + riskLevel: 'read', + description: 'Get AFL Games Quarters', + }, + 'afl.getAflGamePlayerStatistics': { + riskLevel: 'read', + description: 'Get AFL Game Player Statistics', + }, + 'afl.getAflStandings': { + riskLevel: 'read', + description: 'Get AFL Standings', + }, + 'baseball.getBaseballGamesHeadToHead': { + riskLevel: 'read', + description: 'Get Baseball Games Head-to-Head', + }, + 'formula1.getFormula1Circuits': { + riskLevel: 'read', + description: 'Get Formula 1 Circuits', + }, + 'formula1.getFormula1Competitions': { + riskLevel: 'read', + description: 'Get Formula 1 Competitions', + }, + 'formula1.getFormula1Races': { + riskLevel: 'read', + description: 'Get Formula 1 Races', + }, + 'formula1.getFormula1DriverRankings': { + riskLevel: 'read', + description: 'Get Formula 1 Driver Rankings', + }, + 'formula1.getFormula1TeamRankings': { + riskLevel: 'read', + description: 'Get Formula 1 Team Rankings', + }, + 'formula1.getFormula1StartingGrid': { + riskLevel: 'read', + description: 'Get Formula 1 Starting Grid', + }, + 'formula1.getFastestLapsRankings': { + riskLevel: 'read', + description: 'Get Fastest Laps Rankings', + }, + 'formula1.getRaceRankings': { + riskLevel: 'read', + description: 'Get Race Rankings', + }, + 'mma.getMmaCategories': { + riskLevel: 'read', + description: 'Get MMA Categories', + }, + 'mma.getMmaFighters': { + riskLevel: 'read', + description: 'Get MMA Fighters', + }, + 'mma.getMmaFights': { + riskLevel: 'read', + description: 'Get MMA Fights', + }, + 'mma.getMmaFightResults': { + riskLevel: 'read', + description: 'Get MMA Fight Results', + }, + 'mma.getMmaFighterStatistics': { + riskLevel: 'read', + description: 'Get MMA Fighter Statistics', + }, + 'mma.getFightersRecords': { + riskLevel: 'read', + description: 'Get Fighters Records', + }, +} satisfies RequiredPluginEndpointMeta; + +const defaultAuthType: AuthTypes = 'api_key' as const; + +export const apiSportsAuthConfig = { + api_key: {}, +} as const satisfies PluginAuthConfig; + +export type BaseApiSportsPlugin = + CorsairPlugin< + 'apisports', + typeof ApiSportsSchema, + typeof apiSportsEndpointsNested, + typeof apiSportsWebhooksNested, + T, + typeof defaultAuthType + >; + +export type InternalApiSportsPlugin = + BaseApiSportsPlugin; + +export type ExternalApiSportsPlugin = + BaseApiSportsPlugin; + +export function apisports( + incomingOptions: ApiSportsPluginOptions & T = {} as ApiSportsPluginOptions & + T, +): ExternalApiSportsPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + return { + id: 'apisports', + authConfig: apiSportsAuthConfig, + schema: ApiSportsSchema, + options, + hooks: options.hooks, + webhookHooks: undefined, + endpoints: apiSportsEndpointsNested, + webhooks: apiSportsWebhooksNested, + endpointMeta: apiSportsEndpointMeta, + endpointSchemas: apiSportsEndpointSchemas, + pluginWebhookMatcher: undefined, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async (ctx: ApiSportsKeyBuilderContext, source) => { + if (source === 'endpoint' && options.key) { + return options.key; + } + if (source === 'endpoint' && ctx.authType === 'api_key') { + const res = await ctx.keys.get_api_key(); + if (!res) { + throw new AuthMissingError('apisports', 'api_key'); + } + return res; + } + throw new AuthMissingError('apisports', 'api_key'); + }, + } satisfies InternalApiSportsPlugin; +} + +export type { + ApiSportsEndpointInputs, + ApiSportsEndpointOutputs, + ApiSportsQueryInput, + ApiSportsResponse, +} from './endpoints/types'; + +export { + ApiSportsEndpointInputSchemas, + ApiSportsEndpointOutputSchemas, + ApiSportsQueryInputSchema, + ApiSportsResponseSchema, +} from './endpoints/types'; diff --git a/packages/apisports/jest.config.cjs b/packages/apisports/jest.config.cjs new file mode 100644 index 000000000..26c3be375 --- /dev/null +++ b/packages/apisports/jest.config.cjs @@ -0,0 +1,29 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: ['**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + }, + moduleNameMapper: { + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 60000, + verbose: true, +}; diff --git a/packages/apisports/package.json b/packages/apisports/package.json new file mode 100644 index 000000000..e8ffcbd43 --- /dev/null +++ b/packages/apisports/package.json @@ -0,0 +1,45 @@ +{ + "name": "@corsair-dev/apisports", + "version": "0.1.0", + "description": "API-Sports plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "dotenv": "^17.2.3", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "apisports", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/apisports/schema/database.ts b/packages/apisports/schema/database.ts new file mode 100644 index 000000000..892096ace --- /dev/null +++ b/packages/apisports/schema/database.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const ApiSportsQueryRecord = z.object({ + sport: z.string().optional(), + path: z.string().optional(), + queriedAt: z.coerce.date().nullable().optional(), +}); + +export type ApiSportsQueryRecord = z.infer; diff --git a/packages/apisports/schema/index.ts b/packages/apisports/schema/index.ts new file mode 100644 index 000000000..2ad135eee --- /dev/null +++ b/packages/apisports/schema/index.ts @@ -0,0 +1,8 @@ +import { ApiSportsQueryRecord } from './database'; + +export const ApiSportsSchema = { + version: '1.0.0', + entities: { + queries: ApiSportsQueryRecord, + }, +} as const; diff --git a/packages/apisports/tsconfig.json b/packages/apisports/tsconfig.json new file mode 100644 index 000000000..46639aa0f --- /dev/null +++ b/packages/apisports/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node", "jest"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./**/*"], + "exclude": ["dist", "node_modules", "scripts"], + "references": [] +} diff --git a/packages/apisports/tsup.config.ts b/packages/apisports/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/apisports/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index f5141405a..90b47976a 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -29,6 +29,7 @@ export const BaseProviders = [ 'ambee', 'amplitude', 'apilabz', + 'apisports', 'asana', 'bitwarden', 'bluesky', @@ -140,6 +141,7 @@ export const ProviderDisplayNames = { ambee: 'Ambee', amplitude: 'Amplitude', apilabz: 'API Labz', + apisports: 'API-Sports', asana: 'Asana', bitwarden: 'Bitwarden', bluesky: 'Bluesky', @@ -258,6 +260,7 @@ export type AllProviders = | 'ambee' | 'amplitude' | 'apilabz' + | 'apisports' | 'asana' | 'bitwarden' | 'bluesky' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9da9c65a7..d013fbb29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -692,6 +692,33 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/apisports: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + dotenv: + specifier: ^17.2.3 + version: 17.4.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/app: dependencies: '@ai-sdk/mcp':