nhl-api-ts 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -2
  3. package/dist/api/edge.d.mts +180 -0
  4. package/dist/api/edge.mjs +184 -0
  5. package/dist/api/game-center.d.mts +84 -0
  6. package/dist/api/game-center.mjs +74 -0
  7. package/dist/api/helpers.d.mts +47 -0
  8. package/dist/api/helpers.mjs +106 -0
  9. package/dist/api/misc.d.mts +40 -0
  10. package/dist/api/misc.mjs +43 -0
  11. package/dist/api/players.d.mts +28 -0
  12. package/dist/api/players.mjs +18 -0
  13. package/dist/api/query/builder.d.mts +22 -0
  14. package/dist/api/query/builder.mjs +30 -0
  15. package/dist/api/query/filters.d.mts +93 -0
  16. package/dist/api/query/filters.mjs +118 -0
  17. package/dist/api/query/index.d.mts +4 -0
  18. package/dist/api/query/index.mjs +4 -0
  19. package/dist/api/query/sorting/goalie-sorts.d.mts +61 -0
  20. package/dist/api/query/sorting/goalie-sorts.mjs +54 -0
  21. package/dist/api/query/sorting/sorting-options.d.mts +136 -0
  22. package/dist/api/query/sorting/sorting-options.mjs +94 -0
  23. package/dist/api/schedule.d.mts +117 -0
  24. package/dist/api/schedule.mjs +131 -0
  25. package/dist/api/standings.d.mts +29 -0
  26. package/dist/api/standings.mjs +38 -0
  27. package/dist/api/stats.d.mts +153 -0
  28. package/dist/api/stats.mjs +166 -0
  29. package/dist/api/teams.d.mts +59 -0
  30. package/dist/api/teams.mjs +91 -0
  31. package/dist/cli/colors.d.mts +14 -0
  32. package/dist/cli/colors.mjs +13 -0
  33. package/dist/cli/commands.d.mts +27 -0
  34. package/dist/cli/commands.mjs +695 -0
  35. package/dist/cli/run.d.mts +9 -0
  36. package/dist/cli/run.mjs +313 -0
  37. package/dist/cli.d.mts +2 -0
  38. package/dist/cli.mjs +13 -0
  39. package/dist/config.d.mts +20 -0
  40. package/dist/config.mjs +4 -0
  41. package/dist/context.d.mts +6 -0
  42. package/dist/context.mjs +1 -0
  43. package/dist/errors.d.mts +35 -0
  44. package/dist/errors.mjs +49 -0
  45. package/dist/http-client.d.mts +27 -0
  46. package/dist/http-client.mjs +95 -0
  47. package/dist/index.d.mts +22 -0
  48. package/dist/index.mjs +15 -0
  49. package/dist/nhl-client.d.mts +101 -0
  50. package/dist/nhl-client.mjs +40 -0
  51. package/dist/types/common.d.mts +81 -0
  52. package/dist/types/common.mjs +2 -0
  53. package/dist/types/edge.d.mts +1024 -0
  54. package/dist/types/edge.mjs +1 -0
  55. package/dist/types/game-center.d.mts +1194 -0
  56. package/dist/types/game-center.mjs +1 -0
  57. package/dist/types/helpers.d.mts +17 -0
  58. package/dist/types/helpers.mjs +1 -0
  59. package/dist/types/index.d.mts +10 -0
  60. package/dist/types/index.mjs +1 -0
  61. package/dist/types/misc.d.mts +68 -0
  62. package/dist/types/misc.mjs +2 -0
  63. package/dist/types/players.d.mts +42 -0
  64. package/dist/types/players.mjs +1 -0
  65. package/dist/types/schedule.d.mts +456 -0
  66. package/dist/types/schedule.mjs +1 -0
  67. package/dist/types/standings.d.mts +104 -0
  68. package/dist/types/standings.mjs +1 -0
  69. package/dist/types/stats.d.mts +1086 -0
  70. package/dist/types/stats.mjs +1 -0
  71. package/dist/types/teams.d.mts +29 -0
  72. package/dist/types/teams.mjs +1 -0
  73. package/dist/utils.d.mts +4 -0
  74. package/dist/utils.mjs +3 -0
  75. package/package.json +85 -3
@@ -0,0 +1,9 @@
1
+ import type { ClientOptions } from '#src/config.mjs';
2
+ import { type NHLClient } from '#src/nhl-client.mjs';
3
+ export interface CliIO {
4
+ stdout: (text: string) => void;
5
+ stderr: (text: string) => void;
6
+ createClient?: (options: ClientOptions) => NHLClient;
7
+ }
8
+ /** Runs the CLI and resolves with the process exit code. */
9
+ export declare function runCli(argv: string[], io: CliIO): Promise<number>;
@@ -0,0 +1,313 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parseArgs } from 'node:util';
3
+ import { colors } from '#src/cli/colors.mjs';
4
+ import { MODULES, UsageError, } from '#src/cli/commands.mjs';
5
+ import { NHLApiException } from '#src/errors.mjs';
6
+ import { createNHLClient } from '#src/nhl-client.mjs';
7
+ const BIN = 'nhl-api-ts';
8
+ const kebab = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
9
+ const GLOBAL_OPTIONS = {
10
+ help: { type: 'boolean', short: 'h' },
11
+ version: { type: 'boolean', short: 'v' },
12
+ debug: { type: 'boolean' },
13
+ timeout: { type: 'string' },
14
+ 'follow-redirects': { type: 'boolean' },
15
+ compact: { type: 'boolean' },
16
+ };
17
+ const GLOBAL_HELP = `Global options:
18
+ -h, --help Show help (also per module / command)
19
+ -v, --version Show version
20
+ --compact Print JSON on a single line
21
+ --debug Log requests to stderr
22
+ --timeout <seconds> Request timeout (default: 10)
23
+ --no-follow-redirects Do not follow HTTP redirects`;
24
+ function version() {
25
+ try {
26
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
27
+ return pkg.version;
28
+ }
29
+ catch {
30
+ return 'unknown';
31
+ }
32
+ }
33
+ // ---- Help -------------------------------------------------------------------
34
+ function table(rows, indent = 2) {
35
+ const width = Math.max(0, ...rows.map(([label]) => label.length));
36
+ return rows
37
+ .map(([label, description]) => `${' '.repeat(indent)}${label.padEnd(width)} ${description}`)
38
+ .join('\n');
39
+ }
40
+ function rootHelp() {
41
+ return `Usage: ${BIN} ${colors.pass('<module> <command>')} [args] [options]
42
+
43
+ Command line access to the NHL API and NHL EDGE stats. Results are printed
44
+ as JSON on stdout.
45
+
46
+ Modules:
47
+ ${table(MODULES.map((moduleSpec) => [moduleSpec.name, moduleSpec.description]))}
48
+
49
+ ${GLOBAL_HELP}
50
+
51
+ Examples:
52
+ ${BIN} schedule daily-schedule --date 2024-10-09
53
+ ${BIN} game-center boxscore 2023020280
54
+ ${BIN} stats skater-stats-with-query-context summary --season-start 20232024 --position C
55
+
56
+ Run '${BIN} <module> --help' to list a module's commands.`;
57
+ }
58
+ function moduleHelp(mod) {
59
+ return `Usage: ${BIN} ${mod.name} ${colors.pass('<command>')} [args] [options]
60
+
61
+ ${mod.description}
62
+
63
+ Commands:
64
+ ${table(mod.commands.map((command) => [command.name, command.description]))}
65
+
66
+ Run '${BIN} ${mod.name} <command> --help' for command options.`;
67
+ }
68
+ function argLabel(arg) {
69
+ const flag = `--${kebab(arg.name)}`;
70
+ if (arg.type === 'boolean') {
71
+ return flag;
72
+ }
73
+ return `${flag} <${kebab(arg.name)}>`;
74
+ }
75
+ function commandHelp(mod, cmd) {
76
+ const required = cmd.args.filter((arg) => arg.required);
77
+ const optional = cmd.args.filter((arg) => !arg.required);
78
+ const usage = [
79
+ BIN,
80
+ mod.name,
81
+ cmd.name,
82
+ ...required.map((arg) => colors.pass(`<${kebab(arg.name)}>`)),
83
+ optional.length ? '[options]' : '',
84
+ ]
85
+ .filter(Boolean)
86
+ .join(' ');
87
+ const describe = (arg) => {
88
+ const extras = [
89
+ arg.choices ? `one of: ${arg.choices.join(', ')}` : '',
90
+ arg.default !== undefined ? `default: ${arg.default}` : '',
91
+ ].filter(Boolean);
92
+ return extras.length
93
+ ? `${arg.description} (${extras.join('; ')})`
94
+ : arg.description;
95
+ };
96
+ const sections = [`Usage: ${usage}`, cmd.description];
97
+ if (required.length) {
98
+ sections.push(`Arguments (positional, or as flags):\n${table(required.map((arg) => [
99
+ `<${kebab(arg.name)}>, ${argLabel(arg)}`,
100
+ describe(arg),
101
+ ]))}`);
102
+ }
103
+ if (optional.length) {
104
+ sections.push(`Options:\n${table(optional.map((arg) => [argLabel(arg), describe(arg)]))}`);
105
+ }
106
+ sections.push(GLOBAL_HELP);
107
+ return sections.join('\n\n');
108
+ }
109
+ // ---- Parsing ----------------------------------------------------------------
110
+ /** First two non-option tokens are the module and command names. */
111
+ function findTargets(argv) {
112
+ const targets = [];
113
+ for (let index = 0; index < argv.length && targets.length < 2; index++) {
114
+ const token = argv[index];
115
+ if (token === '--') {
116
+ break;
117
+ }
118
+ if (token === '--timeout') {
119
+ index++;
120
+ continue;
121
+ }
122
+ if (!token.startsWith('-')) {
123
+ targets.push(token);
124
+ }
125
+ }
126
+ return targets;
127
+ }
128
+ function convert(arg, raw) {
129
+ const flag = `--${kebab(arg.name)}`;
130
+ if (arg.type === 'boolean') {
131
+ return raw;
132
+ }
133
+ const value = String(raw);
134
+ if (arg.choices && !arg.choices.includes(value)) {
135
+ throw new UsageError(`${flag} must be one of: ${arg.choices.join(', ')} (got '${value}')`);
136
+ }
137
+ switch (arg.type) {
138
+ case 'int': {
139
+ if (!/^-?\d+$/.test(value)) {
140
+ throw new UsageError(`${flag} must be an integer (got '${value}')`);
141
+ }
142
+ return Number(value);
143
+ }
144
+ case 'number': {
145
+ const parsed = Number(value);
146
+ if (value.trim() === '' || Number.isNaN(parsed)) {
147
+ throw new UsageError(`${flag} must be a number (got '${value}')`);
148
+ }
149
+ return parsed;
150
+ }
151
+ case 'list':
152
+ return value
153
+ .split(',')
154
+ .map((part) => part.trim())
155
+ .filter(Boolean);
156
+ case 'int-list':
157
+ return value
158
+ .split(',')
159
+ .map((part) => part.trim())
160
+ .filter(Boolean)
161
+ .map((part) => {
162
+ if (!/^-?\d+$/.test(part)) {
163
+ throw new UsageError(`${flag} must be comma-separated integers`);
164
+ }
165
+ return Number(part);
166
+ });
167
+ case 'json':
168
+ try {
169
+ return JSON.parse(value);
170
+ }
171
+ catch {
172
+ throw new UsageError(`${flag} must be valid JSON`);
173
+ }
174
+ default:
175
+ return value;
176
+ }
177
+ }
178
+ function parseCommand(argv, cmd) {
179
+ const commandOptions = {};
180
+ for (const arg of cmd?.args ?? []) {
181
+ commandOptions[kebab(arg.name)] = {
182
+ type: arg.type === 'boolean' ? 'boolean' : 'string',
183
+ };
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = parseArgs({
188
+ args: argv,
189
+ allowPositionals: true,
190
+ allowNegative: true,
191
+ strict: true,
192
+ options: { ...GLOBAL_OPTIONS, ...commandOptions },
193
+ });
194
+ }
195
+ catch (err) {
196
+ throw new UsageError(err.message);
197
+ }
198
+ const raw = parsed.values;
199
+ const globals = {};
200
+ for (const key of Object.keys(GLOBAL_OPTIONS)) {
201
+ globals[key] = raw[key];
202
+ }
203
+ if (globals.help || !cmd) {
204
+ return { globals, values: {} };
205
+ }
206
+ // Drop the module and command names; the rest fill required args in order.
207
+ const extra = parsed.positionals.slice(2);
208
+ const values = {};
209
+ for (const arg of cmd.args) {
210
+ const flagValue = raw[kebab(arg.name)];
211
+ if (flagValue !== undefined) {
212
+ values[arg.name] = convert(arg, flagValue);
213
+ }
214
+ else if (arg.required && extra.length > 0) {
215
+ values[arg.name] = convert(arg, extra.shift());
216
+ }
217
+ }
218
+ if (extra.length > 0) {
219
+ throw new UsageError(`unexpected argument '${extra[0]}'`);
220
+ }
221
+ const missing = cmd.args.filter((arg) => arg.required && values[arg.name] === undefined);
222
+ if (missing.length > 0) {
223
+ throw new UsageError(`missing required argument${missing.length > 1 ? 's' : ''}: ${missing
224
+ .map((arg) => `<${kebab(arg.name)}>`)
225
+ .join(', ')}`);
226
+ }
227
+ return { globals, values };
228
+ }
229
+ function clientOptions(globals) {
230
+ const options = {};
231
+ if (globals.debug) {
232
+ options.debug = true;
233
+ }
234
+ if (globals['follow-redirects'] === false) {
235
+ options.followRedirects = false;
236
+ }
237
+ if (globals.timeout !== undefined) {
238
+ const timeout = Number(globals.timeout);
239
+ if (!(timeout > 0)) {
240
+ throw new UsageError(`--timeout must be a positive number of seconds (got '${globals.timeout}')`);
241
+ }
242
+ options.timeout = timeout;
243
+ }
244
+ return options;
245
+ }
246
+ // ---- Entry ------------------------------------------------------------------
247
+ /** Runs the CLI and resolves with the process exit code. */
248
+ export async function runCli(argv, io) {
249
+ const createClient = io.createClient ?? createNHLClient;
250
+ const [moduleName, commandName] = findTargets(argv);
251
+ const mod = MODULES.find((moduleSpec) => moduleSpec.name === moduleName);
252
+ const cmd = mod?.commands.find((command) => command.name === commandName);
253
+ const usageError = (message, help) => {
254
+ io.stderr(`${colors.error(`error: ${message}`)}\n\n${help}`);
255
+ return 2;
256
+ };
257
+ let parsed;
258
+ try {
259
+ parsed = parseCommand(argv, cmd);
260
+ }
261
+ catch (err) {
262
+ const help = cmd && mod ? commandHelp(mod, cmd) : mod ? moduleHelp(mod) : rootHelp();
263
+ return usageError(err.message, help);
264
+ }
265
+ const { globals, values } = parsed;
266
+ if (globals.version) {
267
+ io.stdout(version());
268
+ return 0;
269
+ }
270
+ if (!moduleName) {
271
+ if (globals.help) {
272
+ io.stdout(rootHelp());
273
+ return 0;
274
+ }
275
+ return usageError('missing <module>', rootHelp());
276
+ }
277
+ if (!mod) {
278
+ return usageError(`unknown module '${moduleName}'`, rootHelp());
279
+ }
280
+ if (!commandName) {
281
+ if (globals.help) {
282
+ io.stdout(moduleHelp(mod));
283
+ return 0;
284
+ }
285
+ return usageError('missing <command>', moduleHelp(mod));
286
+ }
287
+ if (!cmd) {
288
+ return usageError(`unknown command '${commandName}' for module '${mod.name}'`, moduleHelp(mod));
289
+ }
290
+ if (globals.help) {
291
+ io.stdout(commandHelp(mod, cmd));
292
+ return 0;
293
+ }
294
+ try {
295
+ const client = createClient(clientOptions(globals));
296
+ const result = await cmd.run(client, values);
297
+ io.stdout(globals.compact
298
+ ? JSON.stringify(result)
299
+ : JSON.stringify(result, null, 2));
300
+ return 0;
301
+ }
302
+ catch (err) {
303
+ if (err instanceof UsageError || err instanceof RangeError) {
304
+ return usageError(err.message, commandHelp(mod, cmd));
305
+ }
306
+ if (err instanceof NHLApiException) {
307
+ io.stderr(colors.error(`error: ${err.message} (HTTP ${err.statusCode})`));
308
+ return 1;
309
+ }
310
+ io.stderr(colors.error(`error: ${err?.message ?? String(err)}`));
311
+ return 1;
312
+ }
313
+ }
package/dist/cli.d.mts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.mjs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '#src/cli/run.mjs';
3
+ // Exit quietly when the reader goes away (e.g. `nhl-api-ts ... | head`).
4
+ process.stdout.on('error', (err) => {
5
+ if (err.code === 'EPIPE') {
6
+ process.exit(0);
7
+ }
8
+ throw err;
9
+ });
10
+ process.exitCode = await runCli(process.argv.slice(2), {
11
+ stdout: (text) => process.stdout.write(`${text}\n`),
12
+ stderr: (text) => process.stderr.write(`${text}\n`),
13
+ });
@@ -0,0 +1,20 @@
1
+ export interface ClientOptions {
2
+ /** Set to true for extra logging (written to stderr). Defaults to false. */
3
+ debug?: boolean;
4
+ /** Request timeout in seconds. Defaults to 10. */
5
+ timeout?: number;
6
+ /**
7
+ * Some endpoints use redirects. This is the case when using endpoints that
8
+ * use "/now" in them, which redirect to today's data. Defaults to true.
9
+ */
10
+ followRedirects?: boolean;
11
+ /** Overrides the fetch implementation used for requests. Defaults to the global fetch. */
12
+ fetch?: typeof fetch;
13
+ }
14
+ export interface Config {
15
+ readonly debug: boolean;
16
+ readonly timeout: number;
17
+ readonly followRedirects: boolean;
18
+ readonly fetch?: typeof fetch;
19
+ }
20
+ export declare function resolveConfig(options?: ClientOptions): Readonly<Config>;
@@ -0,0 +1,4 @@
1
+ export function resolveConfig(options = {}) {
2
+ const { debug = false, timeout = 10, followRedirects = true, fetch, } = options;
3
+ return { debug, timeout, followRedirects, fetch };
4
+ }
@@ -0,0 +1,6 @@
1
+ import type { Endpoint, QueryParams } from '#src/http-client.mjs';
2
+ export type Get = (endpoint: Endpoint, resource: string, params?: QueryParams) => Promise<Response>;
3
+ export interface Context {
4
+ get: Get;
5
+ sleep: (seconds: number) => Promise<void>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ /** NHL API specific error codes. */
2
+ export declare const NHLApiErrorCode: {
3
+ readonly RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND';
4
+ readonly RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED';
5
+ readonly SERVER_ERROR: 'SERVER_ERROR';
6
+ readonly BAD_REQUEST: 'BAD_REQUEST';
7
+ readonly UNAUTHORIZED: 'UNAUTHORIZED';
8
+ };
9
+ export type NHLApiErrorCode = (typeof NHLApiErrorCode)[keyof typeof NHLApiErrorCode];
10
+ /** Base exception for NHL API errors. */
11
+ export declare class NHLApiException extends Error {
12
+ readonly statusCode: number;
13
+ readonly errorCode?: NHLApiErrorCode;
14
+ constructor(message: string, statusCode: number, errorCode?: NHLApiErrorCode);
15
+ }
16
+ /** Raised when a resource is not found (404). */
17
+ export declare class ResourceNotFoundException extends NHLApiException {
18
+ constructor(message: string, statusCode?: number);
19
+ }
20
+ /** Raised when rate limit is exceeded (429). */
21
+ export declare class RateLimitExceededException extends NHLApiException {
22
+ constructor(message: string, statusCode?: number);
23
+ }
24
+ /** Raised for server errors (5xx). */
25
+ export declare class ServerErrorException extends NHLApiException {
26
+ constructor(message: string, statusCode: number);
27
+ }
28
+ /** Raised for client errors (400). */
29
+ export declare class BadRequestException extends NHLApiException {
30
+ constructor(message: string, statusCode?: number);
31
+ }
32
+ /** Raised for authentication errors (401). */
33
+ export declare class UnauthorizedException extends NHLApiException {
34
+ constructor(message: string, statusCode?: number);
35
+ }
@@ -0,0 +1,49 @@
1
+ /** NHL API specific error codes. */
2
+ export const NHLApiErrorCode = {
3
+ RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
4
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
5
+ SERVER_ERROR: 'SERVER_ERROR',
6
+ BAD_REQUEST: 'BAD_REQUEST',
7
+ UNAUTHORIZED: 'UNAUTHORIZED',
8
+ };
9
+ /** Base exception for NHL API errors. */
10
+ export class NHLApiException extends Error {
11
+ statusCode;
12
+ errorCode;
13
+ constructor(message, statusCode, errorCode) {
14
+ super(message);
15
+ this.name = new.target.name;
16
+ this.statusCode = statusCode;
17
+ this.errorCode = errorCode;
18
+ }
19
+ }
20
+ /** Raised when a resource is not found (404). */
21
+ export class ResourceNotFoundException extends NHLApiException {
22
+ constructor(message, statusCode = 404) {
23
+ super(message, statusCode, NHLApiErrorCode.RESOURCE_NOT_FOUND);
24
+ }
25
+ }
26
+ /** Raised when rate limit is exceeded (429). */
27
+ export class RateLimitExceededException extends NHLApiException {
28
+ constructor(message, statusCode = 429) {
29
+ super(message, statusCode, NHLApiErrorCode.RATE_LIMIT_EXCEEDED);
30
+ }
31
+ }
32
+ /** Raised for server errors (5xx). */
33
+ export class ServerErrorException extends NHLApiException {
34
+ constructor(message, statusCode) {
35
+ super(message, statusCode, NHLApiErrorCode.SERVER_ERROR);
36
+ }
37
+ }
38
+ /** Raised for client errors (400). */
39
+ export class BadRequestException extends NHLApiException {
40
+ constructor(message, statusCode = 400) {
41
+ super(message, statusCode, NHLApiErrorCode.BAD_REQUEST);
42
+ }
43
+ }
44
+ /** Raised for authentication errors (401). */
45
+ export class UnauthorizedException extends NHLApiException {
46
+ constructor(message, statusCode = 401) {
47
+ super(message, statusCode, NHLApiErrorCode.UNAUTHORIZED);
48
+ }
49
+ }
@@ -0,0 +1,27 @@
1
+ import type { Config } from '#src/config.mjs';
2
+ import type { Get } from '#src/context.mjs';
3
+ import { NHLApiException } from '#src/errors.mjs';
4
+ export declare const Endpoint: {
5
+ readonly API_WEB_V1: 'https://api-web.nhle.com/v1/';
6
+ readonly API_CORE: 'https://api.nhle.com/';
7
+ readonly API_STATS: 'https://api.nhle.com/stats/rest/';
8
+ };
9
+ export type Endpoint = (typeof Endpoint)[keyof typeof Endpoint];
10
+ export type QueryParamValue = string | number | boolean | null | undefined;
11
+ export type QueryParams = Record<string, QueryParamValue>;
12
+ export declare function buildUrl(endpoint: Endpoint, resource: string, queryParams?: QueryParams): string;
13
+ /** Maps a non-2xx HTTP status to the matching NHLApiException subclass. */
14
+ export declare function errorForStatus(status: number, message: string): NHLApiException;
15
+ /** Builds the error message for a non-2xx response, sniffing a JSON `message` field. */
16
+ export declare function readErrorMessage(response: Response, url: string): Promise<string>;
17
+ /**
18
+ * Builds a `Get` function bound to the given config.
19
+ *
20
+ * @throws ResourceNotFoundException When the resource is not found
21
+ * @throws RateLimitExceededException When rate limit is exceeded
22
+ * @throws ServerErrorException When server returns 5xx error
23
+ * @throws BadRequestException When request is malformed
24
+ * @throws UnauthorizedException When authentication fails
25
+ * @throws NHLApiException For other unexpected errors
26
+ */
27
+ export declare function createGet(config: Config): Get;
@@ -0,0 +1,95 @@
1
+ import { BadRequestException, NHLApiException, RateLimitExceededException, ResourceNotFoundException, ServerErrorException, UnauthorizedException, } from '#src/errors.mjs';
2
+ export const Endpoint = {
3
+ API_WEB_V1: 'https://api-web.nhle.com/v1/',
4
+ API_CORE: 'https://api.nhle.com/',
5
+ API_STATS: 'https://api.nhle.com/stats/rest/',
6
+ };
7
+ /** Converts a query param value the same way httpx does (True -> "true", None -> ""). */
8
+ function paramToString(value) {
9
+ if (value === null || value === undefined) {
10
+ return '';
11
+ }
12
+ return String(value);
13
+ }
14
+ export function buildUrl(endpoint, resource, queryParams) {
15
+ const url = `${endpoint}${resource}`;
16
+ if (!queryParams) {
17
+ return url;
18
+ }
19
+ const search = new URLSearchParams();
20
+ for (const [key, value] of Object.entries(queryParams)) {
21
+ search.append(key, paramToString(value));
22
+ }
23
+ const qs = search.toString();
24
+ if (!qs) {
25
+ return url;
26
+ }
27
+ return `${url}${url.includes('?') ? '&' : '?'}${qs}`;
28
+ }
29
+ /** Maps a non-2xx HTTP status to the matching NHLApiException subclass. */
30
+ export function errorForStatus(status, message) {
31
+ if (status === 404) {
32
+ return new ResourceNotFoundException(message);
33
+ }
34
+ if (status === 429) {
35
+ return new RateLimitExceededException(message);
36
+ }
37
+ if (status === 400) {
38
+ return new BadRequestException(message);
39
+ }
40
+ if (status === 401) {
41
+ return new UnauthorizedException(message);
42
+ }
43
+ if (status >= 500 && status < 600) {
44
+ return new ServerErrorException(message, status);
45
+ }
46
+ return new NHLApiException(`Unexpected error: ${message}`, status);
47
+ }
48
+ /** Builds the error message for a non-2xx response, sniffing a JSON `message` field. */
49
+ export async function readErrorMessage(response, url) {
50
+ let errorMessage = `Request to ${url} failed`;
51
+ try {
52
+ const json = await response.json();
53
+ if (json !== null && typeof json === 'object' && !Array.isArray(json)) {
54
+ const detail = json.message;
55
+ if (detail) {
56
+ errorMessage = `${errorMessage}: ${detail}`;
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Response isn't JSON or doesn't have a message field
62
+ }
63
+ return errorMessage;
64
+ }
65
+ /**
66
+ * Builds a `Get` function bound to the given config.
67
+ *
68
+ * @throws ResourceNotFoundException When the resource is not found
69
+ * @throws RateLimitExceededException When rate limit is exceeded
70
+ * @throws ServerErrorException When server returns 5xx error
71
+ * @throws BadRequestException When request is malformed
72
+ * @throws UnauthorizedException When authentication fails
73
+ * @throws NHLApiException For other unexpected errors
74
+ */
75
+ export function createGet(config) {
76
+ const debug = (message) => {
77
+ if (config.debug) {
78
+ console.error(`nhl-api-ts.http_client - DEBUG - ${message}`);
79
+ }
80
+ };
81
+ return async (endpoint, resource, queryParams) => {
82
+ const fullUrl = buildUrl(endpoint, resource, queryParams);
83
+ debug(`GET: ${fullUrl}`);
84
+ const doFetch = config.fetch ?? globalThis.fetch;
85
+ const response = await doFetch(fullUrl, {
86
+ signal: AbortSignal.timeout(config.timeout * 1000),
87
+ redirect: config.followRedirects ? 'follow' : 'manual',
88
+ });
89
+ if (!response.ok) {
90
+ const message = await readErrorMessage(response, resource);
91
+ throw errorForStatus(response.status, message);
92
+ }
93
+ return response;
94
+ };
95
+ }
@@ -0,0 +1,22 @@
1
+ export type { EdgePlayerParams, EdgeSeasonParams, EdgeTeamParams, } from '#src/api/edge.mjs';
2
+ export * as edge from '#src/api/edge.mjs';
3
+ export * as gameCenter from '#src/api/game-center.mjs';
4
+ export * as helpers from '#src/api/helpers.mjs';
5
+ export * as misc from '#src/api/misc.mjs';
6
+ export * as players from '#src/api/players.mjs';
7
+ export * from '#src/api/query/index.mjs';
8
+ export type { DailySchedule } from '#src/api/schedule.mjs';
9
+ export * as schedule from '#src/api/schedule.mjs';
10
+ export { normalizeDate } from '#src/api/schedule.mjs';
11
+ export * as standings from '#src/api/standings.mjs';
12
+ export type { GoalieStatsSummaryParams, SkaterStatsSummaryParams, SkaterStatsWithQueryContextParams, TeamSummaryParams, } from '#src/api/stats.mjs';
13
+ export * as stats from '#src/api/stats.mjs';
14
+ export type { Team } from '#src/api/teams.mjs';
15
+ export * as teams from '#src/api/teams.mjs';
16
+ export * from '#src/config.mjs';
17
+ export type { Context, Get } from '#src/context.mjs';
18
+ export * from '#src/errors.mjs';
19
+ export { buildUrl, createGet, Endpoint, type QueryParams, type QueryParamValue, } from '#src/http-client.mjs';
20
+ export { createNHLClient, type NHLClient } from '#src/nhl-client.mjs';
21
+ export type * from '#src/types/index.mjs';
22
+ export type { Id } from '#src/utils.mjs';
package/dist/index.mjs ADDED
@@ -0,0 +1,15 @@
1
+ export * as edge from '#src/api/edge.mjs';
2
+ export * as gameCenter from '#src/api/game-center.mjs';
3
+ export * as helpers from '#src/api/helpers.mjs';
4
+ export * as misc from '#src/api/misc.mjs';
5
+ export * as players from '#src/api/players.mjs';
6
+ export * from '#src/api/query/index.mjs';
7
+ export * as schedule from '#src/api/schedule.mjs';
8
+ export { normalizeDate } from '#src/api/schedule.mjs';
9
+ export * as standings from '#src/api/standings.mjs';
10
+ export * as stats from '#src/api/stats.mjs';
11
+ export * as teams from '#src/api/teams.mjs';
12
+ export * from '#src/config.mjs';
13
+ export * from '#src/errors.mjs';
14
+ export { buildUrl, createGet, Endpoint, } from '#src/http-client.mjs';
15
+ export { createNHLClient } from '#src/nhl-client.mjs';