@databallr/sdk 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 databallr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # @databallr/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript client for the [databallr API](https://api.databallr.com/docs):
4
+ NBA and WNBA stats, player and team data, WOWY, shot quality, and SixRings.
5
+
6
+ ```bash
7
+ npm install @databallr/sdk@next
8
+ ```
9
+
10
+ Releases are 0.x on the `next` dist-tag while method names settle, so a minor
11
+ version may break.
12
+
13
+ ```ts
14
+ import { Databallr } from "@databallr/sdk";
15
+
16
+ const dbr = new Databallr({ apiKey: process.env.DATABALLR_API_KEY });
17
+
18
+ // One method per operation: path and query parameters in one object.
19
+ const players = await dbr.nba.players.search({ q: "jokic", limit: 5 });
20
+ const stats = await dbr.nba.players.byPlayerIdStats({ player_id: 203999, year: 2026 });
21
+ const combos = await dbr.nba.pbp.lineupCombos({ team_abbr: "DEN" });
22
+
23
+ // Or any path directly.
24
+ const standings = await dbr.get("/v1/nba/standings", { query: { season: 2026 } });
25
+ ```
26
+
27
+ - Create a key at https://accounts.databallr.com/account/api-keys. Keys start
28
+ with `dbrk_` and call `https://api.databallr.com`. `baseUrl` (or
29
+ `DATABALLR_BASE_URL`) points the client at another host.
30
+ - Paths, parameters, bodies, and responses are typed from the API's OpenAPI
31
+ document. Only operations a personal API key can call are included.
32
+ - A 429 is retried after its `Retry-After`; a 502, 503, 504, timeout, or
33
+ network error is retried with backoff. `maxRetries` (default 2) and
34
+ `timeoutMs` (default 30 s, per attempt) can be set on the client or per
35
+ call, along with `signal` and `headers`.
36
+ - Non-2xx responses throw `DataballrError` (`status`, `code`, `message`,
37
+ `requestId`, `requiredScope`, `retryAfter`). Network failures throw
38
+ `DataballrConnectionError`, and timeouts its subclass
39
+ `DataballrTimeoutError`.
40
+ - Responses are returned exactly as the API sends them.
41
+ - The client refuses to run in a browser, where the key would be public,
42
+ unless you pass `allowBrowser: true`.
43
+
44
+ MIT licensed.
@@ -0,0 +1,44 @@
1
+ import { methods } from "./generated/methods.ts";
2
+ import type { Call, Methods } from "./types.ts";
3
+ export declare const PRODUCTION_BASE_URL = "https://api.databallr.com";
4
+ /** Mirrors `@databallr/api-schemas/api-key-prefix`; a spec keeps them equal. */
5
+ export declare const PRODUCTION_API_KEY_PREFIX = "dbrk_";
6
+ export interface DataballrOptions {
7
+ /** A personal API key. Defaults to `process.env.DATABALLR_API_KEY`. */
8
+ apiKey?: string;
9
+ /**
10
+ * Defaults to `process.env.DATABALLR_BASE_URL`, then
11
+ * `https://api.databallr.com` (which accepts only `dbrk_` keys).
12
+ */
13
+ baseUrl?: string;
14
+ /** Defaults to the global `fetch`. */
15
+ fetch?: typeof fetch;
16
+ /** Per attempt, including reading the body. Defaults to 30 seconds. */
17
+ timeoutMs?: number;
18
+ /**
19
+ * Retries after a 429 (once its `Retry-After` passes), a 502, 503 or 504, a
20
+ * timeout, or a network error. Every operation is a read, so a retry is
21
+ * safe, but it counts against the key's rate limit. Defaults to 2.
22
+ */
23
+ maxRetries?: number;
24
+ /**
25
+ * A key in a browser bundle is readable by anyone who loads the page, so
26
+ * construction in a browser throws unless this is set.
27
+ */
28
+ allowBrowser?: boolean;
29
+ }
30
+ /**
31
+ * Generated methods, e.g. `dbr.nba.players.search({ q: "jokic" })`. The
32
+ * constructor assigns every one from the same table, so none is left unset.
33
+ */
34
+ export interface Databallr extends Methods<typeof methods> {
35
+ }
36
+ export declare class Databallr {
37
+ #private;
38
+ readonly baseUrl: string;
39
+ constructor(options?: DataballrOptions);
40
+ /** `GET` any key-callable path, typed from the API's OpenAPI document. */
41
+ readonly get: Call<"get">;
42
+ /** `POST` one of the read-only query operations. */
43
+ readonly post: Call<"post">;
44
+ }
package/dist/client.js ADDED
@@ -0,0 +1,199 @@
1
+ import { DataballrConnectionError, DataballrTimeoutError, errorFromResponse } from "./errors.js";
2
+ import { methods } from "./generated/methods.js";
3
+ export const PRODUCTION_BASE_URL = "https://api.databallr.com";
4
+ export const PRODUCTION_API_KEY_PREFIX = "dbrk_";
5
+ const RETRY_STATUSES = new Set([429, 502, 503, 504]);
6
+ const MAX_RETRY_AFTER_MS = 60_000;
7
+ export class Databallr {
8
+ baseUrl;
9
+ #apiKey;
10
+ #fetch;
11
+ #timeoutMs;
12
+ #maxRetries;
13
+ constructor(options = {}) {
14
+ if (!options.allowBrowser && isBrowser()) {
15
+ throw new Error("Databallr: refusing to run in a browser, where the API key would be public. " +
16
+ "Call the API from a server, or pass allowBrowser: true if you accept that.");
17
+ }
18
+ const apiKey = options.apiKey ?? env("DATABALLR_API_KEY");
19
+ if (!apiKey) {
20
+ throw new Error("Databallr: no API key. Pass { apiKey } or set DATABALLR_API_KEY. " +
21
+ "Create one at https://accounts.databallr.com/account/api-keys.");
22
+ }
23
+ const baseUrl = trimSlash(options.baseUrl ?? env("DATABALLR_BASE_URL") ?? PRODUCTION_BASE_URL);
24
+ if (baseUrl === PRODUCTION_BASE_URL && !apiKey.startsWith(PRODUCTION_API_KEY_PREFIX)) {
25
+ throw new Error(`Databallr: ${PRODUCTION_BASE_URL} accepts only keys that start with ${PRODUCTION_API_KEY_PREFIX}.`);
26
+ }
27
+ const fetchImpl = options.fetch ?? globalThis.fetch;
28
+ if (typeof fetchImpl !== "function") {
29
+ throw new Error("Databallr: no global fetch. Pass { fetch } on this runtime.");
30
+ }
31
+ this.baseUrl = baseUrl;
32
+ this.#apiKey = apiKey;
33
+ this.#fetch = fetchImpl;
34
+ this.#timeoutMs = options.timeoutMs ?? 30_000;
35
+ this.#maxRetries = options.maxRetries ?? 2;
36
+ Object.assign(this, this.#bind(methods));
37
+ }
38
+ get = (path, ...[options]) => this.#request("GET", path, options);
39
+ post = (path, ...[options]) => this.#request("POST", path, options);
40
+ #bind(table) {
41
+ return Object.fromEntries(Object.entries(table).map(([name, target]) => {
42
+ if (typeof target !== "string")
43
+ return [name, this.#bind(target)];
44
+ if (target.startsWith("POST ")) {
45
+ const path = target.slice("POST ".length);
46
+ return [
47
+ name,
48
+ (body, options) => this.#request("POST", path, { ...options, body }),
49
+ ];
50
+ }
51
+ const placeholders = new Set(Array.from(target.matchAll(/\{([^}]+)\}/g), (m) => m[1]));
52
+ return [
53
+ name,
54
+ (params = {}, options) => {
55
+ const path = {};
56
+ const query = {};
57
+ for (const [key, value] of Object.entries(params)) {
58
+ (placeholders.has(key) ? path : query)[key] = value;
59
+ }
60
+ return this.#request("GET", target, { ...options, path, query });
61
+ },
62
+ ];
63
+ }));
64
+ }
65
+ async #request(method, path, options) {
66
+ const url = new URL(this.baseUrl + fillPath(path, options?.path));
67
+ appendQuery(url, options?.query);
68
+ const headers = {
69
+ accept: "application/json",
70
+ ...options?.headers,
71
+ authorization: `Bearer ${this.#apiKey}`,
72
+ };
73
+ let body;
74
+ if (options?.body !== undefined) {
75
+ headers["content-type"] = "application/json";
76
+ body = JSON.stringify(options.body);
77
+ }
78
+ const signal = options?.signal;
79
+ const timeoutMs = options?.timeoutMs ?? this.#timeoutMs;
80
+ const maxRetries = options?.maxRetries ?? this.#maxRetries;
81
+ for (let attempt = 0;; attempt++) {
82
+ let result;
83
+ try {
84
+ result = await this.#attempt(url, { method, headers, body }, signal, timeoutMs);
85
+ }
86
+ catch (error) {
87
+ if (signal?.aborted)
88
+ throw error;
89
+ const failure = error instanceof DataballrTimeoutError
90
+ ? error
91
+ : new DataballrConnectionError(`${method} ${url.pathname} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
92
+ if (attempt >= maxRetries)
93
+ throw failure;
94
+ await sleep(backoffMs(attempt), signal);
95
+ continue;
96
+ }
97
+ const { response, content } = result;
98
+ if (response.ok) {
99
+ if (typeof content !== "string")
100
+ return content;
101
+ return isJson(response) ? JSON.parse(content) : content;
102
+ }
103
+ const error = errorFromResponse(response, typeof content === "string" ? content : new TextDecoder().decode(content));
104
+ const retryAfterMs = error.retryAfter === undefined ? undefined : error.retryAfter * 1000;
105
+ const retryable = RETRY_STATUSES.has(response.status) &&
106
+ (retryAfterMs === undefined || retryAfterMs <= MAX_RETRY_AFTER_MS);
107
+ if (!retryable || attempt >= maxRetries)
108
+ throw error;
109
+ await sleep(retryAfterMs ?? backoffMs(attempt), signal);
110
+ }
111
+ }
112
+ async #attempt(url, init, signal, timeoutMs) {
113
+ const controller = new AbortController();
114
+ const onAbort = () => controller.abort(signal?.reason);
115
+ if (signal?.aborted)
116
+ onAbort();
117
+ else
118
+ signal?.addEventListener("abort", onAbort, { once: true });
119
+ let timedOut = false;
120
+ const timer = setTimeout(() => {
121
+ timedOut = true;
122
+ controller.abort();
123
+ }, timeoutMs);
124
+ try {
125
+ const response = await this.#fetch(url, { ...init, signal: controller.signal });
126
+ const content = isJson(response) || (response.headers.get("content-type") ?? "").startsWith("text/")
127
+ ? await response.text()
128
+ : await response.arrayBuffer();
129
+ return { response, content };
130
+ }
131
+ catch (error) {
132
+ if (timedOut) {
133
+ throw new DataballrTimeoutError(`${init.method} ${url.pathname} timed out after ${timeoutMs} ms`, { cause: error });
134
+ }
135
+ throw error;
136
+ }
137
+ finally {
138
+ clearTimeout(timer);
139
+ signal?.removeEventListener("abort", onAbort);
140
+ }
141
+ }
142
+ }
143
+ function backoffMs(attempt) {
144
+ return Math.min(8000, 500 * 2 ** attempt) * (1 - Math.random() * 0.25);
145
+ }
146
+ function sleep(ms, signal) {
147
+ return new Promise((resolve, reject) => {
148
+ if (signal?.aborted)
149
+ return reject(signal.reason);
150
+ const onAbort = () => {
151
+ clearTimeout(timer);
152
+ reject(signal?.reason);
153
+ };
154
+ const timer = setTimeout(() => {
155
+ signal?.removeEventListener("abort", onAbort);
156
+ resolve();
157
+ }, ms);
158
+ signal?.addEventListener("abort", onAbort, { once: true });
159
+ });
160
+ }
161
+ function isJson(response) {
162
+ return (response.headers.get("content-type") ?? "").includes("json");
163
+ }
164
+ function fillPath(template, params) {
165
+ return template.replace(/\{([^}]+)\}/g, (_, name) => {
166
+ const value = params?.[name];
167
+ if (value === undefined || value === null) {
168
+ throw new Error(`Databallr: missing path parameter "${name}" for ${template}`);
169
+ }
170
+ return encodeURIComponent(String(value));
171
+ });
172
+ }
173
+ function appendQuery(url, query) {
174
+ if (!query)
175
+ return;
176
+ for (const [name, value] of Object.entries(query)) {
177
+ if (value === undefined || value === null)
178
+ continue;
179
+ if (Array.isArray(value)) {
180
+ for (const item of value)
181
+ url.searchParams.append(name, String(item));
182
+ }
183
+ else {
184
+ url.searchParams.set(name, String(value));
185
+ }
186
+ }
187
+ }
188
+ function env(name) {
189
+ const vars = globalThis.process
190
+ ?.env;
191
+ return vars?.[name] || undefined;
192
+ }
193
+ function isBrowser() {
194
+ const g = globalThis;
195
+ return typeof g.window !== "undefined" && typeof g.document !== "undefined";
196
+ }
197
+ function trimSlash(url) {
198
+ return url.replace(/\/+$/, "");
199
+ }
@@ -0,0 +1,40 @@
1
+ interface DataballrErrorFields {
2
+ readonly status: number;
3
+ /** The shared envelope's `error`, or `http_error` when the body is not that envelope. */
4
+ readonly code: string;
5
+ readonly requestId: string | undefined;
6
+ /** The scope a 403 says would allow the call, from `WWW-Authenticate`. */
7
+ readonly requiredScope: string | undefined;
8
+ /** Seconds to wait before retrying, from `Retry-After`. */
9
+ readonly retryAfter: number | undefined;
10
+ /** The parsed JSON body, or the raw text when it was not JSON. */
11
+ readonly body: unknown;
12
+ }
13
+ /**
14
+ * Thrown for every non-2xx response. `code` is `invalid_token`,
15
+ * `insufficient_scope`, `rate_limited`, ... or `http_error`.
16
+ */
17
+ export declare class DataballrError extends Error implements DataballrErrorFields {
18
+ readonly name = "DataballrError";
19
+ readonly status: number;
20
+ readonly code: string;
21
+ readonly requestId: string | undefined;
22
+ readonly requiredScope: string | undefined;
23
+ readonly retryAfter: number | undefined;
24
+ readonly body: unknown;
25
+ constructor(message: string, fields: DataballrErrorFields);
26
+ }
27
+ /**
28
+ * The request never produced a response: DNS, TLS, a reset, or a timeout,
29
+ * after any retries. Aborting with your own `signal` rejects with its reason
30
+ * instead.
31
+ */
32
+ export declare class DataballrConnectionError extends Error {
33
+ readonly name: string;
34
+ }
35
+ /** An attempt ran past `timeoutMs`, after any retries. */
36
+ export declare class DataballrTimeoutError extends DataballrConnectionError {
37
+ readonly name = "DataballrTimeoutError";
38
+ }
39
+ export declare function errorFromResponse(response: Response, text: string): DataballrError;
40
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,45 @@
1
+ export class DataballrError extends Error {
2
+ name = "DataballrError";
3
+ constructor(message, fields) {
4
+ super(message);
5
+ Object.assign(this, fields);
6
+ }
7
+ }
8
+ export class DataballrConnectionError extends Error {
9
+ name = "DataballrConnectionError";
10
+ }
11
+ export class DataballrTimeoutError extends DataballrConnectionError {
12
+ name = "DataballrTimeoutError";
13
+ }
14
+ export function errorFromResponse(response, text) {
15
+ let body = text;
16
+ try {
17
+ body = JSON.parse(text);
18
+ }
19
+ catch { }
20
+ const envelope = isEnvelope(body) ? body : undefined;
21
+ const code = envelope?.error ?? "http_error";
22
+ return new DataballrError(stringOrUndefined(envelope?.message) ??
23
+ (envelope ? code : `Request failed with status ${response.status}`), {
24
+ status: response.status,
25
+ code,
26
+ requestId: stringOrUndefined(envelope?.request_id),
27
+ requiredScope: response.headers.get("www-authenticate")?.match(/\bscope="([^"]*)"/)?.[1],
28
+ retryAfter: secondsFromRetryAfter(response.headers.get("retry-after")),
29
+ body,
30
+ });
31
+ }
32
+ function secondsFromRetryAfter(header) {
33
+ if (header === null)
34
+ return undefined;
35
+ const seconds = Number(header);
36
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
37
+ }
38
+ function isEnvelope(value) {
39
+ return (typeof value === "object" &&
40
+ value !== null &&
41
+ typeof value.error === "string");
42
+ }
43
+ function stringOrUndefined(value) {
44
+ return typeof value === "string" ? value : undefined;
45
+ }