@softize/opus 13.0.0 → 14.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.
- package/CHANGELOG.md +81 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +276 -6
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/code-style.md +4 -1
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/App.tsx +11 -6
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +5 -6
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +44 -137
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +26 -22
- package/src/ui/components/patterns/list.tsx +18 -12
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +37 -54
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +21 -25
- package/src/ui/components/patterns/view.tsx +29 -22
- package/src/ui/components/primitives/alert.tsx +1 -27
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +5 -41
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +39 -30
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +72 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +1 -25
- package/src/ui/components/primitives/drawer.tsx +10 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +12 -9
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +6 -6
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +5 -1
- package/src/ui/components/primitives/tabs.tsx +6 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +36 -3
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +13 -2
- package/src/ui/docs/content/action-trigger.md +12 -5
- package/src/ui/docs/content/action-view.md +11 -3
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +17 -1
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +20 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +5 -4
- package/src/ui/docs/content/sidebar.md +13 -47
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +12 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +10 -16
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -147
package/src/seed/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* implementação server-only que a CLI carrega pelo `opus.config.ts`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/** Variante de dados que um seed sabe aplicar (ex.: `minimal`, `demo`). */
|
|
8
9
|
export interface SeedProfile {
|
|
9
10
|
/** Situação que este perfil representa para quem vai aplicá-lo. */
|
|
10
11
|
description: string
|
|
@@ -12,13 +13,16 @@ export interface SeedProfile {
|
|
|
12
13
|
expected?: Readonly<Record<string, number>>
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Limites de onde o seed pode rodar. */
|
|
15
17
|
export interface SeedSafety {
|
|
16
18
|
/** Escopos de dados em que o seed pode abrir conexão. */
|
|
17
19
|
scopes: readonly SeedScope[]
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** Escopos de banco em que seeds podem abrir conexão; nunca produção. */
|
|
20
23
|
export type SeedScope = 'local' | 'test' | 'isolated-preview'
|
|
21
24
|
|
|
25
|
+
/** Declaração pura de um seed — o que `defineSeed` valida e o manifesto publica. */
|
|
22
26
|
export interface SeedDefinition {
|
|
23
27
|
/** Identidade estável, em segmentos lowercase separados por ponto. */
|
|
24
28
|
name: string
|
|
@@ -31,12 +35,14 @@ export interface SeedDefinition {
|
|
|
31
35
|
safety: SeedSafety
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/** Contexto entregue a `plan`, `apply` e `verify` do binding. */
|
|
34
39
|
export interface SeedContext<Database = unknown> {
|
|
35
40
|
database: Database | null
|
|
36
41
|
profile: string
|
|
37
42
|
scope: SeedScope
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/** Contexto da asserção de alvo, executada antes de qualquer binding. */
|
|
40
46
|
export interface SeedTargetContext<Database = unknown> {
|
|
41
47
|
database: Database | null
|
|
42
48
|
scope: SeedScope
|
|
@@ -50,28 +56,34 @@ export type SeedTargetAssertion<Database = unknown> = (
|
|
|
50
56
|
context: SeedTargetContext<Database>,
|
|
51
57
|
) => boolean | Promise<boolean>
|
|
52
58
|
|
|
59
|
+
/** Resultado de `plan`: o que a aplicação faria, sem tocar o banco. */
|
|
53
60
|
export interface SeedPlan {
|
|
54
61
|
summary: string
|
|
55
62
|
operations: readonly string[]
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/** Resultado de `apply` e `verify`: resumo e métricas observadas. */
|
|
58
66
|
export interface SeedReport {
|
|
59
67
|
summary: string
|
|
60
68
|
metrics: Readonly<Record<string, number>>
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
/** Implementação server-only de um seed, ligada à declaração por `bindSeed`. */
|
|
63
72
|
export interface SeedBinding<Database = unknown> {
|
|
64
73
|
plan(context: SeedContext<Database>): SeedPlan | Promise<SeedPlan>
|
|
65
74
|
apply(context: SeedContext<Database>): SeedReport | Promise<SeedReport>
|
|
66
75
|
verify(context: SeedContext<Database>): SeedReport | Promise<SeedReport>
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
/** Seed com declaração e binding — a forma que o registro em `opus.config.ts` consome. */
|
|
69
79
|
export interface BoundSeed<Database = unknown> extends SeedDefinition {
|
|
70
80
|
readonly binding: SeedBinding<Database>
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
/** Item aceito no registro; declaração sem binding é diagnosticada como `seed.unbound`. */
|
|
73
84
|
export type SeedRegistryItem = SeedDefinition | BoundSeed
|
|
74
85
|
|
|
86
|
+
/** Problema encontrado ao verificar o registro de seeds. */
|
|
75
87
|
export interface SeedDiagnostic {
|
|
76
88
|
code:
|
|
77
89
|
| 'seed.duplicate'
|
|
@@ -82,6 +94,7 @@ export interface SeedDiagnostic {
|
|
|
82
94
|
seed: string
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
/** Resultado de `checkSeedRegistry`. */
|
|
85
98
|
export interface SeedRegistryCheck {
|
|
86
99
|
ok: boolean
|
|
87
100
|
diagnostics: readonly SeedDiagnostic[]
|
|
@@ -94,6 +107,7 @@ const PROFILE_RE = /^[a-z][a-z0-9-]*$/
|
|
|
94
107
|
const SCOPE_RE = /^[a-z][a-z0-9-]*$/
|
|
95
108
|
const SEED_SCOPES = new Set<SeedScope>(['local', 'test', 'isolated-preview'])
|
|
96
109
|
|
|
110
|
+
/** Valida e devolve a declaração de um seed, preservando o tipo literal. Lança em declaração inválida. */
|
|
97
111
|
export function defineSeed<const Definition extends SeedDefinition>(
|
|
98
112
|
definition: Definition,
|
|
99
113
|
): Definition {
|
|
@@ -129,6 +143,7 @@ export function publicSeedDefinition(seed: SeedDefinition): SeedDefinition {
|
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
|
|
146
|
+
/** Liga a implementação server-only (`plan`, `apply`, `verify`) a uma declaração validada. */
|
|
132
147
|
export function bindSeed<Database = unknown>(
|
|
133
148
|
definition: SeedDefinition,
|
|
134
149
|
binding: SeedBinding<Database>,
|
|
@@ -138,6 +153,7 @@ export function bindSeed<Database = unknown>(
|
|
|
138
153
|
return { ...definition, binding }
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
/** Type guard: `true` se o valor é uma declaração de seed válida. */
|
|
141
157
|
export function isSeedDefinition(value: unknown): value is SeedDefinition {
|
|
142
158
|
try {
|
|
143
159
|
validateDefinition(value)
|
|
@@ -147,6 +163,7 @@ export function isSeedDefinition(value: unknown): value is SeedDefinition {
|
|
|
147
163
|
}
|
|
148
164
|
}
|
|
149
165
|
|
|
166
|
+
/** Type guard: `true` se o valor é um seed declarado e com binding completo. */
|
|
150
167
|
export function isBoundSeed(value: unknown): value is BoundSeed {
|
|
151
168
|
if (!isSeedDefinition(value)) return false
|
|
152
169
|
try {
|
|
@@ -157,6 +174,10 @@ export function isBoundSeed(value: unknown): value is BoundSeed {
|
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Verifica o registro de seeds: duplicidade, binding ausente, dependência
|
|
179
|
+
* inexistente e ciclo. Devolve a ordem topológica quando não há ciclo.
|
|
180
|
+
*/
|
|
160
181
|
export function checkSeedRegistry(items: readonly SeedRegistryItem[]): SeedRegistryCheck {
|
|
161
182
|
const diagnostics: SeedDiagnostic[] = []
|
|
162
183
|
const byName = new Map<string, BoundSeed>()
|
|
@@ -237,6 +258,7 @@ export function checkSeedRegistry(items: readonly SeedRegistryItem[]): SeedRegis
|
|
|
237
258
|
}
|
|
238
259
|
}
|
|
239
260
|
|
|
261
|
+
/** Resolve o perfil a aplicar (`requested` ou `defaultProfile`); lança se não existir no seed. */
|
|
240
262
|
export function resolveSeedProfile(seed: SeedDefinition, requested?: string): string {
|
|
241
263
|
const profile = requested ?? seed.defaultProfile
|
|
242
264
|
if (!Object.hasOwn(seed.profiles, profile)) {
|
|
@@ -247,6 +269,10 @@ export function resolveSeedProfile(seed: SeedDefinition, requested?: string): st
|
|
|
247
269
|
return profile
|
|
248
270
|
}
|
|
249
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Falha fechado antes de abrir conexão: exige `NODE_ENV` de desenvolvimento ou
|
|
274
|
+
* teste, escopo informado e permitido por `safety.scopes`.
|
|
275
|
+
*/
|
|
250
276
|
export function assertSeedExecutionAllowed(
|
|
251
277
|
seed: SeedDefinition,
|
|
252
278
|
scope: string | undefined,
|
|
@@ -274,6 +300,7 @@ function runtimeNodeEnvironment(): string | undefined {
|
|
|
274
300
|
).process?.env?.NODE_ENV
|
|
275
301
|
}
|
|
276
302
|
|
|
303
|
+
/** Garante que o retorno de `plan` tem a forma `{ summary, operations }`. */
|
|
277
304
|
export function validateSeedPlan(value: unknown): SeedPlan {
|
|
278
305
|
if (!isRecord(value) || !isNonEmptyString(value.summary) || !Array.isArray(value.operations)) {
|
|
279
306
|
throw new Error('plan precisa retornar { summary, operations }')
|
|
@@ -284,6 +311,7 @@ export function validateSeedPlan(value: unknown): SeedPlan {
|
|
|
284
311
|
return value as unknown as SeedPlan
|
|
285
312
|
}
|
|
286
313
|
|
|
314
|
+
/** Garante que o retorno de `apply`/`verify` tem a forma `{ summary, metrics }` com números finitos. */
|
|
287
315
|
export function validateSeedReport(value: unknown): SeedReport {
|
|
288
316
|
if (!isRecord(value) || !isNonEmptyString(value.summary) || !isMetrics(value.metrics)) {
|
|
289
317
|
throw new Error('apply/verify precisa retornar { summary, metrics } com valores numéricos finitos')
|
|
@@ -291,6 +319,7 @@ export function validateSeedReport(value: unknown): SeedReport {
|
|
|
291
319
|
return value as unknown as SeedReport
|
|
292
320
|
}
|
|
293
321
|
|
|
322
|
+
/** Compara as métricas do relatório com `profiles[profile].expected`; lança listando as divergências. */
|
|
294
323
|
export function assertSeedExpectedMetrics(
|
|
295
324
|
seed: SeedDefinition,
|
|
296
325
|
profile: string,
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
RuntimeRef,
|
|
29
29
|
ServerAdapter,
|
|
30
30
|
} from '../../core/index.ts'
|
|
31
|
+
import { readPackageVersion } from '../../core/package-version.ts'
|
|
31
32
|
import {
|
|
32
33
|
toOpenAPISpec,
|
|
33
34
|
type OpenAPIInfo,
|
|
@@ -58,7 +59,8 @@ export interface FastifyServerOptions {
|
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Info do OpenAPI document (title, version, description). Quando ausente,
|
|
61
|
-
* usa
|
|
62
|
+
* usa `{ title: 'Opus API', version: <versão do SDK> }` — sobrescreva pra
|
|
63
|
+
* publicar a identidade da sua API.
|
|
62
64
|
*/
|
|
63
65
|
openapiInfo?: OpenAPIInfo
|
|
64
66
|
|
|
@@ -73,11 +75,12 @@ export interface FastifyServerOptions {
|
|
|
73
75
|
// Adapter factory
|
|
74
76
|
// =============================================================================
|
|
75
77
|
|
|
78
|
+
/** ServerAdapter que monta as actions como rotas numa instância Fastify existente. */
|
|
76
79
|
export function fastifyServer(options: FastifyServerOptions): ServerAdapter {
|
|
77
80
|
const {
|
|
78
81
|
app,
|
|
79
82
|
apiPrefix = '/api',
|
|
80
|
-
openapiInfo = { title: '
|
|
83
|
+
openapiInfo = { title: 'Opus API', version: readPackageVersion() },
|
|
81
84
|
openapiServers,
|
|
82
85
|
traceContext = false,
|
|
83
86
|
} = options
|
|
@@ -34,7 +34,8 @@ import type {
|
|
|
34
34
|
RuntimeRef,
|
|
35
35
|
ServerAdapter,
|
|
36
36
|
} from '../../core/index.ts'
|
|
37
|
-
import { error } from '../../core/index.ts'
|
|
37
|
+
import { error, UNHANDLED_ERROR_MESSAGE } from '../../core/index.ts'
|
|
38
|
+
import { readPackageVersion } from '../../core/package-version.ts'
|
|
38
39
|
import {
|
|
39
40
|
toOpenAPISpec,
|
|
40
41
|
type OpenAPIInfo,
|
|
@@ -68,7 +69,8 @@ export interface NodeServerOptions {
|
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* Info do OpenAPI document (title, version, description). Quando ausente,
|
|
71
|
-
* usa
|
|
72
|
+
* usa `{ title: 'Opus API', version: <versão do SDK> }` — sobrescreva pra
|
|
73
|
+
* publicar a identidade da sua API.
|
|
72
74
|
*/
|
|
73
75
|
openapiInfo?: OpenAPIInfo
|
|
74
76
|
|
|
@@ -97,11 +99,12 @@ export interface NodeServerHandle {
|
|
|
97
99
|
|
|
98
100
|
type RouteFn = (req: IncomingMessage, res: ServerResponse, url: URL) => Promise<void>
|
|
99
101
|
|
|
102
|
+
/** ServerAdapter sobre `node:http` puro, sem framework; expõe `handler` pra plugar em qualquer servidor. */
|
|
100
103
|
export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
|
|
101
104
|
const {
|
|
102
105
|
apiPrefix = '/api',
|
|
103
106
|
endpoints,
|
|
104
|
-
openapiInfo = { title: '
|
|
107
|
+
openapiInfo = { title: 'Opus API', version: readPackageVersion() },
|
|
105
108
|
openapiServers,
|
|
106
109
|
traceContext = false,
|
|
107
110
|
} = options
|
|
@@ -164,7 +167,7 @@ export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
|
|
|
164
167
|
res,
|
|
165
168
|
400,
|
|
166
169
|
driverResult(
|
|
167
|
-
{ code: 'server.invalid_json', category: 'validation', message: '
|
|
170
|
+
{ code: 'server.invalid_json', category: 'validation', message: 'O corpo da requisição não é um JSON válido' },
|
|
168
171
|
action.name,
|
|
169
172
|
requestId,
|
|
170
173
|
),
|
|
@@ -205,7 +208,7 @@ export function nodeServer(options: NodeServerOptions = {}): NodeServerHandle {
|
|
|
205
208
|
res,
|
|
206
209
|
500,
|
|
207
210
|
driverResult(
|
|
208
|
-
{ code: 'server.unhandled', category: 'internal', message: String(err) },
|
|
211
|
+
{ code: 'server.unhandled', category: 'internal', message: UNHANDLED_ERROR_MESSAGE, cause: String(err) },
|
|
209
212
|
deriveActionName(url.pathname, apiPrefix),
|
|
210
213
|
crypto.randomUUID(),
|
|
211
214
|
),
|
|
@@ -319,7 +322,7 @@ function driverResult(
|
|
|
319
322
|
|
|
320
323
|
function routeNotFound(pathname: string, apiPrefix: string): ActionResult<never> {
|
|
321
324
|
return driverResult(
|
|
322
|
-
{ code: 'server.route_not_found', category: 'not_found', message: `
|
|
325
|
+
{ code: 'server.route_not_found', category: 'not_found', message: `Rota não encontrada: ${pathname}` },
|
|
323
326
|
deriveActionName(pathname, apiPrefix),
|
|
324
327
|
crypto.randomUUID(),
|
|
325
328
|
)
|
package/src/server/index.ts
CHANGED
|
@@ -181,7 +181,9 @@ function serializeError(err: ActionError): Record<string, unknown> {
|
|
|
181
181
|
if (err.i18nParams !== undefined) out.i18nParams = err.i18nParams
|
|
182
182
|
if (err.field !== undefined) out.field = err.field
|
|
183
183
|
if (err.issues !== undefined) out.issues = err.issues
|
|
184
|
-
|
|
184
|
+
// Erro interno não leva `cause` para a wire: é o texto cru da exceção (driver, SQL), que
|
|
185
|
+
// pertence ao log do servidor, não à pessoa nem ao cliente.
|
|
186
|
+
if (err.cause !== undefined && err.category !== 'internal') out.cause = String(err.cause)
|
|
185
187
|
if (err.meta !== undefined) out.meta = err.meta
|
|
186
188
|
return out
|
|
187
189
|
}
|
|
@@ -29,6 +29,7 @@ function metaPathOf(filePath: string): string {
|
|
|
29
29
|
return `${filePath}.meta.json`
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** StorageAdapter que grava objetos no sistema de arquivos local, com metadados em `<arquivo>.meta.json`. */
|
|
32
33
|
export function fsStorage(options: FsStorageOptions): StorageAdapter {
|
|
33
34
|
const root = path.resolve(options.root)
|
|
34
35
|
const baseUrl = options.baseUrl?.replace(/\/+$/, '')
|
package/src/testing/index.ts
CHANGED
|
@@ -250,7 +250,7 @@ async function validate<T>(
|
|
|
250
250
|
throw error({
|
|
251
251
|
code: kind === 'input' ? 'validation.invalid_input' : 'validation.invalid_output',
|
|
252
252
|
category: kind === 'input' ? 'validation' : 'internal',
|
|
253
|
-
message: kind === 'input' ? '
|
|
253
|
+
message: kind === 'input' ? 'Dados de entrada inválidos' : 'Dados de saída inválidos (validação em dev)',
|
|
254
254
|
issues: result.issues.map((iss) => ({
|
|
255
255
|
path: (iss.path ?? []).join('.'),
|
|
256
256
|
code: 'invalid',
|
|
@@ -294,7 +294,7 @@ export async function runAction<In, Out, ParsedIn = In>(
|
|
|
294
294
|
throw error({
|
|
295
295
|
code: 'auth.unauthenticated',
|
|
296
296
|
category: 'authentication',
|
|
297
|
-
message: '
|
|
297
|
+
message: 'Autenticação necessária',
|
|
298
298
|
})
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -305,7 +305,7 @@ export async function runAction<In, Out, ParsedIn = In>(
|
|
|
305
305
|
? Boolean(evalExpression(parseExpression(spec), { user: ctx.user, input: parsed, ctx, ...(loaded ?? {}) }))
|
|
306
306
|
: await spec(ctx, parsed, loaded)
|
|
307
307
|
if (decision === false) {
|
|
308
|
-
throw error({ code: 'auth.forbidden', category: 'authorization', message: '
|
|
308
|
+
throw error({ code: 'auth.forbidden', category: 'authorization', message: 'Acesso negado' })
|
|
309
309
|
}
|
|
310
310
|
if (decision !== true) throw decision
|
|
311
311
|
}
|
|
@@ -37,7 +37,10 @@ export interface ActionListDialogProps<TItem, TInput extends Record<string, unkn
|
|
|
37
37
|
note?: ReactNode
|
|
38
38
|
/** Ação à direita da linha — em geral o botão de criar. */
|
|
39
39
|
actions?: ReactNode
|
|
40
|
-
|
|
40
|
+
/** Textos dos estados, repassados ao ActionList (e dele ao DataState). */
|
|
41
|
+
emptyMessage?: string
|
|
42
|
+
errorMessage?: string
|
|
43
|
+
retryLabel?: string
|
|
41
44
|
/** Sobrepõe o vazio derivado (items.length === 0) — ex.: form inline aberto. */
|
|
42
45
|
empty?: (items: TItem[]) => boolean
|
|
43
46
|
/** Carga EXTRA agregada à do fetch (ex.: a query irmã que os children precisam). */
|
|
@@ -57,7 +60,9 @@ export function ActionListDialog<TItem, TInput extends Record<string, unknown> =
|
|
|
57
60
|
description,
|
|
58
61
|
note,
|
|
59
62
|
actions,
|
|
60
|
-
|
|
63
|
+
emptyMessage,
|
|
64
|
+
errorMessage,
|
|
65
|
+
retryLabel,
|
|
61
66
|
empty,
|
|
62
67
|
loading,
|
|
63
68
|
className,
|
|
@@ -80,7 +85,9 @@ export function ActionListDialog<TItem, TInput extends Record<string, unknown> =
|
|
|
80
85
|
<ActionList<TInput, TItem>
|
|
81
86
|
action={action as unknown as ListAction<TInput, TItem>}
|
|
82
87
|
input={(input ?? {}) as TInput}
|
|
83
|
-
{...(
|
|
88
|
+
{...(emptyMessage !== undefined ? { emptyMessage } : {})}
|
|
89
|
+
{...(errorMessage !== undefined ? { errorMessage } : {})}
|
|
90
|
+
{...(retryLabel !== undefined ? { retryLabel } : {})}
|
|
84
91
|
{...(empty !== undefined ? { empty } : {})}
|
|
85
92
|
{...(loading !== undefined ? { loading } : {})}
|
|
86
93
|
>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Namespace `dialog` de propósito: `window.alert/confirm/prompt` são globais do browser, e
|
|
14
14
|
* um import esquecido cairia no nativo (cinza, sem tema) sem erro. `window.dialog` não
|
|
15
|
-
* existe — a família fica imune ao footgun.
|
|
15
|
+
* existe — a família fica imune ao footgun.
|
|
16
16
|
*
|
|
17
17
|
* Por baixo é o <Dialog mode="alert">: `role="alertdialog"`, não fecha no clique fora, foco preso
|
|
18
18
|
* — a semântica que uma interrupção que EXIGE resposta pede. ESC/Cancelar resolve o "não"
|
|
@@ -59,16 +59,12 @@ export interface ConfirmOptions extends DialogBase {
|
|
|
59
59
|
cancel?: string
|
|
60
60
|
/** Contexto do botão de confirmação. */
|
|
61
61
|
context?: Extract<UiContext, 'primary' | 'danger'>
|
|
62
|
-
/** @deprecated Use `context="danger"`. */
|
|
63
|
-
variant?: 'default' | 'destructive'
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/** `prompt` — uma string livre. Form de verdade (campos, contrato) → ActionFormDialog. */
|
|
67
65
|
export interface PromptOptions extends DialogBase {
|
|
68
66
|
cancel?: string
|
|
69
67
|
context?: Extract<UiContext, 'primary' | 'danger'>
|
|
70
|
-
/** @deprecated Use `context="danger"`. */
|
|
71
|
-
variant?: 'default' | 'destructive'
|
|
72
68
|
placeholder?: string
|
|
73
69
|
defaultValue?: string
|
|
74
70
|
}
|
|
@@ -107,16 +103,6 @@ function requireHost(fn: string): Error | null {
|
|
|
107
103
|
)
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
function rejectAmbiguousContext(
|
|
111
|
-
fn: string,
|
|
112
|
-
options: ConfirmOptions | PromptOptions,
|
|
113
|
-
): Error | null {
|
|
114
|
-
if (options.context === undefined || options.variant === undefined) return null
|
|
115
|
-
return new Error(
|
|
116
|
-
`${fn} não permite combinar context com uma variante semântica legada.`,
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
106
|
function validateChoose<TResult extends string>(options: ChooseOptions<TResult>): Error | null {
|
|
121
107
|
if (options.actions.length === 0) {
|
|
122
108
|
return new Error('dialog.choose() exige pelo menos uma ação.')
|
|
@@ -145,8 +131,6 @@ export const dialog = {
|
|
|
145
131
|
},
|
|
146
132
|
/** Pergunta sim/não. `Promise<boolean>` — fechar/ESC resolve `false`. */
|
|
147
133
|
confirm(options: ConfirmOptions): Promise<boolean> {
|
|
148
|
-
const ambiguous = rejectAmbiguousContext('dialog.confirm()', options)
|
|
149
|
-
if (ambiguous !== null) return Promise.reject(ambiguous)
|
|
150
134
|
const err = requireHost('dialog.confirm()')
|
|
151
135
|
if (err !== null) return Promise.reject(err)
|
|
152
136
|
return new Promise<boolean>((resolve) => {
|
|
@@ -155,8 +139,6 @@ export const dialog = {
|
|
|
155
139
|
},
|
|
156
140
|
/** Pede uma string. `Promise<string | null>` — fechar/ESC/Cancelar resolve `null`. */
|
|
157
141
|
prompt(options: PromptOptions): Promise<string | null> {
|
|
158
|
-
const ambiguous = rejectAmbiguousContext('dialog.prompt()', options)
|
|
159
|
-
if (ambiguous !== null) return Promise.reject(ambiguous)
|
|
160
142
|
const err = requireHost('dialog.prompt()')
|
|
161
143
|
if (err !== null) return Promise.reject(err)
|
|
162
144
|
return new Promise<string | null>((resolve) => {
|
|
@@ -180,11 +162,6 @@ export const dialog = {
|
|
|
180
162
|
},
|
|
181
163
|
}
|
|
182
164
|
|
|
183
|
-
/** Alias retrocompatível — `confirm()` é `dialog.confirm()`. */
|
|
184
|
-
export function confirm(options: ConfirmOptions): Promise<boolean> {
|
|
185
|
-
return dialog.confirm(options)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
165
|
/**
|
|
189
166
|
* O host — um por app, no shell. Enfileira: uma por vez (duas caixas empilhadas seriam
|
|
190
167
|
* ambíguas sobre qual clique respondeu o quê).
|
|
@@ -228,11 +205,8 @@ export function DialogHost(): React.ReactElement {
|
|
|
228
205
|
else p.resolve(value)
|
|
229
206
|
}
|
|
230
207
|
|
|
231
|
-
const variant = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
|
|
232
|
-
? current.variant
|
|
233
|
-
: undefined
|
|
234
208
|
const context = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
|
|
235
|
-
? current.context ??
|
|
209
|
+
? current.context ?? 'primary'
|
|
236
210
|
: 'primary'
|
|
237
211
|
const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
|
|
238
212
|
|
|
@@ -344,6 +318,3 @@ export function DialogHost(): React.ReactElement {
|
|
|
344
318
|
</Dialog>
|
|
345
319
|
)
|
|
346
320
|
}
|
|
347
|
-
|
|
348
|
-
/** Alias retrocompatível — `<ConfirmHost />` é `<DialogHost />`. */
|
|
349
|
-
export const ConfirmHost = DialogHost
|