@lexnode/legal-data-sdk 1.0.0 → 1.24.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/dist/errors.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Os códigos de recusa que o contrato declara, mais os que este cliente cunha
3
+ * para falhas locais.
4
+ *
5
+ * Existe para o `switch` do consumidor ter autocompletar e para o erro de
6
+ * digitação virar erro de compilação: o motor responde 404 tanto para
7
+ * `PROCESS_NOT_FOUND` quanto para `MOVEMENTS_NOT_AVAILABLE`, e quem distingue
8
+ * os dois por literal redigitado descobre o engano em produção. A lista é
9
+ * fechada para o que se conhece e aberta para o que vier (ver `LexNodeError`).
10
+ */
11
+ export const LEXNODE_ERROR_CODES = [
12
+ "INVALID_MONITOR",
13
+ "TRIBUNAL_NOT_SUPPORTED",
14
+ "PERSONAL_DOCUMENT_QUERY_NOT_ALLOWED",
15
+ "VALIDATION_ERROR",
16
+ "UNAUTHORIZED",
17
+ "QUOTA_EXCEEDED",
18
+ "INSUFFICIENT_SCOPE",
19
+ "LGPD_FORBIDDEN",
20
+ "PLAN_FEATURE_NOT_INCLUDED",
21
+ "SUB_TENANT_LIMIT_EXCEEDED",
22
+ "SANDBOX_UPSTREAM_DISABLED",
23
+ "MOVEMENTS_NOT_AVAILABLE",
24
+ "PRECEDENT_NOT_FOUND",
25
+ "PROCESS_NOT_FOUND",
26
+ "RESOURCE_NOT_FOUND",
27
+ "RESOURCE_CONFLICT",
28
+ "QUERY_TOO_BROAD",
29
+ "RATE_LIMITED",
30
+ "UPSTREAM_RATE_LIMITED",
31
+ "INTERNAL_ERROR",
32
+ "UPSTREAM_UNAVAILABLE",
33
+ "DATABASE_TIMEOUT",
34
+ "DATABASE_UNAVAILABLE",
35
+ "SYNC_ABANDONED",
36
+ "UPSTREAM_TIMEOUT",
37
+ // Fora da tabela pública, mas na porta do tenant: a credencial recusada pela
38
+ // própria conta, e o alvo nomeado que não existe (ou já existe).
39
+ "ACCOUNT_SUSPENDED",
40
+ "ENTITLEMENT_EXPIRED",
41
+ "PROJECT_DISABLED",
42
+ "SUBSCRIPTION_INACTIVE",
43
+ "MONITOR_ALREADY_EXISTS",
44
+ "MONITOR_NOT_FOUND",
45
+ "SEARCH_JOB_NOT_FOUND",
46
+ "PUBLICATION_NOT_FOUND",
47
+ "EVENT_NOT_FOUND",
48
+ "TEXT_ALERT_NOT_FOUND",
49
+ "TEXT_SEARCH_NOT_FOUND",
50
+ // Cunhados aqui: nunca vêm do motor.
51
+ "CLIENT_TIMEOUT",
52
+ "CLIENT_NETWORK_ERROR",
53
+ "CLIENT_INVALID_REQUEST",
54
+ "CLIENT_MISSING_PATH_PARAM",
55
+ "INVALID_RESPONSE",
56
+ "SEARCH_JOB_BUDGET_EXCEEDED",
57
+ "PAGINATION_LIMIT_REACHED",
58
+ ];
59
+ const RAW_MESSAGE_MAX_CHARS = 500;
60
+ function truncate(value) {
61
+ return value.length <= RAW_MESSAGE_MAX_CHARS
62
+ ? value
63
+ : `${value.slice(0, RAW_MESSAGE_MAX_CHARS - 3)}...`;
64
+ }
65
+ function asRecord(value) {
66
+ return value && typeof value === "object" && !Array.isArray(value)
67
+ ? value
68
+ : null;
69
+ }
70
+ function readFieldErrors(details) {
71
+ const bag = asRecord(details?.fieldErrors);
72
+ if (!bag)
73
+ return null;
74
+ const out = {};
75
+ for (const [field, messages] of Object.entries(bag)) {
76
+ if (Array.isArray(messages)) {
77
+ out[field] = messages.map((entry) => String(entry));
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * Toda falha que o cliente produz — do motor, da rede ou do prazo — chega como
84
+ * esta classe.
85
+ *
86
+ * Um único tipo para o `catch` do consumidor: ele decide por `retryable` e
87
+ * `code`, não por farejar a forma do que caiu. `requestId` vem junto porque é
88
+ * o que o suporte pede; perdê-lo transforma um incidente rastreável num relato.
89
+ */
90
+ export class LexNodeError extends Error {
91
+ name = "LexNodeError";
92
+ /** O código do contrato (`RATE_LIMITED`, `PROCESS_NOT_FOUND`, …). */
93
+ code;
94
+ /** Status HTTP da recusa — ou o que o cliente atribuiu à falha local. */
95
+ status;
96
+ class;
97
+ retryable;
98
+ /** Status real da fonte a montante, quando o motor o preservou. */
99
+ upstreamStatus;
100
+ requestId;
101
+ /** Erros por campo de uma recusa de validação. */
102
+ fieldErrors;
103
+ details;
104
+ retryAfterSeconds;
105
+ rateLimit;
106
+ constructor(init) {
107
+ super(init.message, init.cause === undefined ? {} : { cause: init.cause });
108
+ this.code = init.code;
109
+ this.status = init.status;
110
+ this.class = init.errorClass;
111
+ this.retryable = init.retryable;
112
+ this.upstreamStatus = init.upstreamStatus ?? null;
113
+ this.requestId = init.requestId ?? null;
114
+ this.fieldErrors = init.fieldErrors ?? null;
115
+ this.details = init.details ?? null;
116
+ this.retryAfterSeconds = init.retryAfterSeconds ?? null;
117
+ this.rateLimit = init.rateLimit ?? null;
118
+ }
119
+ /**
120
+ * A recusa como o motor a descreveu. A classe e o `retryable` saem do corpo
121
+ * quando ele os traz; o status é o da resposta, que é o que de fato chegou.
122
+ */
123
+ static fromEnvelope(envelope, status, rateLimit) {
124
+ const body = asRecord(envelope);
125
+ const error = asRecord(body?.error);
126
+ if (!error || typeof error.code !== "string")
127
+ return null;
128
+ const details = asRecord(error.details);
129
+ const retryAfter = details?.retryAfterSeconds;
130
+ return new LexNodeError({
131
+ code: error.code,
132
+ message: typeof error.message === "string" && error.message
133
+ ? error.message
134
+ : `A API respondeu ${status}.`,
135
+ status,
136
+ errorClass: error.class === "RETRYABLE" ? "RETRYABLE" : "TERMINAL",
137
+ retryable: typeof error.retryable === "boolean"
138
+ ? error.retryable
139
+ : error.class === "RETRYABLE",
140
+ requestId: typeof error.requestId === "string" ? error.requestId : null,
141
+ upstreamStatus: typeof error.upstreamStatus === "number" ? error.upstreamStatus : null,
142
+ fieldErrors: readFieldErrors(details),
143
+ details,
144
+ retryAfterSeconds: typeof retryAfter === "number"
145
+ ? retryAfter
146
+ : (rateLimit?.retryAfterSeconds ?? null),
147
+ rateLimit,
148
+ });
149
+ }
150
+ /** Prazo esgotado antes de a resposta chegar: nada foi decidido, repetir cabe. */
151
+ static timeout(timeoutMs, cause) {
152
+ return new LexNodeError({
153
+ code: "CLIENT_TIMEOUT",
154
+ message: `A API não respondeu em ${timeoutMs} ms.`,
155
+ status: 504,
156
+ errorClass: "RETRYABLE",
157
+ retryable: true,
158
+ cause,
159
+ });
160
+ }
161
+ /** A requisição não alcançou o motor: DNS, conexão, TLS, socket derrubado. */
162
+ static network(cause) {
163
+ const detail = cause instanceof Error ? cause.message : String(cause);
164
+ return new LexNodeError({
165
+ code: "CLIENT_NETWORK_ERROR",
166
+ message: `Não foi possível alcançar a API: ${truncate(detail)}`,
167
+ status: 502,
168
+ errorClass: "RETRYABLE",
169
+ retryable: true,
170
+ cause,
171
+ });
172
+ }
173
+ /**
174
+ * Respondeu, mas não no envelope do contrato — proxy, página de erro, HTML.
175
+ *
176
+ * 502 porque o que quebrou está no caminho, não no pedido; o status real
177
+ * sobrevive em `upstreamStatus`, e a mensagem carrega o corpo cru truncado,
178
+ * que é a única pista de quem respondeu.
179
+ */
180
+ static invalidResponse(status, raw, rateLimit) {
181
+ return new LexNodeError({
182
+ code: "INVALID_RESPONSE",
183
+ message: `Resposta fora do envelope da API (HTTP ${status}): ${truncate(raw)}`,
184
+ status: 502,
185
+ errorClass: "RETRYABLE",
186
+ retryable: true,
187
+ upstreamStatus: status,
188
+ // O 429 de uma borda ou de um proxy não vem no envelope, mas traz o
189
+ // `Retry-After` do mesmo jeito — e é por este campo que o recuo decide.
190
+ retryAfterSeconds: rateLimit?.retryAfterSeconds ?? null,
191
+ rateLimit,
192
+ });
193
+ }
194
+ /**
195
+ * O pedido não chegou a sair: cabeçalho que o `fetch` recusa montar, corpo
196
+ * que não serializa.
197
+ *
198
+ * TERMINAL de propósito. Repetir o que o transporte barrou antes de abrir o
199
+ * socket não tem como dar certo, e classificá-lo como falha de rede manda o
200
+ * consumidor gastar as tentativas num defeito do próprio código.
201
+ */
202
+ static invalidRequest(message, cause) {
203
+ return new LexNodeError({
204
+ code: "CLIENT_INVALID_REQUEST",
205
+ message,
206
+ status: 400,
207
+ errorClass: "TERMINAL",
208
+ retryable: false,
209
+ cause,
210
+ });
211
+ }
212
+ }
213
+ /** Guarda de tipo para o `catch`, sem depender do `instanceof` cruzar realms. */
214
+ export function isLexNodeError(value) {
215
+ return (value instanceof LexNodeError ||
216
+ (value instanceof Error && value.name === "LexNodeError"));
217
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Até onde a fonte foi lida — o que qualifica um resultado vazio.
3
+ *
4
+ * Um `total: 0` sozinho não distingue "não existe" de "a fonte ainda não
5
+ * indexou". Quem confunde os dois diz ao usuário que o processo não existe e
6
+ * repete a busca, pagando de novo por uma resposta que não pode mudar. O motor
7
+ * publica a data-limite do índice em dois eixos — `updatedThrough` na busca de
8
+ * processo, `lastEditionDate` na leitura do diário — e este módulo é a leitura
9
+ * dos dois pelo mesmo nome.
10
+ */
11
+ export type FreshnessAxis = "PROCESS" | "PUBLICATION";
12
+ export type FreshnessState =
13
+ /** A fonte declarou até quando leu. */
14
+ "KNOWN"
15
+ /** A fonte não respondeu à sondagem, ou a resposta não traz o campo. */
16
+ | "INDETERMINATE";
17
+ export interface Freshness {
18
+ state: FreshnessState;
19
+ axis: FreshnessAxis | null;
20
+ /** A data-limite do índice da fonte, quando declarada. */
21
+ readThrough: Date | null;
22
+ /** Quando a resposta montou essa data-limite. */
23
+ checkedAt: Date | null;
24
+ tribunalAlias: string | null;
25
+ }
26
+ /** O retrato completo do frescor de uma resposta. */
27
+ export declare function freshnessOf(result: unknown): Freshness;
28
+ /** A data-limite do índice da fonte, ou `null` quando ela não a declarou. */
29
+ export declare function readThrough(result: unknown): Date | null;
30
+ export interface NotYetIndexedOptions {
31
+ /**
32
+ * A data a partir da qual o alvo existiria — a distribuição do processo, o
33
+ * dia da publicação. Com ela, a resposta deixa de ser um palpite: a fonte
34
+ * lida só até antes dessa data não pode ter visto o alvo.
35
+ */
36
+ since?: Date | string;
37
+ }
38
+ /**
39
+ * Se o vazio desta resposta NÃO prova ausência.
40
+ *
41
+ * Verdadeiro quando a fonte não declarou até onde leu (o estado indeterminado
42
+ * — ausência de campo é estado, nunca erro) ou quando ela leu até antes de
43
+ * `since`. Falso quando há resultado, e falso quando a fonte declarou uma
44
+ * data-limite que cobre o que se perguntou: aí o vazio é resposta.
45
+ */
46
+ export declare function isNotYetIndexed(result: unknown, options?: NotYetIndexedOptions): boolean;
47
+ /** Uma frase em pt-BR sobre o alcance da fonte, pronta para a tela. */
48
+ export declare function describeFreshness(result: unknown): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Até onde a fonte foi lida — o que qualifica um resultado vazio.
3
+ *
4
+ * Um `total: 0` sozinho não distingue "não existe" de "a fonte ainda não
5
+ * indexou". Quem confunde os dois diz ao usuário que o processo não existe e
6
+ * repete a busca, pagando de novo por uma resposta que não pode mudar. O motor
7
+ * publica a data-limite do índice em dois eixos — `updatedThrough` na busca de
8
+ * processo, `lastEditionDate` na leitura do diário — e este módulo é a leitura
9
+ * dos dois pelo mesmo nome.
10
+ */
11
+ function asRecord(value) {
12
+ return value && typeof value === "object" && !Array.isArray(value)
13
+ ? value
14
+ : null;
15
+ }
16
+ /** Aceita o envelope inteiro ou só o `data` — o consumidor tem os dois à mão. */
17
+ function payloadOf(value) {
18
+ const record = asRecord(value);
19
+ if (!record)
20
+ return null;
21
+ return asRecord(record.data) ?? record;
22
+ }
23
+ function toDate(value) {
24
+ if (typeof value !== "string" || !value)
25
+ return null;
26
+ // A data do diário vem como AAAA-MM-DD; lê-la sem fuso a jogaria para o dia
27
+ // anterior em qualquer máquina a oeste de Greenwich.
28
+ const iso = /^\d{4}-\d{2}-\d{2}$/u.test(value)
29
+ ? `${value}T00:00:00.000Z`
30
+ : value;
31
+ const parsed = new Date(iso);
32
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
33
+ }
34
+ /** O retrato completo do frescor de uma resposta. */
35
+ export function freshnessOf(result) {
36
+ const payload = payloadOf(result);
37
+ const sourceIndex = asRecord(payload?.sourceIndex);
38
+ if (!sourceIndex) {
39
+ return {
40
+ state: "INDETERMINATE",
41
+ axis: null,
42
+ readThrough: null,
43
+ checkedAt: null,
44
+ tribunalAlias: null,
45
+ };
46
+ }
47
+ const axis = "lastEditionDate" in sourceIndex ? "PUBLICATION" : "PROCESS";
48
+ const limite = toDate(axis === "PUBLICATION"
49
+ ? sourceIndex.lastEditionDate
50
+ : sourceIndex.updatedThrough);
51
+ return {
52
+ state: limite ? "KNOWN" : "INDETERMINATE",
53
+ axis,
54
+ readThrough: limite,
55
+ checkedAt: toDate(sourceIndex.checkedAt),
56
+ tribunalAlias: typeof sourceIndex.tribunalAlias === "string"
57
+ ? sourceIndex.tribunalAlias
58
+ : null,
59
+ };
60
+ }
61
+ /** A data-limite do índice da fonte, ou `null` quando ela não a declarou. */
62
+ export function readThrough(result) {
63
+ return freshnessOf(result).readThrough;
64
+ }
65
+ function hasResults(payload) {
66
+ if (!payload)
67
+ return false;
68
+ if (Array.isArray(payload.items) && payload.items.length > 0)
69
+ return true;
70
+ return typeof payload.total === "number" && payload.total > 0;
71
+ }
72
+ /**
73
+ * Se o vazio desta resposta NÃO prova ausência.
74
+ *
75
+ * Verdadeiro quando a fonte não declarou até onde leu (o estado indeterminado
76
+ * — ausência de campo é estado, nunca erro) ou quando ela leu até antes de
77
+ * `since`. Falso quando há resultado, e falso quando a fonte declarou uma
78
+ * data-limite que cobre o que se perguntou: aí o vazio é resposta.
79
+ */
80
+ export function isNotYetIndexed(result, options = {}) {
81
+ const payload = payloadOf(result);
82
+ if (hasResults(payload))
83
+ return false;
84
+ const { readThrough: limite } = freshnessOf(result);
85
+ if (!limite)
86
+ return true;
87
+ const since = typeof options.since === "string"
88
+ ? toDate(options.since)
89
+ : (options.since ?? null);
90
+ if (!since)
91
+ return false;
92
+ return limite.getTime() < since.getTime();
93
+ }
94
+ function ddmmaaaa(date) {
95
+ const dia = String(date.getUTCDate()).padStart(2, "0");
96
+ const mes = String(date.getUTCMonth() + 1).padStart(2, "0");
97
+ return `${dia}/${mes}/${date.getUTCFullYear()}`;
98
+ }
99
+ /** Uma frase em pt-BR sobre o alcance da fonte, pronta para a tela. */
100
+ export function describeFreshness(result) {
101
+ const { readThrough: limite } = freshnessOf(result);
102
+ return limite
103
+ ? `fonte lida até ${ddmmaaaa(limite)}`
104
+ : "fonte sem data-limite conhecida";
105
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cliente de runtime e tipos do contrato do motor de dados jurídicos.
3
+ *
4
+ * **Servidor apenas.** A credencial é uma chave de CONTA: no browser ela está
5
+ * publicada. Um app com usuários finais fala com o próprio backend, e é o
6
+ * backend que fala com o motor, atribuindo cada chamada ao seu sub-tenant por
7
+ * `X-Office-Id`.
8
+ *
9
+ * Sem dependências de runtime: `fetch` global e `node:crypto`, Node >= 20.
10
+ */
11
+ export type * from "../schema.js";
12
+ export { createLexNodeClient } from "./client.js";
13
+ export type { LexNodeClient, LexNodeClientOptions } from "./client.js";
14
+ export { LEXNODE_ERROR_CODES, LexNodeError, isLexNodeError } from "./errors.js";
15
+ export type { LexNodeErrorClass, LexNodeErrorCode, LexNodeErrorInit, } from "./errors.js";
16
+ export type { Body, CallOptions, Data, HttpMethod, LexNodeResult, Meta, MetaExtras, MethodOf, Operation, PathParams, Query, RateLimitSnapshot, RequestArgs, RetryPolicy, } from "./types.js";
17
+ export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, LEXNODE_WEBHOOK_HEADERS, WEBHOOK_EVENT_TYPES, WEBHOOK_TEST_EVENT_TYPE, isKnownWebhookEventType, isReceiverFault, isWebhookTest, parseWebhookEvent, verifyWebhookSignature, webhookRejectionStatus, } from "./webhooks.js";
18
+ export type { MonitorSyncFailedPayload, MonitorSyncSucceededPayload, PeriodEndingPayload, ProcessMovementCreatedPayload, ProcessSourceIndex, PublicationCreatedPayload, PublicationHarvestCompletedPayload, PublicationTextAlertMatch, PublicationTextAlertMatchedPayload, QuotaLowPayload, SubscribableWebhookEventType, SearchJobFailedPayload, SearchJobSucceededPayload, SubscriptionPastDuePayload, SuspensionSignal, UnknownWebhookEvent, VerifyWebhookOptions, WebhookEvent, WebhookEventEnvelope, WebhookEventType, WebhookHeaders, WebhookParseRejection, WebhookParseResult, WebhookRawBody, WebhookRejection, WebhookTestPayload, WebhookVerification, } from "./webhooks.js";
19
+ export { paginate } from "./paginate.js";
20
+ export type { PaginateOptions } from "./paginate.js";
21
+ export { searchProcesses, waitForSearchJob } from "./search.js";
22
+ export type { ProcessSearchOutcome, ProcessSearchResult, QueuedSearchJob, SearchJob, SearchJobState, WaitForSearchJobOptions, } from "./search.js";
23
+ export { describeFreshness, freshnessOf, isNotYetIndexed, readThrough, } from "./freshness.js";
24
+ export type { Freshness, FreshnessAxis, FreshnessState, NotYetIndexedOptions, } from "./freshness.js";
25
+ export { isFirstAppearance, resubscribeOnFirstAppearance, unwatchProcess, watchProcess, } from "./watch.js";
26
+ export type { FirstAppearanceEvent, ResubscribeSubscriber, UnwatchProcessInput, WatchProcessInput, WatchProcessResult, } from "./watch.js";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cliente de runtime e tipos do contrato do motor de dados jurídicos.
3
+ *
4
+ * **Servidor apenas.** A credencial é uma chave de CONTA: no browser ela está
5
+ * publicada. Um app com usuários finais fala com o próprio backend, e é o
6
+ * backend que fala com o motor, atribuindo cada chamada ao seu sub-tenant por
7
+ * `X-Office-Id`.
8
+ *
9
+ * Sem dependências de runtime: `fetch` global e `node:crypto`, Node >= 20.
10
+ */
11
+ export { createLexNodeClient } from "./client.js";
12
+ export { LEXNODE_ERROR_CODES, LexNodeError, isLexNodeError } from "./errors.js";
13
+ export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, LEXNODE_WEBHOOK_HEADERS, WEBHOOK_EVENT_TYPES, WEBHOOK_TEST_EVENT_TYPE, isKnownWebhookEventType, isReceiverFault, isWebhookTest, parseWebhookEvent, verifyWebhookSignature, webhookRejectionStatus, } from "./webhooks.js";
14
+ export { paginate } from "./paginate.js";
15
+ export { searchProcesses, waitForSearchJob } from "./search.js";
16
+ export { describeFreshness, freshnessOf, isNotYetIndexed, readThrough, } from "./freshness.js";
17
+ export { isFirstAppearance, resubscribeOnFirstAppearance, unwatchProcess, watchProcess, } from "./watch.js";
@@ -0,0 +1,21 @@
1
+ import type { paths } from "../schema.js";
2
+ import type { LexNodeClient } from "./client.js";
3
+ import type { Data, MethodOf, RequestArgs } from "./types.js";
4
+ export interface PaginateOptions {
5
+ /**
6
+ * Teto de páginas. É EXPLÍCITO de propósito: cada página é uma chamada
7
+ * medida, e um laço sem teto sobre uma conta grande gasta a franquia inteira
8
+ * antes de alguém perceber.
9
+ */
10
+ maxPages?: number;
11
+ }
12
+ /**
13
+ * Percorre uma rota paginada página a página, na forma que ela usa.
14
+ *
15
+ * ```ts
16
+ * for await (const page of paginate(client, "/v1/monitors", "get", {})) {
17
+ * for (const monitor of page) console.log(monitor.id);
18
+ * }
19
+ * ```
20
+ */
21
+ export declare function paginate<P extends keyof paths, M extends MethodOf<P>>(client: LexNodeClient, path: P, method: M, args: RequestArgs<P, M>, options?: PaginateOptions): AsyncGenerator<Data<P, M>, void, undefined>;
@@ -0,0 +1,106 @@
1
+ import { LexNodeError } from "./errors.js";
2
+ /**
3
+ * O laço de páginas, escrito uma vez.
4
+ *
5
+ * O motor pagina de três jeitos, herdados do que cada superfície precisava: o
6
+ * cursor no `meta` do envelope (rotas cujo `data` é um array), o cursor dentro
7
+ * do `data` (rotas com `items`), e o par página/tamanho ou deslocamento/teto da
8
+ * consulta de acervo. Sem isto, cada consumidor reescreve o mesmo laço três
9
+ * vezes — e o que ele erra é sempre o mesmo: para na primeira página, ou anda
10
+ * para sempre porque o cursor deixou de avançar.
11
+ *
12
+ * O cursor do `meta` não é declarado no contrato gerado (o `meta` daquelas
13
+ * rotas é aberto), então é lido aqui com conferência de tipo em runtime.
14
+ */
15
+ const DEFAULT_MAX_PAGES = 100;
16
+ function asRecord(value) {
17
+ return value && typeof value === "object" && !Array.isArray(value)
18
+ ? value
19
+ : null;
20
+ }
21
+ function readCursor(bag) {
22
+ const cursor = bag?.nextCursor;
23
+ return typeof cursor === "string" && cursor ? cursor : null;
24
+ }
25
+ function readNumber(bag, key) {
26
+ const value = bag?.[key];
27
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
28
+ }
29
+ /** Quantos itens a página trouxe, para o deslocamento andar pelo que veio. */
30
+ function pageLength(data, payload) {
31
+ if (Array.isArray(data))
32
+ return data.length;
33
+ const items = payload?.items;
34
+ return Array.isArray(items) ? items.length : 0;
35
+ }
36
+ /**
37
+ * A consulta da próxima página, ou `null` quando a última chegou.
38
+ *
39
+ * As três formas são decididas aqui, uma vez, na ordem em que o motor as usa —
40
+ * cursor primeiro, porque é a única que não depende de um `total`.
41
+ */
42
+ function nextQuery(data, meta, query) {
43
+ const payload = asRecord(data);
44
+ const cursor = readCursor(asRecord(meta)) ?? readCursor(payload);
45
+ if (cursor)
46
+ return { ...query, cursor };
47
+ const total = readNumber(payload, "total");
48
+ if (total === null)
49
+ return null;
50
+ const pageSize = readNumber(payload, "pageSize");
51
+ const atual = readNumber(payload, "page");
52
+ if (pageSize !== null && atual !== null) {
53
+ return atual * pageSize >= total
54
+ ? null
55
+ : { ...query, page: atual + 1, pageSize };
56
+ }
57
+ if (typeof query.offset === "number" || typeof query.limit === "number") {
58
+ const lidos = pageLength(data, payload);
59
+ const offset = typeof query.offset === "number" ? query.offset : 0;
60
+ const percorrido = offset + lidos;
61
+ return lidos === 0 || percorrido >= total
62
+ ? null
63
+ : { ...query, offset: percorrido };
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Percorre uma rota paginada página a página, na forma que ela usa.
69
+ *
70
+ * ```ts
71
+ * for await (const page of paginate(client, "/v1/monitors", "get", {})) {
72
+ * for (const monitor of page) console.log(monitor.id);
73
+ * }
74
+ * ```
75
+ */
76
+ export async function* paginate(client, path, method, args, options = {}) {
77
+ const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
78
+ let query = {
79
+ ...(args.params
80
+ ?.query ?? {}),
81
+ };
82
+ let visto = null;
83
+ for (let pagina = 1; pagina <= maxPages; pagina += 1) {
84
+ const { data, meta } = await client.request(path, method, {
85
+ ...args,
86
+ params: { ...args.params, query },
87
+ });
88
+ yield data;
89
+ const proxima = nextQuery(data, meta, query);
90
+ // Cursor que não anda é laço infinito disfarçado de paginação.
91
+ if (!proxima ||
92
+ (proxima.cursor !== undefined && proxima.cursor === visto)) {
93
+ return;
94
+ }
95
+ visto = proxima.cursor;
96
+ query = proxima;
97
+ }
98
+ throw new LexNodeError({
99
+ code: "PAGINATION_LIMIT_REACHED",
100
+ message: `A paginação de ${String(path)} passou de ${maxPages} páginas; aumente maxPages ou recorte a consulta.`,
101
+ status: 400,
102
+ errorClass: "TERMINAL",
103
+ retryable: false,
104
+ details: { path: String(path), maxPages },
105
+ });
106
+ }
@@ -0,0 +1,64 @@
1
+ import type { LexNodeClient } from "./client.js";
2
+ import type { Body, CallOptions, Data, Meta, MetaExtras } from "./types.js";
3
+ /**
4
+ * A busca de processos responde de dois jeitos na MESMA operação: 200 com o
5
+ * resultado pronto, 202 com um trabalho enfileirado. O discriminador é o
6
+ * código HTTP — o corpo do 202 traz `jobId` e nenhum `items`, mas farejar
7
+ * campos é conclusão por ausência, e o status é a afirmação.
8
+ */
9
+ type SearchData = Data<"/v1/processes/search", "post">;
10
+ type SearchMeta = Meta<"/v1/processes/search", "post"> & MetaExtras;
11
+ /** O resultado já pronto, servido dentro da própria chamada. */
12
+ export type ProcessSearchResult = Extract<SearchData, {
13
+ items: unknown;
14
+ }>;
15
+ /** O trabalho enfileirado, a ser acompanhado por `waitForSearchJob`. */
16
+ export type QueuedSearchJob = Extract<SearchData, {
17
+ jobId: unknown;
18
+ }>;
19
+ export type ProcessSearchOutcome = {
20
+ kind: "sync";
21
+ result: ProcessSearchResult;
22
+ meta: SearchMeta;
23
+ } | {
24
+ kind: "async";
25
+ job: QueuedSearchJob;
26
+ meta: SearchMeta;
27
+ };
28
+ /** O trabalho como a consulta de estado o devolve. */
29
+ export type SearchJob = Data<"/v1/search-jobs/{jobId}", "get">;
30
+ export type SearchJobState = SearchJob["status"];
31
+ export interface WaitForSearchJobOptions {
32
+ /**
33
+ * Teto de espera, em ms. É EXPLÍCITO de propósito: num ambiente serverless a
34
+ * espera consome a franquia da função, e um laço sem orçamento gasta a
35
+ * requisição inteira antes de o trabalho concluir.
36
+ */
37
+ budgetMs?: number;
38
+ /** Primeira espera entre consultas, em ms; dobra até `maxPollIntervalMs`. */
39
+ pollIntervalMs?: number;
40
+ maxPollIntervalMs?: number;
41
+ signal?: AbortSignal;
42
+ /** Chamado a cada leitura de estado, para a UI acompanhar. */
43
+ onState?: (state: SearchJobState, job: SearchJob) => void;
44
+ officeId?: string | null;
45
+ }
46
+ /**
47
+ * Busca processos e diz, no tipo, qual dos dois desfechos aconteceu.
48
+ *
49
+ * Sem isto, o consumidor lê `data.items` num corpo que só tem `jobId` e chama
50
+ * de "nenhum resultado" o que é "ainda estou buscando".
51
+ */
52
+ export declare function searchProcesses(client: LexNodeClient, body: Body<"/v1/processes/search", "post">, opts?: CallOptions): Promise<ProcessSearchOutcome>;
53
+ /**
54
+ * Espera um trabalho de busca chegar ao fim, com recuo e orçamento.
55
+ *
56
+ * O estouro do orçamento é um erro PRÓPRIO e retentável: o trabalho continua
57
+ * na fila do motor, e quem espera pode voltar a acompanhá-lo depois — ou
58
+ * receber o `SEARCH_JOB_COMPLETED` por webhook e não esperar nada.
59
+ *
60
+ * Não há o que esperar com chave de teste (`lapi_test_`): ela não enfileira
61
+ * trabalho, e a submissão assíncrona responde 403 `SANDBOX_UPSTREAM_DISABLED`.
62
+ */
63
+ export declare function waitForSearchJob(client: LexNodeClient, jobId: string, options?: WaitForSearchJobOptions): Promise<SearchJob>;
64
+ export {};