@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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema — `defineEntity` + relations + inferência de tipos.
|
|
3
|
+
*
|
|
4
|
+
* Uma entidade é a descrição declarativa de um recorte de dado persistido
|
|
5
|
+
* (campos, relações, índices). Serve de fonte única pra:
|
|
6
|
+
* - tipo TS da linha (`EntityRow`), input de create (`EntityInsert`) e
|
|
7
|
+
* update (`EntityUpdate`);
|
|
8
|
+
* - colunas resolvidas (`entityColumns`) que o data/migration adapter lê;
|
|
9
|
+
* - projeção de view/search (consumido pelas actions com `entity:`).
|
|
10
|
+
*
|
|
11
|
+
* Puro: `defineEntity` valida e devolve o config (identity), sem efeito
|
|
12
|
+
* colateral — igual `defineAction`/`defineDomain`. A materialização (migration,
|
|
13
|
+
* repo tipado) é do adapter, não daqui. Ver §15 do protocolo.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import { error } from '../core/index.ts'
|
|
18
|
+
import type { ColumnMeta, LogicalType } from './drivers/zod.ts'
|
|
19
|
+
|
|
20
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Relations
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export type RelationKind = 'belongsTo' | 'hasOne' | 'hasMany' | 'manyToMany'
|
|
27
|
+
|
|
28
|
+
export interface Relation {
|
|
29
|
+
kind: RelationKind
|
|
30
|
+
/** Nome da entidade alvo (referência por string — resolvida no register). */
|
|
31
|
+
target: string
|
|
32
|
+
/** FK local (belongsTo). */
|
|
33
|
+
from?: string
|
|
34
|
+
/** FK no alvo (hasOne/hasMany). */
|
|
35
|
+
to?: string
|
|
36
|
+
/** Tabela de junção (manyToMany). */
|
|
37
|
+
through?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** FK local → alvo. Ex: `belongsTo('user', { from: 'ownerId' })`. */
|
|
41
|
+
export const belongsTo = (target: string, opts: { from: string }): Relation => ({
|
|
42
|
+
kind: 'belongsTo',
|
|
43
|
+
target,
|
|
44
|
+
from: opts.from,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** FK no alvo, 1:1. Ex: `hasOne('profile', { to: 'userId' })`. */
|
|
48
|
+
export const hasOne = (target: string, opts: { to: string }): Relation => ({
|
|
49
|
+
kind: 'hasOne',
|
|
50
|
+
target,
|
|
51
|
+
to: opts.to,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/** FK no alvo, 1:N. Ex: `hasMany('attachment', { to: 'dealId' })`. */
|
|
55
|
+
export const hasMany = (target: string, opts: { to: string }): Relation => ({
|
|
56
|
+
kind: 'hasMany',
|
|
57
|
+
target,
|
|
58
|
+
to: opts.to,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/** N:N via tabela de junção. Ex: `manyToMany('tag', { through: 'deal_tags' })`. */
|
|
62
|
+
export const manyToMany = (target: string, opts: { through: string }): Relation => ({
|
|
63
|
+
kind: 'manyToMany',
|
|
64
|
+
target,
|
|
65
|
+
through: opts.through,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Config
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
export interface IndexSpec {
|
|
73
|
+
on: string[]
|
|
74
|
+
unique?: boolean
|
|
75
|
+
using?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type EntityFields = Record<string, LogicalType<any>>
|
|
79
|
+
|
|
80
|
+
export interface EntityConfig<F extends EntityFields = EntityFields> {
|
|
81
|
+
/** Nome singular da entidade. `[a-z][a-zA-Z0-9_]*`. */
|
|
82
|
+
name: string
|
|
83
|
+
/** Doc de negócio da entidade — o que ela é/representa. A fonte de
|
|
84
|
+
* entendimento; flui pro manifest/lente (substitui a prosa do domain.md). */
|
|
85
|
+
description?: string
|
|
86
|
+
/** Nome da tabela. Default: plural simples de `name`. */
|
|
87
|
+
table?: string
|
|
88
|
+
fields: F
|
|
89
|
+
relations?: Record<string, Relation>
|
|
90
|
+
indexes?: IndexSpec[]
|
|
91
|
+
/** Injeta `createdAt`/`updatedAt` (geridos pelo runtime). */
|
|
92
|
+
timestamps?: boolean
|
|
93
|
+
/** Injeta `deletedAt`; search/view filtram deletados por default. */
|
|
94
|
+
softDelete?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// Inferência de tipos
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {}
|
|
102
|
+
|
|
103
|
+
type FieldT<X> = X extends LogicalType<infer T> ? T : never
|
|
104
|
+
|
|
105
|
+
type HasPk<F> = true extends {
|
|
106
|
+
[K in keyof F]: F[K] extends { readonly __pk: true } ? true : false
|
|
107
|
+
}[keyof F]
|
|
108
|
+
? true
|
|
109
|
+
: false
|
|
110
|
+
|
|
111
|
+
type HasDefault<X> = X extends { readonly __hasDefault: true } ? true : false
|
|
112
|
+
type IsNullable<X> = null extends FieldT<X> ? true : false
|
|
113
|
+
|
|
114
|
+
type IdPart<F> = HasPk<F> extends true ? {} : { id: string }
|
|
115
|
+
type TimestampPart<E> = E extends { timestamps: true }
|
|
116
|
+
? { createdAt: string; updatedAt: string }
|
|
117
|
+
: {}
|
|
118
|
+
type SoftDeletePart<E> = E extends { softDelete: true } ? { deletedAt: string | null } : {}
|
|
119
|
+
|
|
120
|
+
/** Linha completa da entidade (id auto + campos + timestamps + softDelete). */
|
|
121
|
+
export type EntityRow<E extends EntityConfig> = Simplify<
|
|
122
|
+
IdPart<E['fields']> &
|
|
123
|
+
{ [K in keyof E['fields']]: FieldT<E['fields'][K]> } &
|
|
124
|
+
TimestampPart<E> &
|
|
125
|
+
SoftDeletePart<E>
|
|
126
|
+
>
|
|
127
|
+
|
|
128
|
+
// Insert: sem colunas geradas; campos com default/nullable são opcionais.
|
|
129
|
+
type OptionalInsertKeys<F> = {
|
|
130
|
+
[K in keyof F]: HasDefault<F[K]> extends true
|
|
131
|
+
? K
|
|
132
|
+
: IsNullable<F[K]> extends true
|
|
133
|
+
? K
|
|
134
|
+
: never
|
|
135
|
+
}[keyof F]
|
|
136
|
+
type RequiredInsertKeys<F> = Exclude<keyof F, OptionalInsertKeys<F>>
|
|
137
|
+
|
|
138
|
+
/** Input de create: sem id auto/timestamps; default/nullable opcionais. */
|
|
139
|
+
export type EntityInsert<E extends EntityConfig> = Simplify<
|
|
140
|
+
{ [K in RequiredInsertKeys<E['fields']>]: FieldT<E['fields'][K]> } & {
|
|
141
|
+
[K in OptionalInsertKeys<E['fields']>]?: FieldT<E['fields'][K]>
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
|
|
145
|
+
/** Input de update: partial do insert. */
|
|
146
|
+
export type EntityUpdate<E extends EntityConfig> = Partial<EntityInsert<E>>
|
|
147
|
+
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// defineEntity
|
|
150
|
+
// =============================================================================
|
|
151
|
+
|
|
152
|
+
const ENTITY_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Constrói e valida uma entidade. Em sucesso devolve o próprio config
|
|
156
|
+
* (identity, pra preservar inferência). Erros são `ActionError` com
|
|
157
|
+
* `category: 'internal'`.
|
|
158
|
+
*/
|
|
159
|
+
export function defineEntity<const E extends EntityConfig>(config: E): E {
|
|
160
|
+
validateEntity(config)
|
|
161
|
+
return config
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateEntity(config: EntityConfig): void {
|
|
165
|
+
if (typeof config.name !== 'string' || config.name.length === 0) {
|
|
166
|
+
throw error({
|
|
167
|
+
code: 'entity.invalid_name',
|
|
168
|
+
category: 'internal',
|
|
169
|
+
message: 'Entity "name" deve ser string não-vazia',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
if (!ENTITY_NAME_RE.test(config.name)) {
|
|
173
|
+
throw error({
|
|
174
|
+
code: 'entity.invalid_name_format',
|
|
175
|
+
category: 'internal',
|
|
176
|
+
message: `Entity name "${config.name}" deve casar com [a-z][a-zA-Z0-9_]*`,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const pkFields = Object.entries(config.fields).filter(([, f]) => f.column.pk === true)
|
|
181
|
+
if (pkFields.length > 1) {
|
|
182
|
+
throw error({
|
|
183
|
+
code: 'entity.multiple_pk',
|
|
184
|
+
category: 'internal',
|
|
185
|
+
message: `Entity "${config.name}" tem ${pkFields.length} primary keys (${pkFields
|
|
186
|
+
.map(([k]) => k)
|
|
187
|
+
.join(', ')}); declare no máximo uma`,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (config.relations !== undefined) {
|
|
192
|
+
for (const [relName, rel] of Object.entries(config.relations)) {
|
|
193
|
+
if (typeof rel.target !== 'string' || rel.target.length === 0) {
|
|
194
|
+
throw error({
|
|
195
|
+
code: 'entity.invalid_relation_target',
|
|
196
|
+
category: 'internal',
|
|
197
|
+
message: `Relation "${relName}" em "${config.name}" tem target inválido`,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// Materialização (lida pelo data/migration adapter)
|
|
206
|
+
// =============================================================================
|
|
207
|
+
|
|
208
|
+
export interface ResolvedColumn {
|
|
209
|
+
name: string
|
|
210
|
+
/** `undefined` em colunas injetadas (id/timestamps/softDelete). */
|
|
211
|
+
logicalType?: string
|
|
212
|
+
column: ColumnMeta
|
|
213
|
+
/** `true` em colunas geridas pelo runtime (id auto, timestamps, deletedAt). */
|
|
214
|
+
generated: boolean
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve as colunas da entidade incluindo as injetadas (id auto quando não há
|
|
219
|
+
* pk declarada, timestamps, deletedAt). É o input do migration adapter.
|
|
220
|
+
*/
|
|
221
|
+
export function entityColumns(config: EntityConfig): ResolvedColumn[] {
|
|
222
|
+
const cols: ResolvedColumn[] = []
|
|
223
|
+
const hasPk = Object.values(config.fields).some((f) => f.column.pk === true)
|
|
224
|
+
|
|
225
|
+
if (!hasPk) {
|
|
226
|
+
cols.push({ name: 'id', logicalType: 'uuid', column: { pk: true }, generated: true })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const [name, field] of Object.entries(config.fields)) {
|
|
230
|
+
cols.push({
|
|
231
|
+
name,
|
|
232
|
+
logicalType: field.meta.logicalType,
|
|
233
|
+
column: field.column,
|
|
234
|
+
generated: false,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (config.timestamps === true) {
|
|
239
|
+
cols.push({ name: 'createdAt', logicalType: 'datetime', column: {}, generated: true })
|
|
240
|
+
cols.push({ name: 'updatedAt', logicalType: 'datetime', column: {}, generated: true })
|
|
241
|
+
}
|
|
242
|
+
if (config.softDelete === true) {
|
|
243
|
+
cols.push({
|
|
244
|
+
name: 'deletedAt',
|
|
245
|
+
logicalType: 'datetime',
|
|
246
|
+
column: { nullable: true },
|
|
247
|
+
generated: true,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return cols
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Primary key da entidade. `auto: true` quando é o `id` injetado (nenhum campo
|
|
256
|
+
* declarou `.pk()`) — nesse caso o repo gera o valor (uuid).
|
|
257
|
+
*/
|
|
258
|
+
export function entityPk(config: EntityConfig): { name: string; auto: boolean } {
|
|
259
|
+
for (const [name, field] of Object.entries(config.fields)) {
|
|
260
|
+
if (field.column.pk === true) return { name, auto: false }
|
|
261
|
+
}
|
|
262
|
+
return { name: 'id', auto: true }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Schemas Zod derivados (input/output de actions a partir da entidade)
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
/** Schema Zod da linha completa (output): todos os campos + colunas geradas. */
|
|
270
|
+
export function entityRowSchema(config: EntityConfig): z.ZodObject<z.ZodRawShape> {
|
|
271
|
+
const shape: z.ZodRawShape = {}
|
|
272
|
+
for (const col of entityColumns(config)) {
|
|
273
|
+
const field = config.fields[col.name]
|
|
274
|
+
shape[col.name] =
|
|
275
|
+
field !== undefined
|
|
276
|
+
? field.zod()
|
|
277
|
+
: col.column.nullable === true
|
|
278
|
+
? z.string().nullable()
|
|
279
|
+
: z.string()
|
|
280
|
+
}
|
|
281
|
+
return z.object(shape)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Schema Zod de create: só campos declarados; nullable/default viram opcionais. */
|
|
285
|
+
export function entityInsertSchema(config: EntityConfig): z.ZodObject<z.ZodRawShape> {
|
|
286
|
+
const shape: z.ZodRawShape = {}
|
|
287
|
+
for (const [name, field] of Object.entries(config.fields)) {
|
|
288
|
+
const base = field.zod()
|
|
289
|
+
shape[name] =
|
|
290
|
+
field.column.nullable === true || field.column.hasDefault === true
|
|
291
|
+
? base.optional()
|
|
292
|
+
: base
|
|
293
|
+
}
|
|
294
|
+
return z.object(shape)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Schema Zod de update: partial do insert. */
|
|
298
|
+
export function entityUpdateSchema(config: EntityConfig): z.ZodObject<z.ZodRawShape> {
|
|
299
|
+
return entityInsertSchema(config).partial()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Nome da tabela: `table` explícito ou plural simples de `name`. */
|
|
303
|
+
export function entityTable(config: EntityConfig): string {
|
|
304
|
+
if (config.table !== undefined) return config.table
|
|
305
|
+
const n = config.name
|
|
306
|
+
if (n.endsWith('y')) return `${n.slice(0, -1)}ies`
|
|
307
|
+
if (/(s|x|z|ch|sh)$/.test(n)) return `${n}es`
|
|
308
|
+
return `${n}s`
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* `true` se `value` parece um `EntityConfig`. Usado pra coletar entidades de
|
|
313
|
+
* `domain.models` (que é `unknown`) sem confundir com action/reaction/model.
|
|
314
|
+
* Heurística: tem `name` string + `fields` objeto e **não** tem `kind`
|
|
315
|
+
* (descarta `ActionDef`).
|
|
316
|
+
*/
|
|
317
|
+
export function isEntityConfig(value: unknown): value is EntityConfig {
|
|
318
|
+
if (typeof value !== 'object' || value === null) return false
|
|
319
|
+
const v = value as Record<string, unknown>
|
|
320
|
+
return (
|
|
321
|
+
typeof v.name === 'string' &&
|
|
322
|
+
typeof v.fields === 'object' &&
|
|
323
|
+
v.fields !== null &&
|
|
324
|
+
!('kind' in v)
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// Drift-check (comparador puro — sem Kysely)
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
/** Shape mínimo de coluna pra comparar entidade ↔ banco. */
|
|
333
|
+
export interface ColumnShape {
|
|
334
|
+
name: string
|
|
335
|
+
nullable: boolean
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type DriftKind =
|
|
339
|
+
| 'missing_table'
|
|
340
|
+
| 'missing_column'
|
|
341
|
+
| 'extra_column'
|
|
342
|
+
| 'nullable_mismatch'
|
|
343
|
+
|
|
344
|
+
export interface DriftFinding {
|
|
345
|
+
table: string
|
|
346
|
+
column?: string
|
|
347
|
+
kind: DriftKind
|
|
348
|
+
message: string
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Convenção de nome de coluna. `snake` (default) mapeia camelCase → snake_case
|
|
353
|
+
* (idiomático Postgres); `identity` mantém; ou uma fn custom. Ver `docs/data-layer.md`.
|
|
354
|
+
*/
|
|
355
|
+
export type NamingStrategy = 'snake' | 'identity' | ((name: string) => string)
|
|
356
|
+
|
|
357
|
+
/** camelCase → snake_case. `ownerId` → `owner_id`, `id` → `id`. */
|
|
358
|
+
export function snakeCase(name: string): string {
|
|
359
|
+
return name.replace(/([A-Z])/g, '_$1').toLowerCase()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Resolve a estratégia num mapeador `nome → coluna`. */
|
|
363
|
+
export function resolveNaming(naming: NamingStrategy): (name: string) => string {
|
|
364
|
+
if (naming === 'snake') return snakeCase
|
|
365
|
+
if (naming === 'identity') return (n) => n
|
|
366
|
+
return naming
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Colunas desejadas (do `defineEntity`) no shape de comparação, com os nomes já
|
|
371
|
+
* mapeados pela `naming` (default `snake`) — pra bater com a introspecção crua
|
|
372
|
+
* do banco no drift-check.
|
|
373
|
+
*/
|
|
374
|
+
export function desiredColumns(
|
|
375
|
+
config: EntityConfig,
|
|
376
|
+
naming: NamingStrategy = 'snake',
|
|
377
|
+
): ColumnShape[] {
|
|
378
|
+
const map = resolveNaming(naming)
|
|
379
|
+
return entityColumns(config).map((c) => ({
|
|
380
|
+
name: map(c.name),
|
|
381
|
+
nullable: c.column.nullable === true,
|
|
382
|
+
}))
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Compara o schema desejado (entidade) com o real (banco). Puro: recebe os dois
|
|
387
|
+
* lados já normalizados. `actual: null` significa tabela inexistente.
|
|
388
|
+
*
|
|
389
|
+
* v1: compara presença e nullable. Tipos/defaults/índices/FK ficam pro passo 2.
|
|
390
|
+
*/
|
|
391
|
+
export function diffColumns(
|
|
392
|
+
table: string,
|
|
393
|
+
desired: ColumnShape[],
|
|
394
|
+
actual: ColumnShape[] | null,
|
|
395
|
+
): DriftFinding[] {
|
|
396
|
+
if (actual === null) {
|
|
397
|
+
return [{ table, kind: 'missing_table', message: `Tabela "${table}" não existe` }]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const findings: DriftFinding[] = []
|
|
401
|
+
const actualByName = new Map(actual.map((c) => [c.name, c]))
|
|
402
|
+
const desiredNames = new Set(desired.map((c) => c.name))
|
|
403
|
+
|
|
404
|
+
for (const d of desired) {
|
|
405
|
+
const a = actualByName.get(d.name)
|
|
406
|
+
if (a === undefined) {
|
|
407
|
+
findings.push({
|
|
408
|
+
table,
|
|
409
|
+
column: d.name,
|
|
410
|
+
kind: 'missing_column',
|
|
411
|
+
message: `Coluna "${table}.${d.name}" existe na entidade mas não no banco`,
|
|
412
|
+
})
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
if (a.nullable !== d.nullable) {
|
|
416
|
+
findings.push({
|
|
417
|
+
table,
|
|
418
|
+
column: d.name,
|
|
419
|
+
kind: 'nullable_mismatch',
|
|
420
|
+
message: `Coluna "${table}.${d.name}" deveria ser ${
|
|
421
|
+
d.nullable ? 'NULL-able' : 'NOT NULL'
|
|
422
|
+
} mas está ${a.nullable ? 'NULL-able' : 'NOT NULL'} no banco`,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const a of actual) {
|
|
428
|
+
if (!desiredNames.has(a.name)) {
|
|
429
|
+
findings.push({
|
|
430
|
+
table,
|
|
431
|
+
column: a.name,
|
|
432
|
+
kind: 'extra_column',
|
|
433
|
+
message: `Coluna "${table}.${a.name}" existe no banco mas não na entidade`,
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return findings
|
|
439
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema/format/locale — helpers internos de formatação.
|
|
3
|
+
*
|
|
4
|
+
* Wrappa `Intl.*` com defaults pt-BR. Usado pelas factories de `t.*` pra
|
|
5
|
+
* implementar `.format(value, { locale })` sem repetir boilerplate.
|
|
6
|
+
*
|
|
7
|
+
* Não é parte da superfície pública — consumers acessam via `t.money().format(...)`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LOCALE = 'pt-BR'
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Numbers / decimals / percent
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Formata número decimal seguindo locale. `value` é string canônica
|
|
18
|
+
* (dot como decimal separator) — convertemos pra Number antes do `Intl`.
|
|
19
|
+
*/
|
|
20
|
+
export function formatDecimal(
|
|
21
|
+
value: string,
|
|
22
|
+
locale: string = DEFAULT_LOCALE,
|
|
23
|
+
options?: Intl.NumberFormatOptions,
|
|
24
|
+
): string {
|
|
25
|
+
const n = Number(value)
|
|
26
|
+
return new Intl.NumberFormat(locale, options).format(n)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse string decimal aceitando pt-BR ("1.234,56") ou en ("1234.56").
|
|
31
|
+
* Heurística: se contém vírgula, assume pt-BR (ponto = milhar, vírgula = decimal).
|
|
32
|
+
* Caso contrário, deixa como está. Resultado é string canônica (dot decimal).
|
|
33
|
+
*/
|
|
34
|
+
export function parseDecimal(input: string): string {
|
|
35
|
+
const trimmed = input.trim()
|
|
36
|
+
if (trimmed.includes(',')) {
|
|
37
|
+
// pt-BR: remove pontos (milhar), troca vírgula por ponto.
|
|
38
|
+
return trimmed.replace(/\./g, '').replace(',', '.')
|
|
39
|
+
}
|
|
40
|
+
return trimmed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Money — bigint cents
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Formata bigint cents como moeda no locale.
|
|
49
|
+
* Ex: formatMoney(123456n, 'BRL', 'pt-BR') → "R$ 1.234,56"
|
|
50
|
+
*/
|
|
51
|
+
export function formatMoney(
|
|
52
|
+
cents: bigint,
|
|
53
|
+
currency: string,
|
|
54
|
+
locale: string = DEFAULT_LOCALE,
|
|
55
|
+
): string {
|
|
56
|
+
// bigint → number pra Intl. Pra valores acima de Number.MAX_SAFE_INTEGER
|
|
57
|
+
// perdemos precisão, mas é trade-off aceitável pra formatação de display.
|
|
58
|
+
const value = Number(cents) / 100
|
|
59
|
+
return new Intl.NumberFormat(locale, {
|
|
60
|
+
style: 'currency',
|
|
61
|
+
currency,
|
|
62
|
+
}).format(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse string de moeda em bigint cents. Aceita "R$ 1.234,56", "1234.56",
|
|
67
|
+
* "1,234.56" (en com milhar), "1234,56" (pt-BR), etc.
|
|
68
|
+
* Retorna `null` se input não tem dígitos.
|
|
69
|
+
*/
|
|
70
|
+
export function parseMoney(input: string): bigint | null {
|
|
71
|
+
// Remove símbolos de moeda / espaços / NBSP — sobra dígitos, separadores e sinal.
|
|
72
|
+
const cleaned = input.replace(/[^\d.,\-]/g, '').trim()
|
|
73
|
+
if (cleaned === '' || cleaned === '-') return null
|
|
74
|
+
|
|
75
|
+
const negative = cleaned.startsWith('-')
|
|
76
|
+
const body = negative ? cleaned.slice(1) : cleaned
|
|
77
|
+
|
|
78
|
+
// Determina separador decimal: o último ponto-ou-vírgula é decimal se tiver
|
|
79
|
+
// exatamente 2 dígitos depois. Caso contrário, sem decimal.
|
|
80
|
+
const lastDot = body.lastIndexOf('.')
|
|
81
|
+
const lastComma = body.lastIndexOf(',')
|
|
82
|
+
const lastSep = Math.max(lastDot, lastComma)
|
|
83
|
+
|
|
84
|
+
let intPart: string
|
|
85
|
+
let fracPart: string
|
|
86
|
+
|
|
87
|
+
if (lastSep === -1) {
|
|
88
|
+
intPart = body
|
|
89
|
+
fracPart = ''
|
|
90
|
+
} else {
|
|
91
|
+
const tail = body.slice(lastSep + 1)
|
|
92
|
+
if (tail.length === 2) {
|
|
93
|
+
intPart = body.slice(0, lastSep).replace(/[.,]/g, '')
|
|
94
|
+
fracPart = tail
|
|
95
|
+
} else {
|
|
96
|
+
// Sem decimal — todos os separadores são milhar.
|
|
97
|
+
intPart = body.replace(/[.,]/g, '')
|
|
98
|
+
fracPart = ''
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
intPart = intPart.replace(/\D/g, '')
|
|
103
|
+
fracPart = fracPart.padEnd(2, '0').slice(0, 2)
|
|
104
|
+
|
|
105
|
+
if (intPart === '') intPart = '0'
|
|
106
|
+
|
|
107
|
+
const cents = BigInt(intPart) * 100n + BigInt(fracPart)
|
|
108
|
+
return negative ? -cents : cents
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Datetime / date / time — pt-BR aware
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Formata ISO datetime ("2026-05-28T13:45:00Z") como string humana.
|
|
117
|
+
* pt-BR → "28/05/2026 13:45". Outros locales delegam pra Intl.
|
|
118
|
+
*/
|
|
119
|
+
export function formatDatetime(iso: string, locale: string = DEFAULT_LOCALE): string {
|
|
120
|
+
const d = new Date(iso)
|
|
121
|
+
return new Intl.DateTimeFormat(locale, {
|
|
122
|
+
day: '2-digit',
|
|
123
|
+
month: '2-digit',
|
|
124
|
+
year: 'numeric',
|
|
125
|
+
hour: '2-digit',
|
|
126
|
+
minute: '2-digit',
|
|
127
|
+
hour12: false,
|
|
128
|
+
}).format(d)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Formata date "YYYY-MM-DD" como string humana. pt-BR → "28/05/2026".
|
|
133
|
+
* Constrói o Date em UTC pra evitar drift de timezone que mudaria o dia.
|
|
134
|
+
*/
|
|
135
|
+
export function formatDate(date: string, locale: string = DEFAULT_LOCALE): string {
|
|
136
|
+
const [y, m, d] = date.split('-').map(Number)
|
|
137
|
+
const utc = new Date(Date.UTC(y!, m! - 1, d!))
|
|
138
|
+
return new Intl.DateTimeFormat(locale, {
|
|
139
|
+
day: '2-digit',
|
|
140
|
+
month: '2-digit',
|
|
141
|
+
year: 'numeric',
|
|
142
|
+
timeZone: 'UTC',
|
|
143
|
+
}).format(utc)
|
|
144
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema — driver-agnostic schema utilities.
|
|
3
|
+
*
|
|
4
|
+
* Aqui mora:
|
|
5
|
+
* - Metadata machinery (`attachLogicalType`, `getLogicalType`) — armazena
|
|
6
|
+
* logical type info anexada a qualquer Schema, independente de driver.
|
|
7
|
+
* - Tipo `LogicalTypeMeta` re-exportado de `@softize/opus/core`.
|
|
8
|
+
*
|
|
9
|
+
* Drivers (zod) implementam factories (`t.*`) que usam essa machinery.
|
|
10
|
+
* OpenAPI gen (`./openapi`) consome a metadata pra anotar specs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { LogicalTypeMeta } from '../core/index.ts'
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Metadata mechanism (driver-agnostic)
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
// A implementação MUDOU pro core (a UI lê a meta do contrato e a fronteira SPA
|
|
20
|
+
// só deixa ui tocar ui/lib/core) — re-export mantém a API pública daqui.
|
|
21
|
+
export { attachLogicalType, getLogicalType } from '../core/logical-type.ts'
|
|
22
|
+
|
|
23
|
+
export type { LogicalTypeMeta }
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Entity layer (defineEntity + relations + inferência)
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
defineEntity,
|
|
31
|
+
belongsTo,
|
|
32
|
+
hasOne,
|
|
33
|
+
hasMany,
|
|
34
|
+
manyToMany,
|
|
35
|
+
entityColumns,
|
|
36
|
+
entityTable,
|
|
37
|
+
entityPk,
|
|
38
|
+
entityRowSchema,
|
|
39
|
+
entityInsertSchema,
|
|
40
|
+
entityUpdateSchema,
|
|
41
|
+
desiredColumns,
|
|
42
|
+
diffColumns,
|
|
43
|
+
isEntityConfig,
|
|
44
|
+
snakeCase,
|
|
45
|
+
resolveNaming,
|
|
46
|
+
} from './entity.ts'
|
|
47
|
+
export type {
|
|
48
|
+
EntityConfig,
|
|
49
|
+
EntityFields,
|
|
50
|
+
EntityRow,
|
|
51
|
+
EntityInsert,
|
|
52
|
+
EntityUpdate,
|
|
53
|
+
Relation,
|
|
54
|
+
RelationKind,
|
|
55
|
+
IndexSpec,
|
|
56
|
+
ResolvedColumn,
|
|
57
|
+
ColumnShape,
|
|
58
|
+
DriftKind,
|
|
59
|
+
DriftFinding,
|
|
60
|
+
NamingStrategy,
|
|
61
|
+
} from './entity.ts'
|
|
62
|
+
|
|
63
|
+
// — Scaffold de migration ——————————————————————————————————————————————————————
|
|
64
|
+
export { scaffoldMigration, scaffoldFileContent, kyselyColumnType } from './scaffold.ts'
|
|
65
|
+
export type { ScaffoldResult } from './scaffold.ts'
|