@riligar/contract 1.0.0 → 1.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/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@riligar/contract",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "description": "O contrato de API da stack RiLiGar: envelope único, catálogo de códigos de erro e os helpers que tornam impossível responder fora dele.",
6
+ "description": "O contrato de API da stack RiLiGar: envelope \u00fanico, cat\u00e1logo de c\u00f3digos de erro e os helpers que tornam imposs\u00edvel responder fora dele.",
7
7
  "main": "src/index.js",
8
- "files": ["src", "README.md"],
8
+ "files": [
9
+ "src",
10
+ "README.md"
11
+ ],
9
12
  "exports": {
10
13
  ".": "./src/index.js",
11
14
  "./codes": "./src/codes.js",
@@ -15,14 +18,25 @@
15
18
  "test": "bun test tests/",
16
19
  "release": "semantic-release"
17
20
  },
18
- "peerDependencies": { "zod": "^4.0.0" },
19
- "peerDependenciesMeta": { "zod": { "optional": true } },
21
+ "peerDependencies": {
22
+ "zod": "^4.0.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "zod": {
26
+ "optional": true
27
+ }
28
+ },
20
29
  "devDependencies": {
21
30
  "zod": "^4.5.4",
22
31
  "semantic-release": "^24.0.0",
23
32
  "@semantic-release/changelog": "^6.0.3",
24
33
  "@semantic-release/git": "^10.0.1"
25
34
  },
26
- "publishConfig": { "access": "public" },
27
- "repository": { "type": "git", "url": "git+https://github.com/riligar-applications/contract.git" }
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/riligar-applications/contract.git"
41
+ }
28
42
  }
package/src/index.js CHANGED
@@ -8,6 +8,18 @@
8
8
  //
9
9
  // A regra de ouro é a que este pacote torna mecânica: **um agente que aprendeu
10
10
  // um produto da RiLiGar já sabe usar os outros seis.**
11
+ //
12
+ // POR QUE OS SCHEMAS NÃO SAEM DAQUI
13
+ //
14
+ // `schemas.js` importa Zod, que é peer OPCIONAL — um worker que só precisa
15
+ // responder não deve carregar um validador de 60 kB. Mas enquanto este arquivo
16
+ // reexportava os schemas, o import era eager: `import { falha } from
17
+ // '@riligar/contract'` puxava `schemas.js`, que puxava Zod, e a instalação
18
+ // quebrava com "Cannot find package 'zod'" em quem seguiu a peer como
19
+ // opcional. O peer opcional não era opcional.
20
+ //
21
+ // Quem quer os schemas importa `@riligar/contract/schemas`, e aí Zod é
22
+ // requisito legítimo daquele caminho — declarado no export map, não escondido
23
+ // numa cadeia de reexports.
11
24
  export { CODES, STATUS_BY_CODE, RETRIABLE, isCode, toCode, codeFromStatus, ALIASES } from './codes.js'
12
- export { ok, criado, colecao, falha, falhaComStatus, corpoDeFalha, paginacao } from './respostas.js'
13
- export { codeSchema, errorSchema, pageSchema, collectionSchema, resourceSchema, chavesCamelCase } from './schemas.js'
25
+ export { ok, criado, colecao, falha, falhaComStatus, corpoDeFalha, paginacao, pagina } from './respostas.js'
package/src/respostas.js CHANGED
@@ -48,17 +48,42 @@ export const criado = (recurso, { headers } = {}) => responder(recurso, 201, hea
48
48
  *
49
49
  * `hasMore` é explícito de propósito: o consumidor não deveria precisar
50
50
  * comparar `offset + limit` com `total` para saber se continua paginando.
51
- * Quando `total` não é conhecido (contar custaria uma query a mais em coisa
52
- * que ninguém pagina), `hasMore` sai do tamanho da página cheia.
51
+ *
52
+ * ────────────────────────────────────────────────────────────────────────────
53
+ * `total: null` QUANDO NINGUÉM CONTOU — e isso é o ponto deste helper.
54
+ *
55
+ * Até 10/09 os defaults preenchiam `page` a partir do próprio array: uma rota
56
+ * que NUNCA paginou devolvia `{limit: N, offset: 0, total: N, hasMore: false}`,
57
+ * byte a byte idêntico ao de uma rota que paginou e chegou ao fim. Das 39
58
+ * rotas de coleção da stack, 29 não paginavam e todas as 29 afirmavam
59
+ * `hasMore: false` — o campo que existe para o agente não ter de deduzir
60
+ * deduzia errado em 87% dos casos.
61
+ *
62
+ * Agora: quem não passa `total` recebe `total: null` e `hasMore: null`. Nulo é
63
+ * "não sei", e é diferente de zero e de falso. O agente que lê `total: null`
64
+ * sabe que precisa perguntar de outro jeito; o que lê `hasMore: false` para de
65
+ * paginar — e essas duas conclusões não podem sair da mesma resposta.
66
+ *
67
+ * Quem pagina de verdade passa `total` e recebe `hasMore` calculado. Quem
68
+ * devolve a lista inteira de propósito passa `total: itens.length`, e aí o
69
+ * `hasMore: false` é uma afirmação, não um default.
53
70
  */
54
71
  export const colecao = (itens, pagina = {}, { status = 200, headers } = {}) => {
55
72
  const items = Array.isArray(itens) ? itens : []
56
73
  const limit = pagina.limit ?? items.length
57
74
  const offset = pagina.offset ?? 0
58
- const total = pagina.total ?? offset + items.length
59
- const hasMore = pagina.hasMore ?? offset + items.length < total
60
75
 
61
- return responder({ items, page: { limit, offset, total, hasMore } }, status, headers)
76
+ // `null` quando NINGUÉM contou. `total: 0` é uma contagem legítima.
77
+ const total = pagina.total ?? null
78
+ const hasMore = pagina.hasMore ?? (total === null ? null : offset + items.length < total)
79
+
80
+ /*
81
+ * `extra` vai na RAIZ, não dentro de `page`. O `activeDeployId` do Hoster e
82
+ * as `contagens` do Monitors são contexto do CONJUNTO, não paginação —
83
+ * dentro de `page` eles diriam que são, e um cliente genérico que lê
84
+ * `page` para paginar tropeçaria neles.
85
+ */
86
+ return responder({ items, page: { limit, offset, total, hasMore }, ...(pagina.extra ?? {}) }, status, headers)
62
87
  }
63
88
 
64
89
  /**
@@ -118,6 +143,33 @@ export const paginacao = (url, { limitPadrao = 50, limitMaximo = 200 } = {}) =>
118
143
  return { limit, offset }
119
144
  }
120
145
 
146
+ /**
147
+ * A página de uma consulta paginada, pronta para o `colecao`.
148
+ *
149
+ * Existe para tornar MECÂNICO o que a stack errava à mão em duas frentes:
150
+ *
151
+ * 1. **O `limit` reportado tem de ser o APLICADO.** Duas rotas clampavam na
152
+ * camada de dados e reportavam o valor cru da query: com `?limit=5000`
153
+ * devolviam 200 linhas e diziam `page:{limit:5000, hasMore:false}`. O
154
+ * cliente conclui "pedi 5000, vieram 200, logo acabou" — e para de paginar
155
+ * no meio.
156
+ *
157
+ * 2. **`total` vem de quem contou.** Passar `total` é obrigatório aqui, porque
158
+ * o `colecao` sozinho não tem como saber se ninguém contou ou se a conta
159
+ * deu o tamanho do array.
160
+ *
161
+ * const { limit, offset } = paginacao(url)
162
+ * const linhas = await buscar(limit, offset)
163
+ * const [{ total }] = await contar()
164
+ * return colecao(linhas, pagina({ limit, offset, total }))
165
+ */
166
+ export const pagina = ({ limit, offset = 0, total, extra }) => {
167
+ if (!Number.isFinite(total)) {
168
+ throw new TypeError('pagina() exige `total` numérico — use colecao(itens) direto quando ninguém contou.')
169
+ }
170
+ return { limit, offset, total, extra }
171
+ }
172
+
121
173
  /**
122
174
  * Uma falha de que só se sabe o status HTTP.
123
175
  *