@oondemand/create-central-oon 0.5.6 → 0.6.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/README.md +41 -89
- package/bin/cli.js +4 -17
- package/docs/ADVANCED_UX_PATTERNS.md +3 -2
- package/docs/AGENTS.md +4 -5
- package/docs/AGENT_WORKFLOW.md +1 -1
- package/docs/BACKEND_API.md +2 -4
- package/docs/BACKEND_PATTERNS.md +1 -2
- package/docs/BACKEND_PROCESS_MANIFEST.md +6 -6
- package/docs/CAPABILITIES.md +2 -3
- package/docs/CHECKLIST_IMPLEMENTACAO.md +4 -4
- package/docs/CORE_UPGRADE.md +2 -2
- package/docs/FRONTEND_API.md +20 -23
- package/docs/FRONTEND_CODE_FIRST.md +35 -0
- package/docs/FRONTEND_PATTERNS.md +13 -15
- package/docs/METADATA_CRUD_UI.md +1 -1
- package/docs/PORTAL_COCKPIT_PATTERNS.md +10 -145
- package/docs/REACTIVE_DOMAIN_FORMULAS.md +2 -2
- package/docs/releases/0.6.0.md +23 -0
- package/package.json +1 -1
- package/schemas/central.app.schema.json +2 -16
- package/src/conformance.js +42 -106
- package/src/index.js +4 -28
- package/src/render.js +4 -30
- package/templates/_base/README.md +7 -6
- package/templates/_base/backend/package.json +1 -1
- package/templates/_base/central.app.json +4 -9
- package/templates/_base/frontend/package.json +1 -1
- package/templates/_base/frontend/src/app/app.tsx +25 -0
- package/templates/_base/frontend/src/app/navigation.ts +5 -0
- package/templates/_base/frontend/src/app/routes.tsx +6 -0
- package/templates/_base/frontend/src/app/theme.ts +8 -0
- package/templates/_base/frontend/src/components/gitkeep +1 -0
- package/templates/_base/frontend/src/features/gitkeep +1 -0
- package/templates/_base/frontend/src/layouts/gitkeep +1 -0
- package/templates/_base/frontend/src/main.tsx +3 -8
- package/templates/_base/frontend/src/pages/HomePage.tsx +25 -0
- package/templates/_base/frontend/tsconfig.json +1 -1
- package/templates/_base/package.json +1 -1
- package/docs/CONNECTORS_AND_INTEGRATIONS.md +0 -66
- package/docs/FRONTEND_MANIFEST_REFERENCE.md +0 -487
- package/templates/_base/backend/src/integrations/gitkeep +0 -0
- package/templates/_base/frontend/central.ui.json +0 -58
- package/templates/_base/frontend/src/collections/gitkeep +0 -0
- package/templates/_base/frontend/src/dashboards/gitkeep +0 -0
- package/templates/_base/frontend/src/documents/gitkeep +0 -0
- package/templates/_base/frontend/src/overrides/gitkeep +0 -0
- package/templates/_base/frontend/src/pipelines/gitkeep +0 -0
- package/templates/basic/backend/src/models/Pessoa.js +0 -21
- package/templates/basic/frontend/central.ui.json +0 -8
- package/templates/documentos-fiscais/backend/central.config.js +0 -32
- package/templates/documentos-fiscais/backend/src/models/DocumentoFiscal.js +0 -23
- package/templates/documentos-fiscais/frontend/central.ui.json +0 -16
- package/templates/multi-moedas/backend/central.config.js +0 -32
- package/templates/multi-moedas/backend/src/models/Cotacao.js +0 -23
- package/templates/multi-moedas/backend/src/models/Moeda.js +0 -15
- package/templates/multi-moedas/frontend/central.ui.json +0 -11
- package/templates/omie-sidecar/backend/central.config.js +0 -33
- package/templates/omie-sidecar/backend/src/mappings/pessoa-cliente.js +0 -20
- package/templates/omie-sidecar/backend/src/models/Pessoa.js +0 -24
- package/templates/omie-sidecar/backend/src/triggers/Pessoa.js +0 -14
- package/templates/omie-sidecar/frontend/central.ui.json +0 -8
- package/templates/pedidos-marketplace/backend/central.config.js +0 -32
- package/templates/pedidos-marketplace/backend/src/models/Pedido.js +0 -22
- package/templates/pedidos-marketplace/backend/src/models/Produto.js +0 -16
- package/templates/pedidos-marketplace/frontend/central.ui.json +0 -10
- package/templates/servicos-prestados/backend/central.config.js +0 -32
- package/templates/servicos-prestados/backend/src/models/Cliente.js +0 -15
- package/templates/servicos-prestados/backend/src/models/ServicoPrestado.js +0 -22
- package/templates/servicos-prestados/frontend/central.ui.json +0 -10
- package/templates/servicos-tomados/backend/central.config.js +0 -32
- package/templates/servicos-tomados/backend/src/models/Pessoa.js +0 -15
- package/templates/servicos-tomados/backend/src/models/Servico.js +0 -22
- package/templates/servicos-tomados/frontend/central.ui.json +0 -10
|
@@ -1,150 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Portais e cockpits code-first
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Portais e cockpits usam o mesmo contrato de frontend dos demais Apps. Defina o
|
|
4
|
+
App com `defineOonApp`, componha rotas com `defineOonRoutes` e inicialize com
|
|
5
|
+
`startOonApp`.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Use páginas locais para jornadas transversais e components do Core para
|
|
8
|
+
autenticação, autorização, tenant, shell, estados e acesso HTTP. O `appKind` não
|
|
9
|
+
cria uma exceção arquitetural nem restringe customização local.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Para jornadas multi-tenant, passe o tenant específico da operação ao cliente
|
|
12
|
+
HTTP. A seleção visual nunca substitui a autorização do backend.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
Consulte `FRONTEND_CODE_FIRST.md`, `FRONTEND_API.md` e `RBAC_SECURITY.md`.
|
|
10
15
|
|
|
11
|
-
| Perfil | Uso |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| `root-central` | Central de Ativações, raiz de confiança. |
|
|
14
|
-
| `member-central` | Central cliente/licenciada, ativada por instância. |
|
|
15
|
-
| `portal-cockpit` | Portal/Cockpit first-party, autenticado por AppClient/BFF. |
|
|
16
|
-
|
|
17
|
-
## Auth modes
|
|
18
|
-
|
|
19
|
-
| `auth.mode` | Uso |
|
|
20
|
-
| --- | --- |
|
|
21
|
-
| `bearer` | Token local simples, compatível com Centrais existentes. |
|
|
22
|
-
| `cookie` | Sessão futura via cookie HTTP-only. |
|
|
23
|
-
| `external-sso` | Redireciona para login externo. |
|
|
24
|
-
| `central-instance` | Central membro usando instância ativada. |
|
|
25
|
-
| `central-client` | Portal/Cockpit usando AppClient/BFF. |
|
|
26
|
-
|
|
27
|
-
## Capabilities no manifesto
|
|
28
|
-
|
|
29
|
-
Além de `permissions`, o manifesto pode declarar `capabilities`. Elas são permissões dinâmicas vindas da Central de Ativações e evitam criar campos fixos para cada produto.
|
|
30
|
-
|
|
31
|
-
Exemplos:
|
|
32
|
-
|
|
33
|
-
```txt
|
|
34
|
-
apps:read
|
|
35
|
-
users:manage
|
|
36
|
-
tickets:read
|
|
37
|
-
tickets:assign
|
|
38
|
-
copilots:read
|
|
39
|
-
copilots:test
|
|
40
|
-
billing:read
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
O Core trata `permissions` e `capabilities` como requisitos de UI. A segurança real continua no backend.
|
|
44
|
-
|
|
45
|
-
## Manifesto recomendado
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{
|
|
49
|
-
"schemaVersion": 2,
|
|
50
|
-
"name": "Portal Cliente",
|
|
51
|
-
"slug": "portal-cliente",
|
|
52
|
-
"appKind": "portal-cockpit",
|
|
53
|
-
"auth": {
|
|
54
|
-
"mode": "central-client",
|
|
55
|
-
"tokenParam": "code"
|
|
56
|
-
},
|
|
57
|
-
"layout": {
|
|
58
|
-
"shell": "portal",
|
|
59
|
-
"sidebar": "core",
|
|
60
|
-
"topbar": "none",
|
|
61
|
-
"header": "none",
|
|
62
|
-
"footer": "core"
|
|
63
|
-
},
|
|
64
|
-
"navigation": {
|
|
65
|
-
"mode": "manual",
|
|
66
|
-
"items": [
|
|
67
|
-
{ "label": "Meus Apps", "href": "/apps", "capabilities": ["apps:read"], "order": 10 },
|
|
68
|
-
{ "label": "Suporte", "href": "/suporte", "capabilities": ["tickets:read"], "order": 20 },
|
|
69
|
-
{ "label": "Copilotos", "href": "/copilotos", "capabilities": ["copilots:read"], "order": 30 },
|
|
70
|
-
{ "label": "Usuários", "href": "/usuarios", "capabilities": ["users:manage"], "order": 40 }
|
|
71
|
-
]
|
|
72
|
-
},
|
|
73
|
-
"pages": [
|
|
74
|
-
{ "path": "/apps", "label": "Meus Apps", "component": "AppsPortalPage", "capabilities": ["apps:read"] },
|
|
75
|
-
{ "path": "/suporte", "label": "Suporte", "component": "SupportPage", "capabilities": ["tickets:read"] },
|
|
76
|
-
{ "path": "/copilotos", "label": "Copilotos", "component": "CopilotsPage", "capabilities": ["copilots:read"] },
|
|
77
|
-
{ "path": "/usuarios", "label": "Usuários", "component": "UsersPermissionsPage", "capabilities": ["users:manage"] }
|
|
78
|
-
],
|
|
79
|
-
"collections": [],
|
|
80
|
-
"pipelines": [],
|
|
81
|
-
"documents": []
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Bootstrap recomendado
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { startFromManifest } from "@oondemand/oon-core-front";
|
|
89
|
-
import manifest from "../central.ui.json";
|
|
90
|
-
import { AppsPortalPage } from "./custom/AppsPortalPage";
|
|
91
|
-
import { SupportPage } from "./custom/SupportPage";
|
|
92
|
-
import { CopilotsPage } from "./custom/CopilotsPage";
|
|
93
|
-
import { UsersPermissionsPage } from "./custom/UsersPermissionsPage";
|
|
94
|
-
|
|
95
|
-
startFromManifest(manifest, {
|
|
96
|
-
apiBaseUrl: import.meta.env.VITE_API_URL,
|
|
97
|
-
appKind: "portal-cockpit",
|
|
98
|
-
auth: {
|
|
99
|
-
mode: "central-client"
|
|
100
|
-
},
|
|
101
|
-
customComponents: {
|
|
102
|
-
AppsPortalPage,
|
|
103
|
-
SupportPage,
|
|
104
|
-
CopilotsPage,
|
|
105
|
-
UsersPermissionsPage
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Contrato esperado do BFF
|
|
111
|
-
|
|
112
|
-
O frontend conversa apenas com o BFF do portal. O BFF fala com a Central de Ativações usando AppClient.
|
|
113
|
-
|
|
114
|
-
Rotas genéricas esperadas no BFF podem espelhar a Central de Ativações:
|
|
115
|
-
|
|
116
|
-
```http
|
|
117
|
-
GET /api/portal/contexto
|
|
118
|
-
GET /api/portal/apps
|
|
119
|
-
GET /api/portal/apps/:appCode
|
|
120
|
-
GET /api/portal/apps/:appCode/capabilities
|
|
121
|
-
POST /api/portal/apps/:appCode/authorize
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
O BFF também pode expor rotas de domínio próprias, como:
|
|
125
|
-
|
|
126
|
-
```http
|
|
127
|
-
GET /api/suporte/tickets
|
|
128
|
-
POST /api/suporte/tickets
|
|
129
|
-
GET /api/copilotos/assistentes
|
|
130
|
-
POST /api/copilotos/assistentes/:id/testar
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Essas rotas de domínio validam capability na Central de Ativações antes de executar a ação.
|
|
134
|
-
|
|
135
|
-
## Regra de segurança
|
|
136
|
-
|
|
137
|
-
O frontend nunca deve receber `clientSecret`, `x-oon-instance-token`, hash de credencial ou segredo completo. Portais devem falar com um BFF próprio, e o BFF fala com a Central de Ativações usando AppClient.
|
|
138
|
-
|
|
139
|
-
## Quando usar página custom
|
|
140
|
-
|
|
141
|
-
Use página custom apenas quando a tela não for CRUD/esteira/documento declarativo, por exemplo:
|
|
142
|
-
|
|
143
|
-
- cards de apps licenciados;
|
|
144
|
-
- matriz de permissões;
|
|
145
|
-
- cockpit de status;
|
|
146
|
-
- tickets de suporte;
|
|
147
|
-
- gestão de copilotos;
|
|
148
|
-
- onboarding orientado por negócio.
|
|
149
|
-
|
|
150
|
-
Mesmo nesses casos, o Shell, Router, Auth, Menu, Guards e SDK HTTP devem continuar no Core.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A partir do contrato declarativo de domínio, campos com `computed` são recalculados imediatamente nos formulários padrão do `@oondemand/oon-core-front`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
O App não precisa repetir a fórmula no frontend nem criar componentes React específicos. A declaração continua existindo uma única vez no `central.domain.json` do backend.
|
|
6
6
|
|
|
7
7
|
## Fluxo
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ A Central não precisa repetir a fórmula em `central.ui.json` nem criar compone
|
|
|
21
21
|
- formulário principal em modal com abas (`CoreTabbedDetail`);
|
|
22
22
|
- criação e edição;
|
|
23
23
|
- formulários derivados integralmente da metadata;
|
|
24
|
-
- formulários com
|
|
24
|
+
- formulários com apresentação local, desde que a model continue sendo carregada pela metadata do Core.
|
|
25
25
|
|
|
26
26
|
## Exemplo
|
|
27
27
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# OonCore 0.6.0
|
|
2
|
+
|
|
3
|
+
A linha 0.6 substitui o bootstrap orientado a manifesto por uma API pública code-first, preservando a governança da Plataforma Oon.
|
|
4
|
+
|
|
5
|
+
## Destaques
|
|
6
|
+
|
|
7
|
+
- bootstrap com `defineOonApp` e `startOonApp`;
|
|
8
|
+
- rotas, menus, páginas, componentes e temas tipados;
|
|
9
|
+
- scaffold único e neutro em `_base`;
|
|
10
|
+
- `central.app.json` schema v2;
|
|
11
|
+
- remoção integral dos templates selecionáveis, Omie e provider genérico de integrações;
|
|
12
|
+
- resolução de capabilities técnicas pela Plataforma Oon, sem expor detalhes de implementação aos Apps;
|
|
13
|
+
- documentação distribuída atualizada para desenvolvimento assistido por Agents.
|
|
14
|
+
|
|
15
|
+
## Upgrade
|
|
16
|
+
|
|
17
|
+
- alinhe `@oondemand/oon-core-back`, `@oondemand/oon-core-front` e `@oondemand/create-central-oon` na linha `0.6.x`;
|
|
18
|
+
- declare compatibilidade `>=0.6.0 <0.7.0`;
|
|
19
|
+
- migre o frontend para `defineOonApp`/`startOonApp`;
|
|
20
|
+
- remova flags e módulos legados;
|
|
21
|
+
- execute docs check, conformance, typecheck, testes e smoke offline antes da publicação.
|
|
22
|
+
|
|
23
|
+
A versão 0.6 é incompatível com os contratos removidos da linha 0.5.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://schemas.oondemand.com.br/central.app.
|
|
3
|
+
"$id": "https://schemas.oondemand.com.br/central.app.v2.schema.json",
|
|
4
4
|
"title": "Oon Central App Manifest",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"type": "string"
|
|
20
20
|
},
|
|
21
21
|
"schemaVersion": {
|
|
22
|
-
"const":
|
|
22
|
+
"const": 2
|
|
23
23
|
},
|
|
24
24
|
"id": {
|
|
25
25
|
"type": "string",
|
|
@@ -67,14 +67,6 @@
|
|
|
67
67
|
"rbac"
|
|
68
68
|
]
|
|
69
69
|
},
|
|
70
|
-
"accessPolicy": {
|
|
71
|
-
"enum": [
|
|
72
|
-
"tenant_members",
|
|
73
|
-
"explicit_users",
|
|
74
|
-
"rbac"
|
|
75
|
-
],
|
|
76
|
-
"description": "Compatibilidade legada. Novos manifestos devem usar accessMode e authorizationMode."
|
|
77
|
-
},
|
|
78
70
|
"deploymentMode": {
|
|
79
71
|
"enum": [
|
|
80
72
|
"singleton",
|
|
@@ -105,12 +97,6 @@
|
|
|
105
97
|
"pipelines": {
|
|
106
98
|
"type": "boolean"
|
|
107
99
|
},
|
|
108
|
-
"integrations": {
|
|
109
|
-
"type": "boolean"
|
|
110
|
-
},
|
|
111
|
-
"omie": {
|
|
112
|
-
"type": "boolean"
|
|
113
|
-
},
|
|
114
100
|
"assistants": {
|
|
115
101
|
"type": "boolean"
|
|
116
102
|
},
|
package/src/conformance.js
CHANGED
|
@@ -5,19 +5,7 @@ const path = require("node:path");
|
|
|
5
5
|
|
|
6
6
|
const IGNORED_DIRS = new Set([".git", ".ooncore", "node_modules", "dist", "build", "coverage"]);
|
|
7
7
|
const EXECUTABLE_EXTENSIONS = new Set([".js", ".cjs", ".mjs", ".ts", ".tsx", ".jsx"]);
|
|
8
|
-
const
|
|
9
|
-
"IntegrationExecution",
|
|
10
|
-
"IntegrationOutbox",
|
|
11
|
-
"IntegrationInbox",
|
|
12
|
-
"WebhookInbox",
|
|
13
|
-
"ExecutionHistory",
|
|
14
|
-
"ProviderRegistry",
|
|
15
|
-
"DistributedLock",
|
|
16
|
-
"OmieConfiguracao",
|
|
17
|
-
"OmieCategoria",
|
|
18
|
-
"OmieContaCorrente",
|
|
19
|
-
"OmieBaixaPagamento",
|
|
20
|
-
]);
|
|
8
|
+
const PUBLIC_FRONT_SUBPATHS = new Set(["ui", "routing", "theme", "hooks", "testing"]);
|
|
21
9
|
|
|
22
10
|
function normalize(relativePath) {
|
|
23
11
|
return relativePath.split(path.sep).join("/");
|
|
@@ -49,15 +37,15 @@ function fallbackValidateAppManifest(manifest) {
|
|
|
49
37
|
];
|
|
50
38
|
const missing = required.filter((key) => manifest?.[key] === undefined);
|
|
51
39
|
if (missing.length) throw new Error(`campos obrigatórios ausentes: ${missing.join(", ")}.`);
|
|
52
|
-
if (manifest.schemaVersion !==
|
|
40
|
+
if (manifest.schemaVersion !== 2) throw new Error("schemaVersion deve ser 2.");
|
|
53
41
|
if (!Array.isArray(manifest.capabilities)) throw new Error("capabilities deve ser uma lista.");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!hasCanonicalAccess && !hasLegacyAccess) {
|
|
57
|
-
throw new Error("informe accessMode e authorizationMode (ou accessPolicy em manifesto legado).");
|
|
42
|
+
if (manifest.accessMode === undefined || manifest.authorizationMode === undefined) {
|
|
43
|
+
throw new Error("informe accessMode e authorizationMode.");
|
|
58
44
|
}
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
const supportedModules = new Set(["collections", "documents", "pipelines", "assistants", "currencies"]);
|
|
46
|
+
for (const [moduleName, enabled] of Object.entries(manifest.modules || {})) {
|
|
47
|
+
if (!supportedModules.has(moduleName)) throw new Error(`módulo não suportado: ${moduleName}.`);
|
|
48
|
+
if (typeof enabled !== "boolean") throw new Error(`modules.${moduleName} deve ser booleano.`);
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
const optionalDimensions = {
|
|
@@ -78,16 +66,11 @@ function fallbackValidateAppManifest(manifest) {
|
|
|
78
66
|
"member-central": { tenancyModel: "single_tenant", availability: "licensed", accessMode: "explicit_users", authorizationMode: "rbac", deploymentMode: "dedicated" },
|
|
79
67
|
"portal-cockpit": { tenancyModel: "multi_tenant", availability: "common", accessMode: "tenant_members", authorizationMode: "none", deploymentMode: "shared" },
|
|
80
68
|
}[manifest.appKind] || {};
|
|
81
|
-
const legacy = {
|
|
82
|
-
tenant_members: { accessMode: "tenant_members", authorizationMode: "none" },
|
|
83
|
-
explicit_users: { accessMode: "explicit_users", authorizationMode: "none" },
|
|
84
|
-
rbac: { accessMode: "explicit_users", authorizationMode: "rbac" },
|
|
85
|
-
}[manifest.accessPolicy] || {};
|
|
86
69
|
const dimensions = {
|
|
87
70
|
tenancyModel: manifest.tenancyModel || defaults.tenancyModel,
|
|
88
71
|
availability: manifest.availability || defaults.availability,
|
|
89
|
-
accessMode: manifest.accessMode ||
|
|
90
|
-
authorizationMode: manifest.authorizationMode ||
|
|
72
|
+
accessMode: manifest.accessMode || defaults.accessMode,
|
|
73
|
+
authorizationMode: manifest.authorizationMode || defaults.authorizationMode,
|
|
91
74
|
deploymentMode: manifest.deploymentMode || defaults.deploymentMode,
|
|
92
75
|
};
|
|
93
76
|
if (dimensions.tenancyModel === "none" && dimensions.accessMode === "tenant_members") {
|
|
@@ -137,72 +120,21 @@ function checkAppManifest(root, issues) {
|
|
|
137
120
|
}
|
|
138
121
|
}
|
|
139
122
|
|
|
140
|
-
function appKind(root) {
|
|
141
|
-
try {
|
|
142
|
-
return JSON.parse(readText(root, "central.app.json")).appKind;
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function isPortalExtension(candidate) {
|
|
149
|
-
return candidate.startsWith("frontend/src/portal/");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
123
|
function checkBootstrap(root, files, issues) {
|
|
153
|
-
const portalCockpit = appKind(root) === "portal-cockpit";
|
|
154
124
|
const file = files.find((candidate) => /^frontend\/src\/main\.(?:t|j)sx?$/.test(candidate));
|
|
155
125
|
if (!file) {
|
|
156
126
|
addIssue(issues, "FRONTEND_BOOTSTRAP_REQUIRED", "frontend/src/main.tsx", "bootstrap gerado não encontrado.");
|
|
157
127
|
return;
|
|
158
128
|
}
|
|
159
129
|
const source = readText(root, file);
|
|
160
|
-
if (!/\
|
|
130
|
+
if (!/\bstartOonApp\b/.test(source)) {
|
|
161
131
|
addIssue(
|
|
162
132
|
issues,
|
|
163
133
|
"FRONTEND_BOOTSTRAP_INVALID",
|
|
164
134
|
file,
|
|
165
|
-
"deve iniciar o Core com
|
|
135
|
+
"deve iniciar o Core com startOonApp.",
|
|
166
136
|
);
|
|
167
137
|
}
|
|
168
|
-
const forbidden = [
|
|
169
|
-
/structuredClone\s*\(/,
|
|
170
|
-
/\b(?:configure|prepare|transform)[A-Z_a-z0-9]*\s*\(/,
|
|
171
|
-
/\b(?:registry|pageComponents|cellRenderers|fieldRenderers|cardRenderers|layoutSlots)\b/,
|
|
172
|
-
/\.find\s*\(/,
|
|
173
|
-
/\.push\s*\(/,
|
|
174
|
-
/Object\.assign\s*\(/,
|
|
175
|
-
];
|
|
176
|
-
for (const pattern of forbidden) {
|
|
177
|
-
if (pattern.test(source)) {
|
|
178
|
-
addIssue(issues, "FRONTEND_BOOTSTRAP_TRANSFORM", file, `transform local detectado: ${pattern}.`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const localImports = [...source.matchAll(/from\s+["'](\.\.?\/[^"']+)["']/g)]
|
|
182
|
-
.map((match) => match[1])
|
|
183
|
-
.filter((value) => !value.endsWith(".json"));
|
|
184
|
-
const forbiddenLocalImports = portalCockpit
|
|
185
|
-
? localImports.filter((value) => !value.startsWith("./portal/"))
|
|
186
|
-
: localImports;
|
|
187
|
-
if (forbiddenLocalImports.length) {
|
|
188
|
-
addIssue(
|
|
189
|
-
issues,
|
|
190
|
-
"FRONTEND_BOOTSTRAP_LOCAL_CODE",
|
|
191
|
-
file,
|
|
192
|
-
`imports locais executáveis não permitidos: ${forbiddenLocalImports.join(", ")}.`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const extraFrontendSources = files.filter(
|
|
197
|
-
(candidate) => candidate.startsWith("frontend/src/")
|
|
198
|
-
&& candidate !== file
|
|
199
|
-
&& candidate !== "frontend/src/vite-env.d.ts"
|
|
200
|
-
&& path.basename(candidate) !== ".gitkeep"
|
|
201
|
-
&& !(portalCockpit && isPortalExtension(candidate)),
|
|
202
|
-
);
|
|
203
|
-
for (const extra of extraFrontendSources) {
|
|
204
|
-
addIssue(issues, "FRONTEND_LOCAL_EXTENSION", extra, "frontend declarativo deve manter somente o bootstrap gerado.");
|
|
205
|
-
}
|
|
206
138
|
}
|
|
207
139
|
|
|
208
140
|
function checkCentralConfig(root, files, issues) {
|
|
@@ -222,42 +154,46 @@ function checkCentralConfig(root, files, issues) {
|
|
|
222
154
|
}
|
|
223
155
|
}
|
|
224
156
|
|
|
225
|
-
function checkDomainManifest(root, issues) {
|
|
226
|
-
const file = "backend/central.domain.json";
|
|
227
|
-
if (!fs.existsSync(path.join(root, file))) return;
|
|
228
|
-
try {
|
|
229
|
-
const manifest = JSON.parse(readText(root, file));
|
|
230
|
-
for (const model of manifest.models || []) {
|
|
231
|
-
if (TECHNICAL_MODEL_NAMES.has(model.name) || /(?:Outbox|Inbox|Execution|Provider|Lock|Webhook|Omie)/i.test(model.name)) {
|
|
232
|
-
addIssue(issues, "TECHNICAL_MODEL", file, `model técnica não permitida na Central: ${model.name}.`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
} catch (error) {
|
|
236
|
-
addIssue(issues, "DOMAIN_MANIFEST_INVALID", file, error.message);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
157
|
function checkExecutableFiles(root, files, issues) {
|
|
241
|
-
const suspiciousPath = /(?:^|\/)(?:workers?|runtime|registr(?:y|ies)|clients?|patches?|infrastructure|infra)(?:\/|\.)/i;
|
|
242
|
-
const omieClient = /(?:^|\/)(?:omie[-_.]?(?:client|http|service)|clients?\/omie)(?:\.|\/)/i;
|
|
243
|
-
const genericPage = /^frontend\/src\/(?:pages|components)\/(?:Core|Integration|Integracao|Settings|Configurac)/i;
|
|
244
158
|
const mongoosePatch = /mongoose\.(?:Model|Query|Document)\.prototype|\.prototype\.(?:save|updateOne|findOneAndUpdate)\s*=|patchMongoose/i;
|
|
245
|
-
const
|
|
159
|
+
const remoteCode = /(?:import\s*\(\s*|from\s+|src\s*=\s*)["']https?:\/\//i;
|
|
160
|
+
const dynamicCode = /\b(?:eval|Function)\s*\(/;
|
|
161
|
+
const secretInBundle = /(?:import\.meta\.env|process\.env)\.[A-Z0-9_]*(?:SECRET|PASSWORD|TOKEN|PRIVATE_KEY|API_KEY)/;
|
|
162
|
+
const disabledGuard = /enable(?:RouteGuard|PermissionGate)\s*:\s*false/;
|
|
246
163
|
|
|
247
164
|
for (const file of files) {
|
|
248
165
|
if (!EXECUTABLE_EXTENSIONS.has(path.extname(file))) continue;
|
|
249
|
-
if (suspiciousPath.test(file)) addIssue(issues, "GENERIC_RUNTIME", file, "runtime/worker/registry local não permitido.");
|
|
250
|
-
if (omieClient.test(file)) addIssue(issues, "OMIE_CLIENT", file, "cliente Omie deve pertencer ao adaptador nativo do Core.");
|
|
251
|
-
if (genericPage.test(file)) addIssue(issues, "GENERIC_PAGE", file, "página genérica deve pertencer ao OonCore.");
|
|
252
|
-
|
|
253
166
|
const source = readText(root, file);
|
|
254
167
|
if (mongoosePatch.test(source)) addIssue(issues, "MONGOOSE_PATCH", file, "patch de Mongoose não permitido na Central.");
|
|
255
|
-
if (file
|
|
256
|
-
|
|
168
|
+
if (!file.startsWith("frontend/")) continue;
|
|
169
|
+
if (remoteCode.test(source) || dynamicCode.test(source)) addIssue(issues, "FRONTEND_REMOTE_CODE", file, "código remoto ou avaliação dinâmica não é permitido no bundle.");
|
|
170
|
+
if (secretInBundle.test(source)) addIssue(issues, "FRONTEND_SECRET", file, "secret, token, senha ou chave privada não pode entrar no bundle frontend.");
|
|
171
|
+
if (disabledGuard.test(source)) addIssue(issues, "FRONTEND_SECURITY_GUARD", file, "guards obrigatórios do OonCore não podem ser desabilitados.");
|
|
172
|
+
for (const match of source.matchAll(/@oondemand\/oon-core-front\/([^"'\s)]+)/g)) {
|
|
173
|
+
const subpath = match[1].split("/")[0];
|
|
174
|
+
if (!PUBLIC_FRONT_SUBPATHS.has(subpath)) addIssue(issues, "FRONTEND_INTERNAL_IMPORT", file, `subpath interno do OonCore não permitido: ${match[0]}.`);
|
|
257
175
|
}
|
|
258
176
|
}
|
|
259
177
|
}
|
|
260
178
|
|
|
179
|
+
function checkFrontendDependencies(root, issues) {
|
|
180
|
+
const file = "frontend/package.json";
|
|
181
|
+
if (!fs.existsSync(path.join(root, file))) return;
|
|
182
|
+
try {
|
|
183
|
+
const manifest = JSON.parse(readText(root, file));
|
|
184
|
+
const dependencies = { ...manifest.dependencies, ...manifest.devDependencies, ...manifest.optionalDependencies };
|
|
185
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
186
|
+
if (typeof spec !== "string" || !spec.startsWith("file:")) continue;
|
|
187
|
+
const target = path.resolve(root, "frontend", spec.slice(5));
|
|
188
|
+
const relative = path.relative(path.join(root, "frontend"), target);
|
|
189
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) continue;
|
|
190
|
+
addIssue(issues, "FRONTEND_FILE_DEPENDENCY", file, `dependência ${name} aponta para fora de frontend/: ${spec}.`);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
addIssue(issues, "FRONTEND_PACKAGE_INVALID", file, error.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
261
197
|
function checkAllowedExtensions(files, issues) {
|
|
262
198
|
const allowed = new Set([
|
|
263
199
|
"", ".json", ".md", ".txt", ".yml", ".yaml", ".js", ".cjs", ".mjs", ".ts", ".tsx", ".jsx",
|
|
@@ -295,8 +231,8 @@ function checkConformance({ cwd = process.cwd() } = {}) {
|
|
|
295
231
|
checkAppManifest(root, issues);
|
|
296
232
|
checkBootstrap(root, files, issues);
|
|
297
233
|
checkCentralConfig(root, files, issues);
|
|
298
|
-
checkDomainManifest(root, issues);
|
|
299
234
|
checkExecutableFiles(root, files, issues);
|
|
235
|
+
checkFrontendDependencies(root, issues);
|
|
300
236
|
checkAllowedExtensions(files, issues);
|
|
301
237
|
checkLocalRuntimeSafety(root, files, issues);
|
|
302
238
|
return { ok: issues.length === 0, root, filesChecked: files.length, issues };
|
package/src/index.js
CHANGED
|
@@ -11,20 +11,6 @@ const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
|
11
11
|
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
|
|
12
12
|
const DOCS_DIR = path.join(PACKAGE_ROOT, "docs");
|
|
13
13
|
|
|
14
|
-
const TEMPLATES = [
|
|
15
|
-
{ id: "basic", description: "Central mínima com uma coleção dinâmica (Pessoa)." },
|
|
16
|
-
{ id: "omie-sidecar", description: "Coleção + integração Omie (mapping de cliente)." },
|
|
17
|
-
{ id: "servicos-tomados", description: "Serviços tomados com esteira por status." },
|
|
18
|
-
{ id: "servicos-prestados", description: "Serviços prestados com esteira por etapa." },
|
|
19
|
-
{ id: "pedidos-marketplace", description: "Pedidos de marketplace com esteira." },
|
|
20
|
-
{ id: "documentos-fiscais", description: "Documentos fiscais com aprovação." },
|
|
21
|
-
{ id: "multi-moedas", description: "Moedas + cotações (módulo currencies)." },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function listTemplates() {
|
|
25
|
-
return TEMPLATES;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
14
|
function toSlug(value) {
|
|
29
15
|
return String(value)
|
|
30
16
|
.normalize("NFD")
|
|
@@ -307,13 +293,6 @@ function checkDocs(opts = {}) {
|
|
|
307
293
|
}
|
|
308
294
|
|
|
309
295
|
async function run(opts) {
|
|
310
|
-
const template = TEMPLATES.find((t) => t.id === opts.template);
|
|
311
|
-
if (!template) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`Template "${opts.template}" não existe. Use --list para ver os disponíveis.`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
296
|
const slug = toSlug(opts.name);
|
|
318
297
|
if (!slug) throw new Error("Nome de Central inválido.");
|
|
319
298
|
|
|
@@ -328,14 +307,12 @@ async function run(opts) {
|
|
|
328
307
|
}
|
|
329
308
|
}
|
|
330
309
|
|
|
331
|
-
console.log(`\n⚙ Criando
|
|
310
|
+
console.log(`\n⚙ Criando oon-app "${tokens.name}"`);
|
|
332
311
|
|
|
333
|
-
//
|
|
312
|
+
// Fundação canônica única. Não existem overlays ou templates funcionais.
|
|
334
313
|
copyTemplate(path.join(TEMPLATES_DIR, "_base"), targetDir, tokens);
|
|
335
|
-
// 2. Overlay do template escolhido (vence sobre o _base).
|
|
336
|
-
copyTemplate(path.join(TEMPLATES_DIR, template.id), targetDir, tokens, { mergeCentralUi: true });
|
|
337
314
|
|
|
338
|
-
//
|
|
315
|
+
// Contexto local neutro para Agents derivado da versão instalada.
|
|
339
316
|
syncDocs({ cwd: targetDir });
|
|
340
317
|
|
|
341
318
|
console.log("✔ Arquivos gerados.");
|
|
@@ -371,13 +348,12 @@ ${cd} # Sincronizar/validar documentação local do Core para Agents
|
|
|
371
348
|
npm run dev
|
|
372
349
|
|
|
373
350
|
O backend expõe /core/metadata; o frontend renderiza as telas a partir dele.
|
|
374
|
-
|
|
351
|
+
Crie páginas, componentes e features em frontend/src e modele o domínio no backend.
|
|
375
352
|
`);
|
|
376
353
|
}
|
|
377
354
|
|
|
378
355
|
module.exports = {
|
|
379
356
|
run,
|
|
380
|
-
listTemplates,
|
|
381
357
|
toSlug,
|
|
382
358
|
toPascal,
|
|
383
359
|
toTitle,
|
package/src/render.js
CHANGED
|
@@ -42,32 +42,12 @@ function applyTokens(content, tokens) {
|
|
|
42
42
|
.replace(/__PASCAL__/g, tokens.pascal);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function mergePages(basePages = [], overlayPages = []) {
|
|
46
|
-
const merged = [...basePages];
|
|
47
|
-
for (const page of overlayPages) {
|
|
48
|
-
const index = page?.path
|
|
49
|
-
? merged.findIndex((candidate) => candidate?.path === page.path)
|
|
50
|
-
: -1;
|
|
51
|
-
if (index >= 0) merged[index] = page;
|
|
52
|
-
else merged.push(page);
|
|
53
|
-
}
|
|
54
|
-
return merged;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function mergeCentralUiManifest(base, overlay) {
|
|
58
|
-
return {
|
|
59
|
-
...base,
|
|
60
|
-
...overlay,
|
|
61
|
-
pages: mergePages(base.pages, overlay.pages),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
45
|
/**
|
|
66
46
|
* Copia recursivamente `srcDir` para `destDir`, substituindo tokens em
|
|
67
47
|
* arquivos de texto. Arquivos já existentes no destino são sobrescritos
|
|
68
48
|
* (overlay): o template escolhido vence sobre o _base.
|
|
69
49
|
*/
|
|
70
|
-
function copyTemplate(srcDir, destDir, tokens
|
|
50
|
+
function copyTemplate(srcDir, destDir, tokens) {
|
|
71
51
|
if (!fs.existsSync(srcDir)) return;
|
|
72
52
|
fs.mkdirSync(destDir, { recursive: true });
|
|
73
53
|
|
|
@@ -79,18 +59,12 @@ function copyTemplate(srcDir, destDir, tokens, options = {}) {
|
|
|
79
59
|
const to = path.join(destDir, destName);
|
|
80
60
|
|
|
81
61
|
if (entry.isDirectory()) {
|
|
82
|
-
copyTemplate(from, to, tokens
|
|
62
|
+
copyTemplate(from, to, tokens);
|
|
83
63
|
} else {
|
|
84
64
|
const raw = fs.readFileSync(from);
|
|
85
65
|
if (isText(from)) {
|
|
86
66
|
const rendered = applyTokens(raw.toString("utf8"), tokens);
|
|
87
|
-
|
|
88
|
-
const base = JSON.parse(fs.readFileSync(to, "utf8"));
|
|
89
|
-
const overlay = JSON.parse(rendered);
|
|
90
|
-
fs.writeFileSync(to, JSON.stringify(mergeCentralUiManifest(base, overlay), null, 2) + "\n");
|
|
91
|
-
} else {
|
|
92
|
-
fs.writeFileSync(to, rendered);
|
|
93
|
-
}
|
|
67
|
+
fs.writeFileSync(to, rendered);
|
|
94
68
|
} else {
|
|
95
69
|
fs.writeFileSync(to, raw);
|
|
96
70
|
}
|
|
@@ -98,4 +72,4 @@ function copyTemplate(srcDir, destDir, tokens, options = {}) {
|
|
|
98
72
|
}
|
|
99
73
|
}
|
|
100
74
|
|
|
101
|
-
module.exports = { copyTemplate, applyTokens, isText
|
|
75
|
+
module.exports = { copyTemplate, applyTokens, isText };
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# __NAME__
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
App Oon code-first gerado com `create-central-oon`.
|
|
4
4
|
|
|
5
5
|
## Fronteira arquitetural
|
|
6
6
|
|
|
7
7
|
- `central.app.json` declara identidade, `appKind`, módulos, capabilities e compatibilidade com o OonCore;
|
|
8
8
|
- `backend/central.domain.json` declara models e regras do domínio;
|
|
9
|
-
- `frontend/
|
|
9
|
+
- `frontend/src/app` compõe rotas, navegação, layouts e temas;
|
|
10
|
+
- `frontend/src/{pages,components,features}` contém a experiência local;
|
|
10
11
|
- `backend/central.config.js` fica reservado a extensões excepcionais de runtime;
|
|
11
|
-
- `frontend/src/main.tsx`
|
|
12
|
+
- `frontend/src/main.tsx` chama o bootstrap code-first `startOonApp`.
|
|
12
13
|
|
|
13
|
-
Autenticação,
|
|
14
|
+
Autenticação, guards, shell, RBAC, metadata, CRUD e infraestrutura pertencem ao OonCore.
|
|
14
15
|
|
|
15
16
|
## Documentação e conformidade
|
|
16
17
|
|
|
@@ -39,8 +40,8 @@ O comando inicia backend e frontend exclusivamente em loopback, abre uma sessão
|
|
|
39
40
|
## Evoluindo a Central
|
|
40
41
|
|
|
41
42
|
1. Declare domínio e fórmulas em `backend/central.domain.json`.
|
|
42
|
-
2.
|
|
43
|
+
2. Crie rotas, páginas, componentes, layouts e temas no frontend.
|
|
43
44
|
3. Use validações, triggers, hooks, mappings e regras somente para comportamento específico do negócio.
|
|
44
45
|
4. Execute `npm run check` antes de abrir o PR.
|
|
45
46
|
|
|
46
|
-
O OonCore
|
|
47
|
+
O OonCore fornece a fundação; o App implementa sua experiência e seu domínio.
|