@databolsa/sdk 1.0.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.
@@ -0,0 +1,412 @@
1
+ import {
2
+ type DataBolsaClient,
3
+ NotInPreviewError,
4
+ type ScreenFiisParams,
5
+ type ScreenStocksParams,
6
+ } from "./client";
7
+ import type {
8
+ CorporateEventsResponse,
9
+ CryptoQuotesResponse,
10
+ DividendsResponse,
11
+ DocumentsResponse,
12
+ ExpectationsResponse,
13
+ Fii,
14
+ FiiDistributionsResponse,
15
+ FiiIndicatorsResponse,
16
+ FiiReportsResponse,
17
+ FiiScreenResponse,
18
+ HealthResponse,
19
+ IngestHealthResponse,
20
+ IndexCompositionResponse,
21
+ IndexMeta,
22
+ IndexQuotesResponse,
23
+ IndicatorHistoryResponse,
24
+ BdrListResponse,
25
+ BdrProfile,
26
+ BdrQuotesResponse,
27
+ InsiderResponse,
28
+ MacroGearsResponse,
29
+ OptionExpiriesResponse,
30
+ OptionsChainResponse,
31
+ OptionsQuotesResponse,
32
+ QuotesResponse,
33
+ RangeParams,
34
+ RegimeSnapshot,
35
+ ScreenStocksResponse,
36
+ SearchResult,
37
+ SeriesResponse,
38
+ Stock,
39
+ StockIndicatorsResponse,
40
+ TesouroBondsResponse,
41
+ YieldCurveResponse,
42
+ } from "./types";
43
+
44
+ /**
45
+ * Cliente HTTP real — fala com a Serving API (`@databolsa/api`) pelo contrato
46
+ * `api/openapi.yaml`. Implementa a MESMA interface do FixtureClient, então a UI
47
+ * não muda: `getClient()` (lib/api/index.ts) escolhe um ou outro por env.
48
+ *
49
+ * Princípio de degradação graciosa: a API responde **501** para qualquer rota
50
+ * `/v1/*` ainda não servida (notFoundHandler). Tratamos 501 (e 404 de recurso
51
+ * inexistente) como {@link NotInPreviewError} — a mesma exceção que a UI já sabe
52
+ * exibir. Assim cada método chama o path real do contrato e "acende" sozinho
53
+ * quando o endpoint correspondente passar a existir no servidor, sem editar aqui.
54
+ *
55
+ * A API é a camada ABERTA (sem dados de usuário): a autenticação é só um cheque
56
+ * de chave opcional. Quando existir um control-plane à frente (mint/assinatura de
57
+ * chave, rate-limit), `baseUrl` aponta para ele e o token vem de `getToken()`.
58
+ */
59
+ export interface HttpClientOptions {
60
+ /** Token bearer a anexar (opcional). Hoje a API de dev é aberta. */
61
+ getToken?: () => string | null | undefined;
62
+ /** Chave estática (atalho p/ Node/CLI). Ignorada se `getToken` for passada. */
63
+ apiKey?: string | null;
64
+ }
65
+
66
+ type Query = Record<string, string | number | boolean | null | undefined>;
67
+
68
+ export class HttpClient implements DataBolsaClient {
69
+ private readonly base: string;
70
+ private readonly getToken?: () => string | null | undefined;
71
+
72
+ constructor(baseUrl: string, opts: HttpClientOptions = {}) {
73
+ // baseUrl absoluto (http(s)://…) → chamada direta (cross-origin, precisa CORS).
74
+ // baseUrl relativo (ex.: "/") → MESMA ORIGEM: o browser fala com a própria origem
75
+ // e o Next faz proxy p/ a API (next.config.ts) — funciona local, tunelado ou atrás
76
+ // do gateway, sem o browser saber onde a API mora. As rotas vivem sob /v1.
77
+ const isAbsolute = /^https?:\/\//i.test(baseUrl);
78
+ // `globalThis.location` em vez do `window` nu: o SDK é agnóstico (sem lib DOM),
79
+ // resolve a origem no browser e cai em "" no Node (onde baseUrl deve ser absoluto).
80
+ const loc = (globalThis as { location?: { origin?: string } }).location;
81
+ const origin = isAbsolute ? baseUrl.replace(/\/+$/, "") : (loc?.origin ?? "");
82
+ this.base = `${origin}/v1`;
83
+ this.getToken = opts.getToken ?? (opts.apiKey ? () => opts.apiKey : undefined);
84
+ }
85
+
86
+ private async request<T>(path: string, query?: Query): Promise<T> {
87
+ const url = new URL(this.base + path);
88
+ if (query) {
89
+ for (const [k, v] of Object.entries(query)) {
90
+ if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
91
+ }
92
+ }
93
+
94
+ const headers: Record<string, string> = { accept: "application/json" };
95
+ const token = this.getToken?.();
96
+ if (token) headers.authorization = `Bearer ${token}`;
97
+
98
+ let res: Response;
99
+ try {
100
+ res = await fetch(url, { headers });
101
+ } catch (cause) {
102
+ // Rede indisponível (API fora do ar, CORS, DNS) — erro real, não "preview".
103
+ throw new Error(`Falha de rede ao chamar ${path}`, { cause });
104
+ }
105
+
106
+ if (res.ok) return (await res.json()) as T;
107
+
108
+ // 501 (rota não servida nesta versão) e 404 (recurso inexistente) → a UI
109
+ // trata como "fora do preview"; demais status são erros de verdade.
110
+ if (res.status === 501 || res.status === 404) {
111
+ throw new NotInPreviewError(decodeURIComponent(path.replace(/^\//, "")));
112
+ }
113
+ const detail = await this.problemDetail(res);
114
+ throw new Error(`API ${res.status} em ${path}${detail ? `: ${detail}` : ""}`);
115
+ }
116
+
117
+ /** Extrai `detail`/`title` de um application/problem+json, se houver. */
118
+ private async problemDetail(res: Response): Promise<string | null> {
119
+ try {
120
+ const body = (await res.json()) as { detail?: string; title?: string };
121
+ return body.detail ?? body.title ?? null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ // --- saúde ---------------------------------------------------------------
128
+ getHealth(): Promise<HealthResponse> {
129
+ return this.request<HealthResponse>("/health");
130
+ }
131
+
132
+ getIngestHealth(): Promise<IngestHealthResponse> {
133
+ return this.request<IngestHealthResponse>("/ingest");
134
+ }
135
+
136
+ // --- screener (o universo real de ações) ---------------------------------
137
+ screenStocks(params?: ScreenStocksParams): Promise<ScreenStocksResponse> {
138
+ return this.request<ScreenStocksResponse>("/screener/stocks", {
139
+ sector: params?.sector,
140
+ segment: params?.segment,
141
+ sort: params?.sort,
142
+ limit: params?.limit,
143
+ cursor: params?.cursor,
144
+ pl_min: params?.pl_min,
145
+ pl_max: params?.pl_max,
146
+ pvp_min: params?.pvp_min,
147
+ pvp_max: params?.pvp_max,
148
+ dy_min: params?.dy_min,
149
+ roe_min: params?.roe_min,
150
+ ev_ebitda_max: params?.ev_ebitda_max,
151
+ div_liq_ebitda_max: params?.div_liq_ebitda_max,
152
+ });
153
+ }
154
+
155
+ // --- ações ---------------------------------------------------------------
156
+ getStock(ticker: string): Promise<Stock> {
157
+ return this.request<Stock>(`/stocks/${enc(ticker)}`);
158
+ }
159
+
160
+ listQuotes(
161
+ ticker: string,
162
+ params?: RangeParams & { adjusted?: boolean; limit?: number },
163
+ ): Promise<QuotesResponse> {
164
+ return this.request<QuotesResponse>(`/stocks/${enc(ticker)}/quotes`, {
165
+ from: params?.from,
166
+ to: params?.to,
167
+ limit: params?.limit,
168
+ // só envia quando explicitamente não-ajustado (servidor: adjusted = v !== "false")
169
+ adjusted: params?.adjusted === false ? "false" : undefined,
170
+ });
171
+ }
172
+
173
+ getStockIndicators(ticker: string, params?: { at?: string }): Promise<StockIndicatorsResponse> {
174
+ return this.request<StockIndicatorsResponse>(`/stocks/${enc(ticker)}/indicators`, {
175
+ at: params?.at,
176
+ });
177
+ }
178
+
179
+ getStockIndicatorHistory(
180
+ ticker: string,
181
+ name: string,
182
+ params?: RangeParams,
183
+ ): Promise<IndicatorHistoryResponse> {
184
+ // Contrato: /stocks/{ticker}/indicators/history?name=… (não /{name}/history).
185
+ return this.request<IndicatorHistoryResponse>(`/stocks/${enc(ticker)}/indicators/history`, {
186
+ name,
187
+ from: params?.from,
188
+ to: params?.to,
189
+ });
190
+ }
191
+
192
+ listDividends(ticker: string, params?: RangeParams): Promise<DividendsResponse> {
193
+ return this.request<DividendsResponse>(`/stocks/${enc(ticker)}/dividends`, {
194
+ from: params?.from,
195
+ to: params?.to,
196
+ });
197
+ }
198
+
199
+ listCorporateEvents(ticker: string, params?: RangeParams): Promise<CorporateEventsResponse> {
200
+ return this.request<CorporateEventsResponse>(`/stocks/${enc(ticker)}/events`, {
201
+ from: params?.from,
202
+ to: params?.to,
203
+ });
204
+ }
205
+
206
+ listCompanyDocuments(
207
+ cvmCode: number,
208
+ params?: RangeParams & { category?: string },
209
+ ): Promise<DocumentsResponse> {
210
+ return this.request<DocumentsResponse>(`/companies/${cvmCode}/documents`, {
211
+ from: params?.from,
212
+ to: params?.to,
213
+ category: params?.category,
214
+ });
215
+ }
216
+
217
+ listInsiderMoves(ticker: string, params?: RangeParams): Promise<InsiderResponse> {
218
+ // Pendência de contrato (sem operationId ainda) — 501 até a rota existir.
219
+ return this.request<InsiderResponse>(`/stocks/${enc(ticker)}/insider`, {
220
+ from: params?.from,
221
+ to: params?.to,
222
+ });
223
+ }
224
+
225
+ // --- FIIs ----------------------------------------------------------------
226
+ screenFiis(params?: ScreenFiisParams): Promise<FiiScreenResponse> {
227
+ return this.request<FiiScreenResponse>("/screener/fiis", {
228
+ segment: params?.segment,
229
+ paper: params?.paper === undefined ? undefined : String(params.paper),
230
+ sort: params?.sort,
231
+ limit: params?.limit,
232
+ cursor: params?.cursor,
233
+ });
234
+ }
235
+
236
+ getFii(ticker: string): Promise<Fii> {
237
+ return this.request<Fii>(`/fiis/${enc(ticker)}`);
238
+ }
239
+
240
+ getFiiIndicators(ticker: string, params?: { at?: string }): Promise<FiiIndicatorsResponse> {
241
+ return this.request<FiiIndicatorsResponse>(`/fiis/${enc(ticker)}/indicators`, { at: params?.at });
242
+ }
243
+
244
+ getFiiIndicatorHistory(
245
+ ticker: string,
246
+ name: string,
247
+ params?: RangeParams,
248
+ ): Promise<IndicatorHistoryResponse> {
249
+ return this.request<IndicatorHistoryResponse>(`/fiis/${enc(ticker)}/indicators/history`, {
250
+ name,
251
+ from: params?.from,
252
+ to: params?.to,
253
+ });
254
+ }
255
+
256
+ listFiiDistributions(ticker: string, params?: RangeParams): Promise<FiiDistributionsResponse> {
257
+ return this.request<FiiDistributionsResponse>(`/fiis/${enc(ticker)}/distributions`, {
258
+ from: params?.from,
259
+ to: params?.to,
260
+ });
261
+ }
262
+
263
+ listFiiReports(ticker: string, params?: RangeParams): Promise<FiiReportsResponse> {
264
+ return this.request<FiiReportsResponse>(`/fiis/${enc(ticker)}/reports`, {
265
+ from: params?.from,
266
+ to: params?.to,
267
+ });
268
+ }
269
+
270
+ // --- índices -------------------------------------------------------------
271
+ listIndices(): Promise<IndexMeta[]> {
272
+ return this.request<IndexMeta[]>("/indices");
273
+ }
274
+
275
+ listIndexQuotes(code: string, params?: RangeParams & { limit?: number }): Promise<IndexQuotesResponse> {
276
+ return this.request<IndexQuotesResponse>(`/indices/${enc(code)}/quotes`, {
277
+ from: params?.from,
278
+ to: params?.to,
279
+ limit: params?.limit,
280
+ });
281
+ }
282
+
283
+ getIndexComposition(code: string): Promise<IndexCompositionResponse> {
284
+ return this.request<IndexCompositionResponse>(`/indices/${enc(code)}/composition`);
285
+ }
286
+
287
+ // --- séries / renda fixa -------------------------------------------------
288
+ getSeries(
289
+ source: string,
290
+ seriesId: string,
291
+ params?: RangeParams & { accumulated?: "none" | "12m" | "ytd" },
292
+ ): Promise<SeriesResponse> {
293
+ return this.request<SeriesResponse>(`/series/${enc(source)}/${enc(seriesId)}`, {
294
+ from: params?.from,
295
+ to: params?.to,
296
+ accumulated: params?.accumulated,
297
+ });
298
+ }
299
+
300
+ getYieldCurve(params?: { kind?: "nominal" | "real" }): Promise<YieldCurveResponse> {
301
+ return this.request<YieldCurveResponse>("/bonds/tesouro/yield-curve", { kind: params?.kind });
302
+ }
303
+
304
+ listTesouroBonds(params?: {
305
+ type?: string;
306
+ maturity?: string;
307
+ date?: string;
308
+ limit?: number;
309
+ }): Promise<TesouroBondsResponse> {
310
+ return this.request<TesouroBondsResponse>("/bonds/tesouro", {
311
+ type: params?.type,
312
+ maturity: params?.maturity,
313
+ date: params?.date,
314
+ limit: params?.limit,
315
+ });
316
+ }
317
+
318
+ // --- macro ---------------------------------------------------------------
319
+ getMarketExpectations(
320
+ indicator: "ipca" | "selic" | "pib" | "cambio",
321
+ params?: { reference?: string } & RangeParams,
322
+ ): Promise<ExpectationsResponse> {
323
+ return this.request<ExpectationsResponse>("/macro/expectations", {
324
+ indicator,
325
+ reference: params?.reference,
326
+ from: params?.from,
327
+ to: params?.to,
328
+ });
329
+ }
330
+
331
+ getMacroRegime(params?: { at?: string }): Promise<RegimeSnapshot> {
332
+ return this.request<RegimeSnapshot>("/macro/regime", { at: params?.at });
333
+ }
334
+
335
+ getMacroGears(params?: { gear?: string }): Promise<MacroGearsResponse> {
336
+ return this.request<MacroGearsResponse>("/macro/gears", { gear: params?.gear });
337
+ }
338
+
339
+ listCryptoQuotes(
340
+ symbol: string,
341
+ params?: RangeParams & { interval?: "1d" | "1h"; limit?: number },
342
+ ): Promise<CryptoQuotesResponse> {
343
+ return this.request<CryptoQuotesResponse>(`/crypto/${enc(symbol)}/quotes`, {
344
+ interval: params?.interval,
345
+ from: params?.from,
346
+ to: params?.to,
347
+ limit: params?.limit,
348
+ });
349
+ }
350
+
351
+ // --- opções --------------------------------------------------------------
352
+ listOptionsChain(
353
+ underlying: string,
354
+ params?: { expiry?: string; type?: "call" | "put" },
355
+ ): Promise<OptionsChainResponse> {
356
+ return this.request<OptionsChainResponse>(`/options/${enc(underlying)}/chain`, {
357
+ expiry: params?.expiry,
358
+ type: params?.type,
359
+ });
360
+ }
361
+
362
+ listOptionExpiries(underlying: string): Promise<OptionExpiriesResponse> {
363
+ return this.request<OptionExpiriesResponse>(`/options/${enc(underlying)}/expiries`);
364
+ }
365
+
366
+ listOptionQuotes(
367
+ optionTicker: string,
368
+ params?: RangeParams & { limit?: number; cursor?: string },
369
+ ): Promise<OptionsQuotesResponse> {
370
+ return this.request<OptionsQuotesResponse>(`/options/${enc(optionTicker)}/quotes`, {
371
+ from: params?.from,
372
+ to: params?.to,
373
+ limit: params?.limit,
374
+ cursor: params?.cursor,
375
+ });
376
+ }
377
+
378
+ // --- BDRs ----------------------------------------------------------------
379
+ listBdrs(params?: { search?: string; limit?: number; cursor?: string }): Promise<BdrListResponse> {
380
+ return this.request<BdrListResponse>("/bdr", {
381
+ search: params?.search,
382
+ limit: params?.limit,
383
+ cursor: params?.cursor,
384
+ });
385
+ }
386
+
387
+ getBdr(ticker: string): Promise<BdrProfile> {
388
+ return this.request<BdrProfile>(`/bdr/${enc(ticker)}`);
389
+ }
390
+
391
+ listBdrQuotes(
392
+ ticker: string,
393
+ params?: RangeParams & { limit?: number; cursor?: string },
394
+ ): Promise<BdrQuotesResponse> {
395
+ return this.request<BdrQuotesResponse>(`/bdr/${enc(ticker)}/quotes`, {
396
+ from: params?.from,
397
+ to: params?.to,
398
+ limit: params?.limit,
399
+ cursor: params?.cursor,
400
+ });
401
+ }
402
+
403
+ // --- busca ---------------------------------------------------------------
404
+ search(q: string, params?: { limit?: number }): Promise<SearchResult[]> {
405
+ return this.request<SearchResult[]>("/search", { q, limit: params?.limit });
406
+ }
407
+ }
408
+
409
+ /** Segmento de path seguro (tickers/códigos vêm da UI/URL). */
410
+ function enc(segment: string): string {
411
+ return encodeURIComponent(segment);
412
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@databolsa/sdk` — cliente TypeScript tipado da Serving API aberta.
3
+ *
4
+ * Casca fina e **agnóstica** (sem React, sem dependências): o `HttpClient`
5
+ * implementa a interface {@link DataBolsaClient} sobre o `fetch` nativo, com
6
+ * degradação 501/404 → {@link NotInPreviewError} e Bearer opcional. Os tipos
7
+ * saem do contrato `api/openapi.yaml` (gerados em `src/schema.ts` via
8
+ * `bun run --cwd packages/sdk gen:schema`).
9
+ *
10
+ * Consumidores montam suas próprias camadas por cima: o app web envolve em
11
+ * hooks React Query (apps/web/lib/queries.ts); um script Node usa direto.
12
+ */
13
+
14
+ // Cliente (valor) + nome amigável p/ uso externo.
15
+ export { HttpClient, HttpClient as DataBolsa } from "./http-client";
16
+ export type { HttpClientOptions } from "./http-client";
17
+
18
+ // Contrato do cliente, params de busca e a exceção de degradação.
19
+ export { NotInPreviewError } from "./client";
20
+ export type { DataBolsaClient, ScreenStocksParams, ScreenFiisParams } from "./client";
21
+
22
+ // Todos os tipos de domínio e de resposta derivados do schema.
23
+ export type * from "./types";
24
+
25
+ // Tipos crus do contrato (paths/operations/components), p/ quem quiser cavar.
26
+ export type { paths, components, operations } from "./schema";