@softize/opus 8.6.6
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 +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema/zod — Zod-based logical type factories.
|
|
3
|
+
*
|
|
4
|
+
* Catálogo de factories que retornam `LogicalType<T>`: um objeto que carrega
|
|
5
|
+
* o schema Zod subjacente (`zod()`), metadata logical (`meta`), e os métodos
|
|
6
|
+
* `parse / format / validate` pra operar sobre instâncias do tipo canônico.
|
|
7
|
+
*
|
|
8
|
+
* Adapters (DB, UI, OpenAPI) continuam lendo a metadata via `getLogicalType()`
|
|
9
|
+
* sobre o schema Zod (recuperado por `.zod()`) pra mapear semântica em
|
|
10
|
+
* comportamento concreto.
|
|
11
|
+
*
|
|
12
|
+
* Uso:
|
|
13
|
+
* import { z } from 'zod'
|
|
14
|
+
* import { t } from '@softize/opus/schema/zod'
|
|
15
|
+
*
|
|
16
|
+
* const Email = t.email()
|
|
17
|
+
* Email.parse(' Foo@Bar.COM ') // → 'foo@bar.com'
|
|
18
|
+
* Email.validate('not-an-email') // → { ok: false, error: '...' }
|
|
19
|
+
*
|
|
20
|
+
* defineAction({
|
|
21
|
+
* input: z.object({
|
|
22
|
+
* email: t.email().zod(),
|
|
23
|
+
* amount: t.money({ currency: 'BRL' }).zod(),
|
|
24
|
+
* paidAt: t.datetime().zod(),
|
|
25
|
+
* }),
|
|
26
|
+
* })
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { z, type ZodTypeAny, type ZodSchema } from 'zod'
|
|
30
|
+
import { attachLogicalType, type LogicalTypeMeta } from '../index.ts'
|
|
31
|
+
import {
|
|
32
|
+
formatDatetime,
|
|
33
|
+
formatDate,
|
|
34
|
+
formatDecimal,
|
|
35
|
+
formatMoney,
|
|
36
|
+
parseDecimal,
|
|
37
|
+
parseMoney,
|
|
38
|
+
} from '../format/locale.ts'
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Interface pública: LogicalType<T>
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
export type ValidateResult<T> =
|
|
45
|
+
| { ok: true; value: T }
|
|
46
|
+
| { ok: false; error: string }
|
|
47
|
+
|
|
48
|
+
export interface FormatOptions {
|
|
49
|
+
locale?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LogicalType<T> {
|
|
53
|
+
/** Schema Zod subjacente — pra reuso em z.object({ field: t.email().zod() }). */
|
|
54
|
+
zod(): ZodSchema<T>
|
|
55
|
+
/** Metadata: { logicalType, params? }. */
|
|
56
|
+
meta: LogicalTypeMeta
|
|
57
|
+
/** Parse de input cru (form value, query param, JSON) pro tipo canônico T. */
|
|
58
|
+
parse(input: unknown): T
|
|
59
|
+
/**
|
|
60
|
+
* Format pra display humano. Locale default 'pt-BR' onde aplicável
|
|
61
|
+
* (datetime, money, percent, decimal). Tipos string/url/uuid/etc
|
|
62
|
+
* retornam `String(value)`.
|
|
63
|
+
*/
|
|
64
|
+
format(value: T, options?: FormatOptions): string
|
|
65
|
+
/** Validação completa. Retorna ok:true com valor parseado, ou ok:false com erro humano. */
|
|
66
|
+
validate(value: unknown): ValidateResult<T>
|
|
67
|
+
|
|
68
|
+
// — Modifiers de coluna (entidade) — encadeáveis, retornam novo LogicalType —
|
|
69
|
+
/** Metadata de coluna acumulada pelos modifiers. Lida pelo data/migration adapter. */
|
|
70
|
+
readonly column: ColumnMeta
|
|
71
|
+
/** Marca como primary key. Convenção: encadeie `.pk()` por último. */
|
|
72
|
+
pk(): LogicalType<T> & { readonly __pk: true }
|
|
73
|
+
/** Constraint UNIQUE. */
|
|
74
|
+
unique(): LogicalType<T>
|
|
75
|
+
/** Coluna NULL-able. Muda o tipo inferido pra `T | null`. */
|
|
76
|
+
nullable(): LogicalType<T | null>
|
|
77
|
+
/** Índice nesta coluna. `using` é hint de tipo (gin, etc) que o adapter resolve. */
|
|
78
|
+
index(opts?: { using?: string }): LogicalType<T>
|
|
79
|
+
/** Default no DDL (e no schema). Torna o campo opcional no insert. */
|
|
80
|
+
default(value: NonNullable<T>): LogicalType<T> & { readonly __hasDefault: true }
|
|
81
|
+
/** Foreign key pra outra entidade (por nome). `field` default = pk do alvo. */
|
|
82
|
+
references(entity: string, field?: string): LogicalType<T>
|
|
83
|
+
/** Doc de negócio do campo (a fonte de entendimento). Encadeável; flui pro manifest. */
|
|
84
|
+
doc(text: string): LogicalType<T>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Metadata de coluna acumulada pelos modifiers de entidade. Canal separado da
|
|
89
|
+
* validação (Zod): o data/migration adapter lê isto pra emitir o DDL.
|
|
90
|
+
*/
|
|
91
|
+
export interface ColumnMeta {
|
|
92
|
+
pk?: boolean
|
|
93
|
+
unique?: boolean
|
|
94
|
+
nullable?: boolean
|
|
95
|
+
index?: boolean | { using?: string }
|
|
96
|
+
hasDefault?: boolean
|
|
97
|
+
default?: unknown
|
|
98
|
+
references?: { entity: string; field?: string }
|
|
99
|
+
/** Doc de negócio do campo — a fonte de entendimento. Flui pro manifest/lente. */
|
|
100
|
+
doc?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Helper interno: build LogicalType
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
interface BuildArgs<T> {
|
|
108
|
+
logicalType: string
|
|
109
|
+
params?: Record<string, unknown>
|
|
110
|
+
zod: ZodSchema<T>
|
|
111
|
+
parse: (input: unknown) => T
|
|
112
|
+
format?: (value: T, options?: FormatOptions) => string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface FieldCore<T> {
|
|
116
|
+
zod: ZodSchema<T>
|
|
117
|
+
meta: LogicalTypeMeta
|
|
118
|
+
parse: (input: unknown) => T
|
|
119
|
+
format: (value: T, options?: FormatOptions) => string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function build<T>(args: BuildArgs<T>): LogicalType<T> {
|
|
123
|
+
const meta: LogicalTypeMeta =
|
|
124
|
+
args.params === undefined
|
|
125
|
+
? { logicalType: args.logicalType }
|
|
126
|
+
: { logicalType: args.logicalType, params: args.params }
|
|
127
|
+
// Mantém o contrato antigo: metadata anexada ao schema Zod também,
|
|
128
|
+
// pra adapters (openapi, db, ui) que leem via getLogicalType().
|
|
129
|
+
attachLogicalType(args.zod as unknown as object, meta)
|
|
130
|
+
|
|
131
|
+
const format = args.format ?? ((value: T): string => String(value))
|
|
132
|
+
return makeField({ zod: args.zod, meta, parse: args.parse, format }, {})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Constrói o `LogicalType` imutável a partir do core + metadata de coluna.
|
|
137
|
+
* Cada modifier (`pk`, `nullable`, ...) devolve um novo `LogicalType`, sem
|
|
138
|
+
* mutar o anterior — encadeamento é puro.
|
|
139
|
+
*/
|
|
140
|
+
function makeField<T>(core: FieldCore<T>, column: ColumnMeta): LogicalType<T> {
|
|
141
|
+
return {
|
|
142
|
+
zod: () => core.zod,
|
|
143
|
+
meta: core.meta,
|
|
144
|
+
parse: core.parse,
|
|
145
|
+
format: core.format,
|
|
146
|
+
column,
|
|
147
|
+
validate(value: unknown): ValidateResult<T> {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = core.parse(value)
|
|
150
|
+
const result = core.zod.safeParse(parsed)
|
|
151
|
+
if (result.success) return { ok: true, value: result.data }
|
|
152
|
+
const issue = result.error.issues[0]
|
|
153
|
+
return { ok: false, error: humanizeIssue(issue) }
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return { ok: false, error: humanizeError(err) }
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
pk: () =>
|
|
159
|
+
makeField(core, { ...column, pk: true }) as LogicalType<T> & {
|
|
160
|
+
readonly __pk: true
|
|
161
|
+
},
|
|
162
|
+
unique: () => makeField(core, { ...column, unique: true }),
|
|
163
|
+
nullable: () =>
|
|
164
|
+
makeField<T | null>(
|
|
165
|
+
{
|
|
166
|
+
zod: core.zod.nullable() as unknown as ZodSchema<T | null>,
|
|
167
|
+
meta: core.meta,
|
|
168
|
+
parse: (input) => (input === null || input === undefined ? null : core.parse(input)),
|
|
169
|
+
format: (value, options) => (value === null ? '' : core.format(value, options)),
|
|
170
|
+
},
|
|
171
|
+
{ ...column, nullable: true },
|
|
172
|
+
),
|
|
173
|
+
index: (opts) =>
|
|
174
|
+
makeField(core, {
|
|
175
|
+
...column,
|
|
176
|
+
index: opts?.using !== undefined ? { using: opts.using } : true,
|
|
177
|
+
}),
|
|
178
|
+
default: (value) =>
|
|
179
|
+
makeField(core, { ...column, hasDefault: true, default: value }) as LogicalType<T> & {
|
|
180
|
+
readonly __hasDefault: true
|
|
181
|
+
},
|
|
182
|
+
references: (entity, field) =>
|
|
183
|
+
makeField(core, {
|
|
184
|
+
...column,
|
|
185
|
+
references: field !== undefined ? { entity, field } : { entity },
|
|
186
|
+
}),
|
|
187
|
+
doc: (text) => makeField(core, { ...column, doc: text }),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function humanizeIssue(issue: z.ZodIssue | undefined): string {
|
|
192
|
+
/* v8 ignore next */
|
|
193
|
+
if (issue === undefined) return 'valor inválido'
|
|
194
|
+
return issue.message
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function humanizeError(err: unknown): string {
|
|
198
|
+
/* v8 ignore next 2 */
|
|
199
|
+
if (!(err instanceof Error)) return 'valor inválido'
|
|
200
|
+
return err.message
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// Catálogo: primitivos
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
const string = (): LogicalType<string> =>
|
|
208
|
+
build({
|
|
209
|
+
logicalType: 'string',
|
|
210
|
+
zod: z.string(),
|
|
211
|
+
parse: (input) => {
|
|
212
|
+
if (typeof input === 'string') return input
|
|
213
|
+
throw new Error('esperava string')
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const text = (): LogicalType<string> =>
|
|
218
|
+
build({
|
|
219
|
+
logicalType: 'text',
|
|
220
|
+
zod: z.string(),
|
|
221
|
+
parse: (input) => {
|
|
222
|
+
if (typeof input === 'string') return input
|
|
223
|
+
throw new Error('esperava texto')
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const int = (): LogicalType<number> =>
|
|
228
|
+
build({
|
|
229
|
+
logicalType: 'int',
|
|
230
|
+
zod: z.number().int(),
|
|
231
|
+
parse: (input) => {
|
|
232
|
+
if (typeof input === 'number') return input
|
|
233
|
+
if (typeof input === 'string' && input.trim() !== '') {
|
|
234
|
+
const n = Number(input)
|
|
235
|
+
if (!Number.isNaN(n)) return n
|
|
236
|
+
}
|
|
237
|
+
throw new Error('esperava inteiro')
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const bigint_ = (): LogicalType<bigint> =>
|
|
242
|
+
build({
|
|
243
|
+
logicalType: 'bigint',
|
|
244
|
+
zod: z.bigint(),
|
|
245
|
+
parse: (input) => {
|
|
246
|
+
if (typeof input === 'bigint') return input
|
|
247
|
+
if (typeof input === 'number' && Number.isInteger(input)) return BigInt(input)
|
|
248
|
+
if (typeof input === 'string' && /^-?\d+$/.test(input.trim())) {
|
|
249
|
+
return BigInt(input.trim())
|
|
250
|
+
}
|
|
251
|
+
throw new Error('esperava bigint')
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
interface DecimalParams {
|
|
256
|
+
precision?: number
|
|
257
|
+
scale?: number
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const decimal = (params?: DecimalParams): LogicalType<string> => {
|
|
261
|
+
const zodSchema = z.string().regex(/^-?\d+(\.\d+)?$/, 'esperava decimal')
|
|
262
|
+
return build({
|
|
263
|
+
logicalType: 'decimal',
|
|
264
|
+
...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
|
|
265
|
+
zod: zodSchema,
|
|
266
|
+
parse: (input) => {
|
|
267
|
+
if (typeof input === 'number') return String(input)
|
|
268
|
+
if (typeof input === 'string') return parseDecimal(input)
|
|
269
|
+
throw new Error('esperava decimal')
|
|
270
|
+
},
|
|
271
|
+
format: (value, options) => formatDecimal(value, options?.locale),
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const boolean = (): LogicalType<boolean> =>
|
|
276
|
+
build({
|
|
277
|
+
logicalType: 'boolean',
|
|
278
|
+
zod: z.boolean(),
|
|
279
|
+
parse: (input) => {
|
|
280
|
+
if (typeof input === 'boolean') return input
|
|
281
|
+
if (input === 1 || input === 0) return input === 1
|
|
282
|
+
if (typeof input === 'string') {
|
|
283
|
+
const v = input.trim().toLowerCase()
|
|
284
|
+
if (v === 'true' || v === '1') return true
|
|
285
|
+
if (v === 'false' || v === '0') return false
|
|
286
|
+
}
|
|
287
|
+
throw new Error('esperava boolean')
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const json = (): LogicalType<unknown> =>
|
|
292
|
+
build({
|
|
293
|
+
logicalType: 'json',
|
|
294
|
+
zod: z.unknown(),
|
|
295
|
+
parse: (input) => input,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// =============================================================================
|
|
299
|
+
// Catálogo: tempo
|
|
300
|
+
// =============================================================================
|
|
301
|
+
|
|
302
|
+
interface DatetimeParams {
|
|
303
|
+
precision?: 'second' | 'millisecond'
|
|
304
|
+
offset?: boolean
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const datetime = (params?: DatetimeParams): LogicalType<string> => {
|
|
308
|
+
const options: { offset: boolean; precision?: number } = {
|
|
309
|
+
offset: params?.offset ?? true,
|
|
310
|
+
}
|
|
311
|
+
if (params?.precision === 'millisecond') options.precision = 3
|
|
312
|
+
const zodSchema = z.string().datetime(options)
|
|
313
|
+
return build({
|
|
314
|
+
logicalType: 'datetime',
|
|
315
|
+
...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
|
|
316
|
+
zod: zodSchema,
|
|
317
|
+
parse: (input) => {
|
|
318
|
+
if (input instanceof Date) return input.toISOString()
|
|
319
|
+
if (typeof input === 'string') return input
|
|
320
|
+
throw new Error('esperava datetime ISO ou Date')
|
|
321
|
+
},
|
|
322
|
+
format: (value, opts) => {
|
|
323
|
+
if (opts?.locale === undefined) return value
|
|
324
|
+
return formatDatetime(value, opts.locale)
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const date = (): LogicalType<string> =>
|
|
330
|
+
build({
|
|
331
|
+
logicalType: 'date',
|
|
332
|
+
zod: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'esperava YYYY-MM-DD'),
|
|
333
|
+
parse: (input) => {
|
|
334
|
+
if (input instanceof Date) {
|
|
335
|
+
// Componentes UTC pra evitar drift de timezone.
|
|
336
|
+
const y = input.getUTCFullYear()
|
|
337
|
+
const m = String(input.getUTCMonth() + 1).padStart(2, '0')
|
|
338
|
+
const d = String(input.getUTCDate()).padStart(2, '0')
|
|
339
|
+
return `${y}-${m}-${d}`
|
|
340
|
+
}
|
|
341
|
+
if (typeof input === 'string') return input
|
|
342
|
+
throw new Error('esperava data')
|
|
343
|
+
},
|
|
344
|
+
format: (value, opts) => {
|
|
345
|
+
if (opts?.locale === undefined) return value
|
|
346
|
+
return formatDate(value, opts.locale)
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const time = (): LogicalType<string> =>
|
|
351
|
+
build({
|
|
352
|
+
logicalType: 'time',
|
|
353
|
+
zod: z
|
|
354
|
+
.string()
|
|
355
|
+
.regex(/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/, 'esperava HH:mm[:ss]'),
|
|
356
|
+
parse: (input) => {
|
|
357
|
+
if (typeof input === 'string') return input
|
|
358
|
+
throw new Error('esperava time')
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
const duration = (): LogicalType<string> =>
|
|
363
|
+
build({
|
|
364
|
+
logicalType: 'duration',
|
|
365
|
+
zod: z.string().duration(),
|
|
366
|
+
parse: (input) => {
|
|
367
|
+
if (typeof input === 'string') return input
|
|
368
|
+
throw new Error('esperava duration ISO 8601')
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const timezone = (): LogicalType<string> =>
|
|
373
|
+
build({
|
|
374
|
+
logicalType: 'timezone',
|
|
375
|
+
zod: z
|
|
376
|
+
.string()
|
|
377
|
+
.regex(/^[A-Z][A-Za-z_]+\/[A-Z][A-Za-z_/]+$/, 'esperava IANA tz'),
|
|
378
|
+
parse: (input) => {
|
|
379
|
+
if (typeof input === 'string') return input
|
|
380
|
+
throw new Error('esperava timezone')
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// =============================================================================
|
|
385
|
+
// Catálogo: identificadores & formatos
|
|
386
|
+
// =============================================================================
|
|
387
|
+
|
|
388
|
+
const email = (): LogicalType<string> => {
|
|
389
|
+
const zodSchema = z
|
|
390
|
+
.string()
|
|
391
|
+
.trim()
|
|
392
|
+
.toLowerCase()
|
|
393
|
+
.pipe(z.string().email('email inválido')) as unknown as ZodSchema<string>
|
|
394
|
+
return build({
|
|
395
|
+
logicalType: 'email',
|
|
396
|
+
zod: zodSchema,
|
|
397
|
+
parse: (input) => {
|
|
398
|
+
if (typeof input === 'string') return input.trim().toLowerCase()
|
|
399
|
+
throw new Error('esperava email')
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface PhoneParams {
|
|
405
|
+
country?: string
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const phone = (params?: PhoneParams): LogicalType<string> => {
|
|
409
|
+
const zodSchema = z
|
|
410
|
+
.string()
|
|
411
|
+
.regex(/^\+\d{8,15}$/, 'esperava telefone E.164 (+ e 8-15 dígitos)')
|
|
412
|
+
return build({
|
|
413
|
+
logicalType: 'phone',
|
|
414
|
+
...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
|
|
415
|
+
zod: zodSchema,
|
|
416
|
+
parse: (input) => {
|
|
417
|
+
if (typeof input === 'string') return input.trim()
|
|
418
|
+
throw new Error('esperava telefone')
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
interface UrlParams {
|
|
424
|
+
protocols?: string[]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const url = (params?: UrlParams): LogicalType<string> => {
|
|
428
|
+
const allowed = new Set(params?.protocols ?? ['http', 'https'])
|
|
429
|
+
const zodSchema = z
|
|
430
|
+
.string()
|
|
431
|
+
.url('URL inválida')
|
|
432
|
+
.refine(
|
|
433
|
+
(value) => {
|
|
434
|
+
try {
|
|
435
|
+
return allowed.has(new URL(value).protocol.replace(':', ''))
|
|
436
|
+
} catch {
|
|
437
|
+
/* v8 ignore next */
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
{ message: `protocolo deve ser um de: ${[...allowed].join(', ')}` },
|
|
442
|
+
)
|
|
443
|
+
return build({
|
|
444
|
+
logicalType: 'url',
|
|
445
|
+
...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
|
|
446
|
+
zod: zodSchema as unknown as ZodSchema<string>,
|
|
447
|
+
parse: (input) => {
|
|
448
|
+
if (input instanceof URL) return input.toString()
|
|
449
|
+
if (typeof input === 'string') return input.trim()
|
|
450
|
+
throw new Error('esperava URL')
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const uuid = (): LogicalType<string> =>
|
|
456
|
+
build({
|
|
457
|
+
logicalType: 'uuid',
|
|
458
|
+
zod: z.string().uuid('UUID inválido'),
|
|
459
|
+
parse: (input) => {
|
|
460
|
+
if (typeof input === 'string') return input.trim().toLowerCase()
|
|
461
|
+
throw new Error('esperava UUID')
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const slug = (): LogicalType<string> =>
|
|
466
|
+
build({
|
|
467
|
+
logicalType: 'slug',
|
|
468
|
+
zod: z
|
|
469
|
+
.string()
|
|
470
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'esperava slug kebab-case'),
|
|
471
|
+
parse: (input) => {
|
|
472
|
+
if (typeof input === 'string') return input.trim()
|
|
473
|
+
throw new Error('esperava slug')
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// =============================================================================
|
|
478
|
+
// Catálogo: domínio
|
|
479
|
+
// =============================================================================
|
|
480
|
+
|
|
481
|
+
interface MoneyParams {
|
|
482
|
+
currency?: string
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const money = (params?: MoneyParams): LogicalType<bigint> => {
|
|
486
|
+
const currencyCode = params?.currency ?? 'BRL'
|
|
487
|
+
return build({
|
|
488
|
+
logicalType: 'money',
|
|
489
|
+
...(params !== undefined ? { params: params as Record<string, unknown> } : {}),
|
|
490
|
+
zod: z.bigint(),
|
|
491
|
+
parse: (input) => {
|
|
492
|
+
if (typeof input === 'bigint') return input
|
|
493
|
+
if (typeof input === 'number' && Number.isInteger(input)) return BigInt(input)
|
|
494
|
+
if (typeof input === 'string') {
|
|
495
|
+
const cents = parseMoney(input)
|
|
496
|
+
if (cents !== null) return cents
|
|
497
|
+
}
|
|
498
|
+
throw new Error('esperava valor monetário')
|
|
499
|
+
},
|
|
500
|
+
format: (value, opts) => formatMoney(value, currencyCode, opts?.locale),
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const currency = (): LogicalType<string> =>
|
|
505
|
+
build({
|
|
506
|
+
logicalType: 'currency',
|
|
507
|
+
zod: z.string().regex(/^[A-Z]{3}$/, 'esperava ISO 4217'),
|
|
508
|
+
parse: (input) => {
|
|
509
|
+
if (typeof input === 'string') return input.trim().toUpperCase()
|
|
510
|
+
throw new Error('esperava código de moeda')
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const country = (): LogicalType<string> =>
|
|
515
|
+
build({
|
|
516
|
+
logicalType: 'country',
|
|
517
|
+
zod: z.string().regex(/^[A-Z]{2}$/, 'esperava ISO 3166 alpha-2'),
|
|
518
|
+
parse: (input) => {
|
|
519
|
+
if (typeof input === 'string') return input.trim().toUpperCase()
|
|
520
|
+
throw new Error('esperava país')
|
|
521
|
+
},
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const locale_ = (): LogicalType<string> =>
|
|
525
|
+
build({
|
|
526
|
+
logicalType: 'locale',
|
|
527
|
+
zod: z.string().regex(/^[a-z]{2,3}(-[A-Z]{2})?$/, 'esperava locale BCP 47'),
|
|
528
|
+
parse: (input) => {
|
|
529
|
+
if (typeof input === 'string') return input.trim()
|
|
530
|
+
throw new Error('esperava locale')
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const percent = (): LogicalType<number> =>
|
|
535
|
+
build({
|
|
536
|
+
logicalType: 'percent',
|
|
537
|
+
// Convenção: 0..100 (display friendly). Schemas que queiram 0..1
|
|
538
|
+
// podem usar t.decimal() ou similar.
|
|
539
|
+
zod: z.number(),
|
|
540
|
+
parse: (input) => {
|
|
541
|
+
if (typeof input === 'number') return input
|
|
542
|
+
if (typeof input === 'string') {
|
|
543
|
+
const cleaned = input.trim().replace('%', '').replace(',', '.')
|
|
544
|
+
const n = Number(cleaned)
|
|
545
|
+
if (!Number.isNaN(n)) return n
|
|
546
|
+
}
|
|
547
|
+
throw new Error('esperava percent')
|
|
548
|
+
},
|
|
549
|
+
format: (value, opts) => {
|
|
550
|
+
const locale = opts?.locale ?? 'pt-BR'
|
|
551
|
+
return new Intl.NumberFormat(locale, {
|
|
552
|
+
style: 'percent',
|
|
553
|
+
minimumFractionDigits: 0,
|
|
554
|
+
maximumFractionDigits: 2,
|
|
555
|
+
}).format(value / 100)
|
|
556
|
+
},
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
// =============================================================================
|
|
560
|
+
// Catálogo: texto rico
|
|
561
|
+
// =============================================================================
|
|
562
|
+
|
|
563
|
+
const markdown = (): LogicalType<string> =>
|
|
564
|
+
build({
|
|
565
|
+
logicalType: 'markdown',
|
|
566
|
+
zod: z.string(),
|
|
567
|
+
parse: (input) => {
|
|
568
|
+
if (typeof input === 'string') return input
|
|
569
|
+
throw new Error('esperava markdown')
|
|
570
|
+
},
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const html = (): LogicalType<string> =>
|
|
574
|
+
build({
|
|
575
|
+
logicalType: 'html',
|
|
576
|
+
zod: z.string(),
|
|
577
|
+
parse: (input) => {
|
|
578
|
+
if (typeof input === 'string') return input
|
|
579
|
+
throw new Error('esperava HTML')
|
|
580
|
+
},
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// =============================================================================
|
|
584
|
+
// Catálogo: composição
|
|
585
|
+
// =============================================================================
|
|
586
|
+
|
|
587
|
+
const enum_ = <T extends [string, ...string[]]>(values: T): LogicalType<T[number]> => {
|
|
588
|
+
const zodSchema = z.enum(values)
|
|
589
|
+
return build<T[number]>({
|
|
590
|
+
logicalType: 'enum',
|
|
591
|
+
params: { values },
|
|
592
|
+
zod: zodSchema as unknown as ZodSchema<T[number]>,
|
|
593
|
+
parse: (input) => {
|
|
594
|
+
if (typeof input === 'string' && (values as readonly string[]).includes(input)) {
|
|
595
|
+
return input as T[number]
|
|
596
|
+
}
|
|
597
|
+
throw new Error(`esperava um de: ${values.join(', ')}`)
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Aceita ZodSchema ou LogicalType<U>. Internamente extrai o Zod schema
|
|
604
|
+
* via `.zod()` quando recebe LogicalType.
|
|
605
|
+
*/
|
|
606
|
+
const array = <U>(of: LogicalType<U> | ZodTypeAny): LogicalType<U[]> => {
|
|
607
|
+
const inner = isLogicalType(of) ? of.zod() : (of as unknown as ZodSchema<U>)
|
|
608
|
+
const zodSchema = z.array(inner)
|
|
609
|
+
return build({
|
|
610
|
+
logicalType: 'array',
|
|
611
|
+
zod: zodSchema,
|
|
612
|
+
parse: (input) => {
|
|
613
|
+
if (Array.isArray(input)) return input as U[]
|
|
614
|
+
throw new Error('esperava array')
|
|
615
|
+
},
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const object = <S extends z.ZodRawShape>(
|
|
620
|
+
shape: S,
|
|
621
|
+
): LogicalType<z.infer<z.ZodObject<S>>> => {
|
|
622
|
+
const zodSchema = z.object(shape)
|
|
623
|
+
return build({
|
|
624
|
+
logicalType: 'object',
|
|
625
|
+
zod: zodSchema as unknown as ZodSchema<z.infer<z.ZodObject<S>>>,
|
|
626
|
+
parse: (input) => {
|
|
627
|
+
if (input !== null && typeof input === 'object') {
|
|
628
|
+
return input as z.infer<z.ZodObject<S>>
|
|
629
|
+
}
|
|
630
|
+
throw new Error('esperava objeto')
|
|
631
|
+
},
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isLogicalType(value: unknown): value is LogicalType<unknown> {
|
|
636
|
+
return (
|
|
637
|
+
value !== null &&
|
|
638
|
+
typeof value === 'object' &&
|
|
639
|
+
'zod' in value &&
|
|
640
|
+
typeof (value as { zod: unknown }).zod === 'function'
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// =============================================================================
|
|
645
|
+
// t.dict — primeira classe
|
|
646
|
+
// =============================================================================
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Meta de cada chave de um dict. `label` obrigatório; resto livre. `doc` é o
|
|
650
|
+
* entendimento de negócio da chave (o que aquele estado significa, quando se
|
|
651
|
+
* entra/sai) — a fonte rica de consulta rápida; flui pro manifest/lente.
|
|
652
|
+
*/
|
|
653
|
+
export interface DictEntryMeta {
|
|
654
|
+
label: string
|
|
655
|
+
doc?: string
|
|
656
|
+
color?: string
|
|
657
|
+
icon?: string
|
|
658
|
+
order?: number
|
|
659
|
+
[key: string]: unknown
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Option pronta pra UI: combina key + meta da entrada.
|
|
664
|
+
*/
|
|
665
|
+
export type DictOption<K extends string, M extends DictEntryMeta> = M & {
|
|
666
|
+
value: K
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export interface DictType<K extends string, M extends DictEntryMeta> {
|
|
670
|
+
/** Schema Zod equivalente: `z.enum(keys)`. */
|
|
671
|
+
zod(): z.ZodEnum<[K, ...K[]]>
|
|
672
|
+
/** Metadata logical: { logicalType: 'dict', params: { keys, entries } }. */
|
|
673
|
+
meta: LogicalTypeMeta
|
|
674
|
+
/** Lista de chaves na ordem de declaração. */
|
|
675
|
+
keys(): K[]
|
|
676
|
+
/**
|
|
677
|
+
* Label da chave. Segundo arg é placeholder pra i18n — no v1 sempre retorna
|
|
678
|
+
* `entries[key].label` (locale ignorado). Documentado como dívida técnica.
|
|
679
|
+
*/
|
|
680
|
+
labelFor(key: K, locale?: string): string
|
|
681
|
+
/** Meta completa da chave (label + extras). */
|
|
682
|
+
metaFor(key: K): M
|
|
683
|
+
/** Lista pronta pra `<Select options={...} />`: `{ value, ...meta }`. */
|
|
684
|
+
options(): DictOption<K, M>[]
|
|
685
|
+
/** True se `key` está no dict. */
|
|
686
|
+
has(key: string): key is K
|
|
687
|
+
/** Doc de negócio do dicionário inteiro (o que esse vocabulário representa). */
|
|
688
|
+
doc?: string
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Opções do dict: `doc` = o entendimento do vocabulário como um todo. */
|
|
692
|
+
export interface DictOpts {
|
|
693
|
+
doc?: string
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
697
|
+
entries: M,
|
|
698
|
+
opts?: DictOpts,
|
|
699
|
+
): DictType<Extract<keyof M, string>, M[keyof M]> => {
|
|
700
|
+
type K = Extract<keyof M, string>
|
|
701
|
+
const keys = Object.keys(entries) as K[]
|
|
702
|
+
if (keys.length === 0) {
|
|
703
|
+
throw new Error('t.dict precisa de pelo menos uma entrada')
|
|
704
|
+
}
|
|
705
|
+
const zodSchema = z.enum(keys as [K, ...K[]])
|
|
706
|
+
const meta: LogicalTypeMeta = {
|
|
707
|
+
logicalType: 'dict',
|
|
708
|
+
params: { keys, entries, ...(opts?.doc !== undefined ? { doc: opts.doc } : {}) },
|
|
709
|
+
}
|
|
710
|
+
attachLogicalType(zodSchema as unknown as object, meta)
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
zod: () => zodSchema,
|
|
714
|
+
meta,
|
|
715
|
+
...(opts?.doc !== undefined ? { doc: opts.doc } : {}),
|
|
716
|
+
keys: () => [...keys],
|
|
717
|
+
labelFor: (key, _locale) => {
|
|
718
|
+
// _locale reservado pra i18n futuro; no v1 sempre retorna o label cru.
|
|
719
|
+
return (entries[key] as M[keyof M]).label
|
|
720
|
+
},
|
|
721
|
+
metaFor: (key) => entries[key] as M[keyof M],
|
|
722
|
+
options: () =>
|
|
723
|
+
keys.map((k) => ({
|
|
724
|
+
...(entries[k] as M[keyof M]),
|
|
725
|
+
value: k,
|
|
726
|
+
})) as DictOption<K, M[keyof M]>[],
|
|
727
|
+
has: (key: string): key is K =>
|
|
728
|
+
Object.prototype.hasOwnProperty.call(entries, key),
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// =============================================================================
|
|
733
|
+
// Namespace exportado
|
|
734
|
+
// =============================================================================
|
|
735
|
+
|
|
736
|
+
export const t = {
|
|
737
|
+
string,
|
|
738
|
+
text,
|
|
739
|
+
int,
|
|
740
|
+
bigint: bigint_,
|
|
741
|
+
decimal,
|
|
742
|
+
boolean,
|
|
743
|
+
json,
|
|
744
|
+
datetime,
|
|
745
|
+
date,
|
|
746
|
+
time,
|
|
747
|
+
duration,
|
|
748
|
+
timezone,
|
|
749
|
+
email,
|
|
750
|
+
phone,
|
|
751
|
+
url,
|
|
752
|
+
uuid,
|
|
753
|
+
slug,
|
|
754
|
+
money,
|
|
755
|
+
currency,
|
|
756
|
+
country,
|
|
757
|
+
locale: locale_,
|
|
758
|
+
percent,
|
|
759
|
+
markdown,
|
|
760
|
+
html,
|
|
761
|
+
enum: enum_,
|
|
762
|
+
array,
|
|
763
|
+
object,
|
|
764
|
+
dict,
|
|
765
|
+
} as const
|