@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
package/src/dsl/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/dsl — Public entry.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
AstNode,
|
|
7
|
+
LiteralNode,
|
|
8
|
+
ArrayNode,
|
|
9
|
+
IdentifierNode,
|
|
10
|
+
MemberAccessNode,
|
|
11
|
+
UnaryOpNode,
|
|
12
|
+
BinaryOpNode,
|
|
13
|
+
BinaryOp,
|
|
14
|
+
InNode,
|
|
15
|
+
BetweenNode,
|
|
16
|
+
LikeNode,
|
|
17
|
+
IsNullNode,
|
|
18
|
+
FunctionCallNode,
|
|
19
|
+
} from './types.ts'
|
|
20
|
+
|
|
21
|
+
export { parseExpression } from './parser.ts'
|
|
22
|
+
export { evalExpression, type EvalContext } from './eval.ts'
|
|
23
|
+
export { compileToKysely, compileWhere, type KyselyBindings } from './kysely.ts'
|
|
24
|
+
export {
|
|
25
|
+
parseLoad,
|
|
26
|
+
resolveLoadArgs,
|
|
27
|
+
type LoadSpec,
|
|
28
|
+
type LoadArg,
|
|
29
|
+
} from './loads.ts'
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/dsl — AST → Kysely where callback.
|
|
3
|
+
*
|
|
4
|
+
* Compila uma AST de expressão em `(eb) => Expression` consumível por
|
|
5
|
+
* `query.where(...)` do Kysely.
|
|
6
|
+
*
|
|
7
|
+
* Pra que isso valha, o DSL precisa estar no escopo da tabela (identifiers
|
|
8
|
+
* raiz são colunas; ex: `status == 'open'` vira `eb('status', '=', 'open')`).
|
|
9
|
+
*
|
|
10
|
+
* Contexto adicional (`user`, `now`, etc) é injetado via `bindings` na hora
|
|
11
|
+
* da compilação — esses identifiers viram literais resolvidos.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
15
|
+
import type { AstNode } from './types.ts'
|
|
16
|
+
import { parseExpression } from './parser.ts'
|
|
17
|
+
|
|
18
|
+
export interface KyselyBindings {
|
|
19
|
+
/**
|
|
20
|
+
* Map de identifier raiz → valor. Quando AST referencia
|
|
21
|
+
* `user.kind`, e bindings.user = { kind: 'employee' }, o compiler
|
|
22
|
+
* inline o valor como literal.
|
|
23
|
+
*/
|
|
24
|
+
[name: string]: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface KyselyEb {
|
|
28
|
+
// Subset usado. Tipos estrito do Kysely são complexos; usamos any onde
|
|
29
|
+
// não acrescentam segurança pro caller (Kysely valida em runtime/typecheck
|
|
30
|
+
// no consumer).
|
|
31
|
+
and: (exprs: any[]) => any
|
|
32
|
+
or: (exprs: any[]) => any
|
|
33
|
+
not: (expr: any) => any
|
|
34
|
+
(column: string, op: string, value: unknown): any
|
|
35
|
+
fn: { coalesce: (...args: any[]) => any }
|
|
36
|
+
lit: (v: unknown) => any
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compila AST → callback Kysely. Identifiers raiz desconhecidos viram
|
|
41
|
+
* referências a coluna (string). Identifiers em bindings viram literais.
|
|
42
|
+
*/
|
|
43
|
+
export function compileToKysely(
|
|
44
|
+
node: AstNode,
|
|
45
|
+
bindings: KyselyBindings = {},
|
|
46
|
+
): (eb: any) => any {
|
|
47
|
+
return (eb: any) => compile(node, eb as KyselyEb, bindings)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Atalho pra repository: parsa a string DSL e devolve callback Kysely.
|
|
52
|
+
*
|
|
53
|
+
* query.where(compileWhere('NOT archived', { user }))
|
|
54
|
+
*
|
|
55
|
+
* Equivale a `compileToKysely(parseExpression(src), bindings)` — existe pra
|
|
56
|
+
* que consumidores não precisem importar `parseExpression` separado.
|
|
57
|
+
*/
|
|
58
|
+
export function compileWhere(
|
|
59
|
+
src: string,
|
|
60
|
+
bindings: KyselyBindings = {},
|
|
61
|
+
): (eb: any) => any {
|
|
62
|
+
return compileToKysely(parseExpression(src), bindings)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compile(node: AstNode, eb: KyselyEb, bindings: KyselyBindings): any {
|
|
66
|
+
switch (node.kind) {
|
|
67
|
+
case 'literal':
|
|
68
|
+
return node.value
|
|
69
|
+
case 'array':
|
|
70
|
+
return node.items.map((i) => compile(i, eb, bindings))
|
|
71
|
+
case 'identifier': {
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(bindings, node.name)) {
|
|
73
|
+
return bindings[node.name]
|
|
74
|
+
}
|
|
75
|
+
// identifier raiz desconhecido → nome de coluna
|
|
76
|
+
return { __column: node.name }
|
|
77
|
+
}
|
|
78
|
+
case 'member': {
|
|
79
|
+
// a.b.c → resolve a partir do binding se existir; senão erro
|
|
80
|
+
const root = compile(node.object, eb, bindings)
|
|
81
|
+
if (
|
|
82
|
+
root !== null &&
|
|
83
|
+
typeof root === 'object' &&
|
|
84
|
+
!('__column' in (root as object))
|
|
85
|
+
) {
|
|
86
|
+
return (root as Record<string, unknown>)[node.property]
|
|
87
|
+
}
|
|
88
|
+
throw new Error(
|
|
89
|
+
`DSL kysely: member access em coluna não suportado ('${node.property}')`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
case 'unary': {
|
|
93
|
+
const operand = compile(node.operand, eb, bindings)
|
|
94
|
+
/* v8 ignore next */
|
|
95
|
+
return node.op === 'NOT' ? eb.not(operand) : -(operand as number)
|
|
96
|
+
}
|
|
97
|
+
case 'binary': {
|
|
98
|
+
if (node.op === 'AND') {
|
|
99
|
+
return eb.and([
|
|
100
|
+
compile(node.left, eb, bindings),
|
|
101
|
+
compile(node.right, eb, bindings),
|
|
102
|
+
])
|
|
103
|
+
}
|
|
104
|
+
if (node.op === 'OR') {
|
|
105
|
+
return eb.or([
|
|
106
|
+
compile(node.left, eb, bindings),
|
|
107
|
+
compile(node.right, eb, bindings),
|
|
108
|
+
])
|
|
109
|
+
}
|
|
110
|
+
const left = compile(node.left, eb, bindings)
|
|
111
|
+
const right = compile(node.right, eb, bindings)
|
|
112
|
+
if (isColumn(left)) {
|
|
113
|
+
return eb(columnOf(left), sqlOp(node.op), right)
|
|
114
|
+
}
|
|
115
|
+
// ambos literais: avaliar direto
|
|
116
|
+
switch (node.op) {
|
|
117
|
+
case '==':
|
|
118
|
+
return left === right
|
|
119
|
+
case '!=':
|
|
120
|
+
return left !== right
|
|
121
|
+
case '<':
|
|
122
|
+
return (left as number) < (right as number)
|
|
123
|
+
case '<=':
|
|
124
|
+
return (left as number) <= (right as number)
|
|
125
|
+
case '>':
|
|
126
|
+
return (left as number) > (right as number)
|
|
127
|
+
case '>=':
|
|
128
|
+
return (left as number) >= (right as number)
|
|
129
|
+
case '+':
|
|
130
|
+
return (left as number) + (right as number)
|
|
131
|
+
case '-':
|
|
132
|
+
return (left as number) - (right as number)
|
|
133
|
+
case '*':
|
|
134
|
+
return (left as number) * (right as number)
|
|
135
|
+
case '/':
|
|
136
|
+
return (left as number) / (right as number)
|
|
137
|
+
/* v8 ignore next 2 */
|
|
138
|
+
default:
|
|
139
|
+
throw new Error(`DSL kysely: unhandled binary ${node.op as string}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
case 'in': {
|
|
143
|
+
const subj = compile(node.left, eb, bindings)
|
|
144
|
+
const arr = compile(node.values, eb, bindings) as unknown[]
|
|
145
|
+
if (isColumn(subj)) {
|
|
146
|
+
return eb(columnOf(subj), node.negated ? 'not in' : 'in', arr)
|
|
147
|
+
}
|
|
148
|
+
const has = arr.includes(subj)
|
|
149
|
+
return node.negated ? !has : has
|
|
150
|
+
}
|
|
151
|
+
case 'between': {
|
|
152
|
+
const subj = compile(node.subject, eb, bindings)
|
|
153
|
+
const lo = compile(node.low, eb, bindings)
|
|
154
|
+
const hi = compile(node.high, eb, bindings)
|
|
155
|
+
if (isColumn(subj)) {
|
|
156
|
+
return eb.and([
|
|
157
|
+
eb(columnOf(subj), node.negated ? '<' : '>=', lo),
|
|
158
|
+
eb(columnOf(subj), node.negated ? '>' : '<=', hi),
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
const within = (subj as number) >= (lo as number) && (subj as number) <= (hi as number)
|
|
162
|
+
return node.negated ? !within : within
|
|
163
|
+
}
|
|
164
|
+
case 'like': {
|
|
165
|
+
const subj = compile(node.subject, eb, bindings)
|
|
166
|
+
const pat = compile(node.pattern, eb, bindings)
|
|
167
|
+
if (isColumn(subj)) {
|
|
168
|
+
const op = node.caseInsensitive ? 'ilike' : 'like'
|
|
169
|
+
return eb(columnOf(subj), node.negated ? `not ${op}` : op, pat)
|
|
170
|
+
}
|
|
171
|
+
throw new Error('DSL kysely: LIKE em literal não suportado')
|
|
172
|
+
}
|
|
173
|
+
case 'isNull': {
|
|
174
|
+
const subj = compile(node.subject, eb, bindings)
|
|
175
|
+
if (isColumn(subj)) {
|
|
176
|
+
return eb(columnOf(subj), node.negated ? 'is not' : 'is', null)
|
|
177
|
+
}
|
|
178
|
+
/* v8 ignore next */
|
|
179
|
+
const isNullish = subj === null || subj === undefined
|
|
180
|
+
return node.negated ? !isNullish : isNullish
|
|
181
|
+
}
|
|
182
|
+
case 'call': {
|
|
183
|
+
const args = node.args.map((a) => compile(a, eb, bindings))
|
|
184
|
+
switch (node.name) {
|
|
185
|
+
case 'NOW':
|
|
186
|
+
return new Date().toISOString()
|
|
187
|
+
case 'TODAY':
|
|
188
|
+
return new Date().toISOString().slice(0, 10)
|
|
189
|
+
case 'COALESCE':
|
|
190
|
+
return eb.fn.coalesce(...args)
|
|
191
|
+
case 'LOWER':
|
|
192
|
+
case 'UPPER':
|
|
193
|
+
case 'LENGTH':
|
|
194
|
+
throw new Error(
|
|
195
|
+
`DSL kysely: function '${node.name}' precisa de mapping de coluna explícito (use eb.fn)`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`DSL kysely: unknown function '${node.name}'`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isColumn(v: unknown): v is { __column: string } {
|
|
204
|
+
return (
|
|
205
|
+
typeof v === 'object' &&
|
|
206
|
+
v !== null &&
|
|
207
|
+
'__column' in (v as object) &&
|
|
208
|
+
typeof (v as { __column: unknown }).__column === 'string'
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function columnOf(v: { __column: string }): string {
|
|
213
|
+
return v.__column
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sqlOp(op: string): string {
|
|
217
|
+
switch (op) {
|
|
218
|
+
case '==':
|
|
219
|
+
return '='
|
|
220
|
+
case '!=':
|
|
221
|
+
return '!='
|
|
222
|
+
case '<':
|
|
223
|
+
case '<=':
|
|
224
|
+
case '>':
|
|
225
|
+
case '>=':
|
|
226
|
+
return op
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`DSL kysely: op ${op} não tem SQL equivalente`)
|
|
229
|
+
}
|
|
230
|
+
}
|
package/src/dsl/loads.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/dsl — Loads parser.
|
|
3
|
+
*
|
|
4
|
+
* Sintaxe simples (não usa o parser de expressão):
|
|
5
|
+
*
|
|
6
|
+
* ticket(:id)
|
|
7
|
+
* shipment(:shipmentId)
|
|
8
|
+
* customer(:customerId, status="active")
|
|
9
|
+
*
|
|
10
|
+
* Resolve em { entity, args: { paramKey: inputKey | literalValue } }.
|
|
11
|
+
* Quem executa loads é o runtime (mapeia entity → repository.findX, passa
|
|
12
|
+
* args resolvidos contra input).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface LoadSpec {
|
|
16
|
+
entity: string
|
|
17
|
+
/**
|
|
18
|
+
* Mapeia parâmetro → fonte. Se string começa com ':', é referência ao
|
|
19
|
+
* input do action (`:id` → `input.id`). Senão é literal.
|
|
20
|
+
*/
|
|
21
|
+
args: Record<string, LoadArg>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LoadArg =
|
|
25
|
+
| { source: 'input'; key: string }
|
|
26
|
+
| { source: 'literal'; value: string | number | boolean | null }
|
|
27
|
+
|
|
28
|
+
const LOAD_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)$/
|
|
29
|
+
|
|
30
|
+
export function parseLoad(src: string): LoadSpec {
|
|
31
|
+
const trimmed = src.trim()
|
|
32
|
+
const m = LOAD_RE.exec(trimmed)
|
|
33
|
+
if (m === null) {
|
|
34
|
+
throw new Error(`DSL loads: invalid syntax '${src}'`)
|
|
35
|
+
}
|
|
36
|
+
const entity = m[1]!
|
|
37
|
+
const argsRaw = m[2]!.trim()
|
|
38
|
+
const args: Record<string, LoadArg> = {}
|
|
39
|
+
if (argsRaw.length > 0) {
|
|
40
|
+
const parts = splitArgs(argsRaw)
|
|
41
|
+
let idx = 0
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
const eq = part.indexOf('=')
|
|
44
|
+
if (eq === -1) {
|
|
45
|
+
// forma posicional: `:id` ou `"valor"` — chave é o primeiro arg = `id`
|
|
46
|
+
const value = parseArgValue(part.trim())
|
|
47
|
+
const key = value.source === 'input' ? value.key : `arg${idx}`
|
|
48
|
+
args[key] = value
|
|
49
|
+
} else {
|
|
50
|
+
const key = part.slice(0, eq).trim()
|
|
51
|
+
const value = parseArgValue(part.slice(eq + 1).trim())
|
|
52
|
+
args[key] = value
|
|
53
|
+
}
|
|
54
|
+
idx += 1
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { entity, args }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function splitArgs(src: string): string[] {
|
|
61
|
+
const parts: string[] = []
|
|
62
|
+
let depth = 0
|
|
63
|
+
let inStr: '"' | "'" | null = null
|
|
64
|
+
let buf = ''
|
|
65
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
66
|
+
const c = src[i]!
|
|
67
|
+
if (inStr !== null) {
|
|
68
|
+
buf += c
|
|
69
|
+
if (c === inStr) inStr = null
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (c === '"' || c === "'") {
|
|
73
|
+
inStr = c
|
|
74
|
+
buf += c
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (c === '(' || c === '[') depth += 1
|
|
78
|
+
if (c === ')' || c === ']') depth -= 1
|
|
79
|
+
if (c === ',' && depth === 0) {
|
|
80
|
+
parts.push(buf)
|
|
81
|
+
buf = ''
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
buf += c
|
|
85
|
+
}
|
|
86
|
+
if (buf.trim().length > 0) parts.push(buf)
|
|
87
|
+
return parts
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseArgValue(raw: string): LoadArg {
|
|
91
|
+
if (raw.startsWith(':')) {
|
|
92
|
+
return { source: 'input', key: raw.slice(1) }
|
|
93
|
+
}
|
|
94
|
+
if (raw.startsWith('"') || raw.startsWith("'")) {
|
|
95
|
+
return { source: 'literal', value: raw.slice(1, -1) }
|
|
96
|
+
}
|
|
97
|
+
if (raw === 'true') return { source: 'literal', value: true }
|
|
98
|
+
if (raw === 'false') return { source: 'literal', value: false }
|
|
99
|
+
if (raw === 'null') return { source: 'literal', value: null }
|
|
100
|
+
const n = Number(raw)
|
|
101
|
+
if (!Number.isNaN(n)) return { source: 'literal', value: n }
|
|
102
|
+
// Sem aspas — trata como string crua
|
|
103
|
+
return { source: 'literal', value: raw }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve um LoadSpec contra `input` em args concretos.
|
|
108
|
+
* Devolve `Record<string, any>` pronto pra passar pro repository.findBy.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveLoadArgs(
|
|
111
|
+
spec: LoadSpec,
|
|
112
|
+
input: Record<string, unknown>,
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
const resolved: Record<string, unknown> = {}
|
|
115
|
+
for (const [k, arg] of Object.entries(spec.args)) {
|
|
116
|
+
if (arg.source === 'input') {
|
|
117
|
+
resolved[k] = input[arg.key]
|
|
118
|
+
} else {
|
|
119
|
+
resolved[k] = arg.value
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return resolved
|
|
123
|
+
}
|