@evertonferreira96/agents 0.0.0-stage → 0.1.1
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/.claude/agents/api.md +45 -0
- package/.claude/agents/app.md +41 -0
- package/.claude/agents/architect.md +49 -0
- package/.claude/agents/infra.md +27 -0
- package/.claude/agents/product.md +58 -0
- package/.claude/agents/quick.md +33 -0
- package/.claude/agents/security.md +39 -0
- package/.claude/agents/ui.md +54 -0
- package/.claude/agents/verifier.md +56 -0
- package/.claude/agents/web.md +41 -0
- package/.claude/hooks/config-update-check.mjs +14 -0
- package/.claude/hooks/context-budget.mjs +51 -0
- package/.claude/hooks/graphify-update.sh +16 -0
- package/.claude/hooks/guard-bash.sh +64 -0
- package/.claude/hooks/orchestrator-guard.sh +49 -0
- package/.claude/hooks/post-compact.sh +26 -0
- package/.claude/hooks/readonly-guard.sh +54 -0
- package/.claude/hooks/stop-verify.sh +52 -0
- package/.claude/rules/api.md +21 -0
- package/.claude/rules/db.md +9 -0
- package/.claude/rules/delivery-diagram.md +16 -0
- package/.claude/rules/git-workflow.md +34 -0
- package/.claude/rules/graphify.md +3 -0
- package/.claude/rules/mobile.md +22 -0
- package/.claude/rules/tickets.md +9 -0
- package/.claude/rules/ui.md +14 -0
- package/.claude/rules/web.md +27 -0
- package/.claude/settings.json +114 -0
- package/.claude/skills/animate/RECIPES.md +324 -0
- package/.claude/skills/animate/SKILL.md +208 -0
- package/.claude/skills/animate/agents/openai.yaml +5 -0
- package/.claude/skills/animate-expo/RECIPES.md +385 -0
- package/.claude/skills/animate-expo/SKILL.md +264 -0
- package/.claude/skills/animate-expo/agents/openai.yaml +5 -0
- package/.claude/skills/animation-performance/SKILL.md +223 -0
- package/.claude/skills/animation-performance/agents/openai.yaml +5 -0
- package/.claude/skills/apple-design/SKILL.md +291 -0
- package/.claude/skills/apple-design/agents/openai.yaml +5 -0
- package/.claude/skills/arch-api/SKILL.md +291 -0
- package/.claude/skills/arch-api/agents/openai.yaml +3 -0
- package/.claude/skills/arch-api/references/common.md +174 -0
- package/.claude/skills/arch-api/references/contract.md +289 -0
- package/.claude/skills/arch-api/references/domain.md +301 -0
- package/.claude/skills/arch-api/references/endpoints.md +184 -0
- package/.claude/skills/arch-api/references/review.md +228 -0
- package/.claude/skills/arch-api/references/security.md +151 -0
- package/.claude/skills/arch-app/SKILL.md +278 -0
- package/.claude/skills/arch-app/agents/openai.yaml +3 -0
- package/.claude/skills/arch-app/references/navigation-session.md +257 -0
- package/.claude/skills/arch-app/references/review.md +155 -0
- package/.claude/skills/arch-app/references/screens.md +214 -0
- package/.claude/skills/arch-app/references/services.md +301 -0
- package/.claude/skills/arch-app/references/ui-mobile.md +124 -0
- package/.claude/skills/arch-db/SKILL.md +181 -0
- package/.claude/skills/arch-db/agents/openai.yaml +3 -0
- package/.claude/skills/arch-db/references/indexes-queries.md +251 -0
- package/.claude/skills/arch-db/references/migrations.md +191 -0
- package/.claude/skills/arch-db/references/modeling.md +214 -0
- package/.claude/skills/arch-db/references/mysql.md +262 -0
- package/.claude/skills/arch-db/references/pooling-ops.md +112 -0
- package/.claude/skills/arch-db/references/review.md +143 -0
- package/.claude/skills/arch-db/references/transactions-locks.md +141 -0
- package/.claude/skills/arch-web/SKILL.md +307 -0
- package/.claude/skills/arch-web/agents/openai.yaml +3 -0
- package/.claude/skills/arch-web/references/auth-session-env.md +165 -0
- package/.claude/skills/arch-web/references/blocks.md +198 -0
- package/.claude/skills/arch-web/references/cache.md +124 -0
- package/.claude/skills/arch-web/references/review.md +142 -0
- package/.claude/skills/arch-web/references/services.md +297 -0
- package/.claude/skills/arch-web/references/tests.md +159 -0
- package/.claude/skills/arch-web/references/ui-forms.md +143 -0
- package/.claude/skills/break-ui/CATALOG.md +135 -0
- package/.claude/skills/break-ui/SKILL.md +176 -0
- package/.claude/skills/break-ui/agents/openai.yaml +5 -0
- package/.claude/skills/brief/SKILL.md +91 -0
- package/.claude/skills/brief/agents/openai.yaml +3 -0
- package/.claude/skills/diagnosing-bugs/SKILL.md +138 -0
- package/.claude/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/.claude/skills/emil-design-eng/SKILL.md +675 -0
- package/.claude/skills/emil-design-eng/agents/openai.yaml +5 -0
- package/.claude/skills/find-animation-opportunities/SKILL.md +141 -0
- package/.claude/skills/find-animation-opportunities/agents/openai.yaml +5 -0
- package/.claude/skills/graphify/.graphify_version +1 -0
- package/.claude/skills/graphify/SKILL.md +742 -0
- package/.claude/skills/graphify/agents/openai.yaml +3 -0
- package/.claude/skills/graphify/references/add-watch.md +56 -0
- package/.claude/skills/graphify/references/exports.md +91 -0
- package/.claude/skills/graphify/references/extraction-spec.md +70 -0
- package/.claude/skills/graphify/references/github-and-merge.md +46 -0
- package/.claude/skills/graphify/references/hooks.md +33 -0
- package/.claude/skills/graphify/references/query.md +317 -0
- package/.claude/skills/graphify/references/transcribe.md +52 -0
- package/.claude/skills/graphify/references/update.md +221 -0
- package/.claude/skills/grill-me/SKILL.md +7 -0
- package/.claude/skills/grill-me/agents/openai.yaml +5 -0
- package/.claude/skills/grill-with-docs/SKILL.md +64 -0
- package/.claude/skills/grill-with-docs/agents/openai.yaml +5 -0
- package/.claude/skills/grilling/SKILL.md +30 -0
- package/.claude/skills/grilling/agents/openai.yaml +3 -0
- package/.claude/skills/handoff/SKILL.md +28 -0
- package/.claude/skills/handoff/agents/openai.yaml +5 -0
- package/.claude/skills/impeccable/SKILL.md +89 -0
- package/.claude/skills/impeccable/agents/openai.yaml +5 -0
- package/.claude/skills/impeccable/reference/adapt.md +318 -0
- package/.claude/skills/impeccable/reference/adapt.native.md +58 -0
- package/.claude/skills/impeccable/reference/android.md +46 -0
- package/.claude/skills/impeccable/reference/animate.md +89 -0
- package/.claude/skills/impeccable/reference/audit.md +137 -0
- package/.claude/skills/impeccable/reference/audit.native.md +139 -0
- package/.claude/skills/impeccable/reference/bolder.md +33 -0
- package/.claude/skills/impeccable/reference/clarify.md +94 -0
- package/.claude/skills/impeccable/reference/colorize.md +86 -0
- package/.claude/skills/impeccable/reference/component-review.md +63 -0
- package/.claude/skills/impeccable/reference/craft-floor.md +44 -0
- package/.claude/skills/impeccable/reference/craft.md +5 -0
- package/.claude/skills/impeccable/reference/critique.md +809 -0
- package/.claude/skills/impeccable/reference/degraded/asset-producer.md +42 -0
- package/.claude/skills/impeccable/reference/degraded/documenter.md +24 -0
- package/.claude/skills/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/.claude/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/.claude/skills/impeccable/reference/delight.md +70 -0
- package/.claude/skills/impeccable/reference/distill.md +111 -0
- package/.claude/skills/impeccable/reference/doctor.md +54 -0
- package/.claude/skills/impeccable/reference/document.md +416 -0
- package/.claude/skills/impeccable/reference/extract.md +69 -0
- package/.claude/skills/impeccable/reference/generate.md +101 -0
- package/.claude/skills/impeccable/reference/harden.md +345 -0
- package/.claude/skills/impeccable/reference/hooks.md +113 -0
- package/.claude/skills/impeccable/reference/init.md +131 -0
- package/.claude/skills/impeccable/reference/ios.md +51 -0
- package/.claude/skills/impeccable/reference/layout.md +84 -0
- package/.claude/skills/impeccable/reference/live-setup.md +104 -0
- package/.claude/skills/impeccable/reference/live.md +325 -0
- package/.claude/skills/impeccable/reference/mode-operate.md +21 -0
- package/.claude/skills/impeccable/reference/mode-persuade.md +19 -0
- package/.claude/skills/impeccable/reference/mode-read.md +21 -0
- package/.claude/skills/impeccable/reference/new-work.md +154 -0
- package/.claude/skills/impeccable/reference/onboard.md +234 -0
- package/.claude/skills/impeccable/reference/operate.md +61 -0
- package/.claude/skills/impeccable/reference/optimize.md +258 -0
- package/.claude/skills/impeccable/reference/overdrive.md +127 -0
- package/.claude/skills/impeccable/reference/polish.md +105 -0
- package/.claude/skills/impeccable/reference/quieter.md +99 -0
- package/.claude/skills/impeccable/reference/region-map.md +26 -0
- package/.claude/skills/impeccable/reference/routing.md +24 -0
- package/.claude/skills/impeccable/reference/shape.md +59 -0
- package/.claude/skills/impeccable/reference/typeset.md +80 -0
- package/.claude/skills/impeccable/reference/visualize.md +47 -0
- package/.claude/skills/impeccable/scripts/VERSION +1 -0
- package/.claude/skills/impeccable/scripts/command-metadata.json +98 -0
- package/.claude/skills/impeccable/scripts/data/font-index-failures.json +121 -0
- package/.claude/skills/impeccable/scripts/data/font-index.json +1 -0
- package/.claude/skills/impeccable/scripts/impeccable +206 -0
- package/.claude/skills/impeccable/scripts/impeccable.cmd +214 -0
- package/.claude/skills/impeccable/scripts/live-browser-dom.js +281 -0
- package/.claude/skills/impeccable/scripts/live-browser-ignores.js +242 -0
- package/.claude/skills/impeccable/scripts/live-browser-session.js +148 -0
- package/.claude/skills/impeccable/scripts/live-browser.js +13532 -0
- package/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.claude/skills/improve-animations/AUDIT.md +115 -0
- package/.claude/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/.claude/skills/improve-animations/SKILL.md +110 -0
- package/.claude/skills/improve-animations/agents/openai.yaml +5 -0
- package/.claude/skills/mobile-native/SKILL.md +311 -0
- package/.claude/skills/mobile-native/agents/openai.yaml +3 -0
- package/.claude/skills/orchestrate/SKILL.md +181 -0
- package/.claude/skills/orchestrate/agents/openai.yaml +3 -0
- package/.claude/skills/pick-ui-library/SKILL.md +85 -0
- package/.claude/skills/pick-ui-library/agents/openai.yaml +5 -0
- package/.claude/skills/ponytail/SKILL.md +52 -0
- package/.claude/skills/ponytail/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-audit/SKILL.md +103 -0
- package/.claude/skills/ponytail-audit/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-debt/SKILL.md +44 -0
- package/.claude/skills/ponytail-debt/agents/openai.yaml +3 -0
- package/.claude/skills/ponytail-review/SKILL.md +101 -0
- package/.claude/skills/ponytail-review/agents/openai.yaml +3 -0
- package/.claude/skills/postgres-best-practices/SKILL.md +42 -0
- package/.claude/skills/postgres-best-practices/agents/openai.yaml +3 -0
- package/.claude/skills/postgres-best-practices/references/backup-restore.md +372 -0
- package/.claude/skills/postgres-best-practices/references/bulk-loading.md +315 -0
- package/.claude/skills/postgres-best-practices/references/connection-pooling.md +310 -0
- package/.claude/skills/postgres-best-practices/references/hot-standby.md +221 -0
- package/.claude/skills/postgres-best-practices/references/indexing.md +318 -0
- package/.claude/skills/postgres-best-practices/references/logical-replication.md +610 -0
- package/.claude/skills/postgres-best-practices/references/major-version-upgrades.md +334 -0
- package/.claude/skills/postgres-best-practices/references/performance-diagnostics.md +553 -0
- package/.claude/skills/postgres-best-practices/references/query-optimization.md +294 -0
- package/.claude/skills/postgres-best-practices/references/query-patterns.md +566 -0
- package/.claude/skills/postgres-best-practices/references/schema-design.md +362 -0
- package/.claude/skills/postgres-best-practices/references/security-roles.md +489 -0
- package/.claude/skills/postgres-best-practices/references/transaction-isolation.md +344 -0
- package/.claude/skills/project-docs/SKILL.md +330 -0
- package/.claude/skills/project-docs/agents/openai.yaml +3 -0
- package/.claude/skills/prototype/PICKER.md +197 -0
- package/.claude/skills/prototype/SKILL.md +98 -0
- package/.claude/skills/prototype/agents/openai.yaml +5 -0
- package/.claude/skills/retro/SKILL.md +43 -0
- package/.claude/skills/retro/agents/openai.yaml +5 -0
- package/.claude/skills/review/SKILL.md +74 -0
- package/.claude/skills/review/agents/openai.yaml +3 -0
- package/.claude/skills/review/references/smells.md +23 -0
- package/.claude/skills/review-animations/SKILL.md +120 -0
- package/.claude/skills/review-animations/STANDARDS.md +187 -0
- package/.claude/skills/review-animations/agents/openai.yaml +5 -0
- package/.claude/skills/tdd/SKILL.md +38 -0
- package/.claude/skills/tdd/agents/openai.yaml +3 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/tests.md +77 -0
- package/.claude/skills/triage/SKILL.md +58 -0
- package/.claude/skills/triage/agents/openai.yaml +3 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +378 -0
- package/.claude/skills/ui-ux-pro-max/agents/openai.yaml +5 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/.claude/skills/writing-for-agents/SKILL.md +81 -0
- package/.claude/skills/writing-for-agents/agents/openai.yaml +3 -0
- package/.worktreeinclude +3 -0
- package/AGENTS.md +44 -0
- package/README.md +14 -2
- package/VERSION +1 -0
- package/bin/agents.mjs +45 -0
- package/docs/adr/README.md +31 -0
- package/docs/brief-template.md +3 -0
- package/docs/plans/README.md +73 -0
- package/package.json +46 -4
- package/scripts/bootstrap.mjs +188 -0
- package/scripts/check-catalog.mjs +43 -0
- package/scripts/skills-update.mjs +51 -0
- package/skills-lock.json +269 -0
- package/templates/CLAUDE.project.md +41 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Contrato: props, exec e OpenAPI
|
|
2
|
+
|
|
3
|
+
Abra ao escrever ou revisar um `props.surface` ou `exec.surface`, ao declarar erros, paginação ou decorators de contexto, ao nomear schemas, ao mudar um contrato existente ou ao mexer na geração do `openapi.json`. As regras curtas estão no `SKILL.md` (regras 1 a 8 e 13); aqui fica a mecânica.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. `props.surface`
|
|
8
|
+
|
|
9
|
+
Um arquivo por caso de uso: `surfaces/<acao>.<domain>.props.surface.ts`. Ele é o contrato e só declara; não importa service nem tem lógica.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { registerOpenApi } from '@/common/docs/openapi/registries.docs';
|
|
15
|
+
import { docs } from '@/common/docs/openapi/response.docs';
|
|
16
|
+
import { http } from '@/common/http/enums';
|
|
17
|
+
import {
|
|
18
|
+
customerParamsSchema,
|
|
19
|
+
invoiceDetailResponseSchema,
|
|
20
|
+
invoiceItemInputSchema,
|
|
21
|
+
} from '../entities/invoices.entities';
|
|
22
|
+
|
|
23
|
+
const openapi = new OpenAPIRegistry();
|
|
24
|
+
registerOpenApi(openapi);
|
|
25
|
+
|
|
26
|
+
const Method = 'post';
|
|
27
|
+
const Endpoint = '/customers/:customerId/invoices';
|
|
28
|
+
const OperationId = 'createInvoice';
|
|
29
|
+
const Tags = ['Invoices'];
|
|
30
|
+
|
|
31
|
+
const requestRouteParamsSchema = customerParamsSchema.openapi('CreateInvoiceRouteParamRequest');
|
|
32
|
+
openapi.register('CreateInvoiceRouteParamRequest', requestRouteParamsSchema);
|
|
33
|
+
|
|
34
|
+
const requestBodySchema = z
|
|
35
|
+
.object({
|
|
36
|
+
items: z.array(invoiceItemInputSchema).min(1).max(100),
|
|
37
|
+
notes: z.string().trim().max(2000).nullable().optional(),
|
|
38
|
+
dueAt: z.string().datetime().optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict()
|
|
41
|
+
.openapi('CreateInvoiceBodyRequest');
|
|
42
|
+
openapi.register('CreateInvoiceBodyRequest', requestBodySchema);
|
|
43
|
+
|
|
44
|
+
const responseBodySchema = invoiceDetailResponseSchema().openapi('CreateInvoiceBodyResponse');
|
|
45
|
+
openapi.register('CreateInvoiceBodyResponse', responseBodySchema);
|
|
46
|
+
|
|
47
|
+
openapi.registerPath({
|
|
48
|
+
method: Method,
|
|
49
|
+
path: Endpoint,
|
|
50
|
+
operationId: OperationId,
|
|
51
|
+
tags: Tags,
|
|
52
|
+
summary: 'Cria uma fatura',
|
|
53
|
+
description:
|
|
54
|
+
'Cria a fatura em rascunho. Cliente bloqueado = 422 `invoice.customer_blocked`; item fora do catálogo = 422 `invoice.item_invalid` (`details.items`); código repetido = 409 `invoice.duplicate`. Exige `invoices:create`.',
|
|
55
|
+
security: [{ bearerAuth: [] }],
|
|
56
|
+
request: {
|
|
57
|
+
params: requestRouteParamsSchema,
|
|
58
|
+
body: { required: true, content: { 'application/json': { schema: requestBodySchema } } },
|
|
59
|
+
},
|
|
60
|
+
responses: {
|
|
61
|
+
...docs.response,
|
|
62
|
+
...docs.conflict,
|
|
63
|
+
...docs.unprocessable,
|
|
64
|
+
[http.status.success.created]: {
|
|
65
|
+
description: 'Sucesso',
|
|
66
|
+
content: { 'application/json': { schema: responseBodySchema } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const instructions = {
|
|
72
|
+
settings: {
|
|
73
|
+
endpoint: Endpoint,
|
|
74
|
+
method: Method,
|
|
75
|
+
operationId: OperationId,
|
|
76
|
+
tags: Tags,
|
|
77
|
+
request: { params: requestRouteParamsSchema, body: requestBodySchema },
|
|
78
|
+
response: { body: responseBodySchema },
|
|
79
|
+
},
|
|
80
|
+
openapi,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export namespace Instruction {
|
|
84
|
+
export namespace Params {
|
|
85
|
+
export type Body = z.infer<typeof requestRouteParamsSchema>;
|
|
86
|
+
}
|
|
87
|
+
export namespace Request {
|
|
88
|
+
export type Body = z.infer<typeof requestBodySchema>;
|
|
89
|
+
}
|
|
90
|
+
export namespace Response {
|
|
91
|
+
export type Body = z.infer<typeof responseBodySchema>;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Regras do props:
|
|
97
|
+
|
|
98
|
+
- `registerOpenApi(openapi)` logo depois de criar o registry; sem ele o endpoint não entra no documento.
|
|
99
|
+
- `Endpoint` usa a sintaxe do Express (`:customerId`); o build converte para `{customerId}` no documento.
|
|
100
|
+
- `OperationId` em camelCase, único na API e estável: é o nome do método no client gerado. O nome do arquivo e do service é o kebab dele (`createInvoice` -> `create-invoice`).
|
|
101
|
+
- `Tags` usa uma tag já descrita no build; tag nova ganha descrição lá.
|
|
102
|
+
- Nomes de schema por operação (o par `.openapi` + `register` é a regra 5 do `SKILL.md`):
|
|
103
|
+
|
|
104
|
+
| Peça | Nome |
|
|
105
|
+
|---|---|
|
|
106
|
+
| params de rota | `<Operation>RouteParamRequest` |
|
|
107
|
+
| query | `<Operation>QueryParamRequest` |
|
|
108
|
+
| body de request | `<Operation>BodyRequest` |
|
|
109
|
+
| body de response | `<Operation>BodyResponse` |
|
|
110
|
+
| item ou objeto de domínio reutilizado | nome do domínio em `entities/` (`InvoiceSummary`, `InvoiceListSort`) |
|
|
111
|
+
|
|
112
|
+
- Envelope de resposta compartilhado entre operações é fábrica em `entities/` (`invoiceDetailResponseSchema = () => z.object({ data: invoiceDetailSchema }).strict()`): cada operação nomeia a sua cópia e o item continua um schema só.
|
|
113
|
+
- `summary` curto; `description` diz o que a operação faz, cada `code` de 409/422 com a condição (e o `details` quando houver) e a permissão exigida. É o que o consumidor lê para tratar erro.
|
|
114
|
+
- Extensões `x-*` que o repo adota no `registerPath` (metadados para outros consumidores do documento) entram em todo endpoint novo, copiadas do vizinho e ajustadas; são contrato.
|
|
115
|
+
- Body com `required: true`; query e params entram direto em `request`.
|
|
116
|
+
- `instructions.settings.request` tem só as chaves que existem (`params`, `query`, `body`); `Instruction` tem um namespace por chave (`Params`, `Query`, `Request`) mais `Response`, todos por `z.infer`.
|
|
117
|
+
- Rota pública: `security: []` no `registerPath` (regra 8 do `SKILL.md`). Siga o vizinho público: se ele só omite `security` (como o scaffold da referência gera), o documento herda o `security` global do build e mostra a rota como protegida; registre como melhoria (`review.md` §3).
|
|
118
|
+
- `204` não declara `content`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 2. Registry e geração do documento
|
|
123
|
+
|
|
124
|
+
```txt
|
|
125
|
+
props.surface new OpenAPIRegistry() + registerOpenApi(openapi) -> lista global (common/docs/openapi/registries.docs.ts)
|
|
126
|
+
<domain>.surface.module.ts import './surfaces/<acao>.<domain>.props.surface' (efeito colateral: executa o registro)
|
|
127
|
+
common/docs/openapi/build.ts securitySchemes (bearerAuth) + getOpenApiRegistries() + info, servers, tags; normaliza :param -> {param}
|
|
128
|
+
common/docs/openapi/generate.ts grava openapi.json; o script de geração do repo também gera generated/openapi-client.d.ts
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- O registro acontece quando o arquivo do props é executado. O surface module importa cada props por efeito colateral, e é esse import que o repo usa para garantir o registro: todo props novo ganha a sua linha ali.
|
|
132
|
+
- O exec importa `instructions` por valor e `Instruction` como tipo; por que um props nunca entra por `import type`: anti-padrões do `SKILL.md`.
|
|
133
|
+
- `securitySchemes` é registrado uma vez, no build. Sem ele, o `bearerAuth` que todo endpoint declara aponta para um scheme inexistente e o client não sabe que precisa de token. Se o build também declara `security` global, ele vale para toda operação que não declara a sua (§1).
|
|
134
|
+
- O `servers` do documento casa com o prefixo global do `main.ts` (ex.: `/api/v1`); o `/docs` da app serve o mesmo `buildOpenApiDocument()`, com exposição fail-closed (`security.md` §1).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 3. `exec.surface`
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Controller, HttpCode, Post, UseGuards } from '@nestjs/common';
|
|
142
|
+
import { AuthUser } from '@/common/decorators/auth.user.decorators';
|
|
143
|
+
import { ZdBody, ZdParam } from '@/common/decorators/requests.http.decorators';
|
|
144
|
+
import { AuthUserGuard } from '@/common/guards/auth.user.guard';
|
|
145
|
+
import { http } from '@/common/http/enums';
|
|
146
|
+
import type { IAuthUser } from '@/modules/auth/entities/auth.user.entities';
|
|
147
|
+
import { CreateInvoiceService } from '../services/create-invoice.service';
|
|
148
|
+
import { type Instruction, instructions } from './create-invoice.invoices.props.surface';
|
|
149
|
+
|
|
150
|
+
@Controller()
|
|
151
|
+
export class CreateInvoiceSurface {
|
|
152
|
+
constructor(private readonly service: CreateInvoiceService) {}
|
|
153
|
+
|
|
154
|
+
@Post('customers/:customerId/invoices')
|
|
155
|
+
@UseGuards(AuthUserGuard)
|
|
156
|
+
@HttpCode(http.status.success.created)
|
|
157
|
+
async exec(
|
|
158
|
+
@AuthUser() user: IAuthUser,
|
|
159
|
+
@ZdParam(instructions.settings.request.params) params: Instruction.Params.Body,
|
|
160
|
+
@ZdBody(instructions.settings.request.body) body: Instruction.Request.Body,
|
|
161
|
+
): Promise<Instruction.Response.Body> {
|
|
162
|
+
return this.service.execute({ actorId: user.id, ...params, ...body });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- Path do decorator: regra 1 do `SKILL.md`; o prefixo global (`api/v1`) não entra nele.
|
|
168
|
+
- `@HttpCode(http.status.success.<ok|created|noContent>)` igual ao status de sucesso do props.
|
|
169
|
+
- Um parâmetro por chave de `instructions.settings.request`: `@ZdParam`, `@ZdQuery`, `@ZdBody`, cada um com o schema do contrato e o tipo `Instruction.*`.
|
|
170
|
+
- O retorno é `Promise<Instruction.Response.Body>`; o TypeScript acusa quando o service devolve outra forma.
|
|
171
|
+
- O input do `execute` é `{ actorId: user.id, ...params, ...query, ...body }` mais o contexto extraído por decorator (por que o spread é seguro: regra 4 do `SKILL.md`). Se duas fontes têm a mesma chave, renomeie no schema, não no exec.
|
|
172
|
+
- Rota pública: sem `@UseGuards(AuthUserGuard)` e sem `@AuthUser()`. Pública que personaliza quando há token: `OptionalAuthUserGuard` com o decorator de usuário opcional do repo.
|
|
173
|
+
|
|
174
|
+
### Runtime x documento
|
|
175
|
+
|
|
176
|
+
| Runtime | Documento |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `@UseGuards(AuthUserGuard)` | `security: [{ bearerAuth: [] }]` |
|
|
179
|
+
| `@ZdParam(schema)` | `request.params` |
|
|
180
|
+
| `@ZdQuery(schema)` | `request.query` |
|
|
181
|
+
| `@ZdBody(schema)` | `request.body.content['application/json'].schema` |
|
|
182
|
+
| `@HttpCode(http.status.success.created)` | resposta `201` declarada |
|
|
183
|
+
| decorator do verbo com o path | `method` + `path` do `registerPath` |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 4. Decorators de contexto
|
|
188
|
+
|
|
189
|
+
O service recebe dado extraído, nunca o `Request`. Use os decorators do repo (`common/decorators/`); os da referência:
|
|
190
|
+
|
|
191
|
+
| Decorator | Entrega | Comportamento |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `@AuthUser()` | usuário do token (`IAuthUser`) | exige `AuthUserGuard` antes |
|
|
194
|
+
| usuário opcional (`optional.auth.user.decorators.ts`) | usuário ou ausente | com `OptionalAuthUserGuard` |
|
|
195
|
+
| `@CorrelationId()` | id de correlação do header | ausente = `undefined` |
|
|
196
|
+
| `@ClientIp()` | `request.ip` | depende do `trust proxy` (`security.md` §1) |
|
|
197
|
+
| `@IdempotencyKey()` | header `Idempotency-Key` | ausente ou vazio = 400; declare o header no contrato do endpoint |
|
|
198
|
+
| `@<Tenant>Ref()` | referência do tenant ativo vinda de header (id ou slug) | **nunca lança**: ausente ou fora do formato = `undefined`; quem resolve e recusa é o service |
|
|
199
|
+
|
|
200
|
+
Decorator novo só quando o dado é HTTP-aware e transversal; é pequeno, sem I/O e com teste.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 5. Erros no contrato
|
|
205
|
+
|
|
206
|
+
`common/docs/openapi/response.docs.ts` tem três blocos: `...docs.response` (400, 401, 403, 404), `...docs.conflict` (409: versão, duplicidade, estado) e `...docs.unprocessable` (422: regra de negócio recusou). Quando espalhar cada um: regra 7 do `SKILL.md`.
|
|
207
|
+
|
|
208
|
+
Exemplos de `code` no formato da referência: `invoice.stale`, `invoice.already_paid`, `invoice.required_fields_missing` (com `details.fields`). Shape completo de cada status e o que vai em `details`: `security.md` §7. Os clients reagem por esse `code`: o web mapeia `error.code` no mapper de falha da feature, o app pelo helper de erro dele; por isso ele é contrato.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 6. Paginação, filtros e sort (lado do contrato)
|
|
213
|
+
|
|
214
|
+
Helpers únicos em `common/schemas/paginations.ts`: `paginationQuery` (`page` >= 1, `pageSize` 1 a 100, `z.coerce`), `paginatedResponse(item)` (`{ data, meta: { page, pageSize, total, totalPages } }`) e `toPaginationMeta(page, pageSize, total)` (usado no adapter, `domain.md` §6).
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const requestQueryParamsSchema = paginationQuery
|
|
218
|
+
.extend({
|
|
219
|
+
...invoicesFilterSchema.shape,
|
|
220
|
+
status: z.enum(['open', 'closed', 'all']).default('open'),
|
|
221
|
+
sort: invoiceListSortSchema.default('updatedAt'),
|
|
222
|
+
direction: z.enum(['asc', 'desc']).default('desc'),
|
|
223
|
+
})
|
|
224
|
+
.strict()
|
|
225
|
+
.openapi('ListCustomerInvoicesQueryParamRequest');
|
|
226
|
+
openapi.register('ListCustomerInvoicesQueryParamRequest', requestQueryParamsSchema);
|
|
227
|
+
|
|
228
|
+
const responseBodySchema = paginatedResponse(invoiceSummarySchema).openapi(
|
|
229
|
+
'ListCustomerInvoicesBodyResponse',
|
|
230
|
+
);
|
|
231
|
+
openapi.register('ListCustomerInvoicesBodyResponse', responseBodySchema);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- `sort` é `z.enum` nomeado em `entities/` (`InvoiceListSort`) com default; `direction` é `asc|desc` (siga o nome que o vizinho usa). O adapter mapeia o enum para coluna por mapa literal (segunda trava, `domain.md` §7).
|
|
235
|
+
- O helper base estendido fica sem `.refine()` (refinamento impede ou complica o `.extend`); o consumidor fecha com `.strict()` e o nome dele.
|
|
236
|
+
- Filtros reutilizados entre lista e outras leituras viram um schema em `entities/` (`invoicesFilterSchema`) espalhado por `.shape`.
|
|
237
|
+
- Query chega como string: `z.coerce` para número; boolean por `z.stringbool()` (Zod 4) ou helper do repo, nunca `z.coerce.boolean()`.
|
|
238
|
+
- Parâmetro que pode vir repetido ou CSV é normalizado em `z.preprocess` e o shape real é declarado com `.openapi({ type: 'array', items: ... })`.
|
|
239
|
+
- Ausente não filtra; array vazio não é "sem filtro": o service normaliza vazio para ausente.
|
|
240
|
+
- Texto livre de busca tem `.min(1).max(n)`; o escape de curinga é do adapter.
|
|
241
|
+
- Lista grande, feed ou linha do tempo: cursor opaco (`nextCursor`) em vez de offset alto.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 7. Governança de schemas
|
|
246
|
+
|
|
247
|
+
- Schema de domínio (item, detalhe, enums, params comuns, filtros) mora em `entities/<domain>.entities.ts`, nomeado; request e response da operação moram no props.
|
|
248
|
+
- `common/schemas` só para o que dois ou mais domínios usam.
|
|
249
|
+
- Response nunca deriva do model do ORM.
|
|
250
|
+
- Create e update não compartilham schema quando os obrigatórios diferem: separe `CreateXBodyRequest`, `UpdateXBodyRequest`.
|
|
251
|
+
- Input valida o que o cliente envia; o schema de resposta é o que sai. Vocabulário público diferente do físico é traduzido no mapper e não vaza para o SQL.
|
|
252
|
+
- Todo `enum` e `version` que o client manda de volta tem `describe` dizendo o efeito (`'Versão lida: 409 invoice.stale se mudou'`).
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 8. Versionamento e breaking changes
|
|
257
|
+
|
|
258
|
+
Toda mudança de contrato é classificada no PR.
|
|
259
|
+
|
|
260
|
+
| Tipo | Exemplos | Versão nova? |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `patch` | descrição, exemplo, summary, documentar um erro que já existe em runtime | Não |
|
|
263
|
+
| `minor` | endpoint novo, campo opcional no response, valor de enum compatível, filtro ou header opcional | Normalmente não |
|
|
264
|
+
| `major` | remover ou renomear campo, mudar tipo, mudar required/optional, remover valor de enum, mudar `operationId`, status de sucesso, envelope, paginação (inclusive offset para cursor), semântica de filtro, formato de erro, **mudar um `code`**, remover endpoint | Sim |
|
|
265
|
+
|
|
266
|
+
Um `code` é contrato tanto quanto o schema: o consumidor escolhe comportamento por ele. Major exige versão nova (o prefixo global carrega a versão), migração coordenada ou aprovação explícita.
|
|
267
|
+
|
|
268
|
+
Depreciação, nesta ordem: marcar `deprecated` no OpenAPI; documentar o substituto (e o prazo, se houver); comunicar consumidores conhecidos; manter compatibilidade pelo período combinado; remover só depois do diff aprovado e dos consumidores migrados.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 9. Lint de OpenAPI (se o repo tiver)
|
|
273
|
+
|
|
274
|
+
Com Spectral ou equivalente, como `error`: `operationId` presente, camelCase e único; tags presentes; erro no schema padrão; protegido com `security`; `204` sem body; path param declarado; nenhuma data como objeto. Como `warn`: summary presente, schema de sucesso inline, `meta` de paginação fora do padrão. "Protegido documenta 401" vale só nessa direção: endpoint público (ex.: login) também devolve 401. Sem ferramenta, sugira o ruleset sem bloquear a entrega.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 10. Paridade e CI
|
|
279
|
+
|
|
280
|
+
```txt
|
|
281
|
+
<pm> run openapi:generate gera openapi.json e generated/openapi-client.d.ts
|
|
282
|
+
git diff --exit-code openapi.json quando o repo versiona o documento: contrato mudado sem regenerar falha
|
|
283
|
+
<pm> run check-types o client .d.ts e os Instruction.* compilam
|
|
284
|
+
testes unit valida o retorno do service contra instructions.settings.response.body
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Uma checagem de paridade rota x documento falha quando há handler sem operação no documento (endpoint que o consumidor não conhece, muitas vezes sem guard), operação sem handler ou com path diferente do decorator (método no client que devolve 404) ou `operationId` duplicado (quebra o client). Se o repo tem essa checagem, ela roda no gate; se não tem, o e2e da rota é a trava e a checagem fica registrada como melhoria (`review.md` §3).
|
|
288
|
+
|
|
289
|
+
O PR deixa claro: operações adicionadas, schemas alterados, se houve breaking change, se `operationId`, status, envelope ou `code` mudou. Os clients (web e app) regeneram a partir do `openapi.json` do build.
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Domínio: services, regras, access, ports, adapters e módulos
|
|
2
|
+
|
|
3
|
+
Abra ao escrever ou revisar um service, um `*.rules.ts`, o `<domain>-access.ts`, o mapper, os erros, o port, o adapter (com `.sql.ts` e `-db-errors.ts`), os módulos `sgbds`/`surface`, um worker, outbox, idempotência ou cache. As regras curtas estão no `SKILL.md` (regras 3, 4, 9 a 14); o nível do banco está em `arch-db`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Service: um caso de uso por arquivo
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Injectable } from '@nestjs/common';
|
|
11
|
+
import { UnprocessableDomainException } from '@/common/http/errors';
|
|
12
|
+
import type { AuditWriteInput } from '@/modules/audit/entities/audit.entities';
|
|
13
|
+
import { PermissionsPolicyService } from '@/modules/rbac/services/permissions-policy.service';
|
|
14
|
+
import type { InvoiceItemInput } from '../entities/invoices.entities';
|
|
15
|
+
import { InvoicesRepositoryPort, type NewInvoice } from '../ports/invoices.repository.port';
|
|
16
|
+
import { assertCanActOnCustomer } from './invoices-access';
|
|
17
|
+
import { InvoiceCustomerResolverService } from './invoice-customer.resolver';
|
|
18
|
+
import { loadInvoiceDetail } from './invoice-context';
|
|
19
|
+
import { priceItems } from './pricing.rules';
|
|
20
|
+
|
|
21
|
+
type Input = { actorId: string; customerId: string; items: InvoiceItemInput[]; notes?: string | null; dueAt?: string };
|
|
22
|
+
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class CreateInvoiceService {
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly repository: InvoicesRepositoryPort,
|
|
27
|
+
private readonly customers: InvoiceCustomerResolverService,
|
|
28
|
+
private readonly policy: PermissionsPolicyService,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async execute(input: Input) {
|
|
32
|
+
const customer = await this.customers.require(input.customerId); // 404 se não existe
|
|
33
|
+
const access = await assertCanActOnCustomer(this.policy, input.actorId, customer, 'create');
|
|
34
|
+
if (customer.status === 'BLOCKED') {
|
|
35
|
+
throw new UnprocessableDomainException('invoice.customer_blocked', 'Cliente bloqueado');
|
|
36
|
+
}
|
|
37
|
+
const catalog = await this.repository.findLiveProducts(input.items.map((item) => item.product));
|
|
38
|
+
const priced = priceItems(catalog, input.items); // regra pura; lança 422 invoice.item_invalid
|
|
39
|
+
|
|
40
|
+
const data: NewInvoice = {
|
|
41
|
+
customer: customer.id,
|
|
42
|
+
tenant: customer.tenant,
|
|
43
|
+
notes: input.notes ?? null,
|
|
44
|
+
dueAt: input.dueAt ? new Date(input.dueAt) : null,
|
|
45
|
+
totalAmount: priced.total,
|
|
46
|
+
createdBy: input.actorId,
|
|
47
|
+
};
|
|
48
|
+
const created = await this.repository.createInvoice(data, priced.items, (invoice) => [
|
|
49
|
+
{ actorId: input.actorId, entityType: 'invoice', entityId: invoice.id, action: 'invoice-created' },
|
|
50
|
+
] satisfies AuditWriteInput[]);
|
|
51
|
+
|
|
52
|
+
return loadInvoiceDetail(this.repository, customer, created.id, access);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- O default de negócio de um campo opcional do `Input` (regra 3) é decidido aqui.
|
|
58
|
+
- Ordem: resolver o recurso pai (404) -> `assert*` do access -> estado do recurso (422/409) -> leituras para validar referências -> regras puras -> montar `New*`/`*Write` campo a campo -> um método de escrita do port -> devolver o contrato (`{ data }` ou `{ data, meta }`) pelo mapper.
|
|
59
|
+
- Audit é montado no service e gravado pelo adapter na mesma transação do write (o callback recebe o id criado). Borda: string ISO -> `Date` no `New*`; array vazio -> ausente; `?? null` para opcional que vira coluna.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. `*.rules.ts`: regra pura
|
|
64
|
+
|
|
65
|
+
Função sem I/O e sem DI: posição, preço, transição de estado, campos obrigatórios, formato de código. Recebe dados já carregados e devolve valor ou lança exceção de domínio com `code` (fábrica local no topo do arquivo). Constantes da regra são exportadas (o spec e o adapter as reutilizam). Todo `*.rules.ts` tem `*.rules.spec.ts` ao lado; é o teste mais barato da suíte.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. `<domain>-access.ts`: autorização por recurso
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { PERMISSIONS } from '@/common/auth/permissions';
|
|
73
|
+
import { ForbiddenCustomHttpException, NotFoundDomainException } from '@/common/http/errors';
|
|
74
|
+
import type { CustomerAccess, PermissionsPolicyService } from '@/modules/rbac/services/permissions-policy.service';
|
|
75
|
+
import type { InvoiceCustomerRecord, InvoiceViewer } from '../ports/invoices.repository.port';
|
|
76
|
+
|
|
77
|
+
const MANAGERS = [PERMISSIONS.invoices.manage] as const;
|
|
78
|
+
|
|
79
|
+
export const INVOICE_ACTIONS = {
|
|
80
|
+
create: [PERMISSIONS.invoices.create, ...MANAGERS],
|
|
81
|
+
cancel: [PERMISSIONS.invoices.cancel, ...MANAGERS],
|
|
82
|
+
} as const;
|
|
83
|
+
export type InvoiceAction = keyof typeof INVOICE_ACTIONS;
|
|
84
|
+
|
|
85
|
+
/** Executa a ação no cliente; tenant encerrado não aceita escrita (403). */
|
|
86
|
+
export async function assertCanActOnCustomer(
|
|
87
|
+
policy: PermissionsPolicyService, actorId: string, customer: InvoiceCustomerRecord, action: InvoiceAction,
|
|
88
|
+
): Promise<CustomerAccess> {
|
|
89
|
+
// método do policy service para o escopo do recurso; use o que o repo expõe
|
|
90
|
+
const access = await policy.assert.access.customer({ customer: customer.id, user: actorId, allow: INVOICE_ACTIONS[action] });
|
|
91
|
+
if (customer.tenantStatus === 'CLOSED') throw new ForbiddenCustomHttpException('Operação indisponível');
|
|
92
|
+
return access;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const can = (access: CustomerAccess, action: InvoiceAction) =>
|
|
96
|
+
access.canRead && INVOICE_ACTIONS[action].some((key) => access.permissions.includes(key));
|
|
97
|
+
|
|
98
|
+
export const viewerOf = (actorId: string, access: CustomerAccess): InvoiceViewer => ({
|
|
99
|
+
userId: actorId, privileged: can(access, 'cancel'),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** Fatura restrita: para quem não pode vê-la, ela não existe. */
|
|
103
|
+
export function assertInvoiceVisible(invoice: { restricted: boolean }, viewer: InvoiceViewer): void {
|
|
104
|
+
if (invoice.restricted && !viewer.privileged) throw new NotFoundDomainException('Fatura não encontrada');
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Funções exportadas, não classe: recebem o policy service injetado no service, o ator e o recurso já carregado.
|
|
109
|
+
- `<DOMAIN>_ACTIONS` lista, por ação, as permissões que passam (OR); conjunção é regra no próprio `assert*`.
|
|
110
|
+
- `can(access, action)` alimenta campos de capacidade na resposta (`access: { canCancel }`), para a tela esconder o botão sem adivinhar.
|
|
111
|
+
- `viewerOf` traduz o acesso no que o adapter precisa para filtrar a leitura (visibilidade no SQL).
|
|
112
|
+
- Além do 404 da regra 9: 403 quando o recurso é visível mas a ação não; tenant em estado que bloqueia escrita (cancelado, removido) também é 403.
|
|
113
|
+
- Chaves de permissão vêm de `common/auth/permissions.ts`; permissão nova entra lá e na seed/migration do catálogo.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. Mapper e erros
|
|
118
|
+
|
|
119
|
+
- `services/<domain>.mapper.ts`: funções `serialize<Coisa>(row, ...)` de `*Row` (como o banco devolve: `Date | string`, `numeric` como string, JSON agregado) para o tipo do schema de resposta, sem I/O. Datas por `iso(value)` / `isoOrNull(value)` (`new Date(value).toISOString()`), números por `Number(...)`, listas por `?? []`.
|
|
120
|
+
- `services/<domain>-errors.ts`: fábricas reutilizadas por service e adapter (`staleInvoice = () => new ConflictDomainException(STALE_INVOICE, 'invoice.stale')`). Mensagem para humano no idioma do produto; `code` estável.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 5. Port
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import type { InvoiceStatus } from '@/common/sgbds/generated/client';
|
|
128
|
+
import type { IPaginatedResult } from '@/common/sgbds/ports/core.operations.repository.port';
|
|
129
|
+
import type { AuditWriteInput } from '@/modules/audit/entities/audit.entities';
|
|
130
|
+
import type { InvoiceListSort, InvoicesFilter } from '../entities/invoices.entities';
|
|
131
|
+
|
|
132
|
+
type DateLike = Date | string;
|
|
133
|
+
// InvoiceCustomerRecord, InvoiceViewer, ProductRecord e PricedItem: escritos à mão no mesmo arquivo, como os abaixo.
|
|
134
|
+
|
|
135
|
+
export type InvoiceSummaryRow = { id: string; code: string; status: InvoiceStatus; totalAmount: number | string;
|
|
136
|
+
dueAt: DateLike | null; version: number; createdAt: DateLike };
|
|
137
|
+
export type InvoiceListFilter = InvoicesFilter & { page: number; pageSize: number; sort: InvoiceListSort; direction: 'asc' | 'desc' };
|
|
138
|
+
export type NewInvoice = { customer: string; tenant: string; notes: string | null; dueAt: Date | null;
|
|
139
|
+
totalAmount: number; createdBy: string };
|
|
140
|
+
export type InvoiceWrite = { invoice: string; version: number; actorId: string;
|
|
141
|
+
set: Partial<{ notes: string | null; dueAt: Date | null; status: InvoiceStatus }>; audits: AuditWriteInput[] };
|
|
142
|
+
|
|
143
|
+
export abstract class InvoicesRepositoryPort {
|
|
144
|
+
abstract findCustomer(customerId: string): Promise<InvoiceCustomerRecord | null>;
|
|
145
|
+
abstract findLiveProducts(ids: string[]): Promise<ProductRecord[]>;
|
|
146
|
+
abstract listInvoices(customerId: string, filter: InvoiceListFilter, viewer: InvoiceViewer): Promise<IPaginatedResult<InvoiceSummaryRow[]>>;
|
|
147
|
+
/** Cria numa transação: fatura, itens e audits. */
|
|
148
|
+
abstract createInvoice(data: NewInvoice, items: PricedItem[], audits: (invoice: { id: string }) => AuditWriteInput[]): Promise<{ id: string; code: string }>;
|
|
149
|
+
/** UPDATE com `WHERE version = <lida>`; 0 linhas = 409 invoice.stale. Devolve a versão nova. */
|
|
150
|
+
abstract writeInvoice(change: InvoiceWrite): Promise<number>;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `abstract class <Domain>RepositoryPort` é o token de DI; o service injeta o port, nunca o adapter.
|
|
155
|
+
- Tipos por papel: `*Row` (leitura como o SQL devolve), `*Record` (estado que os writes precisam), `New*` (insert), `*Write` (update com `set` parcial, `version` e audits).
|
|
156
|
+
- Além do que a regra 10 do `SKILL.md` permite do ORM, o port pode fazer `import type` de `entities/` do próprio domínio e de tipos de entrada de outro domínio (audit).
|
|
157
|
+
- Ausência é `null`. Comentário curto no método quando a semântica não é óbvia (transação, filtro aplicado ou não, o que o retorno significa).
|
|
158
|
+
- O scaffold da referência gera o port estendendo o repositório base genérico do módulo de banco (CRUD sobre `unknown`); domínio com regra troca isso por métodos próprios tipados, como os vizinhos maduros.
|
|
159
|
+
- Integração externa do domínio (provider de terceiro usado só por ele) é `ports/<x>.provider.port.ts` com `abstract class I<X>Provider`; capacidade usada por vários domínios vai para `common/` (`common.md`).
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 6. Adapter
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const INVOICE_ERRORS = {
|
|
167
|
+
uq_invoices_code: () => new ConflictDomainException('Código de fatura repetido', 'invoice.duplicate'),
|
|
168
|
+
ck_invoices_total_positive: () => new UnprocessableDomainException('invoice.total_invalid', 'Total precisa ser positivo'),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
@Injectable()
|
|
172
|
+
export class PrismaInvoicesRepositoryAdapter extends InvoicesRepositoryPort {
|
|
173
|
+
constructor(private readonly prisma: PrismaDatabaseService, private readonly pg: NodePostgresDatabaseService) {
|
|
174
|
+
super();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async listInvoices(customerId: string, filter: InvoiceListFilter, viewer: InvoiceViewer) {
|
|
178
|
+
const { page, count } = buildListQueries(customerId, filter, viewer);
|
|
179
|
+
const [rows, total] = await Promise.all([
|
|
180
|
+
this.pg.query<InvoiceSummaryRow>(page.text, page.values),
|
|
181
|
+
this.pg.query<{ total: number }>(count.text, count.values),
|
|
182
|
+
]);
|
|
183
|
+
return { data: rows.rows, meta: toPaginationMeta(filter.page, filter.pageSize, total.rows[0]?.total ?? 0) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async writeInvoice(change: InvoiceWrite) {
|
|
187
|
+
try {
|
|
188
|
+
return await this.prisma.$transaction(async (tx) => {
|
|
189
|
+
const { count } = await tx.invoice.updateMany({
|
|
190
|
+
where: { id: change.invoice, version: change.version, deletedAt: null },
|
|
191
|
+
data: { ...change.set, updatedAt: new Date() },
|
|
192
|
+
});
|
|
193
|
+
if (count === 0) throw staleInvoice();
|
|
194
|
+
await tx.audit.createMany({ data: change.audits.map(toAuditCreateInput) });
|
|
195
|
+
const updated = await tx.invoice.findUnique({ where: { id: change.invoice }, select: { version: true } });
|
|
196
|
+
return updated?.version ?? change.version + 1;
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
rethrowInvoicesDbError(error, INVOICE_ERRORS, { foreignKey: 'Cliente ou produto inexistente', check: 'Dados da fatura fora do formato' });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- `extends <Domain>RepositoryPort` com `super()`; o nome começa pela tecnologia (`Prisma`, `Pg`, `Http`). Injeta os serviços do módulo global de banco: na referência, ORM para escrita, `$transaction` e leitura simples por `select` explícito; driver para leitura quente com SQL de `<domain>.sql.ts`. Siga a divisão do repo.
|
|
206
|
+
- Leitura: `rows[0] ?? null`; conversão leve (`Number(...)`, default de JSON) no adapter, formato de resposta no mapper.
|
|
207
|
+
- Escrita: um método do port = uma transação com tudo que a invariante precisa (linhas, filhos, audits, renumeração); o service nunca vê `tx` (regra 14 do `SKILL.md`).
|
|
208
|
+
- Lock otimista (regra 14): `updateMany` com `version` no `where` e `count === 0` -> `stale<Coisa>()`. O incremento de `version` é do banco (trigger) ou do adapter, conforme o repo.
|
|
209
|
+
- Sequência por pai (código `INV-0042`): `UPDATE ... SET next_number = next_number + 1 ... RETURNING` dentro da transação, que trava a linha do pai até o commit.
|
|
210
|
+
- `try/catch` em todo método de escrita, terminando em `rethrow<Domain>DbError`.
|
|
211
|
+
|
|
212
|
+
### `<domain>-db-errors.ts`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
export function rethrowInvoicesDbError(error: unknown, known: Record<string, () => HttpException>, messages?: Messages): never {
|
|
216
|
+
if (error instanceof HttpException) throw error; // exceção de domínio lançada dentro da transação
|
|
217
|
+
const hit = constraintOf(error, Object.keys(known)); // nome em error.constraint, em meta do ORM ou no texto
|
|
218
|
+
if (hit) throw known[hit]();
|
|
219
|
+
rethrowDbError(error, messages); // common/sgbds/settings/db-errors.ts: genérico + log
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- O mapa constraint/trigger -> fábrica fica no adapter (`<DOMAIN>_ERRORS`); toda constraint que um write pode violar tem entrada, com o `code` declarado no contrato.
|
|
224
|
+
- O genérico (`rethrowDbError`) cobre unique (409), FK (409), CHECK (400), registro inexistente (404; `P2025` no Prisma) e o resto (500), com mensagem de negócio por parâmetro, e loga a causa real. Ramos com `code` específicos do repo (exclusão, códigos lançados por trigger) rodam dentro dele antes dos genéricos: constraint nova com `code` próprio entra no mapa do adapter ou nesses ramos, nunca depois. O client nunca recebe nome de constraint, SQL ou valor.
|
|
225
|
+
- Nunca importe o `-db-errors` de outro domínio; `constraintOf` repetido em vários domínios é candidato a subir para `common/sgbds/settings/`.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 7. `<domain>.sql.ts`: SQL montado por funções puras
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
export type SqlQuery = { text: string; values: unknown[] };
|
|
233
|
+
|
|
234
|
+
class Params {
|
|
235
|
+
readonly values: unknown[] = [];
|
|
236
|
+
add(value: unknown): string { this.values.push(value); return `$${this.values.length}`; }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const INVOICE_LIST_SORT: Record<InvoiceListFilter['sort'], string> = {
|
|
240
|
+
updatedAt: 'i.updated_at', createdAt: 'i.created_at', dueAt: 'i.due_at', total: 'i.total_amount',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export function buildListQueries(customerId: string, filter: InvoiceListFilter, viewer: InvoiceViewer) {
|
|
244
|
+
const build = (withPage: boolean): SqlQuery => {
|
|
245
|
+
const p = new Params();
|
|
246
|
+
const where = [`i.customer = ${p.add(customerId)}::uuid`, 'i.deleted_at IS NULL'];
|
|
247
|
+
if (!viewer.privileged) where.push('i.restricted = false');
|
|
248
|
+
if (filter.search) where.push(`i.code ILIKE ${p.add(likePattern(filter.search))}`);
|
|
249
|
+
if (!withPage) return { text: `SELECT count(*)::int AS total FROM invoices i WHERE ${where.join(' AND ')}`, values: p.values };
|
|
250
|
+
const direction = filter.direction === 'asc' ? 'ASC' : 'DESC';
|
|
251
|
+
return {
|
|
252
|
+
text: `SELECT ${SUMMARY_COLUMNS} FROM invoices i WHERE ${where.join(' AND ')}
|
|
253
|
+
ORDER BY ${INVOICE_LIST_SORT[filter.sort]} ${direction} NULLS LAST, i.id ${direction}
|
|
254
|
+
LIMIT ${p.add(filter.pageSize)}::int OFFSET ${p.add((filter.page - 1) * filter.pageSize)}::int`,
|
|
255
|
+
values: p.values,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
return { page: build(true), count: build(false) };
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- Funções puras que devolvem `{ text, values }`, mais constantes `*_SQL` para query fixa: testáveis sem banco e executáveis no `psql` com o mesmo texto.
|
|
263
|
+
- Além da regra 12 do `SKILL.md`: cast explícito em cada `$n`, tiebreaker na mesma direção do sort, `LIKE` escapado pelo util do repo (`common/utils/sql/`).
|
|
264
|
+
- Todo `$n` adicionado aparece no texto: parâmetro sem uso quebra o `PREPARE` (`could not determine data type`). Por isso a contagem é montada sem os parâmetros só da página.
|
|
265
|
+
- Projeção explícita com alias camelCase; relacionados agregados em JSON na mesma query. A visibilidade do `viewer` entra no `WHERE`, para lista e total baterem.
|
|
266
|
+
- SQL completo de listagem, keyset, índices e `EXPLAIN`: `arch-db` (reference `indexes-queries.md` de lá).
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 8. Módulos e acesso entre domínios
|
|
271
|
+
|
|
272
|
+
- `<domain>.sgbds.module.ts` só liga port -> adapter e exporta o port: `@Module({ providers: [{ provide: InvoicesRepositoryPort, useClass: PrismaInvoicesRepositoryAdapter }], exports: [InvoicesRepositoryPort] })`. Se o domínio oferece um service a outros, o módulo que o declara também o exporta.
|
|
273
|
+
- `<domain>.surface.module.ts`: `imports` com o próprio `SgbdsModule` e os módulos dos domínios de que depende; `controllers` com todos os exec; `providers` com todos os services, resolvers e helpers injetáveis; uma linha `import './surfaces/<acao>.<domain>.props.surface'` por endpoint.
|
|
274
|
+
- `common/surfaces/surface.module.ts` importa e exporta todos os `<Domain>SurfaceModule`. Rotas conflitantes entre módulos (`customers/me/invoices` x `customers/:id`) dependem da ordem: o módulo da rota estática vem antes, com comentário dizendo por quê.
|
|
275
|
+
- Entre domínios, a regra está no `SKILL.md` (Colocação). Complemento: tipos e funções puras do outro domínio (`entities`, mapper de audit) podem ser importados pelo caminho; o que tem estado ou DI chega pelo `exports` do módulo dele.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 9. Workers, outbox, idempotência e cache
|
|
280
|
+
|
|
281
|
+
- **Worker**: `<assunto>.worker.ts` no módulo do domínio consome a fila pela capacidade de fila ou mensageria do repo (copie a fiação de um worker vizinho); é idempotente (a mesma mensagem duas vezes não duplica efeito) e chama services, não adapters de outro domínio.
|
|
282
|
+
- **Outbox**: efeito externo que não pode se perder depois do commit (evento, e-mail transacional, integração). O registro de outbox entra na mesma transação do write; o worker de outbox publica depois do commit, at-least-once; o consumidor é idempotente. Na referência, serviço e worker de outbox moram em `common/messaging`; siga como o vizinho que já publica evento grava o registro.
|
|
283
|
+
- **Idempotência**: exija `Idempotency-Key` quando a operação cria cobrança, pagamento, pedido ou solicitação, dispara integração externa, pode ser repetida por retry, ou não pode duplicar registro. Mecânica pelo `IIdempotencyProvider` de `common/resilience` (confira o vizinho que já o usa):
|
|
284
|
+
|
|
285
|
+
```txt
|
|
286
|
+
acquire({ key, actorId, method, route, payloadHash, ttlSeconds }) -> 'NEW' | 'REPLAY' | 'CONFLICT'
|
|
287
|
+
NEW processa; sucesso -> complete(key, actorId, method, route, responseBody); falha -> release(...)
|
|
288
|
+
REPLAY get(...) e devolve a mesma resposta lógica (em andamento: 409 ou 202, conforme o padrão do repo)
|
|
289
|
+
CONFLICT mesma key com payload diferente -> 409
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
O header entra no contrato (`@IdempotencyKey()`, `contract.md` §4); o hash do payload usa o helper de `common/resilience`; payload sensível não é guardado inteiro.
|
|
293
|
+
|
|
294
|
+
- **Trabalho pesado**: payload limitado no schema (`.max()` em texto e array); o que passa do tempo de request (relatório, lote) vira 202 + fila. Descubra o SLO do projeto; não invente número universal.
|
|
295
|
+
- **Cache** só com chave, escopo (público, usuário, tenant), TTL e invalidação definidos; cache por usuário ou tenant sem chave composta é vazamento; cache de autorização exige invalidação provada por teste.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 10. Fronteira com `arch-db`
|
|
300
|
+
|
|
301
|
+
Aqui (`arch-api`): onde o banco vive na aplicação (`common/sgbds/*`), como o adapter injeta os clientes, a transação dentro do método do adapter, a tradução de constraint para `code`, o contrato de listagem e o comando de paridade migration x schema do repo. Lá (`arch-db`), e não aqui: modelagem e tipos, constraints e nomes, soft delete, FKs, índices e `EXPLAIN`, SQL de listagem completo, keyset, upsert, bulk, locks, isolamento, timeouts e retry, migrations (expand/contract, lock-safety), seeds e backfill, pool (tamanho, quantos por processo) e multi-tenant no banco. Se as duas divergirem no nível do banco, `arch-db` vence.
|