@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/audit — shared helpers/types for audit drivers.
|
|
3
|
+
*
|
|
4
|
+
* Drivers (console, pg) são thin wrappers que implementam AuditSink.
|
|
5
|
+
* Código compartilhado entre drivers mora aqui — hoje, o redator
|
|
6
|
+
* recursivo (`redactDeep`) e a aplicação dele num record (`redactRecord`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AuditRecord } from '../core/index.ts'
|
|
10
|
+
|
|
11
|
+
/** Chaves sensíveis por convenção — o default do `redactDeep`. */
|
|
12
|
+
export const SENSITIVE_KEY_PATTERN = /password|passwd|secret|token|authorization|api[-_]?key|credential/i
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Redator RECURSIVO por nome de chave: em qualquer profundidade (objetos e
|
|
16
|
+
* arrays), valor cuja CHAVE casa o padrão vira '[REDACTED]'. É o complemento do
|
|
17
|
+
* `AuditConfig.redact` por action (dot-paths estáticos): aqui a cobertura é
|
|
18
|
+
* global — `user.create`/`setPassword` não grava senha em claro por esquecimento.
|
|
19
|
+
* Não muta o original; objetos não-planos (Date, Map, class) passam intactos.
|
|
20
|
+
*/
|
|
21
|
+
export function redactDeep(value: unknown, keyPattern: RegExp = SENSITIVE_KEY_PATTERN): unknown {
|
|
22
|
+
const seen = new WeakSet<object>()
|
|
23
|
+
const matches = (key: string): boolean => {
|
|
24
|
+
keyPattern.lastIndex = 0 // padrão /g do chamador não pode ficar stateful
|
|
25
|
+
return keyPattern.test(key)
|
|
26
|
+
}
|
|
27
|
+
const walk = (v: unknown): unknown => {
|
|
28
|
+
if (v === null || typeof v !== 'object') return v
|
|
29
|
+
if (seen.has(v)) return '[CIRCULAR]'
|
|
30
|
+
seen.add(v)
|
|
31
|
+
if (Array.isArray(v)) return v.map(walk)
|
|
32
|
+
const proto: unknown = Object.getPrototypeOf(v)
|
|
33
|
+
if (proto !== Object.prototype && proto !== null) return v
|
|
34
|
+
const out: Record<string, unknown> = {}
|
|
35
|
+
for (const [k, val] of Object.entries(v)) {
|
|
36
|
+
out[k] = matches(k) ? '[REDACTED]' : walk(val)
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
return walk(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Aplica um redator a input/output de um record (o que os drivers persistem). */
|
|
44
|
+
export function redactRecord(
|
|
45
|
+
record: AuditRecord,
|
|
46
|
+
redact: (value: unknown) => unknown,
|
|
47
|
+
): AuditRecord {
|
|
48
|
+
const next: AuditRecord = { ...record, input: redact(record.input) }
|
|
49
|
+
if (record.output !== undefined) next.output = redact(record.output)
|
|
50
|
+
return next
|
|
51
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/auth/better-auth — driver de sessão do better-auth (lado consumidor)
|
|
3
|
+
*
|
|
4
|
+
* Valida a sessão de um IdP (better-auth) encaminhando o cookie da request pro
|
|
5
|
+
* `${baseURL}/api/auth/get-session`. O IdP é a fonte da verdade — este adapter não
|
|
6
|
+
* guarda segredo: os apps confiam no IdP.
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* import { betterAuthSession } from '@softize/opus/auth/better-auth'
|
|
10
|
+
*
|
|
11
|
+
* const auth = betterAuthSession({
|
|
12
|
+
* baseURL: process.env.AUTH_IDP_URL!, // https://auth.example.com
|
|
13
|
+
* can: (user) => (perm) => user.staff === true, // staff-only
|
|
14
|
+
* })
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AuthAdapter, CanFn, User } from '../../core/index.ts'
|
|
18
|
+
|
|
19
|
+
export interface BetterAuthSession {
|
|
20
|
+
user:
|
|
21
|
+
| ({ id: string; email: string; name: string; staff?: boolean } & Record<string, unknown>)
|
|
22
|
+
| null
|
|
23
|
+
session?: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BetterAuthDriverOptions {
|
|
27
|
+
/** Base URL do IdP, ex: `https://auth.example.com`. */
|
|
28
|
+
baseURL: string
|
|
29
|
+
/** Mapeia a sessão do IdP → `User` do core. Default espalha `session.user`. */
|
|
30
|
+
mapUser?: (session: BetterAuthSession) => User
|
|
31
|
+
/** Mapeia a sessão → tenantId. Default null. */
|
|
32
|
+
mapTenant?: (session: BetterAuthSession) => string | null
|
|
33
|
+
/** Constrói o `CanFn`. Default nega tudo (consumer deve plugar). */
|
|
34
|
+
can?: (user: User, session: BetterAuthSession) => CanFn
|
|
35
|
+
/** Timeout do fetch ao IdP (ms). Default 5000. */
|
|
36
|
+
timeoutMs?: number
|
|
37
|
+
/** Nome do adapter (default 'better-auth'). */
|
|
38
|
+
name?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const denyAll: CanFn = () => false
|
|
42
|
+
|
|
43
|
+
export function betterAuthSession(options: BetterAuthDriverOptions): AuthAdapter {
|
|
44
|
+
const { baseURL, mapUser, mapTenant, can, timeoutMs = 5000, name = 'better-auth' } = options
|
|
45
|
+
const base = baseURL.replace(/\/+$/, '')
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name,
|
|
49
|
+
kind: 'auth',
|
|
50
|
+
async resolveContext(req) {
|
|
51
|
+
const headers = forwardHeaders(req)
|
|
52
|
+
if (headers === null) return { user: null, can: denyAll }
|
|
53
|
+
|
|
54
|
+
let session: BetterAuthSession
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${base}/api/auth/get-session`, {
|
|
57
|
+
headers,
|
|
58
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
59
|
+
})
|
|
60
|
+
if (!res.ok) return { user: null, can: denyAll }
|
|
61
|
+
session = (await res.json()) as BetterAuthSession
|
|
62
|
+
} catch {
|
|
63
|
+
// IdP fora do ar / timeout → fail-closed (sem user).
|
|
64
|
+
return { user: null, can: denyAll }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (session === null || session.user === null || session.user === undefined) {
|
|
68
|
+
return { user: null, can: denyAll }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const user = mapUser ? mapUser(session) : ({ ...session.user } as User)
|
|
72
|
+
const tenantId = mapTenant?.(session) ?? null
|
|
73
|
+
return { user, tenantId, can: can ? can(user, session) : denyAll }
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Encaminha `cookie` + `authorization` pro IdP — sem eles o get-session não autentica. */
|
|
79
|
+
function forwardHeaders(req: unknown): Record<string, string> | null {
|
|
80
|
+
if (typeof req !== 'object' || req === null) return null
|
|
81
|
+
const headers = (req as { headers?: Record<string, string | string[] | undefined> }).headers
|
|
82
|
+
if (headers === undefined) return null
|
|
83
|
+
const out: Record<string, string> = {}
|
|
84
|
+
const cookie = pickHeader(headers, 'cookie')
|
|
85
|
+
const authorization = pickHeader(headers, 'authorization')
|
|
86
|
+
if (cookie !== null) out.cookie = cookie
|
|
87
|
+
if (authorization !== null) out.authorization = authorization
|
|
88
|
+
return Object.keys(out).length === 0 ? null : out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pickHeader(
|
|
92
|
+
headers: Record<string, string | string[] | undefined>,
|
|
93
|
+
name: string,
|
|
94
|
+
): string | null {
|
|
95
|
+
const target = name.toLowerCase()
|
|
96
|
+
for (const key of Object.keys(headers)) {
|
|
97
|
+
if (key.toLowerCase() !== target) continue
|
|
98
|
+
const value = headers[key]
|
|
99
|
+
if (value === undefined) return null
|
|
100
|
+
return Array.isArray(value) ? (value[0] ?? null) : value
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/auth/jwt — JWT auth driver
|
|
3
|
+
*
|
|
4
|
+
* Implementa `AuthAdapter` extraindo + validando JWT da request. Padrão
|
|
5
|
+
* comum em backends Node (matchando o que Conversya faz manualmente
|
|
6
|
+
* em `services/*\/middleware/auth.ts`):
|
|
7
|
+
*
|
|
8
|
+
* 1. Extrai token do header `Authorization: Bearer <token>` ou cookie.
|
|
9
|
+
* 2. Valida assinatura + expiração via `jsonwebtoken`.
|
|
10
|
+
* 3. Mapeia payload → `User` + `tenantId` via funções fornecidas.
|
|
11
|
+
* 4. Delega `can()` a um permission engine plugável.
|
|
12
|
+
*
|
|
13
|
+
* Não persiste sessão, não revoga token, não emite — esses são concerns
|
|
14
|
+
* de session store (DB/Redis) que o consumer pluga via `validatePayload`.
|
|
15
|
+
*
|
|
16
|
+
* Uso:
|
|
17
|
+
* import { jwtAuth } from '@softize/opus/auth/jwt'
|
|
18
|
+
*
|
|
19
|
+
* const auth = jwtAuth({
|
|
20
|
+
* secret: process.env.JWT_SECRET!,
|
|
21
|
+
* mapUser: (payload) => ({ id: payload.userId, role: payload.role }),
|
|
22
|
+
* mapTenant: (payload) => payload.tenantId ?? null,
|
|
23
|
+
* can: (user, payload) => (perm) =>
|
|
24
|
+
* hasPermission(user.id, perm, payload),
|
|
25
|
+
* })
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import jwt, { type JwtPayload, type Algorithm } from 'jsonwebtoken'
|
|
29
|
+
const { verify } = jwt
|
|
30
|
+
import type { AuthAdapter, CanFn, User } from '../../core/index.ts'
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Options
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
export interface JwtAuthOptions<P extends JwtPayload = JwtPayload> {
|
|
37
|
+
/** Secret HMAC ou public key. Pode ser string sync ou async resolver. */
|
|
38
|
+
secret: string | Buffer | ((kid?: string) => Promise<string | Buffer> | string | Buffer)
|
|
39
|
+
|
|
40
|
+
/** Algorithms aceitos (default ['HS256']). */
|
|
41
|
+
algorithms?: Algorithm[]
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extração customizada de token. Se ausente, busca em:
|
|
45
|
+
* 1. Header `Authorization: Bearer <token>`
|
|
46
|
+
* 2. Cookie `cookieName` (default 'auth_token')
|
|
47
|
+
* Retorna `null` se não encontrar.
|
|
48
|
+
*/
|
|
49
|
+
tokenExtractor?: (req: unknown) => string | null
|
|
50
|
+
|
|
51
|
+
/** Nome do cookie pra extração default. Default 'auth_token'. */
|
|
52
|
+
cookieName?: string
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validação custom **após** verify do JWT. Útil pra checar sessão
|
|
56
|
+
* em DB (Redis cache, blacklist, etc). Retornar false rejeita.
|
|
57
|
+
*/
|
|
58
|
+
validatePayload?: (payload: P) => boolean | Promise<boolean>
|
|
59
|
+
|
|
60
|
+
/** Mapeia JWT payload pro shape `User` do core. */
|
|
61
|
+
mapUser: (payload: P) => User
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Mapeia JWT payload pra tenantId. Default tenta `payload.tenantId`.
|
|
65
|
+
*/
|
|
66
|
+
mapTenant?: (payload: P) => string | null
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Constrói o `CanFn` baseado em user + payload. Default retorna
|
|
70
|
+
* função que sempre nega (consumer **deve** plugar engine de permissões).
|
|
71
|
+
*/
|
|
72
|
+
can?: (user: User, payload: P) => CanFn
|
|
73
|
+
|
|
74
|
+
/** Nome do adapter (default 'jwt'). */
|
|
75
|
+
name?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Adapter factory
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
export function jwtAuth<P extends JwtPayload = JwtPayload>(
|
|
83
|
+
options: JwtAuthOptions<P>,
|
|
84
|
+
): AuthAdapter {
|
|
85
|
+
const {
|
|
86
|
+
secret,
|
|
87
|
+
algorithms = ['HS256'],
|
|
88
|
+
tokenExtractor,
|
|
89
|
+
cookieName = 'auth_token',
|
|
90
|
+
validatePayload,
|
|
91
|
+
mapUser,
|
|
92
|
+
mapTenant,
|
|
93
|
+
can,
|
|
94
|
+
name = 'jwt',
|
|
95
|
+
} = options
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
kind: 'auth',
|
|
100
|
+
|
|
101
|
+
async resolveContext(req) {
|
|
102
|
+
const token =
|
|
103
|
+
tokenExtractor !== undefined
|
|
104
|
+
? tokenExtractor(req)
|
|
105
|
+
: defaultExtract(req, cookieName)
|
|
106
|
+
|
|
107
|
+
if (token === null) {
|
|
108
|
+
return { user: null, can: denyAll }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolvedSecret =
|
|
112
|
+
typeof secret === 'function' ? await secret() : secret
|
|
113
|
+
|
|
114
|
+
let payload: P
|
|
115
|
+
try {
|
|
116
|
+
payload = verify(token, resolvedSecret, { algorithms }) as P
|
|
117
|
+
} catch {
|
|
118
|
+
return { user: null, can: denyAll }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (validatePayload !== undefined) {
|
|
122
|
+
const valid = await validatePayload(payload)
|
|
123
|
+
if (!valid) return { user: null, can: denyAll }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const user = mapUser(payload)
|
|
127
|
+
const tenantId = mapTenant?.(payload) ?? null
|
|
128
|
+
const userCan = can?.(user, payload) ?? denyAll
|
|
129
|
+
|
|
130
|
+
return { user, tenantId, can: userCan }
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Default token extractor
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
function defaultExtract(req: unknown, cookieName: string): string | null {
|
|
140
|
+
if (typeof req !== 'object' || req === null) return null
|
|
141
|
+
|
|
142
|
+
const headers = (req as { headers?: Record<string, string | string[] | undefined> })
|
|
143
|
+
.headers
|
|
144
|
+
|
|
145
|
+
// Bearer header
|
|
146
|
+
const authHeader = pickHeader(headers, 'authorization')
|
|
147
|
+
if (authHeader !== null && authHeader.toLowerCase().startsWith('bearer ')) {
|
|
148
|
+
return authHeader.slice('Bearer '.length).trim() || null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Cookie
|
|
152
|
+
const cookieHeader = pickHeader(headers, 'cookie')
|
|
153
|
+
if (cookieHeader !== null) {
|
|
154
|
+
const fromCookie = parseCookie(cookieHeader, cookieName)
|
|
155
|
+
if (fromCookie !== null) return fromCookie
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pickHeader(
|
|
162
|
+
headers: Record<string, string | string[] | undefined> | undefined,
|
|
163
|
+
name: string,
|
|
164
|
+
): string | null {
|
|
165
|
+
if (headers === undefined) return null
|
|
166
|
+
const target = name.toLowerCase()
|
|
167
|
+
for (const key of Object.keys(headers)) {
|
|
168
|
+
if (key.toLowerCase() !== target) continue
|
|
169
|
+
const value = headers[key]
|
|
170
|
+
if (value === undefined) return null
|
|
171
|
+
if (Array.isArray(value)) return value[0] ?? null
|
|
172
|
+
return value
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseCookie(header: string, name: string): string | null {
|
|
178
|
+
const parts = header.split(';')
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
const eq = part.indexOf('=')
|
|
181
|
+
if (eq < 0) continue
|
|
182
|
+
const k = part.slice(0, eq).trim()
|
|
183
|
+
if (k === name) return decodeURIComponent(part.slice(eq + 1).trim())
|
|
184
|
+
}
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const denyAll: CanFn = () => false
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/client/fetch — fetch-based client driver.
|
|
3
|
+
*
|
|
4
|
+
* Implementa `ClientAdapter` usando o `fetch` global. Constrói URLs
|
|
5
|
+
* seguindo a mesma convenção do server adapter (path = name → segments,
|
|
6
|
+
* method = POST simple/form, GET search/view).
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* import { fetchClient } from '@softize/opus/client/fetch'
|
|
10
|
+
*
|
|
11
|
+
* const client = fetchClient({
|
|
12
|
+
* baseUrl: 'http://localhost:3001',
|
|
13
|
+
* headers: { 'x-user-id': 'u_alice' },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* const result = await client.run(archiveDeal, { dealId: 'd1' })
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
ActionDef,
|
|
21
|
+
ActionResult,
|
|
22
|
+
ClientAdapter,
|
|
23
|
+
ResultMeta,
|
|
24
|
+
} from '../../core/index.ts'
|
|
25
|
+
import { error } from '../../core/index.ts'
|
|
26
|
+
import { inputSourceFor, methodFor, pathFor } from '../index.ts'
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Options
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export interface FetchClientOptions {
|
|
33
|
+
/** URL base do server. Ex: 'http://localhost:3001'. */
|
|
34
|
+
baseUrl: string
|
|
35
|
+
|
|
36
|
+
/** Prefixo do path (espelha apiPrefix do server adapter). Default '/api'. */
|
|
37
|
+
apiPrefix?: string
|
|
38
|
+
|
|
39
|
+
/** Headers fixos enviados em toda request (ex: auth). */
|
|
40
|
+
headers?: Record<string, string>
|
|
41
|
+
|
|
42
|
+
/** Hook pra injetar headers dinâmicos (ex: token rotativo) por request. */
|
|
43
|
+
beforeRequest?: (init: RequestInit, action: ActionDef) => RequestInit | Promise<RequestInit>
|
|
44
|
+
|
|
45
|
+
/** Override do `fetch` global. Útil pra testes ou edge runtimes. */
|
|
46
|
+
fetch?: typeof globalThis.fetch
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Adapter factory
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
export function fetchClient(options: FetchClientOptions): ClientAdapter {
|
|
54
|
+
const {
|
|
55
|
+
baseUrl,
|
|
56
|
+
apiPrefix = '/api',
|
|
57
|
+
headers: baseHeaders = {},
|
|
58
|
+
beforeRequest,
|
|
59
|
+
fetch: fetchImpl = globalThis.fetch,
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: 'fetch',
|
|
64
|
+
kind: 'client',
|
|
65
|
+
|
|
66
|
+
async run<T>(action: ActionDef, input: unknown): Promise<ActionResult<T>> {
|
|
67
|
+
const method = methodFor(action)
|
|
68
|
+
const source = inputSourceFor(action)
|
|
69
|
+
const path = pathFor(action, apiPrefix)
|
|
70
|
+
const url = buildUrl(baseUrl, path, source === 'query' ? input : undefined)
|
|
71
|
+
|
|
72
|
+
let init: RequestInit = {
|
|
73
|
+
method,
|
|
74
|
+
headers: {
|
|
75
|
+
'content-type': 'application/json',
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
...baseHeaders,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
if (source === 'body') init.body = JSON.stringify(input ?? {})
|
|
81
|
+
|
|
82
|
+
if (beforeRequest !== undefined) {
|
|
83
|
+
init = await beforeRequest(init, action)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let response: Response
|
|
87
|
+
try {
|
|
88
|
+
response = await fetchImpl(url, init)
|
|
89
|
+
} catch (cause) {
|
|
90
|
+
return errorResult<T>(
|
|
91
|
+
action.name,
|
|
92
|
+
error({
|
|
93
|
+
code: 'client.network',
|
|
94
|
+
category: 'dependency',
|
|
95
|
+
message: 'Network request failed',
|
|
96
|
+
cause: String(cause),
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const text = await response.text()
|
|
102
|
+
const parsed = parseJson(text)
|
|
103
|
+
|
|
104
|
+
// Server falou opus? passthrough — inclusive em status != 200
|
|
105
|
+
// (not_found=404, validation=422, conflict=409 etc chegam como envelope).
|
|
106
|
+
if (isActionResultEnvelope(parsed)) {
|
|
107
|
+
return parsed as ActionResult<T>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Daqui pra baixo a resposta NÃO é um envelope opus. Classificamos por
|
|
111
|
+
// status — NUNCA como `internal.unhandled`. `internal.unhandled` é erro de
|
|
112
|
+
// HANDLER do server, e esse só chega via envelope acima. Falha de
|
|
113
|
+
// transporte/gateway é `dependency`, pra UI distinguir "servidor
|
|
114
|
+
// indisponível" (retriable) de "erro interno do app".
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const isServerSide = response.status >= 500
|
|
117
|
+
return errorResult<T>(
|
|
118
|
+
action.name,
|
|
119
|
+
error({
|
|
120
|
+
code: isServerSide ? 'client.unreachable' : 'client.http_error',
|
|
121
|
+
category: 'dependency',
|
|
122
|
+
message: isServerSide
|
|
123
|
+
? `Servidor indisponível (HTTP ${response.status}) em ${action.name}`
|
|
124
|
+
: `HTTP ${response.status} em ${action.name} (resposta não-opus)`,
|
|
125
|
+
retriable: isServerSide,
|
|
126
|
+
meta: { httpStatus: response.status },
|
|
127
|
+
}),
|
|
128
|
+
response.status,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Status 2xx mas corpo não é envelope opus (JSON alheio, HTML, vazio) —
|
|
133
|
+
// contrato quebrado entre client e server, não erro interno do server.
|
|
134
|
+
return errorResult<T>(
|
|
135
|
+
action.name,
|
|
136
|
+
error({
|
|
137
|
+
code: 'client.invalid_response',
|
|
138
|
+
category: 'dependency',
|
|
139
|
+
message: `Resposta de ${action.name} não é um ActionResult opus válido (status ${response.status})`,
|
|
140
|
+
}),
|
|
141
|
+
response.status,
|
|
142
|
+
)
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// Helpers
|
|
149
|
+
// =============================================================================
|
|
150
|
+
|
|
151
|
+
function buildUrl(
|
|
152
|
+
baseUrl: string,
|
|
153
|
+
path: string,
|
|
154
|
+
query?: unknown,
|
|
155
|
+
): string {
|
|
156
|
+
const trimmed = baseUrl.replace(/\/$/, '')
|
|
157
|
+
const url = `${trimmed}${path}`
|
|
158
|
+
if (query === undefined || query === null) return url
|
|
159
|
+
if (typeof query !== 'object') return url
|
|
160
|
+
const params = new URLSearchParams()
|
|
161
|
+
for (const [key, value] of Object.entries(query as Record<string, unknown>)) {
|
|
162
|
+
if (value === undefined || value === null) continue
|
|
163
|
+
params.set(key, String(value))
|
|
164
|
+
}
|
|
165
|
+
const qs = params.toString()
|
|
166
|
+
return qs.length > 0 ? `${url}?${qs}` : url
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Resposta é um `ActionResult` serializado (envelope opus)? */
|
|
170
|
+
function isActionResultEnvelope(value: unknown): boolean {
|
|
171
|
+
return (
|
|
172
|
+
typeof value === 'object' &&
|
|
173
|
+
value !== null &&
|
|
174
|
+
'ok' in value &&
|
|
175
|
+
'meta' in value
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseJson(text: string): unknown {
|
|
180
|
+
if (text.length === 0) return null
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(text)
|
|
183
|
+
} catch {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorResult<T>(
|
|
189
|
+
actionName: string,
|
|
190
|
+
err: ReturnType<typeof error>,
|
|
191
|
+
httpStatus?: number,
|
|
192
|
+
): ActionResult<T> {
|
|
193
|
+
const meta: ResultMeta = {
|
|
194
|
+
actionId: 'client',
|
|
195
|
+
action: actionName,
|
|
196
|
+
durationMs: 0,
|
|
197
|
+
}
|
|
198
|
+
if (httpStatus !== undefined) {
|
|
199
|
+
meta.actionId = `client:${httpStatus}`
|
|
200
|
+
}
|
|
201
|
+
return { ok: false, error: err, meta }
|
|
202
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/client — shared helpers for client drivers.
|
|
3
|
+
*
|
|
4
|
+
* Drivers (fetch, tanstack) usam pra:
|
|
5
|
+
* - construir URL de uma action (mesma convenção do server adapter)
|
|
6
|
+
* - decidir HTTP method + input source (body vs query)
|
|
7
|
+
* - desserializar response → ActionResult
|
|
8
|
+
*
|
|
9
|
+
* Lógica específica de driver (HTTP fetch, cache adapter, etc) vive em
|
|
10
|
+
* `src/drivers/<driver>.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Importa de `@softize/opus/server` pra manter convenção alinhada (mesmo path,
|
|
13
|
+
* mesmo método). Isso garante que client + server sempre falam o mesmo
|
|
14
|
+
* dialeto sem duplicação de regra.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
inputSourceFor,
|
|
19
|
+
methodFor,
|
|
20
|
+
pathFor,
|
|
21
|
+
type HttpMethod,
|
|
22
|
+
} from '../server/index.ts'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tbdlib — `defineAction` factory + type guards
|
|
3
|
+
*
|
|
4
|
+
* `defineAction` é identity function tipada que aceita qualquer variante
|
|
5
|
+
* de `ActionDef` e preserva o subtipo exato pra type narrowing posterior.
|
|
6
|
+
*
|
|
7
|
+
* Validação estrutural acontece no `Runtime.register()`, não aqui. Esta função
|
|
8
|
+
* é puramente sobre **declaração**.
|
|
9
|
+
*
|
|
10
|
+
* Ver §2 do protocolo.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ActionDef,
|
|
15
|
+
FormAction,
|
|
16
|
+
ListAction,
|
|
17
|
+
SimpleAction,
|
|
18
|
+
ViewAction,
|
|
19
|
+
} from './types.ts'
|
|
20
|
+
|
|
21
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// defineAction (overloads per kind para inferência precisa)
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Constrói uma `SimpleAction`. `In` é o lado do fio do schema de input;
|
|
29
|
+
* `ParsedIn` (inferido do output do schema) é o que o handler recebe —
|
|
30
|
+
* ver `ActionBase` em `types.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export function defineAction<In, Out, ParsedIn = In>(
|
|
33
|
+
spec: SimpleAction<In, Out, ParsedIn>,
|
|
34
|
+
): SimpleAction<In, Out, ParsedIn>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Constrói uma `FormAction`. `In` deve ser um objeto cujas chaves casam com
|
|
38
|
+
* a estrutura de `fields`.
|
|
39
|
+
*/
|
|
40
|
+
export function defineAction<In extends Record<string, unknown>, Out, ParsedIn = In>(
|
|
41
|
+
spec: FormAction<In, Out, ParsedIn>,
|
|
42
|
+
): FormAction<In, Out, ParsedIn>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Constrói uma `ListAction`. Output é envelopado em `Paginated<Out>`.
|
|
46
|
+
*/
|
|
47
|
+
export function defineAction<In, Out, ParsedIn = In>(
|
|
48
|
+
spec: ListAction<In, Out, ParsedIn>,
|
|
49
|
+
): ListAction<In, Out, ParsedIn>
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Constrói uma `ViewAction`.
|
|
53
|
+
*/
|
|
54
|
+
export function defineAction<In, Out, ParsedIn = In>(
|
|
55
|
+
spec: ViewAction<In, Out, ParsedIn>,
|
|
56
|
+
): ViewAction<In, Out, ParsedIn>
|
|
57
|
+
|
|
58
|
+
export function defineAction(spec: any): any {
|
|
59
|
+
return spec
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Type guards
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `true` se a action é do kind `simple`.
|
|
68
|
+
*/
|
|
69
|
+
export function isSimpleAction(
|
|
70
|
+
action: ActionDef,
|
|
71
|
+
): action is SimpleAction<any, any> {
|
|
72
|
+
return action.kind === 'simple'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `true` se a action é do kind `form`.
|
|
77
|
+
*/
|
|
78
|
+
export function isFormAction(
|
|
79
|
+
action: ActionDef,
|
|
80
|
+
): action is FormAction<any, any> {
|
|
81
|
+
return action.kind === 'form'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `true` se a action é do kind `search`.
|
|
86
|
+
*/
|
|
87
|
+
export function isListAction(
|
|
88
|
+
action: ActionDef,
|
|
89
|
+
): action is ListAction<any, any> {
|
|
90
|
+
return action.kind === 'list'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `true` se a action é do kind `view`.
|
|
95
|
+
*/
|
|
96
|
+
export function isViewAction(
|
|
97
|
+
action: ActionDef,
|
|
98
|
+
): action is ViewAction<any, any> {
|
|
99
|
+
return action.kind === 'view'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `true` se a action declarou execução em background (apenas `simple` e `form`).
|
|
104
|
+
*/
|
|
105
|
+
export function isBackgroundAction(action: ActionDef): boolean {
|
|
106
|
+
return (
|
|
107
|
+
(action.kind === 'simple' || action.kind === 'form') &&
|
|
108
|
+
action.background?.enabled === true
|
|
109
|
+
)
|
|
110
|
+
}
|