@onroad/core 4.0.0-alpha.4 → 4.0.0-alpha.40
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 +604 -54
- package/dist/OnRoadExpress.d.ts.map +1 -1
- package/dist/OnRoadExpress.js +18 -5
- package/dist/OnRoadExpress.js.map +1 -1
- package/dist/core/AbstractController.d.ts +10 -0
- package/dist/core/AbstractController.d.ts.map +1 -1
- package/dist/core/AbstractController.js +48 -1
- package/dist/core/AbstractController.js.map +1 -1
- package/dist/core/AbstractService.d.ts +5 -0
- package/dist/core/AbstractService.d.ts.map +1 -1
- package/dist/core/AbstractService.js +36 -0
- package/dist/core/AbstractService.js.map +1 -1
- package/dist/core/SequelizeRepository.d.ts +5 -1
- package/dist/core/SequelizeRepository.d.ts.map +1 -1
- package/dist/core/SequelizeRepository.js +26 -1
- package/dist/core/SequelizeRepository.js.map +1 -1
- package/dist/database/ConnectionManager.d.ts +1 -0
- package/dist/database/ConnectionManager.d.ts.map +1 -1
- package/dist/database/ConnectionManager.js +10 -1
- package/dist/database/ConnectionManager.js.map +1 -1
- package/dist/dev/DevServer.d.ts +67 -0
- package/dist/dev/DevServer.d.ts.map +1 -0
- package/dist/dev/DevServer.js +496 -0
- package/dist/dev/DevServer.js.map +1 -0
- package/dist/dev/DevToolsController.d.ts +45 -0
- package/dist/dev/DevToolsController.d.ts.map +1 -0
- package/dist/dev/DevToolsController.js +426 -0
- package/dist/dev/DevToolsController.js.map +1 -0
- package/dist/dev/DevToolsPlugin.d.ts +10 -0
- package/dist/dev/DevToolsPlugin.d.ts.map +1 -0
- package/dist/dev/DevToolsPlugin.js +23 -0
- package/dist/dev/DevToolsPlugin.js.map +1 -0
- package/dist/dev/MigrationCLI.d.ts +19 -0
- package/dist/dev/MigrationCLI.d.ts.map +1 -0
- package/dist/dev/MigrationCLI.js +140 -0
- package/dist/dev/MigrationCLI.js.map +1 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/entity/EntityRegistry.d.ts +26 -0
- package/dist/entity/EntityRegistry.d.ts.map +1 -1
- package/dist/entity/EntityRegistry.js +140 -4
- package/dist/entity/EntityRegistry.js.map +1 -1
- package/dist/entity/decorators.d.ts +1 -0
- package/dist/entity/decorators.d.ts.map +1 -1
- package/dist/entity/decorators.js.map +1 -1
- package/dist/filters/builtins/JwtFilter.d.ts +6 -3
- package/dist/filters/builtins/JwtFilter.d.ts.map +1 -1
- package/dist/filters/builtins/JwtFilter.js +29 -4
- package/dist/filters/builtins/JwtFilter.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/AnexoController.d.ts +11 -0
- package/dist/storage/AnexoController.d.ts.map +1 -0
- package/dist/storage/AnexoController.js +79 -0
- package/dist/storage/AnexoController.js.map +1 -0
- package/dist/storage/AnexoEntity.d.ts +12 -0
- package/dist/storage/AnexoEntity.d.ts.map +1 -0
- package/dist/storage/AnexoEntity.js +62 -0
- package/dist/storage/AnexoEntity.js.map +1 -0
- package/dist/storage/AnexoRepository.d.ts +5 -0
- package/dist/storage/AnexoRepository.d.ts.map +1 -0
- package/dist/storage/AnexoRepository.js +16 -0
- package/dist/storage/AnexoRepository.js.map +1 -0
- package/dist/storage/AnexoService.d.ts +56 -0
- package/dist/storage/AnexoService.d.ts.map +1 -0
- package/dist/storage/AnexoService.js +182 -0
- package/dist/storage/AnexoService.js.map +1 -0
- package/dist/storage/GCSStorageProvider.d.ts +15 -0
- package/dist/storage/GCSStorageProvider.d.ts.map +1 -0
- package/dist/storage/GCSStorageProvider.js +58 -0
- package/dist/storage/GCSStorageProvider.js.map +1 -0
- package/dist/storage/StorageProvider.d.ts +28 -0
- package/dist/storage/StorageProvider.d.ts.map +1 -1
- package/dist/storage/StorageProvider.js.map +1 -1
- package/dist/storage/index.d.ts +5 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
- [Storage](#storage)
|
|
26
26
|
- [Health Endpoint](#health-endpoint)
|
|
27
27
|
- [Graceful Shutdown](#graceful-shutdown)
|
|
28
|
+
- [Local Development (DevServer)](#local-development-devserver)
|
|
29
|
+
- [Migration CLI](#migration-cli)
|
|
28
30
|
- [Testing](#testing)
|
|
29
31
|
- [Subpath Exports](#subpath-exports)
|
|
30
32
|
- [Known Pitfalls](#known-pitfalls)
|
|
@@ -95,72 +97,88 @@ await app.buildServer({ port: 3001 })
|
|
|
95
97
|
|
|
96
98
|
## Architecture Overview
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
100
|
+
TypeRoad follows a multi-tenant architecture designed for high isolation and developer productivity. The framework orchestrates the request lifecycle using three main internal layers: `RequestContext`, `OnRoadContainer` (DI), and the `Model/Repository` layer.
|
|
101
|
+
|
|
102
|
+
### The "Matryoshka" Isolation Pattern
|
|
103
|
+
|
|
104
|
+
Isolation in runtime is achieved through a nested hierarchy that ensures one tenant's request never sees another's data or instances.
|
|
105
|
+
|
|
106
|
+
1. **Nível 1: RequestContext (AsyncLocalStorage)**
|
|
107
|
+
- O `OnRoadExpress` envolve cada execução de rota em um bloco `requestContext.run()`.
|
|
108
|
+
- Atua como um "envelope" de memória global para a thread lógica atual, armazenando metadados como `tenantId`, `logger` e `appToken`.
|
|
109
|
+
- Permite que qualquer parte do sistema acesse o estado da requisição via `getRequestContext()` sem precisar de injeção manual.
|
|
110
|
+
|
|
111
|
+
2. **Nível 2: OnRoadContainer (RequestScope)**
|
|
112
|
+
- Dentro do contexto da requisição, o Container cria um objeto `RequestScope`.
|
|
113
|
+
- Este escopo é um mapa de instâncias isolado por um `UUID` único para aquela chamada HTTP.
|
|
114
|
+
- Se múltiplos serviços injetarem o mesmo `Repository`, o Container garante que eles recebam a **mesma instância** dentro daquela requisição (reuso de estado), mas instâncias **completamente diferentes** de outras requisições concorrentes.
|
|
115
|
+
|
|
116
|
+
3. **Nível 3: Repository & Connection Isolation**
|
|
117
|
+
- Repositórios são instanciados pelo Container e recebem automaticamente o `tenantId` do escopo atual.
|
|
118
|
+
- Ao executar uma query (ex: `find()`), o repositório usa seu atributo interno `this.tenant` para solicitar ao `ConnectionManager` o pool de conexões específico daquele banco de dados.
|
|
119
|
+
- Isso garante que o isolamento chegue até o nível físico do banco de dados (schema ou DB separado).
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
graph TD
|
|
123
|
+
subgraph "Nível 1: RequestContext (AsyncLocalStorage)"
|
|
124
|
+
direction TB
|
|
125
|
+
Metadata["{ tenant: 'cliente_a', logger, token }"]
|
|
126
|
+
|
|
127
|
+
subgraph "Nível 2: OnRoadContainer (RequestScope)"
|
|
128
|
+
direction TB
|
|
129
|
+
ScopeID["Scope: UUID-123 (Tenant: 'cliente_a')"]
|
|
130
|
+
Instances["Map { Controller, Service, Repository }"]
|
|
131
|
+
|
|
132
|
+
subgraph "Nível 3: Instância do Repository"
|
|
133
|
+
direction TB
|
|
134
|
+
Atributo["this.tenant = 'cliente_a'"]
|
|
135
|
+
Metodo["getConnection() -> Pede ao ConnectionManager o banco 'onroad_cliente_a'"]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
124
139
|
```
|
|
125
140
|
|
|
126
141
|
---
|
|
127
142
|
|
|
128
143
|
## DI Container & Decorators
|
|
129
144
|
|
|
130
|
-
TypeRoad uses a decorator-based DI container with automatic class scanning.
|
|
145
|
+
TypeRoad uses a decorator-based DI container with automatic class scanning. It manages the lifecycle of your components through defined scopes.
|
|
131
146
|
|
|
132
147
|
### Decorators
|
|
133
148
|
|
|
134
|
-
| Decorator | Scope | Purpose |
|
|
135
|
-
|
|
136
|
-
| `@
|
|
137
|
-
| `@
|
|
138
|
-
| `@
|
|
139
|
-
| `@
|
|
149
|
+
| Decorator | Scope | Relationship | Purpose |
|
|
150
|
+
|-----------|-------|:---:|---------|
|
|
151
|
+
| `@Controller("/path")` | `REQUEST` | entry-point | Injetado com Services; lida com req/res Express. |
|
|
152
|
+
| `@Service()` | `REQUEST` | logic | Camada de orquestração e regras de negócio. |
|
|
153
|
+
| `@Repository()` | `REQUEST` | data | Camada de acesso ao banco (Sequelize/Mongoose). |
|
|
154
|
+
| `@Injectable()` | `TRANSIENT` | utility | Classes utilitárias instanciadas a cada uso. |
|
|
140
155
|
|
|
141
156
|
### Scopes
|
|
142
157
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
| `REQUEST` | One instance per request scope |
|
|
147
|
-
| `TRANSIENT` | New instance on every resolve |
|
|
158
|
+
- **`SINGLETON`**: Uma única instância para toda a aplicação (ex: `EventBus`, `ConnectionManager`).
|
|
159
|
+
- **`REQUEST`**: Uma instância por requisição HTTP. É o escopo padrão para Controllers e Services, garantindo que o estado do tenant esteja isolado.
|
|
160
|
+
- **`TRANSIENT`**: Uma nova instância é criada toda vez que a classe é injetada ou resolvida.
|
|
148
161
|
|
|
149
|
-
###
|
|
162
|
+
### Controller, Service and Repository linkage
|
|
163
|
+
|
|
164
|
+
The container manages a strict hierarchy: `Controller` -> `Service` -> `Repository`.
|
|
165
|
+
|
|
166
|
+
- **Automatic Wiring**: When you register a module via `app.register([MyController, MyService, MyRepository])`, the container scans the metadata and prepares the injection tree.
|
|
167
|
+
- **Stateful Injection**: A Repository inheriting from `SequelizeRepository` is automatically configured by the container with the current `tenant` and `connectionManager` during its instantiation in the `RequestScope`.
|
|
150
168
|
|
|
151
169
|
```ts
|
|
152
|
-
import {
|
|
170
|
+
import { Service, AbstractService } from "@onroad/core"
|
|
153
171
|
|
|
154
|
-
@
|
|
155
|
-
class
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
@Service()
|
|
173
|
+
class OrdemService extends AbstractService<OrdemRepository> {
|
|
174
|
+
constructor() {
|
|
175
|
+
// Shared state: the container ensures OrdemRepository
|
|
176
|
+
// is instantiated within the same RequestScope as this service.
|
|
177
|
+
super({ repository: OrdemRepository })
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
```
|
|
161
181
|
|
|
162
|
-
Register all classes via `app.register([...])` — the container auto-scans metadata.
|
|
163
|
-
|
|
164
182
|
---
|
|
165
183
|
|
|
166
184
|
## Controllers, Services & Repositories
|
|
@@ -601,7 +619,7 @@ await app.shutdown()
|
|
|
601
619
|
|
|
602
620
|
## Testing
|
|
603
621
|
|
|
604
|
-
TypeRoad uses [Vitest](https://vitest.dev/) with **
|
|
622
|
+
TypeRoad uses [Vitest](https://vitest.dev/) with **242 tests** across 10 test files:
|
|
605
623
|
|
|
606
624
|
```bash
|
|
607
625
|
npm test # Run all tests
|
|
@@ -611,7 +629,7 @@ npm run test:coverage # Coverage report
|
|
|
611
629
|
|
|
612
630
|
| Test File | Tests | Covers |
|
|
613
631
|
|-----------|-------|--------|
|
|
614
|
-
| `container.test.ts` |
|
|
632
|
+
| `container.test.ts` | 19 | DI Container, decorators, scopes |
|
|
615
633
|
| `entity.test.ts` | 14 | Entity, Column, associations |
|
|
616
634
|
| `database.test.ts` | 13 | ConnectionManagers |
|
|
617
635
|
| `filters.test.ts` | 33 | FilterChain, all 5 built-in filters |
|
|
@@ -620,6 +638,7 @@ npm run test:coverage # Coverage report
|
|
|
620
638
|
| `logging.test.ts` | 20 | PinoLogger, OnRoadLogger |
|
|
621
639
|
| `providers.test.ts` | 41 | All 4 providers, graceful-fail, shutdown |
|
|
622
640
|
| `transport.test.ts` | 25 | HttpTransport, MessagingTransport, TransportFactory, RequestContext |
|
|
641
|
+
| `dev.test.ts` | 16 | DevServer, MigrationCLI |
|
|
623
642
|
|
|
624
643
|
---
|
|
625
644
|
|
|
@@ -632,21 +651,184 @@ src/
|
|
|
632
651
|
├── container/ # DI Container + decorators
|
|
633
652
|
├── context/ # RequestContext (AsyncLocalStorage)
|
|
634
653
|
├── core/ # AbstractController/Service/Repository, Sentinel, EventBus
|
|
654
|
+
├── database/ # ConnectionManager abstract + Sequelize/Mongo implementations
|
|
655
|
+
│ └── migrations/ # MigrationRunner (Umzug)
|
|
656
|
+
├── dev/ # DevServer, MigrationCLI — local dev tools
|
|
635
657
|
├── entity/ # @Entity, @Column, @Field, associations
|
|
636
658
|
├── filters/ # FilterChain, @Filter, built-in filters (Cors, JWT, Tenant, Role, RequestContext)
|
|
637
|
-
├── security/ # @Roles, @Public
|
|
638
|
-
├── database/ # ConnectionManager abstract + Sequelize/Mongo implementations
|
|
639
|
-
├── transport/ # InterServiceTransport, HttpTransport, MessagingTransport, TransportFactory
|
|
640
|
-
├── messaging/ # MatchingObject
|
|
641
|
-
├── providers/ # MessagingProvider, RealtimeProvider, TaskSchedulerProvider, SocketProvider
|
|
642
659
|
├── logging/ # OnRoadLogger interface + PinoLogger
|
|
660
|
+
├── messaging/ # MatchingObject
|
|
643
661
|
├── plugins/ # OnRoadPlugin interface
|
|
662
|
+
├── providers/ # MessagingProvider, RealtimeProvider, TaskSchedulerProvider, SocketProvider
|
|
663
|
+
├── security/ # @Roles, @Public
|
|
644
664
|
├── storage/ # StorageProvider abstract
|
|
665
|
+
├── transport/ # InterServiceTransport, HttpTransport, MessagingTransport, TransportFactory
|
|
645
666
|
└── types/ # Express Request augmentation
|
|
646
667
|
```
|
|
647
668
|
|
|
648
669
|
---
|
|
649
670
|
|
|
671
|
+
## Local Development (DevServer)
|
|
672
|
+
|
|
673
|
+
TypeRoad includes a `DevServer` that makes local development self-sufficient — it starts database containers, runs migrations, and logs all environment variables your frontend needs.
|
|
674
|
+
|
|
675
|
+
### Quick Start
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
import "reflect-metadata"
|
|
679
|
+
import { OnRoadExpress } from "@onroad/core"
|
|
680
|
+
import { SequelizeConnectionManager } from "@onroad/core/database"
|
|
681
|
+
import { DevServer } from "@onroad/core/dev"
|
|
682
|
+
|
|
683
|
+
const app = new OnRoadExpress({
|
|
684
|
+
connections: [
|
|
685
|
+
new SequelizeConnectionManager({
|
|
686
|
+
dialect: "postgres",
|
|
687
|
+
host: "localhost",
|
|
688
|
+
port: 5432,
|
|
689
|
+
user: "typeroad",
|
|
690
|
+
password: "typeroad_dev",
|
|
691
|
+
database: "typeroad_dev",
|
|
692
|
+
migrations: {
|
|
693
|
+
path: "./src/migrations",
|
|
694
|
+
runOnConnect: true,
|
|
695
|
+
},
|
|
696
|
+
}),
|
|
697
|
+
],
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
// Register your controllers, services, repositories...
|
|
701
|
+
app.register([/* ... */])
|
|
702
|
+
|
|
703
|
+
const devServer = new DevServer({
|
|
704
|
+
app,
|
|
705
|
+
databases: [{ engine: "postgres", port: 5432 }],
|
|
706
|
+
migrationsPath: "./src/migrations",
|
|
707
|
+
tenants: ["default"],
|
|
708
|
+
frontendVars: {
|
|
709
|
+
REACT_APP_TENANT: "default",
|
|
710
|
+
REACT_APP_WS_URL: "ws://localhost:3000",
|
|
711
|
+
},
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
await devServer.start({ port: 3000 })
|
|
715
|
+
// Press Ctrl+C to stop
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### What DevServer Does
|
|
719
|
+
|
|
720
|
+
1. **Generates `docker-compose.dev.yml`** — PostgreSQL (or MySQL) container with healthcheck, volumes, and sensible defaults
|
|
721
|
+
2. **Starts the container** — `docker compose up -d` automatically
|
|
722
|
+
3. **Waits for readiness** — polls `pg_isready` / `mysqladmin ping` until the DB accepts connections
|
|
723
|
+
4. **Runs migrations** — applies all pending Umzug migrations from your configured path
|
|
724
|
+
5. **Starts the API server** — calls `app.buildServer()` as usual
|
|
725
|
+
6. **Logs frontend env vars** — prints a copy-pasteable block and writes `.env.dev`:
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
729
|
+
│ FRONTEND ENVIRONMENT VARIABLES │
|
|
730
|
+
├──────────────────────────────────────────────────────────────┤
|
|
731
|
+
│ REACT_APP_API_URL=http://localhost:3000
|
|
732
|
+
│ NEXT_PUBLIC_API_URL=http://localhost:3000
|
|
733
|
+
│ VITE_API_URL=http://localhost:3000
|
|
734
|
+
│ DATABASE_URL=postgresql://typeroad:typeroad_dev@localhost:5432/typeroad_dev
|
|
735
|
+
│ REACT_APP_TENANT=default
|
|
736
|
+
└──────────────────────────────────────────────────────────────┘
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### DevServer Config
|
|
740
|
+
|
|
741
|
+
| Option | Type | Default | Description |
|
|
742
|
+
|--------|------|---------|-------------|
|
|
743
|
+
| `app` | `OnRoadExpress` | required | The app instance |
|
|
744
|
+
| `databases` | `DevDatabaseConfig[]` | `[{ engine: "postgres" }]` | Database containers to start |
|
|
745
|
+
| `frontendVars` | `Record<string, string>` | `{}` | Extra vars to log for frontend |
|
|
746
|
+
| `migrationsPath` | `string` | — | Path to migration files |
|
|
747
|
+
| `autoMigrate` | `boolean` | `true` | Run migrations on start |
|
|
748
|
+
| `tenants` | `string[]` | `["default"]` | Tenants to run migrations for |
|
|
749
|
+
| `composeFile` | `string` | `"docker-compose.dev.yml"` | Docker compose filename |
|
|
750
|
+
|
|
751
|
+
### Database Defaults
|
|
752
|
+
|
|
753
|
+
| Engine | Image | Port | User | Password | Database |
|
|
754
|
+
|--------|-------|------|------|----------|----------|
|
|
755
|
+
| `postgres` | `postgres:16-alpine` | `5432` | `typeroad` | `typeroad_dev` | `typeroad_dev` |
|
|
756
|
+
| `mysql` | `mysql:8.0` | `3306` | `typeroad` | `typeroad_dev` | `typeroad_dev` |
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## Migration CLI
|
|
761
|
+
|
|
762
|
+
The `MigrationCLI` class provides a programmatic API for managing database migrations.
|
|
763
|
+
|
|
764
|
+
### Usage
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
import { MigrationCLI } from "@onroad/core/dev"
|
|
768
|
+
import { SequelizeConnectionManager } from "@onroad/core/database"
|
|
769
|
+
|
|
770
|
+
const connection = new SequelizeConnectionManager({
|
|
771
|
+
dialect: "postgres",
|
|
772
|
+
host: "localhost",
|
|
773
|
+
user: "typeroad",
|
|
774
|
+
password: "typeroad_dev",
|
|
775
|
+
database: "typeroad_dev",
|
|
776
|
+
migrations: { path: "./src/migrations" },
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
const cli = new MigrationCLI({
|
|
780
|
+
migrationsPath: "./src/migrations",
|
|
781
|
+
connection,
|
|
782
|
+
tenant: "default",
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
// Run from CLI args
|
|
786
|
+
await cli.run() // reads process.argv
|
|
787
|
+
|
|
788
|
+
// Or call directly
|
|
789
|
+
cli.create("add-users-table") // creates timestamped migration file
|
|
790
|
+
await cli.up() // apply all pending
|
|
791
|
+
await cli.up(1) // apply 1 step
|
|
792
|
+
await cli.down() // revert last migration
|
|
793
|
+
await cli.status() // show status table
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### CLI Commands
|
|
797
|
+
|
|
798
|
+
| Command | Description | Example |
|
|
799
|
+
|---------|-------------|---------|
|
|
800
|
+
| `create <name>` | Create a new migration file | `cli.run(["create", "add-orders"])` |
|
|
801
|
+
| `up [steps]` | Apply pending migrations | `cli.run(["up"])` or `cli.run(["up", "3"])` |
|
|
802
|
+
| `down [steps]` | Revert migrations (default: 1) | `cli.run(["down", "2"])` |
|
|
803
|
+
| `status` | Show applied/pending status | `cli.run(["status"])` |
|
|
804
|
+
|
|
805
|
+
### Migration File Format
|
|
806
|
+
|
|
807
|
+
Generated `.ts` files follow the Umzug/Sequelize pattern:
|
|
808
|
+
|
|
809
|
+
```ts
|
|
810
|
+
import type { QueryInterface, Sequelize } from "sequelize"
|
|
811
|
+
|
|
812
|
+
export default {
|
|
813
|
+
async up(queryInterface: QueryInterface, sequelize: Sequelize): Promise<void> {
|
|
814
|
+
await queryInterface.createTable("ordens", {
|
|
815
|
+
id: { type: sequelize.constructor["DataTypes"].UUID, primaryKey: true },
|
|
816
|
+
descricao: { type: sequelize.constructor["DataTypes"].STRING, allowNull: false },
|
|
817
|
+
createdAt: sequelize.constructor["DataTypes"].DATE,
|
|
818
|
+
updatedAt: sequelize.constructor["DataTypes"].DATE,
|
|
819
|
+
})
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
async down(queryInterface: QueryInterface): Promise<void> {
|
|
823
|
+
await queryInterface.dropTable("ordens")
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Migration state is tracked in the `__typeroad_migrations` table (configurable via `tableName` in `MigrationConfig`).
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
650
832
|
## Subpath Exports
|
|
651
833
|
|
|
652
834
|
Import only what you need:
|
|
@@ -662,6 +844,7 @@ import { HttpTransport, TransportFactory } from "@onroad/core/transport" // Tran
|
|
|
662
844
|
import { MatchingObject } from "@onroad/core/messaging" // Messaging
|
|
663
845
|
import { PinoLogger } from "@onroad/core/logging" // Logging
|
|
664
846
|
import { StorageProvider } from "@onroad/core/storage" // Storage
|
|
847
|
+
import { DevServer, MigrationCLI } from "@onroad/core/dev" // Dev Tools
|
|
665
848
|
```
|
|
666
849
|
|
|
667
850
|
---
|
|
@@ -763,6 +946,373 @@ export class UserRepository extends BaseRepository<User> {
|
|
|
763
946
|
|
|
764
947
|
---
|
|
765
948
|
|
|
949
|
+
### `import type` with relationship decorators
|
|
950
|
+
|
|
951
|
+
TypeScript's `import type` is erased at compile time. If you use `import type { Foo }` and then reference `Foo` as a **runtime value** inside a decorator like `@HasMany(() => Foo, ...)`, the compiled JavaScript will throw `ReferenceError: Foo is not defined` because the import was stripped.
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
// ❌ Wrong — import type is erased, decorator callback fails at runtime
|
|
955
|
+
import type { CampoDeVerificacao } from "./CampoDeVerificacao.js"
|
|
956
|
+
|
|
957
|
+
@Entity("caderno", { engine: "sequelize" })
|
|
958
|
+
export class CadernoDeVerificacao {
|
|
959
|
+
@HasMany(() => CampoDeVerificacao, "cadernoId") // 💥 ReferenceError
|
|
960
|
+
campos!: CampoDeVerificacao[]
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
Fix: use a regular `import` for any class referenced inside a decorator callback:
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
// ✅ Correct — regular import keeps the binding at runtime
|
|
968
|
+
import { CampoDeVerificacao } from "./CampoDeVerificacao.js"
|
|
969
|
+
|
|
970
|
+
@Entity("caderno", { engine: "sequelize" })
|
|
971
|
+
export class CadernoDeVerificacao {
|
|
972
|
+
@HasMany(() => CampoDeVerificacao, "cadernoId")
|
|
973
|
+
campos!: CampoDeVerificacao[]
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
> **Tip:** `import type` is safe for type annotations and generics (`extends AbstractService<Foo>`), but never for decorator arguments, `instanceof`, or any expression evaluated at runtime.
|
|
978
|
+
|
|
979
|
+
---
|
|
980
|
+
|
|
981
|
+
### `emitDecoratorMetadata` causes TDZ errors with circular entity imports
|
|
982
|
+
|
|
983
|
+
When `emitDecoratorMetadata: true` is set in `tsconfig.json`, TypeScript emits `__metadata("design:type", CampoDeVerificacao)` for decorated properties. This accesses the imported class **at class definition time**. If two entities import each other (circular dependency), ESM module evaluation order causes a **Temporal Dead Zone (TDZ)** error:
|
|
984
|
+
|
|
985
|
+
```
|
|
986
|
+
ReferenceError: Cannot access 'CampoDeVerificacao' before initialization
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
The `@HasMany(() => Foo, ...)` arrow function is lazy (only called later by `wireSequelizeAssociations`), but `__metadata("design:type", Foo)` is **eager** — it runs during class definition.
|
|
990
|
+
|
|
991
|
+
```jsonc
|
|
992
|
+
// ❌ Wrong — causes TDZ crash in circular ESM imports
|
|
993
|
+
{
|
|
994
|
+
"compilerOptions": {
|
|
995
|
+
"experimentalDecorators": true,
|
|
996
|
+
"emitDecoratorMetadata": true // 💥
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Fix: disable `emitDecoratorMetadata`. @onroad/core uses only custom metadata keys (`METADATA_KEY.ENTITY`, `ENTITY_COLUMNS`, etc.) and does **not** rely on `design:type`, `design:paramtypes`, or `design:returntype`.
|
|
1002
|
+
|
|
1003
|
+
```jsonc
|
|
1004
|
+
// ✅ Correct — no TDZ, decorators still work
|
|
1005
|
+
{
|
|
1006
|
+
"compilerOptions": {
|
|
1007
|
+
"experimentalDecorators": true,
|
|
1008
|
+
"emitDecoratorMetadata": false
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
---
|
|
1014
|
+
|
|
1015
|
+
### Custom route service methods must extract params from `req` — not expect primitive arguments
|
|
1016
|
+
|
|
1017
|
+
**`AbstractController` always calls custom service methods with the raw `(req, res)` objects.** It only extracts params for the five default CRUD methods (`create`, `read`, `readAll`, `update`, `delete`). Every method registered under `routes:` in the controller config receives `(req, res)` forwarded directly.
|
|
1018
|
+
|
|
1019
|
+
If your service method signature expects primitive parameters (`id: number`, `dataInicio: string`, etc.), it will receive the `req` object instead — causing silent failures (empty queries, NaN ids, wrong SQL filters) because the framework does **not** throw, it just passes the wrong value.
|
|
1020
|
+
|
|
1021
|
+
```ts
|
|
1022
|
+
// ❌ Wrong — service expects primitives; controller passes (req, res)
|
|
1023
|
+
// Controller calls: this.service.findByBetweenDates(req, res)
|
|
1024
|
+
async findByBetweenDates(dataInicio: string, dataFim: string) {
|
|
1025
|
+
// dataInicio === req object → Sequelize WHERE clause receives an object → returns []
|
|
1026
|
+
return this.repository.findAll({ where: { dataDeAbertura: { [Op.gte]: dataInicio } } })
|
|
1027
|
+
}
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
// ✅ Correct — service extracts its own params from req
|
|
1032
|
+
async findByBetweenDates(req: any, res?: any) {
|
|
1033
|
+
const dataInicio: string = req.params.dataInicio
|
|
1034
|
+
const dataFim: string = req.params.dataFim
|
|
1035
|
+
return this.repository.findAll({ where: { dataDeAbertura: { [Op.gte]: dataInicio, [Op.lte]: dataFim } } })
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
**Affected params by HTTP position:**
|
|
1040
|
+
|
|
1041
|
+
| Source | How to extract |
|
|
1042
|
+
|------------------|-----------------------------|
|
|
1043
|
+
| Route param (`:id`) | `req.params.id` |
|
|
1044
|
+
| Query string | `req.query.field` |
|
|
1045
|
+
| Request body | `req.body.field` |
|
|
1046
|
+
| Auth / identity | `req.decoded.userId` |
|
|
1047
|
+
|
|
1048
|
+
> **Rule of thumb:** In a TypeRoad service, if the method is registered as a custom route, **the first argument is always `req`**. Never write `async myMethod(id: number)` for a method that is called through a route config key — always write `async myMethod(req: any, res?: any)` and extract from `req` internally.
|
|
1049
|
+
|
|
1050
|
+
**Scope of impact in `teraprox-api-manutencao` (audit — 2025-04-05):**
|
|
1051
|
+
|
|
1052
|
+
The following service methods were found with the wrong signature (primitive params instead of `req`). All calls through their controller routes were silently broken:
|
|
1053
|
+
|
|
1054
|
+
| Service | Method | Broken params |
|
|
1055
|
+
|---------|--------|---------------|
|
|
1056
|
+
| `OrdemDeServicoService` | `findByBetweenDates` | `dataInicio`, `dataFim` — **fixed** |
|
|
1057
|
+
| `OrdemDeServicoService` | `findByRecursoBetweenDates` | `recursoId`, `dataInicio`, `dataFim` |
|
|
1058
|
+
| `OrdemDeServicoService` | `findByBranchIdBetweenDates` | `branchId`, `dataInicio`, `dataFim` |
|
|
1059
|
+
| `OrdemDeServicoService` | `findByRecursoId` | `recursoId` |
|
|
1060
|
+
| `OrdemDeServicoService` | `findMonitoramentoByCriticidade` | `criticidade` |
|
|
1061
|
+
| `OrdemDeServicoService` | `findByModoDeFalhaId` | `modoDeFalhaId` |
|
|
1062
|
+
| `OrdemDeServicoService` | `encerraOS` | `id`, `form` |
|
|
1063
|
+
| `OrdemDeServicoService` | `setStatusOs` | `id`, `form` |
|
|
1064
|
+
| `OrdemDeServicoService` | `setStatusOsBulk` | `osIds`, `status`, `extraData` |
|
|
1065
|
+
| `OrdemDeServicoService` | `iniciarOsBulk` | `osIds`, `status` |
|
|
1066
|
+
| `OrdemDeServicoService` | `addTarefaToOrdem` | `osId`, `tarefa` |
|
|
1067
|
+
| `OrdemDeServicoService` | `updateDescricaoOs` | `osId`, `descricao` |
|
|
1068
|
+
| `OrdemDeServicoService` | `shiftRecursoOs` | `osId`, `recId` |
|
|
1069
|
+
| `OrdemDeServicoService` | `findInspecoesBetweenDates` | `recursoId`, `dataInicio`, `dataFim` |
|
|
1070
|
+
| `OrdemDeServicoService` | `putAnexos` | `id`, `anexos` |
|
|
1071
|
+
| `OrdemDeServicoService` | `findOrdensInBatch` | `ids` |
|
|
1072
|
+
| `OrdemDeServicoService` | `loadOsForOcpMigration` | `osIds` |
|
|
1073
|
+
| `RecursoService` | `findRecursoFatherByRecursoId` | `recursoId` |
|
|
1074
|
+
| `RecursoService` | `findByTagDescription` | `tag` |
|
|
1075
|
+
| `RecursoService` | `findRecursoByTagId` | `tagId` |
|
|
1076
|
+
| `RecursoService` | `findParadasByRecursoId` | `recursoId` |
|
|
1077
|
+
| `RecursoService` | `findSemParadasByRecursoId` | `recursoId` |
|
|
1078
|
+
| `TarefaService` | `encerrarTarefa` | `id` |
|
|
1079
|
+
| `TarefaService` | `addUnidadeMaterialTarefa` | `tarefaId`, `form` |
|
|
1080
|
+
| `TarefaService` | `addObservacaoTarefa` | `tarefaId`, `form` |
|
|
1081
|
+
| `TarefaService` | `deleteObservacaoTarefa` | `justificativaId` |
|
|
1082
|
+
| `TarefaService` | `loadMultipleIds` | `tarefaIds` |
|
|
1083
|
+
| `TarefaService` | `removeAnexo` | `id`, `anexoKey` |
|
|
1084
|
+
| `SolicitacaoDeServicoService` | `shiftRecursoSs` | `ssId`, `recId` |
|
|
1085
|
+
| `SolicitacaoDeServicoService` | `aprovaSolicitacao` | `id`, `form` |
|
|
1086
|
+
| `SolicitacaoDeServicoService` | `reprovaSolicitacao` | `id`, `form` |
|
|
1087
|
+
| `SolicitacaoDeServicoService` | `updateDescricaoSs` | `ssId`, `descricao` |
|
|
1088
|
+
| `SolicitacaoDeServicoService` | `findByRecursoId` | `recursoId` |
|
|
1089
|
+
| `SolicitacaoDeServicoService` | `findBetweenDates` | `start`, `end` |
|
|
1090
|
+
| `SolicitacaoDeServicoService` | `putAnexos` | `id`, `anexos` |
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
### Custom route key name must exactly match the service method name
|
|
1095
|
+
|
|
1096
|
+
When you register a custom route in the controller config, the key name is used **verbatim** as the service method name to call. If the service was later renamed (e.g. during a refactor), the controller silently returns `null` because the `typeof service[methodName] === 'function'` guard evaluates to `false`.
|
|
1097
|
+
|
|
1098
|
+
```ts
|
|
1099
|
+
// ❌ Wrong — controller key "addModoDeFalhaOS" but service method is "addModoDeFalha"
|
|
1100
|
+
routes: { addModoDeFalhaOS: { method: "post", path: "/..." } }
|
|
1101
|
+
// → service.addModoDeFalhaOS is undefined → always returns null (no error thrown)
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**Known mismatches in `teraprox-api-manutencao` (audit — 2025-04-05):**
|
|
1105
|
+
|
|
1106
|
+
| Controller route key | Expected service method (by key) | Actual service method |
|
|
1107
|
+
|----------------------|-----------------------------------|-----------------------|
|
|
1108
|
+
| `addModoDeFalhaOS` | `addModoDeFalhaOS` | `addModoDeFalha` |
|
|
1109
|
+
| `updateTipoDeOrdem` | `updateTipoDeOrdem` | `setTipoOrdem` |
|
|
1110
|
+
| `updateTipoDeOrdemBulk` | `updateTipoDeOrdemBulk` | `setTipoOrdemBulk` |
|
|
1111
|
+
| `removeAnexo` | `removeAnexo` | `removeAnexoOs` |
|
|
1112
|
+
| `readConcluidasCount` | `readConcluidasCount` | `countTarefasConcluidas` |
|
|
1113
|
+
| `findPlanned` | `findPlanned` | `findPlannedOrdensBetweenDatesV2` |
|
|
1114
|
+
| `findPlannedV3` | `findPlannedV3` | `findPlannedOrdensBetweenDatesV3` |
|
|
1115
|
+
| `findPlannedAgregador` | `findPlannedAgregador` | `findPlannedOrdensWithAgregadores` |
|
|
1116
|
+
| `findRecorrenciaPlanned` | `findRecorrenciaPlanned` | `findPlannedOrdensFromRecorrenciaBetweenDates` |
|
|
1117
|
+
| `findAgregadorPlanned` | `findAgregadorPlanned` | `findPlannedOrdensFromAgregadorBetweenDates` |
|
|
1118
|
+
| `createInBulk` | `createInBulk` | `createInBulkOs` |
|
|
1119
|
+
| `findDashByOs` | `findDashByOs` | `findWithDashboardAssociation` |
|
|
1120
|
+
|
|
1121
|
+
> Fix: either rename the service method to match the route key, or rename the route key to match the service method. Both sides must be in sync.
|
|
1122
|
+
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
### `BelongsToMany` association returns `null` (not `[]`) when no records exist
|
|
1126
|
+
|
|
1127
|
+
Sequelize's `BelongsToMany` association populates the association property with `null` (not an empty array) when no join-table records exist for a given entity. Any code that iterates over that property with `for...of` or `.forEach()` without a guard will throw `TypeError: x is not iterable`.
|
|
1128
|
+
|
|
1129
|
+
```ts
|
|
1130
|
+
// ❌ Wrong — crashes with "TypeError: o.tarefas is not iterable" when OS has no tarefas
|
|
1131
|
+
for (const tarefa of o.tarefas) { ... }
|
|
1132
|
+
o.tarefas.forEach(...)
|
|
1133
|
+
o.tarefas.flatMap(...)
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
```ts
|
|
1137
|
+
// ✅ Correct — guard with nullish coalescing
|
|
1138
|
+
for (const tarefa of (o.tarefas ?? [])) { ... }
|
|
1139
|
+
;(o.tarefas ?? []).forEach(...)
|
|
1140
|
+
ordens.flatMap((o) => (o.tarefas ?? []).map(...))
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
> This was the direct cause of `TypeError: o.tarefas is not iterable` at `OrdemDeServicoService.buildTarefa` (line 235) and `findByBetweenDates`. Apply the same `?? []` guard to **all** `BelongsToMany` associations before iterating.
|
|
1144
|
+
|
|
1145
|
+
---
|
|
1146
|
+
|
|
1147
|
+
## Migration Guide — Lessons Learned (v3 → v4)
|
|
1148
|
+
|
|
1149
|
+
This section documents the key issues found while migrating `teraprox-api-user` from onRoad v3 to TypeRoad v4. Use this as a checklist when migrating other APIs (`api-manutencao`, `api-processo`, etc.).
|
|
1150
|
+
|
|
1151
|
+
### 1. Entity Associations — Use Decorators, Not `buildAssociations()`
|
|
1152
|
+
|
|
1153
|
+
v3 used `buildAssociations()` inside repositories. v4 uses decorators on entity classes. **Every association must be declared on the entity itself.**
|
|
1154
|
+
|
|
1155
|
+
```ts
|
|
1156
|
+
// ❌ v3 — Repository-based associations (REMOVE)
|
|
1157
|
+
class UserRepository extends BaseRepository {
|
|
1158
|
+
buildAssociations() {
|
|
1159
|
+
this.hasMany(UserRole, "userRole")
|
|
1160
|
+
this.belongsTo(Company, "company")
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ✅ v4 — Entity decorator associations
|
|
1165
|
+
@Entity("user", { engine: "sequelize", timestamps: true })
|
|
1166
|
+
class User {
|
|
1167
|
+
@HasMany(() => UserRole, "userId")
|
|
1168
|
+
userRole!: UserRole[]
|
|
1169
|
+
|
|
1170
|
+
@BelongsToMany(() => Company, () => UserCompany, "userId", "companyId")
|
|
1171
|
+
companies!: Company[]
|
|
1172
|
+
}
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
**Checklist per entity:**
|
|
1176
|
+
- Join table entities (e.g. `UserRole`, `UserSetor`, `UserCompany`) **must** declare their FK columns with `@Column` AND the `@BelongsTo` back-reference.
|
|
1177
|
+
|
|
1178
|
+
```ts
|
|
1179
|
+
@Entity("userRole", { engine: "sequelize", timestamps: true })
|
|
1180
|
+
class UserRole {
|
|
1181
|
+
@Column({ type: DataType.STRING })
|
|
1182
|
+
userId!: string
|
|
1183
|
+
|
|
1184
|
+
@Column({ type: DataType.STRING })
|
|
1185
|
+
roleId!: string
|
|
1186
|
+
|
|
1187
|
+
@BelongsTo(() => Role, "roleId")
|
|
1188
|
+
role!: Role
|
|
1189
|
+
}
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
### 2. Includes — Use String-Based Associations, Not Model References
|
|
1193
|
+
|
|
1194
|
+
v3 used `this.repository.fullAssociation` (injected array of `{ model: SequelizeModel }`).
|
|
1195
|
+
v4 uses **string-based `association` names** that match the decorator property name.
|
|
1196
|
+
|
|
1197
|
+
```ts
|
|
1198
|
+
// ❌ v3 — model-based includes (CRASH: model is a raw class, not Sequelize model)
|
|
1199
|
+
const user = await this.repository.findOne({
|
|
1200
|
+
where: { email },
|
|
1201
|
+
include: this.repository.fullAssociation
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
// ✅ v4 — string-based association includes
|
|
1205
|
+
const USER_FULL_INCLUDE = [
|
|
1206
|
+
{ association: "filters" },
|
|
1207
|
+
{ association: "tickets" },
|
|
1208
|
+
{ association: "userRole" },
|
|
1209
|
+
{ association: "userSetor" }
|
|
1210
|
+
]
|
|
1211
|
+
const user = await this.repository.findOne({
|
|
1212
|
+
where: { email },
|
|
1213
|
+
include: USER_FULL_INCLUDE
|
|
1214
|
+
})
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
**Nested includes follow the same pattern:**
|
|
1218
|
+
```ts
|
|
1219
|
+
include: [
|
|
1220
|
+
{
|
|
1221
|
+
association: "userRole",
|
|
1222
|
+
include: [{ association: "role", where: { companyId }, required: false }]
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
association: "userSetor",
|
|
1226
|
+
include: [{ association: "setor", where: { companyId }, required: false }]
|
|
1227
|
+
}
|
|
1228
|
+
]
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
### 3. Raw Queries — Use `getSequelizeConnection()`
|
|
1232
|
+
|
|
1233
|
+
v3 accessed `(this.repository as any).connection.query(...)`.
|
|
1234
|
+
v4 exposes this through `BaseRepository.getSequelizeConnection()`.
|
|
1235
|
+
|
|
1236
|
+
```ts
|
|
1237
|
+
// ❌ v3
|
|
1238
|
+
const [results] = await (this.repository as any).connection.query(sql)
|
|
1239
|
+
|
|
1240
|
+
// ✅ v4 — add this method to your BaseRepository
|
|
1241
|
+
getSequelizeConnection() { return this.getConnection() }
|
|
1242
|
+
|
|
1243
|
+
// Usage in service:
|
|
1244
|
+
const [results] = await this.repository.getSequelizeConnection().query(sql)
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### 4. Remove `fullAssociation` from BaseRepository
|
|
1248
|
+
|
|
1249
|
+
v3 APIs declare `fullAssociation` on `BaseRepository`. **Remove it entirely** — v4 does not use it. Includes are now inline at the query site.
|
|
1250
|
+
|
|
1251
|
+
```ts
|
|
1252
|
+
// ❌ v3
|
|
1253
|
+
export abstract class BaseRepository<T> extends SequelizeRepository<T> {
|
|
1254
|
+
declare readonly fullAssociation: any[]
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// ✅ v4
|
|
1258
|
+
export abstract class BaseRepository<T> extends SequelizeRepository<T> {
|
|
1259
|
+
// No fullAssociation — define includes where you query
|
|
1260
|
+
}
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
### 5. Controller Return Values Are Auto-Serialized
|
|
1264
|
+
|
|
1265
|
+
v4's `OnRoadExpress` automatically calls `res.json(result)` when a controller method returns a value and `res.headersSent` is false. **Do not manually call `res.json()` in controllers if you return a value** — it will cause "headers already sent" errors.
|
|
1266
|
+
|
|
1267
|
+
```ts
|
|
1268
|
+
// ✅ Just return the value — framework serializes it
|
|
1269
|
+
async login(form: any, req: Request, res: Response) {
|
|
1270
|
+
const userData = await this.service.login(form, req, res)
|
|
1271
|
+
return userData // Framework calls res.json(userData)
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### 6. `BelongsToMany` Includes — Beware of Query Hangs
|
|
1276
|
+
|
|
1277
|
+
Using `BelongsToMany` associations (like `User ↔ Company` through `UserCompany`) in Sequelize `include` can cause queries to hang with large datasets or complex joins. **Avoid including BelongsToMany in default/frequent queries.** Use it only in specific methods that need it, and consider raw SQL queries for performance-critical paths.
|
|
1278
|
+
|
|
1279
|
+
### 7. Common Migration Search Patterns
|
|
1280
|
+
|
|
1281
|
+
When migrating an API, search for these patterns and replace them:
|
|
1282
|
+
|
|
1283
|
+
| Search Pattern | Replace With |
|
|
1284
|
+
|---|---|
|
|
1285
|
+
| `this.repository.fullAssociation` | Inline `[{ association: "..." }]` array |
|
|
1286
|
+
| `(this.xxxRepo as any).model` | `{ association: "aliasName" }` |
|
|
1287
|
+
| `(this.xxxRepo as any).xxxRepo` | `{ association: "aliasName" }` |
|
|
1288
|
+
| `(this.repository as any).connection` | `this.repository.getSequelizeConnection()` |
|
|
1289
|
+
| `buildAssociations()` | `@HasMany`, `@BelongsTo`, `@HasOne`, `@BelongsToMany` on entity |
|
|
1290
|
+
| `{ model: SomeClass, as: "..." }` | `{ association: "..." }` |
|
|
1291
|
+
|
|
1292
|
+
### 8. `ARRAY` Column Type
|
|
1293
|
+
|
|
1294
|
+
For PostgreSQL `varchar[]` columns, use the custom `arrayType` option:
|
|
1295
|
+
|
|
1296
|
+
```ts
|
|
1297
|
+
@Column({ type: DataType.ARRAY, arrayType: DataType.STRING })
|
|
1298
|
+
componentesBloqueados!: string[]
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### 9. Env & Local Dev Setup
|
|
1302
|
+
|
|
1303
|
+
Each API needs a `.env` at its server root with:
|
|
1304
|
+
- `LOCAL_NO_JWT=true` — skips JWT validation for local dev
|
|
1305
|
+
- DB connection vars (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DBNAME`)
|
|
1306
|
+
- `TOKEN_KEY` — any string for local token signing
|
|
1307
|
+
- `API_PORT` — local port for the service
|
|
1308
|
+
|
|
1309
|
+
Use `cloud-sql-proxy` for connecting to GCP Cloud SQL locally:
|
|
1310
|
+
```bash
|
|
1311
|
+
./cloud-sql-proxy <PROJECT>:<REGION>:<INSTANCE> --port=5432
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
766
1316
|
## License
|
|
767
1317
|
|
|
768
1318
|
UNLICENSED — HashCodeTI-Brasil
|