@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema/openapi — OpenAPI 3.1 spec generator.
|
|
3
|
+
*
|
|
4
|
+
* Varre lista de `ActionDef` e emite documento OpenAPI completo. Usa
|
|
5
|
+
* `zod-to-json-schema` pra converter schemas Zod em JSON Schema; consumers
|
|
6
|
+
* que rodam outro schema lib (ArkType, Valibot) devem passar um converter
|
|
7
|
+
* próprio via `options.toJsonSchema`.
|
|
8
|
+
*
|
|
9
|
+
* Convenção de rota espelha o que `@softize/opus/server` aplica:
|
|
10
|
+
* - simple/form → POST /api/{name-com-slashes}
|
|
11
|
+
* - search/view → GET /api/{name-com-slashes}
|
|
12
|
+
*
|
|
13
|
+
* Endpoints opcionais (/health, /ready, /openapi.json) não são incluídos
|
|
14
|
+
* — apenas actions registradas viram operations OpenAPI.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ActionDef, ErrorSpec } from '../core/index.ts'
|
|
18
|
+
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Tipos OpenAPI 3.1 (subset usado)
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export interface OpenAPIInfo {
|
|
25
|
+
title: string
|
|
26
|
+
version: string
|
|
27
|
+
description?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OpenAPIServer {
|
|
31
|
+
url: string
|
|
32
|
+
description?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OpenAPISpec {
|
|
36
|
+
openapi: '3.1.0'
|
|
37
|
+
info: OpenAPIInfo
|
|
38
|
+
servers?: OpenAPIServer[]
|
|
39
|
+
paths: Record<string, Record<string, OpenAPIOperation>>
|
|
40
|
+
components: { schemas: Record<string, unknown> }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OpenAPIOperation {
|
|
44
|
+
operationId: string
|
|
45
|
+
summary?: string
|
|
46
|
+
description?: string
|
|
47
|
+
tags?: string[]
|
|
48
|
+
deprecated?: boolean
|
|
49
|
+
requestBody?: {
|
|
50
|
+
required: true
|
|
51
|
+
content: { 'application/json': { schema: unknown } }
|
|
52
|
+
}
|
|
53
|
+
parameters?: Array<{
|
|
54
|
+
name: string
|
|
55
|
+
in: 'query'
|
|
56
|
+
required: boolean
|
|
57
|
+
schema: unknown
|
|
58
|
+
}>
|
|
59
|
+
responses: Record<string, OpenAPIResponse>
|
|
60
|
+
security?: Array<Record<string, string[]>>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface OpenAPIResponse {
|
|
64
|
+
description: string
|
|
65
|
+
content?: { 'application/json': { schema: unknown } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Options
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
export interface ToOpenAPIOptions {
|
|
73
|
+
info: OpenAPIInfo
|
|
74
|
+
servers?: OpenAPIServer[]
|
|
75
|
+
apiPrefix?: string
|
|
76
|
+
/**
|
|
77
|
+
* Converter custom (caso schemas não sejam Zod). Default usa
|
|
78
|
+
* `zod-to-json-schema`. Recebe o schema e devolve JSON Schema.
|
|
79
|
+
*/
|
|
80
|
+
toJsonSchema?: (schema: unknown) => unknown
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Generator
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export function toOpenAPISpec(
|
|
88
|
+
actions: ActionDef[],
|
|
89
|
+
options: ToOpenAPIOptions,
|
|
90
|
+
): OpenAPISpec {
|
|
91
|
+
const {
|
|
92
|
+
info,
|
|
93
|
+
servers,
|
|
94
|
+
apiPrefix = '/api',
|
|
95
|
+
toJsonSchema = defaultToJsonSchema,
|
|
96
|
+
} = options
|
|
97
|
+
|
|
98
|
+
const paths: OpenAPISpec['paths'] = {}
|
|
99
|
+
|
|
100
|
+
for (const action of actions) {
|
|
101
|
+
if (action.internal === true) continue
|
|
102
|
+
const { path, method } = routeFor(action, apiPrefix)
|
|
103
|
+
const operation = buildOperation(action, method, toJsonSchema)
|
|
104
|
+
paths[path] = paths[path] ?? {}
|
|
105
|
+
paths[path]![method] = operation
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
openapi: '3.1.0',
|
|
110
|
+
info,
|
|
111
|
+
...(servers !== undefined && servers.length > 0 ? { servers } : {}),
|
|
112
|
+
paths,
|
|
113
|
+
components: { schemas: {} },
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Internos
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
function defaultToJsonSchema(schema: unknown): unknown {
|
|
122
|
+
return zodToJsonSchema(schema as Parameters<typeof zodToJsonSchema>[0], {
|
|
123
|
+
target: 'openApi3',
|
|
124
|
+
$refStrategy: 'none',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function routeFor(
|
|
129
|
+
action: ActionDef,
|
|
130
|
+
apiPrefix: string,
|
|
131
|
+
): { path: string; method: 'get' | 'post' } {
|
|
132
|
+
const segments = action.name.split('.').map(encodeURIComponent)
|
|
133
|
+
const path = `${apiPrefix}/${segments.join('/')}`
|
|
134
|
+
const method = action.kind === 'simple' || action.kind === 'form' ? 'post' : 'get'
|
|
135
|
+
return { path, method }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildOperation(
|
|
139
|
+
action: ActionDef,
|
|
140
|
+
method: 'get' | 'post',
|
|
141
|
+
toJsonSchema: (s: unknown) => unknown,
|
|
142
|
+
): OpenAPIOperation {
|
|
143
|
+
const op: OpenAPIOperation = {
|
|
144
|
+
operationId: action.name,
|
|
145
|
+
responses: buildResponses(action, toJsonSchema),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (action.summary !== undefined) op.summary = action.summary
|
|
149
|
+
if (action.description !== undefined) op.description = action.description
|
|
150
|
+
if (action.tags !== undefined && action.tags.length > 0) op.tags = action.tags
|
|
151
|
+
|
|
152
|
+
const inputSchema = toJsonSchema(action.input)
|
|
153
|
+
|
|
154
|
+
if (method === 'post') {
|
|
155
|
+
op.requestBody = {
|
|
156
|
+
required: true,
|
|
157
|
+
content: { 'application/json': { schema: inputSchema } },
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
const params = buildQueryParams(inputSchema)
|
|
161
|
+
if (params.length > 0) op.parameters = params
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (action.public !== true) {
|
|
165
|
+
op.security = [{ bearerAuth: [] }]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return op
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildResponses(
|
|
172
|
+
action: ActionDef,
|
|
173
|
+
toJsonSchema: (s: unknown) => unknown,
|
|
174
|
+
): Record<string, OpenAPIResponse> {
|
|
175
|
+
const successStatus = String(action.successStatus ?? 200)
|
|
176
|
+
const responses: Record<string, OpenAPIResponse> = {
|
|
177
|
+
[successStatus]: {
|
|
178
|
+
description: 'Success',
|
|
179
|
+
content: {
|
|
180
|
+
'application/json': {
|
|
181
|
+
schema: wrapEnvelope(toJsonSchema(action.output), action.kind),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
'4XX': errorResponse('Client error', action.errors),
|
|
186
|
+
'5XX': errorResponse('Server error'),
|
|
187
|
+
}
|
|
188
|
+
return responses
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function wrapEnvelope(outputSchema: unknown, kind: ActionDef['kind']): unknown {
|
|
192
|
+
// Search retorna Paginated<Output>; outros kinds retornam Output direto.
|
|
193
|
+
const data =
|
|
194
|
+
kind === 'list'
|
|
195
|
+
? {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
items: { type: 'array', items: outputSchema },
|
|
199
|
+
cursor: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
next: { type: ['string', 'null'] },
|
|
203
|
+
prev: { type: ['string', 'null'] },
|
|
204
|
+
},
|
|
205
|
+
required: ['next'],
|
|
206
|
+
},
|
|
207
|
+
total: { type: 'integer' },
|
|
208
|
+
},
|
|
209
|
+
required: ['items', 'cursor'],
|
|
210
|
+
}
|
|
211
|
+
: outputSchema
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
ok: { const: true },
|
|
217
|
+
data,
|
|
218
|
+
meta: metaSchema(),
|
|
219
|
+
},
|
|
220
|
+
required: ['ok', 'data', 'meta'],
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function errorResponse(description: string, errors?: ErrorSpec[]): OpenAPIResponse {
|
|
225
|
+
const errorCodes =
|
|
226
|
+
errors !== undefined && errors.length > 0
|
|
227
|
+
? { enum: errors.map((e) => e.code) }
|
|
228
|
+
: { type: 'string' }
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
description,
|
|
232
|
+
content: {
|
|
233
|
+
'application/json': {
|
|
234
|
+
schema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
ok: { const: false },
|
|
238
|
+
error: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
code: errorCodes,
|
|
242
|
+
category: { type: 'string' },
|
|
243
|
+
message: { type: 'string' },
|
|
244
|
+
severity: { enum: ['warning', 'error', 'fatal'] },
|
|
245
|
+
retriable: { type: 'boolean' },
|
|
246
|
+
},
|
|
247
|
+
required: ['code', 'category', 'message', 'severity', 'retriable'],
|
|
248
|
+
},
|
|
249
|
+
meta: metaSchema(),
|
|
250
|
+
},
|
|
251
|
+
required: ['ok', 'error', 'meta'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function metaSchema(): unknown {
|
|
259
|
+
return {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
actionId: { type: 'string' },
|
|
263
|
+
action: { type: 'string' },
|
|
264
|
+
durationMs: { type: 'number' },
|
|
265
|
+
requestId: { type: 'string' },
|
|
266
|
+
cached: { type: 'boolean' },
|
|
267
|
+
},
|
|
268
|
+
required: ['actionId', 'action', 'durationMs'],
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface QueryParam {
|
|
273
|
+
name: string
|
|
274
|
+
in: 'query'
|
|
275
|
+
required: boolean
|
|
276
|
+
schema: unknown
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildQueryParams(inputSchema: unknown): QueryParam[] {
|
|
280
|
+
// Flatten top-level object properties em query params individuais.
|
|
281
|
+
if (
|
|
282
|
+
typeof inputSchema !== 'object' ||
|
|
283
|
+
inputSchema === null ||
|
|
284
|
+
!('properties' in inputSchema) ||
|
|
285
|
+
typeof (inputSchema as { properties: unknown }).properties !== 'object'
|
|
286
|
+
) {
|
|
287
|
+
return []
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const props = (inputSchema as { properties: Record<string, unknown> }).properties
|
|
291
|
+
const requiredRaw = (inputSchema as { required?: unknown }).required
|
|
292
|
+
const required = new Set<string>(
|
|
293
|
+
Array.isArray(requiredRaw) ? (requiredRaw as string[]) : [],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return Object.entries(props).map(([name, schema]) => ({
|
|
297
|
+
name,
|
|
298
|
+
in: 'query' as const,
|
|
299
|
+
required: required.has(name),
|
|
300
|
+
schema,
|
|
301
|
+
}))
|
|
302
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/schema — scaffold de migration a partir do diff entidade↔banco.
|
|
3
|
+
*
|
|
4
|
+
* Puro: recebe entidades + `DriftFinding[]` (do drift-check) e emite o **código**
|
|
5
|
+
* de uma migration Kysely (`up`/`down`) como string. Não toca banco nem fs.
|
|
6
|
+
*
|
|
7
|
+
* Filosofia (ver `docs/data-layer.md`): best-effort, **humano revisa**.
|
|
8
|
+
* - `missing_table` → `createTable` completo / `dropTable` (caso limpo, gerado)
|
|
9
|
+
* - `missing_column` → `addColumn` / `dropColumn` (caso limpo, gerado)
|
|
10
|
+
* - `nullable_mismatch` / `extra_column` → vira **nota `// TODO`**, nunca um
|
|
11
|
+
* ALTER/DROP automático (arriscado, dialect-específico).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
entityColumns,
|
|
16
|
+
entityTable,
|
|
17
|
+
type DriftFinding,
|
|
18
|
+
type EntityConfig,
|
|
19
|
+
type ResolvedColumn,
|
|
20
|
+
} from './entity.ts'
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// logicalType → tipo de coluna Kysely (Postgres-leaning)
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
const SQL_TYPE: Record<string, string> = {
|
|
27
|
+
uuid: 'uuid',
|
|
28
|
+
int: 'integer',
|
|
29
|
+
bigint: 'bigint',
|
|
30
|
+
money: 'bigint',
|
|
31
|
+
decimal: 'numeric',
|
|
32
|
+
boolean: 'boolean',
|
|
33
|
+
datetime: 'timestamptz',
|
|
34
|
+
date: 'date',
|
|
35
|
+
json: 'jsonb',
|
|
36
|
+
string: 'text',
|
|
37
|
+
text: 'text',
|
|
38
|
+
email: 'text',
|
|
39
|
+
phone: 'text',
|
|
40
|
+
url: 'text',
|
|
41
|
+
slug: 'text',
|
|
42
|
+
currency: 'text',
|
|
43
|
+
country: 'text',
|
|
44
|
+
locale: 'text',
|
|
45
|
+
markdown: 'text',
|
|
46
|
+
html: 'text',
|
|
47
|
+
timezone: 'text',
|
|
48
|
+
time: 'text',
|
|
49
|
+
duration: 'text',
|
|
50
|
+
enum: 'text',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Tipo de coluna Kysely pro tipo lógico. Fallback `text`. */
|
|
54
|
+
export function kyselyColumnType(logicalType: string | undefined): string {
|
|
55
|
+
if (logicalType === undefined) return 'text'
|
|
56
|
+
return SQL_TYPE[logicalType] ?? 'text'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Scaffold
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
export interface ScaffoldResult {
|
|
64
|
+
up: string
|
|
65
|
+
down: string
|
|
66
|
+
/** Divergências que exigem revisão humana (não geradas automaticamente). */
|
|
67
|
+
notes: string[]
|
|
68
|
+
/** Quantidade de statements gerados automaticamente. */
|
|
69
|
+
statements: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function columnLine(col: ResolvedColumn): string {
|
|
73
|
+
const type = kyselyColumnType(col.logicalType)
|
|
74
|
+
const mods: string[] = []
|
|
75
|
+
if (col.column.pk === true) {
|
|
76
|
+
mods.push('primaryKey()', 'notNull()')
|
|
77
|
+
} else if (col.column.nullable !== true) {
|
|
78
|
+
mods.push('notNull()')
|
|
79
|
+
}
|
|
80
|
+
const builder = mods.length > 0 ? `, (c) => c.${mods.join('.')}` : ''
|
|
81
|
+
return ` .addColumn('${col.name}', '${type}'${builder})`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gera o corpo de uma migration a partir do diff. Retorna `null` se não há nada
|
|
86
|
+
* a fazer (sem statements e sem notas).
|
|
87
|
+
*/
|
|
88
|
+
export function scaffoldMigration(
|
|
89
|
+
entities: EntityConfig[],
|
|
90
|
+
findings: DriftFinding[],
|
|
91
|
+
): ScaffoldResult | null {
|
|
92
|
+
const byTable = new Map<string, DriftFinding[]>()
|
|
93
|
+
for (const f of findings) {
|
|
94
|
+
const list = byTable.get(f.table) ?? []
|
|
95
|
+
list.push(f)
|
|
96
|
+
byTable.set(f.table, list)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const up: string[] = []
|
|
100
|
+
const down: string[] = []
|
|
101
|
+
const notes: string[] = []
|
|
102
|
+
let statements = 0
|
|
103
|
+
|
|
104
|
+
for (const entity of entities) {
|
|
105
|
+
const table = entityTable(entity)
|
|
106
|
+
const list = byTable.get(table)
|
|
107
|
+
if (list === undefined) continue
|
|
108
|
+
const cols = entityColumns(entity)
|
|
109
|
+
|
|
110
|
+
if (list.some((f) => f.kind === 'missing_table')) {
|
|
111
|
+
const lines = cols.map(columnLine).join('\n')
|
|
112
|
+
up.push(` await db.schema.createTable('${table}')\n${lines}\n .execute()`)
|
|
113
|
+
down.push(` await db.schema.dropTable('${table}').execute()`)
|
|
114
|
+
statements++
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const missing = new Set(
|
|
119
|
+
list.filter((f) => f.kind === 'missing_column').map((f) => f.column),
|
|
120
|
+
)
|
|
121
|
+
for (const col of cols) {
|
|
122
|
+
if (missing.has(col.name)) {
|
|
123
|
+
up.push(` await db.schema.alterTable('${table}')\n${columnLine(col)}\n .execute()`)
|
|
124
|
+
down.push(
|
|
125
|
+
` await db.schema.alterTable('${table}').dropColumn('${col.name}').execute()`,
|
|
126
|
+
)
|
|
127
|
+
statements++
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const f of list) {
|
|
132
|
+
if (f.kind === 'nullable_mismatch' || f.kind === 'extra_column') {
|
|
133
|
+
notes.push(`${f.table}.${f.column ?? ''}: ${f.kind} — ${f.message}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (statements === 0 && notes.length === 0) return null
|
|
139
|
+
return { up: up.join('\n\n'), down: down.join('\n\n'), notes, statements }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Monta o arquivo `.ts` completo (imports + up/down) a partir do resultado. */
|
|
143
|
+
export function scaffoldFileContent(result: ScaffoldResult): string {
|
|
144
|
+
const todo =
|
|
145
|
+
result.notes.length > 0
|
|
146
|
+
? `\n // TODO (revisar à mão — arriscado/dialect-específico, não gerado):\n` +
|
|
147
|
+
result.notes.map((n) => ` // - ${n}`).join('\n') +
|
|
148
|
+
'\n'
|
|
149
|
+
: ''
|
|
150
|
+
return `import type { Kysely } from 'kysely'
|
|
151
|
+
|
|
152
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
153
|
+
${result.up}${todo}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function down(db: Kysely<any>): Promise<void> {
|
|
157
|
+
${result.down}
|
|
158
|
+
}
|
|
159
|
+
`
|
|
160
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/server/fastify — Fastify driver
|
|
3
|
+
*
|
|
4
|
+
* Implementa `ServerAdapter` em cima do Fastify 5.x. Auto-monta actions
|
|
5
|
+
* como rotas REST, expõe endpoints opcionais (/health, /ready, ...),
|
|
6
|
+
* resolve contexto via AuthAdapter, mapeia errors → HTTP status.
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* import Fastify from 'fastify'
|
|
10
|
+
* import { createRuntime } from '@softize/opus/core'
|
|
11
|
+
* import { fastifyServer } from '@softize/opus/server/fastify'
|
|
12
|
+
*
|
|
13
|
+
* const app = Fastify()
|
|
14
|
+
* const runtime = createRuntime({
|
|
15
|
+
* server: fastifyServer({ app }),
|
|
16
|
+
* auth: ...,
|
|
17
|
+
* })
|
|
18
|
+
* runtime.register([...])
|
|
19
|
+
* await runtime.start()
|
|
20
|
+
* await app.listen({ port: 3000 })
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
|
24
|
+
import type {
|
|
25
|
+
ActionDef,
|
|
26
|
+
ContextBase,
|
|
27
|
+
EndpointSpec,
|
|
28
|
+
RuntimeRef,
|
|
29
|
+
ServerAdapter,
|
|
30
|
+
} from '../../core/index.ts'
|
|
31
|
+
import {
|
|
32
|
+
toOpenAPISpec,
|
|
33
|
+
type OpenAPIInfo,
|
|
34
|
+
type OpenAPIServer,
|
|
35
|
+
} from '../../schema/openapi.ts'
|
|
36
|
+
import {
|
|
37
|
+
httpStatusFor,
|
|
38
|
+
inputSourceFor,
|
|
39
|
+
methodFor,
|
|
40
|
+
pathFor,
|
|
41
|
+
serializeResult,
|
|
42
|
+
shouldMountPublicly,
|
|
43
|
+
successStatusFor,
|
|
44
|
+
} from '../index.ts'
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Options
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
export interface FastifyServerOptions {
|
|
51
|
+
/** Instância Fastify onde rotas serão registradas. */
|
|
52
|
+
app: FastifyInstance
|
|
53
|
+
|
|
54
|
+
/** Prefixo do path. Default '/api'. */
|
|
55
|
+
apiPrefix?: string
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Info do OpenAPI document (title, version, description). Quando ausente,
|
|
59
|
+
* usa um default genérico — sobrescreva pra publicar API decente.
|
|
60
|
+
*/
|
|
61
|
+
openapiInfo?: OpenAPIInfo
|
|
62
|
+
|
|
63
|
+
/** Servers do OpenAPI document. */
|
|
64
|
+
openapiServers?: OpenAPIServer[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Adapter factory
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
export function fastifyServer(options: FastifyServerOptions): ServerAdapter {
|
|
72
|
+
const {
|
|
73
|
+
app,
|
|
74
|
+
apiPrefix = '/api',
|
|
75
|
+
openapiInfo = { title: 'tbdlib API', version: '0.0.0' },
|
|
76
|
+
openapiServers,
|
|
77
|
+
} = options
|
|
78
|
+
let runtime: RuntimeRef | undefined
|
|
79
|
+
const mountedActions: ActionDef[] = []
|
|
80
|
+
|
|
81
|
+
const requireRuntime = (): RuntimeRef => {
|
|
82
|
+
/* v8 ignore next 3 — defensivo; init() é sempre chamado antes de mount/etc */
|
|
83
|
+
if (runtime === undefined) {
|
|
84
|
+
throw new Error('fastifyServer: runtime not initialized (init not called)')
|
|
85
|
+
}
|
|
86
|
+
return runtime
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
name: 'fastify',
|
|
91
|
+
kind: 'server',
|
|
92
|
+
|
|
93
|
+
init(rt) {
|
|
94
|
+
runtime = rt
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
mount(action) {
|
|
98
|
+
if (!shouldMountPublicly(action)) return
|
|
99
|
+
mountedActions.push(action)
|
|
100
|
+
|
|
101
|
+
const method = methodFor(action)
|
|
102
|
+
const path = pathFor(action, apiPrefix)
|
|
103
|
+
const source = inputSourceFor(action)
|
|
104
|
+
|
|
105
|
+
const handler = async (req: FastifyRequest, reply: FastifyReply) => {
|
|
106
|
+
const rt = requireRuntime()
|
|
107
|
+
const ctx = await resolveCtx(rt, req)
|
|
108
|
+
const input = source === 'body' ? req.body : req.query
|
|
109
|
+
|
|
110
|
+
const result = await rt.execute(action.name, input, ctx)
|
|
111
|
+
const status = result.ok
|
|
112
|
+
? successStatusFor(action)
|
|
113
|
+
: httpStatusFor(result.error)
|
|
114
|
+
|
|
115
|
+
reply.code(status).send(serializeResult(result))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (method === 'POST') app.post(path, handler)
|
|
119
|
+
else app.get(path, handler)
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
mountEndpoints(spec) {
|
|
123
|
+
mountHealth(app, spec)
|
|
124
|
+
mountReady(app, spec, requireRuntime)
|
|
125
|
+
mountOpenAPI(app, spec, mountedActions, {
|
|
126
|
+
info: openapiInfo,
|
|
127
|
+
...(openapiServers !== undefined ? { servers: openapiServers } : {}),
|
|
128
|
+
apiPrefix,
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Context resolution
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
async function resolveCtx(
|
|
139
|
+
rt: RuntimeRef,
|
|
140
|
+
req: FastifyRequest,
|
|
141
|
+
): Promise<ContextBase> {
|
|
142
|
+
const base = await resolveAuthBase(rt, req)
|
|
143
|
+
// Fastify sempre popula req.id (auto-gen ou via genReqId opt). Não é optional.
|
|
144
|
+
return {
|
|
145
|
+
...base,
|
|
146
|
+
requestId: req.id,
|
|
147
|
+
provenance: {
|
|
148
|
+
kind: 'http',
|
|
149
|
+
userId: base.user?.id ?? null,
|
|
150
|
+
requestId: req.id,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function resolveAuthBase(
|
|
156
|
+
rt: RuntimeRef,
|
|
157
|
+
req: FastifyRequest,
|
|
158
|
+
): Promise<{ user: ContextBase['user']; tenantId: string | null; can: ContextBase['can'] }> {
|
|
159
|
+
if (rt.auth === undefined) {
|
|
160
|
+
return { user: null, tenantId: null, can: () => false }
|
|
161
|
+
}
|
|
162
|
+
const resolved = await rt.auth.resolveContext(req)
|
|
163
|
+
return {
|
|
164
|
+
user: resolved.user,
|
|
165
|
+
tenantId: resolved.tenantId ?? null,
|
|
166
|
+
can: resolved.can,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Endpoints
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
function mountHealth(app: FastifyInstance, spec: EndpointSpec): void {
|
|
175
|
+
const cfg = spec.health
|
|
176
|
+
if (cfg === false) return
|
|
177
|
+
const path = pathFromSpec(cfg, '/health')
|
|
178
|
+
app.get(path, async () => ({ ok: true }))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function mountReady(
|
|
182
|
+
app: FastifyInstance,
|
|
183
|
+
spec: EndpointSpec,
|
|
184
|
+
getRuntime: () => RuntimeRef,
|
|
185
|
+
): void {
|
|
186
|
+
const cfg = spec.ready
|
|
187
|
+
if (cfg === false) return
|
|
188
|
+
const path = pathFromSpec(cfg, '/ready')
|
|
189
|
+
app.get(path, async (_req, reply) => {
|
|
190
|
+
const status = await getRuntime().healthCheck()
|
|
191
|
+
reply.code(status.ok ? 200 : 503).send(status)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function pathFromSpec(
|
|
196
|
+
cfg: boolean | { path?: string } | undefined,
|
|
197
|
+
fallback: string,
|
|
198
|
+
): string {
|
|
199
|
+
if (typeof cfg === 'object' && cfg.path !== undefined) return cfg.path
|
|
200
|
+
return fallback
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function mountOpenAPI(
|
|
204
|
+
app: FastifyInstance,
|
|
205
|
+
spec: EndpointSpec,
|
|
206
|
+
actions: ActionDef[],
|
|
207
|
+
options: {
|
|
208
|
+
info: OpenAPIInfo
|
|
209
|
+
servers?: OpenAPIServer[]
|
|
210
|
+
apiPrefix: string
|
|
211
|
+
},
|
|
212
|
+
): void {
|
|
213
|
+
const cfg = spec.openapi
|
|
214
|
+
if (cfg === false) return
|
|
215
|
+
const path =
|
|
216
|
+
typeof cfg === 'object' && cfg.path !== undefined ? cfg.path : '/openapi.json'
|
|
217
|
+
app.get(path, async () =>
|
|
218
|
+
toOpenAPISpec(actions, {
|
|
219
|
+
info: options.info,
|
|
220
|
+
...(options.servers !== undefined ? { servers: options.servers } : {}),
|
|
221
|
+
apiPrefix: options.apiPrefix,
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
}
|