mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-05 14:14:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61c241dde | ||
|
|
9c55c22230 | ||
|
|
fb521f53cd | ||
|
|
acb631e33d | ||
|
|
e616061c21 | ||
|
|
e03b9e7a14 | ||
|
|
2cc8f46418 | ||
|
|
295f1f86ca | ||
|
|
0a5e8001f9 | ||
|
|
b2700d4bb0 | ||
|
|
01c1087e07 | ||
|
|
7dedb3bd33 | ||
|
|
1ff5589a2e | ||
|
|
d24e9a1d80 | ||
|
|
7d3ef1ef92 | ||
|
|
df3f2489f9 | ||
|
|
53245b0067 | ||
|
|
dd699614ae | ||
|
|
6919a2a8d0 | ||
|
|
5c29e07450 | ||
|
|
00304262ba | ||
|
|
bc931c6dd8 | ||
|
|
c444d6e956 | ||
|
|
f7efb757bf | ||
|
|
2f140d213a | ||
|
|
df674dd441 | ||
|
|
09dced9fcd | ||
|
|
165b533172 | ||
|
|
b1dc567394 | ||
|
|
a0a1303515 | ||
|
|
c21d977f7c | ||
|
|
399d3492d3 | ||
|
|
b0b557246e | ||
|
|
bb671a90d6 | ||
|
|
f6ababbe7a | ||
|
|
82290285d0 | ||
|
|
c490814a98 | ||
|
|
7616b1e32c | ||
|
|
3ba2c91893 | ||
|
|
e789076ed4 | ||
|
|
3b6ddaaee6 | ||
|
|
8073194604 |
@@ -66,13 +66,16 @@ jobs:
|
||||
|
||||
- name: Install Helmfile
|
||||
run: |
|
||||
wget https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
||||
curl -fsSLO https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
||||
tar -xzf helmfile_0.148.0_linux_amd64.tar.gz
|
||||
sudo mv helmfile /usr/local/bin/
|
||||
helmfile --version
|
||||
|
||||
- name: Install Helm Diff Plugin
|
||||
run: helm plugin install https://github.com/databus23/helm-diff || true
|
||||
- name: Debug Helm env
|
||||
run: |
|
||||
helm env
|
||||
echo "HOME=$HOME"
|
||||
ls -R $HOME/.local/share/helm || true
|
||||
|
||||
- name: Authenticate with OKE cluster
|
||||
env:
|
||||
|
||||
@@ -1,482 +0,0 @@
|
||||
# Maestro ↔ In-Factory Migration Map
|
||||
|
||||
This document maps all integration points between Maestro and In-Factory that need to be addressed to remove the dependency.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
Frontend<-->Maestro;
|
||||
Maestro<-->DUC;
|
||||
Maestro<-->pi-factory;
|
||||
Maestro<-->in-factory;
|
||||
```
|
||||
|
||||
Maestro connects to **3 external microservices**:
|
||||
- **DUC** (`DUC_URL`) - User management, authentication, permissions
|
||||
- **PI-Factory** (`PIFACTORY_URL`) - Pipelines, Catalog services
|
||||
- **IN-Factory** (`INFACTORY_URL`) - Connections, Connectors, Inputs, Network Config, Transformations
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Category | Count | Impact Level |
|
||||
|----------|-------|--------------|
|
||||
| gRPC Client Configurations | 6 | HIGH |
|
||||
| NestJS Modules | 6 | HIGH |
|
||||
| REST Controllers/Endpoints | 6 | HIGH |
|
||||
| Service Classes | 6 | HIGH |
|
||||
| Configuration Files | 5 | MEDIUM |
|
||||
| Proto Package Dependencies | 2 | HIGH |
|
||||
| Environment Variables | 2 | LOW |
|
||||
|
||||
---
|
||||
|
||||
## 1. gRPC CLIENT CONFIGURATIONS (Files to Migrate)
|
||||
|
||||
These files configure gRPC connections to In-Factory services:
|
||||
|
||||
### 1.1 Connection Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/connection/client.config.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 18
|
||||
process.env.INFACTORY_URL.startsWith('in-factory:')
|
||||
process.env.INFACTORY_URL.includes('0.0.0.0')
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `ConnectionManager.ProtoPackages.WritePackage`
|
||||
- `ConnectionManager.ProtoPackages.ReadPackage`
|
||||
|
||||
### 1.2 Connector Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/connector/client.config.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 18
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `ConnectorManager.ProtoPackages.WritePackage`
|
||||
- `ConnectorManager.ProtoPackages.ReadPackage`
|
||||
|
||||
### 1.3 Inputs Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/inputs/inputs-client.config.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 17
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `Input.ProtoPackages.WritePackage`
|
||||
- `Input.ProtoPackages.ReadPackage`
|
||||
|
||||
### 1.4 Network Config Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/network-config/network-config-client.config.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 19
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `NetworkConfig.ProtoPackages.WritePackage`
|
||||
- `NetworkConfig.ProtoPackages.ReadPackage`
|
||||
|
||||
### 1.5 Connection Test Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/connection-test/connection-test-client.config.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 17
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `ConnectionTest.ProtoPackages.ReadPackage`
|
||||
|
||||
### 1.6 Transformations Client (`INFACTORY_URL`)
|
||||
**File:** `src/modules/transformations/transformations-client.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 18
|
||||
url: process.env.INFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `Transformation.ProtoPackages.WritePackage`
|
||||
|
||||
---
|
||||
|
||||
## 2. SERVICE CLASSES (Business Logic to Migrate)
|
||||
|
||||
### 2.1 Connection Client Service
|
||||
**File:** `src/modules/connection/client.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `CreateConnection()` | ConnectionManagerWriteServices | Write |
|
||||
| `UpdateConnection()` | ConnectionManagerWriteServices | Write |
|
||||
| `DeleteConnection()` | ConnectionManagerWriteServices | Write |
|
||||
| `GetConnectionDetails()` | ConnectionManagerReadServices | Read |
|
||||
| `GetAllConnections()` | ConnectionManagerReadServices | Read |
|
||||
| `GetConnectorAvailableConnectionsByCustomer()` | ConnectionManagerReadServices | Read |
|
||||
| `ValidatePlatformConnections()` | ConnectionManagerReadServices | Read |
|
||||
|
||||
**Key Methods:**
|
||||
- `createConnection()`
|
||||
- `updateConnection()`
|
||||
- `deleteConnection()`
|
||||
- `getConnections()`
|
||||
- `getConnectionDetails()`
|
||||
- `validatePlatformConnections()`
|
||||
|
||||
### 2.2 Connector Client Service
|
||||
**File:** `src/modules/connector/client.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `RegisterConnector()` | ConnectorManagerWriteServices | Write |
|
||||
| `UploadFile()` | ConnectorManagerWriteServices | Write |
|
||||
| `RegisterMultipleConnectorsWithoutImage()` | ConnectorManagerWriteServices | Write |
|
||||
| `UpdateConnectorByID()` | ConnectorManagerWriteServices | Write |
|
||||
| `DeleteConnectorById()` | ConnectorManagerWriteServices | Write |
|
||||
| `GetConnectors()` | ConnectorManagerReadServices | Read |
|
||||
|
||||
### 2.3 Inputs Service
|
||||
**File:** `src/modules/inputs/inputs.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `CreateInput()` | InputWriteService | Write |
|
||||
| `UpdateInput()` | InputWriteService | Write |
|
||||
| `DeleteInput()` | InputWriteService | Write |
|
||||
| `GetAvailableEntities()` | InputReadService | Read |
|
||||
|
||||
### 2.4 Network Config Service
|
||||
**File:** `src/modules/network-config/network-config.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `NetworkConfigCreate()` | NetworkConfigWriteService | Write |
|
||||
| `NetworkConfigUpdate()` | NetworkConfigWriteService | Write |
|
||||
| `NetworkConfigDelete()` | NetworkConfigWriteService | Write |
|
||||
| `NetworkConfigFindAll()` | NetworkConfigReadService | Read |
|
||||
| `NetworkConfigFindOneById()` | NetworkConfigReadService | Read |
|
||||
|
||||
### 2.5 Connection Test Service
|
||||
**File:** `src/modules/connection-test/connection-test.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `TestConnection()` | ConnectionTestReadService | Read |
|
||||
|
||||
### 2.6 Transformations Service
|
||||
**File:** `src/modules/transformations/client.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Direction |
|
||||
|--------|---------|-----------|
|
||||
| `CreateTransformation()` | TransformationWriteService | Write |
|
||||
| `UpdateTransformation()` | TransformationWriteService | Write |
|
||||
| `DeleteTransformation()` | TransformationWriteService | Write |
|
||||
| Various read operations | TransformationReadService | Read |
|
||||
|
||||
---
|
||||
|
||||
## 3. NESTJS MODULES (Module Registration)
|
||||
|
||||
These modules register the gRPC clients and export services:
|
||||
|
||||
| Module | File | Imports | Exports |
|
||||
|--------|------|---------|---------|
|
||||
| ConnectionModule | `src/modules/connection/connection.module.ts` | ConnectionClientConfiguration | ConnectionClientService |
|
||||
| ConnectorModule | `src/modules/connector/connector.module.ts` | ConnectorClientConfiguration | ConnectorClientService |
|
||||
| InputsModule | `src/modules/inputs/inputs.module.ts` | InputsGrpcClient | InputsService |
|
||||
| NetworkConfigModule | `src/modules/network-config/network-config.module.ts` | NetworkConfigGrpcClient | NetworkConfigService |
|
||||
| ConnectionTestModule | `src/modules/connection-test/connection-test.module.ts` | ConnectionTestClientConfiguration | ConnectionTestService |
|
||||
| TransformationsModule | `src/modules/transformations/transformations.module.ts` | TransformationsClientConfiguration | TransformationsService |
|
||||
|
||||
**App Module Registration:** `src/app.module.ts` (Lines 15-21, 50-60)
|
||||
|
||||
---
|
||||
|
||||
## 4. REST CONTROLLERS (API Endpoints to Migrate)
|
||||
|
||||
### 4.1 Connection Controller
|
||||
**File:** `src/modules/connection/connection.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| POST | `/connections` | Create a new connection |
|
||||
| PUT | `/connections/:id` | Update an existing connection |
|
||||
| DELETE | `/connections/:id` | Delete a connection |
|
||||
| GET | `/connections` | Get all connections |
|
||||
| GET | `/connections/:id` | Get connection details |
|
||||
| GET | `/connections/available/:connector_id` | Get available connections by connector |
|
||||
|
||||
### 4.2 Connector Controller
|
||||
**File:** `src/modules/connector/connector.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| POST | `/connectors` | Register a new connector |
|
||||
| POST | `/connectors/uploads` | Upload multiple connector files |
|
||||
| GET | `/connectors` | List all connectors |
|
||||
| PUT | `/connectors/:id` | Update a connector |
|
||||
| DELETE | `/connectors/:id` | Delete a connector |
|
||||
|
||||
### 4.3 Inputs Controller
|
||||
**File:** `src/modules/inputs/inputs.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| GET | `/inputs/available-entities/:plugin` | Get available entities for a plugin |
|
||||
| POST | `/inputs` | Create an input |
|
||||
| PATCH | `/inputs` | Update an input |
|
||||
| DELETE | `/inputs/:id` | Delete an input |
|
||||
|
||||
### 4.4 Network Config Controller
|
||||
**File:** `src/modules/network-config/network-config.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| GET | `/network-configs` | Get all network configurations |
|
||||
| GET | `/network-configs/:id` | Get network config by ID |
|
||||
| POST | `/network-configs` | Create network configuration |
|
||||
| PUT | `/network-configs/:id` | Update network configuration |
|
||||
| DELETE | `/network-configs/:id` | Delete network configuration |
|
||||
|
||||
### 4.5 Connection Test Controller
|
||||
**File:** `src/modules/connection-test/connection-test.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| POST | `/connection-test` | Test a connection |
|
||||
|
||||
### 4.6 Transformations Controller
|
||||
**File:** `src/modules/transformations/transformations.controller.ts`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| POST | `/transformations` | Create transformation |
|
||||
| GET | `/transformations` | Get transformations |
|
||||
| PUT | `/transformations/:id` | Update transformation |
|
||||
| DELETE | `/transformations/:id` | Delete transformation |
|
||||
|
||||
---
|
||||
|
||||
## 5. CONFIGURATION FILES
|
||||
|
||||
### 5.1 Helm Chart Values (Production)
|
||||
**File:** `deploy/helm-chart/values.yaml`
|
||||
```yaml
|
||||
# Lines 32, 42
|
||||
maestro:
|
||||
in_factory_url: in-factory.dadosfera.ai
|
||||
tr_factory_url: in-factory.dadosfera.ai # Transformation factory also uses in-factory
|
||||
```
|
||||
|
||||
### 5.2 Helm Chart Values (Staging)
|
||||
**File:** `deploy/helm-chart/values-stg.yaml`
|
||||
```yaml
|
||||
# Lines 5-6
|
||||
maestro:
|
||||
in_factory_url: in-factory.stg.dadosfera.ai
|
||||
tr_factory_url: in-factory.stg.dadosfera.ai
|
||||
```
|
||||
|
||||
### 5.3 Helmfiles (Production)
|
||||
**File:** `deploy/helmfiles/prd.yaml`
|
||||
```yaml
|
||||
# Lines 15-18, 39-42 (for both maestro and maestro-unimed releases)
|
||||
maestro.in_factory_url: in-factory.dadosfera.ai
|
||||
maestro.tr_factory_url: in-factory.dadosfera.ai
|
||||
```
|
||||
|
||||
### 5.4 Deployment Template
|
||||
**File:** `deploy/helm-chart/templates/deployment.yaml`
|
||||
```yaml
|
||||
# Lines 71-72
|
||||
- name: INFACTORY_URL
|
||||
value: {{ .Values.maestro.in_factory_url }}
|
||||
```
|
||||
|
||||
### 5.5 Environment Type Definition
|
||||
**File:** `environment.d.ts`
|
||||
```typescript
|
||||
// Line 9
|
||||
INFACTORY_URL: string;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PROTO PACKAGE DEPENDENCIES
|
||||
|
||||
### 6.1 Package.json
|
||||
**File:** `package.json`
|
||||
```json
|
||||
{
|
||||
"@dadosfera/protospack": "2.5.3",
|
||||
"@dadosfera/protospack-v2": "3.38.0-beta.14"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Proto Imports from `@dadosfera/protospack-v2` (In-Factory related)
|
||||
|
||||
| Import Path | Used In |
|
||||
|-------------|---------|
|
||||
| `ConnectionManager` | connection/client.config.ts, connection/client.service.ts |
|
||||
| `ConnectionManager/interfaces/messages` | connection/client.service.ts, connection/dtos/connection.ts |
|
||||
| `ConnectionManager/interfaces/entities` | connection/dtos/connection.ts |
|
||||
| `ConnectorManager` | connector/client.config.ts, connector/client.service.ts |
|
||||
| `Input` | inputs/inputs-client.config.ts, inputs/inputs.service.ts |
|
||||
| `Input/interfaces/messages` | inputs/inputs.service.ts |
|
||||
| `Input/interfaces/entities` | inputs/inputs.controller.ts, inputs/inputs.service.ts |
|
||||
| `NetworkConfig` | network-config/network-config-client.config.ts, network-config/network-config.service.ts |
|
||||
| `NetworkConfig/interfaces/entities` | network-config/dto/network-config.ts |
|
||||
| `ConnectionTest` | connection-test/connection-test-client.config.ts, connection-test/connection-test.service.ts |
|
||||
| `Transformation` | transformations/transformations-client.ts, transformations/client.service.ts |
|
||||
|
||||
---
|
||||
|
||||
## 7. MIGRATION STRATEGY OPTIONS
|
||||
|
||||
### Option A: Move In-Factory functionality INTO Maestro
|
||||
**Pros:**
|
||||
- Single service to maintain
|
||||
- No network latency for these operations
|
||||
- Simpler deployment
|
||||
|
||||
**Cons:**
|
||||
- Increases Maestro's responsibility/complexity
|
||||
- Requires database access from Maestro
|
||||
- May require significant refactoring
|
||||
|
||||
**Files to create/migrate:**
|
||||
1. Database models for connections, connectors, inputs, network-configs, transformations
|
||||
2. Repository layer for database operations
|
||||
3. Convert gRPC services to internal services
|
||||
4. Remove all gRPC client configurations
|
||||
|
||||
### Option B: Create REST API wrapper in In-Factory
|
||||
**Pros:**
|
||||
- Minimal changes to Maestro
|
||||
- Can migrate incrementally
|
||||
|
||||
**Cons:**
|
||||
- Still maintains dependency
|
||||
- Additional REST→gRPC translation layer
|
||||
|
||||
### Option C: Direct database access from Maestro
|
||||
**Pros:**
|
||||
- Removes runtime dependency
|
||||
- Better performance
|
||||
|
||||
**Cons:**
|
||||
- Shared database coupling
|
||||
- Complex migration
|
||||
|
||||
---
|
||||
|
||||
## 8. FILES TO MODIFY/DELETE (Summary)
|
||||
|
||||
### High Priority - Core Integration Files
|
||||
```
|
||||
src/modules/connection/client.config.ts → DELETE or REPLACE
|
||||
src/modules/connection/client.service.ts → REPLACE with local implementation
|
||||
src/modules/connector/client.config.ts → DELETE or REPLACE
|
||||
src/modules/connector/client.service.ts → REPLACE with local implementation
|
||||
src/modules/inputs/inputs-client.config.ts → DELETE or REPLACE
|
||||
src/modules/inputs/inputs.service.ts → REPLACE with local implementation
|
||||
src/modules/network-config/network-config-client.config.ts → DELETE or REPLACE
|
||||
src/modules/network-config/network-config.service.ts → REPLACE with local implementation
|
||||
src/modules/connection-test/connection-test-client.config.ts → DELETE or REPLACE
|
||||
src/modules/connection-test/connection-test.service.ts → REPLACE with local implementation
|
||||
src/modules/transformations/transformations-client.ts → DELETE or REPLACE
|
||||
src/modules/transformations/client.service.ts → REPLACE with local implementation
|
||||
```
|
||||
|
||||
### Medium Priority - Module Registration
|
||||
```
|
||||
src/modules/connection/connection.module.ts → UPDATE imports
|
||||
src/modules/connector/connector.module.ts → UPDATE imports
|
||||
src/modules/inputs/inputs.module.ts → UPDATE imports
|
||||
src/modules/network-config/network-config.module.ts → UPDATE imports
|
||||
src/modules/connection-test/connection-test.module.ts → UPDATE imports
|
||||
src/modules/transformations/transformations.module.ts → UPDATE imports
|
||||
src/app.module.ts → UPDATE if module structure changes
|
||||
```
|
||||
|
||||
### Low Priority - Configuration
|
||||
```
|
||||
deploy/helm-chart/values.yaml → REMOVE in_factory_url, tr_factory_url
|
||||
deploy/helm-chart/values-stg.yaml → REMOVE in_factory_url, tr_factory_url
|
||||
deploy/helmfiles/prd.yaml → REMOVE in_factory_url references
|
||||
deploy/helm-chart/templates/deployment.yaml → REMOVE INFACTORY_URL env var
|
||||
environment.d.ts → REMOVE INFACTORY_URL type
|
||||
README.md → UPDATE architecture diagram
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. DEPENDENCY COUNT BY MODULE
|
||||
|
||||
| Module | Files | gRPC Calls | REST Endpoints |
|
||||
|--------|-------|------------|----------------|
|
||||
| Connection | 4 | 7 | 6 |
|
||||
| Connector | 3 | 6 | 5 |
|
||||
| Inputs | 3 | 4 | 4 |
|
||||
| Network Config | 3 | 5 | 5 |
|
||||
| Connection Test | 3 | 1 | 1 |
|
||||
| Transformations | 4 | 4+ | 4 |
|
||||
| **TOTAL** | **20** | **27+** | **25** |
|
||||
|
||||
---
|
||||
|
||||
## 10. DATA MODELS (Proto Messages Used)
|
||||
|
||||
### Connection Manager
|
||||
- `CreateConnectionRequest` / `CreateConnectionResponse`
|
||||
- `UpdateConnectionRequest` / `UpdateConnectionResponse`
|
||||
- `DeleteConnectionRequest` / `DeleteConnectionResponse`
|
||||
- `GetConnectionDetailsRequest` / `GetConnectionDetailsResponse`
|
||||
- `GetAllConnectionsRequest` / `GetAllConnectionsResponse`
|
||||
- `Connection` (entity)
|
||||
- `ConnectionCredential` (entity)
|
||||
|
||||
### Connector Manager
|
||||
- `RegisterConnectorRequest` / `RegisterConnectorResponse`
|
||||
- `UploadFileRequest` / `UploadFileResponse`
|
||||
- `GetConnectorsRequest` / `GetConnectorsResponse`
|
||||
- `Connector` (entity)
|
||||
|
||||
### Input
|
||||
- `CreateInputRequest` / `CreateInputResponse`
|
||||
- `UpdateInputRequest` / `UpdateInputResponse`
|
||||
- `DeleteInputRequest` / `DeleteInputResponse`
|
||||
- `GetAvailableEntitiesRequest` / `GetAvailableEntitiesResponse`
|
||||
- `Info` (entity)
|
||||
|
||||
### Network Config
|
||||
- `NetworkConfigCreateRequest` / `NetworkConfigCreateResponse`
|
||||
- `NetworkConfigUpdateRequest` / `NetworkConfigUpdateResponse`
|
||||
- `NetworkConfigDeleteRequest` / `NetworkConfigDeleteResponse`
|
||||
- `NetworkConfigFindAllRequest` / `NetworkConfigFindAllResponse`
|
||||
- `NetworkConfig` (entity)
|
||||
|
||||
### Connection Test
|
||||
- `TestConnectionRequest` / `TestConnectionResponse`
|
||||
|
||||
### Transformation
|
||||
- `CreateTransformationRequest` / `CreateTransformationResponse`
|
||||
- `UpdateTransformationRequest` / `UpdateTransformationResponse`
|
||||
- `DeleteTransformationRequest` / `DeleteTransformationResponse`
|
||||
|
||||
---
|
||||
|
||||
## NEXT STEPS
|
||||
|
||||
1. **Decide on migration strategy** (Option A, B, or C)
|
||||
2. **Prioritize modules** - Recommend starting with Connection Test (smallest), then Inputs, Network Config, Transformations, Connection, Connector (largest)
|
||||
3. **Create database schema** if moving to Option A
|
||||
4. **Implement local services** one module at a time
|
||||
5. **Update tests** for each migrated module
|
||||
6. **Update deployment configs** to remove INFACTORY_URL
|
||||
7. **Coordinate with In-Factory team** for data migration
|
||||
@@ -1,320 +0,0 @@
|
||||
# In-Factory Side - Migration Requirements
|
||||
|
||||
This document outlines what needs to be addressed in the **In-Factory** service to remove its coupling with Maestro.
|
||||
|
||||
> **Note:** This analysis is based on the Maestro codebase. For a complete analysis, the In-Factory repository should also be reviewed.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture (In-Factory → Maestro)
|
||||
|
||||
Based on the Maestro codebase analysis, **In-Factory** exposes the following gRPC services that Maestro consumes:
|
||||
|
||||
```mermaid
|
||||
graph LR;
|
||||
Maestro -->|gRPC| InFactory;
|
||||
subgraph InFactory Services
|
||||
CM[ConnectionManager]
|
||||
ConM[ConnectorManager]
|
||||
IN[Input]
|
||||
NC[NetworkConfig]
|
||||
CT[ConnectionTest]
|
||||
TR[Transformation]
|
||||
end
|
||||
Maestro --> CM;
|
||||
Maestro --> ConM;
|
||||
Maestro --> IN;
|
||||
Maestro --> NC;
|
||||
Maestro --> CT;
|
||||
Maestro --> TR;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## gRPC Services Exposed by In-Factory
|
||||
|
||||
### 1. ConnectionManager Service
|
||||
|
||||
**Package:** `ConnectionManager` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Write Services (`ConnectionManagerWriteServices`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `CreateConnection` | Create a new data connection | Maestro POST /connections |
|
||||
| `UpdateConnection` | Update an existing connection | Maestro PUT /connections/:id |
|
||||
| `DeleteConnection` | Delete a connection | Maestro DELETE /connections/:id |
|
||||
|
||||
#### Read Services (`ConnectionManagerReadServices`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `GetConnectionDetails` | Get details of a single connection | Maestro GET /connections/:id |
|
||||
| `GetAllConnections` | List all connections for a customer | Maestro GET /connections |
|
||||
| `GetConnectorAvailableConnectionsByCustomer` | Get available connections by connector | Maestro GET /connections/available/:connector_id |
|
||||
| `ValidatePlatformConnections` | Validate connections against platform | Maestro internal |
|
||||
|
||||
---
|
||||
|
||||
### 2. ConnectorManager Service
|
||||
|
||||
**Package:** `ConnectorManager` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Write Services (`ConnectorManagerWriteServices`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `RegisterConnector` | Register a new connector | Maestro POST /connectors |
|
||||
| `UploadFile` | Upload connector files | Maestro POST /connectors/uploads |
|
||||
| `RegisterMultipleConnectorsWithoutImage` | Bulk register connectors | Maestro internal |
|
||||
| `UpdateConnectorByID` | Update a connector | Maestro PUT /connectors/:id |
|
||||
| `DeleteConnectorById` | Delete a connector | Maestro DELETE /connectors/:id |
|
||||
|
||||
#### Read Services (`ConnectorManagerReadServices`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `GetConnectors` | List all connectors | Maestro GET /connectors |
|
||||
|
||||
---
|
||||
|
||||
### 3. Input Service
|
||||
|
||||
**Package:** `Input` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Write Services (`InputWriteService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `CreateInput` | Create a new input configuration | Maestro POST /inputs |
|
||||
| `UpdateInput` | Update input configuration | Maestro PATCH /inputs |
|
||||
| `DeleteInput` | Delete an input | Maestro DELETE /inputs/:id |
|
||||
|
||||
#### Read Services (`InputReadService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `GetAvailableEntities` | Get available entities for a plugin | Maestro GET /inputs/available-entities/:plugin |
|
||||
|
||||
---
|
||||
|
||||
### 4. NetworkConfig Service
|
||||
|
||||
**Package:** `NetworkConfig` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Write Services (`NetworkConfigWriteService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `NetworkConfigCreate` | Create network configuration | Maestro POST /network-configs |
|
||||
| `NetworkConfigUpdate` | Update network configuration | Maestro PUT /network-configs/:id |
|
||||
| `NetworkConfigDelete` | Delete network configuration | Maestro DELETE /network-configs/:id |
|
||||
|
||||
#### Read Services (`NetworkConfigReadService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `NetworkConfigFindAll` | List all network configs | Maestro GET /network-configs |
|
||||
| `NetworkConfigFindOneById` | Get network config by ID | Maestro GET /network-configs/:id |
|
||||
|
||||
---
|
||||
|
||||
### 5. ConnectionTest Service
|
||||
|
||||
**Package:** `ConnectionTest` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Read Services (`ConnectionTestReadService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `TestConnection` | Test a connection's connectivity | Maestro POST /connection-test |
|
||||
|
||||
---
|
||||
|
||||
### 6. Transformation Service
|
||||
|
||||
**Package:** `Transformation` from `@dadosfera/protospack-v2`
|
||||
|
||||
#### Write Services (`TransformationWriteService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| `CreateTransformation` | Create a transformation | Maestro POST /transformations |
|
||||
| `UpdateTransformation` | Update a transformation | Maestro PUT /transformations/:id |
|
||||
| `DeleteTransformation` | Delete a transformation | Maestro DELETE /transformations/:id |
|
||||
|
||||
#### Read Services (`TransformationReadService`)
|
||||
| gRPC Method | Description | Called By |
|
||||
|-------------|-------------|-----------|
|
||||
| Various read operations | Get transformation details | Maestro GET /transformations |
|
||||
|
||||
---
|
||||
|
||||
## Proto Package Ownership
|
||||
|
||||
The proto definitions are managed in `@dadosfera/protospack-v2`:
|
||||
|
||||
```
|
||||
@dadosfera/protospack-v2/
|
||||
├── dist/lib/
|
||||
│ ├── ConnectionManager/
|
||||
│ │ ├── interfaces/messages.ts
|
||||
│ │ └── interfaces/entities.ts
|
||||
│ ├── ConnectorManager/
|
||||
│ │ ├── interfaces/messages.ts
|
||||
│ │ └── interfaces/entities.ts
|
||||
│ ├── Input/
|
||||
│ │ ├── interfaces/messages.ts
|
||||
│ │ └── interfaces/entities.ts
|
||||
│ ├── NetworkConfig/
|
||||
│ │ ├── interfaces/messages.ts
|
||||
│ │ └── interfaces/entities.ts
|
||||
│ ├── ConnectionTest/
|
||||
│ │ ├── interfaces/messages.ts
|
||||
│ │ └── interfaces/entities.ts
|
||||
│ └── Transformation/
|
||||
│ ├── interfaces/messages.ts
|
||||
│ └── interfaces/entities.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## In-Factory Migration Options
|
||||
|
||||
### Option 1: Expose REST API (Keep In-Factory, Add REST Layer)
|
||||
|
||||
**Changes Required in In-Factory:**
|
||||
1. Add REST controllers for all services
|
||||
2. Implement HTTP endpoints mirroring gRPC methods
|
||||
3. Update deployment to expose HTTP port
|
||||
4. Create OpenAPI documentation
|
||||
|
||||
**Pros:**
|
||||
- Minimal architectural changes
|
||||
- Can run both gRPC and REST in parallel during migration
|
||||
- Maestro can switch to REST calls gradually
|
||||
|
||||
**Cons:**
|
||||
- Still maintains service dependency
|
||||
- Adds another communication layer
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Move Logic to Maestro (Deprecate In-Factory for these features)
|
||||
|
||||
**Changes Required in In-Factory:**
|
||||
1. Export database schema/migrations
|
||||
2. Document all business logic
|
||||
3. Provide data migration scripts
|
||||
4. Deprecate gRPC endpoints after migration
|
||||
|
||||
**Changes Required in Maestro:**
|
||||
1. Create database models
|
||||
2. Implement repositories
|
||||
3. Create service layer with same business logic
|
||||
4. Run data migration
|
||||
|
||||
**Pros:**
|
||||
- Removes runtime dependency completely
|
||||
- Simplifies architecture
|
||||
- One less service to maintain
|
||||
|
||||
**Cons:**
|
||||
- Significant development effort
|
||||
- Risk of business logic divergence during migration
|
||||
- Database sharing concerns
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Merge Services (Combine In-Factory into a larger service)
|
||||
|
||||
**Changes Required:**
|
||||
1. Create new combined service
|
||||
2. Migrate both In-Factory and relevant Maestro code
|
||||
3. Update all clients
|
||||
|
||||
**Pros:**
|
||||
- Clean architectural redesign
|
||||
- Opportunity to optimize
|
||||
|
||||
**Cons:**
|
||||
- Largest effort
|
||||
- Risk of disruption
|
||||
|
||||
---
|
||||
|
||||
## Data Migration Considerations
|
||||
|
||||
### Entities Managed by In-Factory
|
||||
|
||||
Based on proto definitions, In-Factory manages:
|
||||
|
||||
1. **Connections**
|
||||
- Connection credentials
|
||||
- Connection metadata
|
||||
- Customer associations
|
||||
|
||||
2. **Connectors**
|
||||
- Connector definitions
|
||||
- Connector images/files
|
||||
- Plugin configurations
|
||||
|
||||
3. **Inputs**
|
||||
- Input configurations
|
||||
- Entity mappings
|
||||
|
||||
4. **Network Configs**
|
||||
- Network configuration settings
|
||||
- Security settings
|
||||
|
||||
5. **Transformations**
|
||||
- Transformation definitions
|
||||
- Transformation scripts
|
||||
|
||||
### Migration Steps
|
||||
1. Export database schema from In-Factory
|
||||
2. Create equivalent schema in target database
|
||||
3. Write data migration scripts
|
||||
4. Validate data integrity
|
||||
5. Switch traffic
|
||||
6. Decommission old service
|
||||
|
||||
---
|
||||
|
||||
## Recommended Investigation for In-Factory Team
|
||||
|
||||
1. **Check for Maestro dependencies in In-Factory**
|
||||
- Does In-Factory call any Maestro APIs?
|
||||
- Are there any shared databases?
|
||||
- Any shared message queues?
|
||||
|
||||
2. **Document database schema**
|
||||
- All tables related to connections, connectors, inputs, network-configs, transformations
|
||||
- Foreign key relationships
|
||||
- Indexes and constraints
|
||||
|
||||
3. **List all consumers**
|
||||
- Besides Maestro, who else calls In-Factory?
|
||||
- Are there other internal services?
|
||||
- Any external integrations?
|
||||
|
||||
4. **Business logic documentation**
|
||||
- Validation rules
|
||||
- Business constraints
|
||||
- Side effects (events, notifications, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Timeline Considerations
|
||||
|
||||
| Phase | Description | Dependencies |
|
||||
|-------|-------------|--------------|
|
||||
| Phase 1 | Analysis & Planning | Both teams available |
|
||||
| Phase 2 | Schema/API Design | Proto definitions finalized |
|
||||
| Phase 3 | Implementation | Development resources |
|
||||
| Phase 4 | Data Migration | Database access, downtime window |
|
||||
| Phase 5 | Testing | QA resources, test environments |
|
||||
| Phase 6 | Cutover | Deployment coordination |
|
||||
| Phase 7 | Decommission | Monitoring, rollback plan |
|
||||
|
||||
---
|
||||
|
||||
## Questions for In-Factory Team
|
||||
|
||||
1. What database does In-Factory use? (PostgreSQL, MongoDB, etc.)
|
||||
2. Are there any async operations? (message queues, event sourcing)
|
||||
3. What is the current data volume for each entity type?
|
||||
4. Are there any scheduled jobs or background processes?
|
||||
5. What monitoring/alerting is in place?
|
||||
6. Are there any data retention policies?
|
||||
7. What is the backup/recovery strategy?
|
||||
@@ -1,486 +0,0 @@
|
||||
# Maestro ↔ PI-Factory Migration Map
|
||||
|
||||
This document maps all integration points between Maestro and PI-Factory that need to be addressed to remove the dependency.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
Frontend<-->Maestro;
|
||||
Maestro<-->DUC;
|
||||
Maestro<-->pi-factory;
|
||||
Maestro<-->in-factory;
|
||||
```
|
||||
|
||||
PI-Factory (`PIFACTORY_URL`) is responsible for:
|
||||
- **Pipeline Management** - Create, read, update, delete pipelines
|
||||
- **Catalog Services** - Data asset management, metadata, previews
|
||||
- **Platform Interfaces** - Dataset cataloging operations
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Category | Count | Impact Level |
|
||||
|----------|-------|--------------|
|
||||
| gRPC Client Configurations | 3 | HIGH |
|
||||
| NestJS Modules | 3 | HIGH |
|
||||
| REST Controllers/Endpoints | 3 (~50 endpoints) | HIGH |
|
||||
| Service Classes | 4 | HIGH |
|
||||
| Configuration Files | 4 | MEDIUM |
|
||||
| Proto Package Dependencies | 2 | HIGH |
|
||||
| Environment Variables | 1 | LOW |
|
||||
|
||||
---
|
||||
|
||||
## 1. gRPC CLIENT CONFIGURATIONS (Files to Migrate)
|
||||
|
||||
These files configure gRPC connections to PI-Factory services:
|
||||
|
||||
### 1.1 Catalog Client (`PIFACTORY_URL`)
|
||||
**File:** `src/modules/catalog/catalog-client.ts`
|
||||
```typescript
|
||||
// Lines 11-12, 19
|
||||
process.env.PIFACTORY_URL.startsWith('pi-factory:')
|
||||
process.env.PIFACTORY_URL.includes('0.0.0.0')
|
||||
url: process.env.PIFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `Catalog.ProtoPackages.ReadPackage`
|
||||
- `Catalog.ProtoPackages.WritePackage`
|
||||
- `PlatformInterfaces.ProtoPackages.WritePackage`
|
||||
|
||||
### 1.2 Pipelines V2 Client (`PIFACTORY_URL`)
|
||||
**File:** `src/modules/pipelinesV2/pipelines-client.ts`
|
||||
```typescript
|
||||
// Lines 13-14, 21
|
||||
url: process.env.PIFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `PipelineV2.ProtoPackages.ReadPackage`
|
||||
- `PipelineV2.ProtoPackages.WritePackage`
|
||||
|
||||
### 1.3 Pipelines Client (Legacy) (`PIFACTORY_URL`)
|
||||
**File:** `src/modules/pipelines/pipelines-client.ts`
|
||||
```typescript
|
||||
// Lines 10-11, 18
|
||||
url: process.env.PIFACTORY_URL
|
||||
```
|
||||
**Proto Services Used:**
|
||||
- `PipelinePackages` from `@dadosfera/protospack`
|
||||
|
||||
---
|
||||
|
||||
## 2. SERVICE CLASSES (Business Logic to Migrate)
|
||||
|
||||
### 2.1 Catalog Service
|
||||
**File:** `src/modules/catalog/catalog.service.ts`
|
||||
|
||||
**gRPC Services Initialized:**
|
||||
- `CatalogReadServices` (from `Catalog.ReadService`)
|
||||
- `CatalogWriteServices` (from `Catalog.WriteService`)
|
||||
- `PlatformInterfacesWriteServices` (from `PlatformInterfaces.WriteService`)
|
||||
|
||||
**gRPC Methods Called:**
|
||||
|
||||
| Method | Service | Description |
|
||||
|--------|---------|-------------|
|
||||
| `GetAllDataAssets()` | CatalogReadServices | Search/list data assets |
|
||||
| `GetOneDataAsset()` | CatalogReadServices | Get single data asset by ID |
|
||||
| `GetOneDataAssetByPipelineAndObject()` | CatalogReadServices | Get asset by pipeline/object |
|
||||
| `GetDatasetDoc()` | CatalogReadServices | Get dataset documentation |
|
||||
| `GetDatasetPreview()` | CatalogReadServices | Get data preview |
|
||||
| `GetDatasetColumnsMetadata()` | CatalogReadServices | Get column metadata |
|
||||
| `GetCustomerTags()` | CatalogReadServices | Get all tags for customer |
|
||||
| `GetDatasetCatalogTask()` | CatalogReadServices | Get catalog task status |
|
||||
| `GetRlsRules()` | CatalogReadServices | Get RLS rules |
|
||||
| `GetOneRlsRule()` | CatalogReadServices | Get single RLS rule |
|
||||
| `GetNimbusDashboards()` | CatalogReadServices | Get Nimbus dashboards |
|
||||
| `CreateDataAsset()` | CatalogWriteServices | Create new data asset |
|
||||
| `UpdateDataAsset()` | CatalogWriteServices | Update existing data asset |
|
||||
| `DeleteDataAsset()` | CatalogWriteServices | Delete data asset |
|
||||
| `ManagePermission()` | CatalogWriteServices | Manage asset permissions |
|
||||
| `RevokePermission()` | CatalogWriteServices | Revoke asset permissions |
|
||||
| `MakeAComment()` | CatalogWriteServices | Add comment to asset |
|
||||
| `UpdateAComment()` | CatalogWriteServices | Update/delete comment |
|
||||
| `TriggerDatasetCataloging()` | CatalogWriteServices | Trigger catalog process |
|
||||
| `AddRlsRule()` | CatalogWriteServices | Add RLS rule |
|
||||
| `RemoveRlsRule()` | CatalogWriteServices | Remove RLS rule |
|
||||
| `RemoveRlsRulesByRlsId()` | CatalogWriteServices | Batch remove by RLS ID |
|
||||
| `RemoveRlsRulesByDashboardId()` | CatalogWriteServices | Batch remove by dashboard |
|
||||
| `GetPiiReporter()` | CatalogWriteServices | Get PII report data |
|
||||
| `CatalogDataAssets()` | PlatformInterfacesWriteServices | Catalog datasets |
|
||||
|
||||
**Additional HTTP Calls to Nimbus:**
|
||||
- `POST ${nimbusUrl}/api/catalog/data-docs/` - Create data docs
|
||||
- `POST ${nimbusUrl}/api/catalog/table-metadata/` - Create table metadata
|
||||
- `POST ${nimbusUrl}/api/catalog/column-metadata/` - Create column metadata
|
||||
- `POST ${nimbusUrl}/api/catalog/data-preview/` - Create data preview
|
||||
|
||||
### 2.2 Pipelines V2 Service
|
||||
**File:** `src/modules/pipelinesV2/pipelines.service.ts`
|
||||
|
||||
**gRPC Services Initialized:**
|
||||
- `PipelineV2ReadService` (from `PipelineV2.ReadService`)
|
||||
- `PipelineV2WriteService` (from `PipelineV2.WriteService`)
|
||||
|
||||
**gRPC Methods Called:**
|
||||
|
||||
| Method | Service | Description |
|
||||
|--------|---------|-------------|
|
||||
| `PipelineV2Create()` | PipelineV2WriteService | Create new pipeline |
|
||||
| `PipelineV2Update()` | PipelineV2WriteService | Update pipeline |
|
||||
| `PipelineV2Remove()` | PipelineV2WriteService | Delete pipeline |
|
||||
| `PipelineV2UploadFile()` | PipelineV2WriteService | Initialize file upload |
|
||||
| `PipelineV2CompleteUploadFile()` | PipelineV2WriteService | Complete file upload |
|
||||
| `PipelineV2FindAll()` | PipelineV2ReadService | List all pipelines |
|
||||
| `PipelineV2FindOne()` | PipelineV2ReadService | Get single pipeline |
|
||||
| `PipelineV2FindObjects()` | PipelineV2ReadService | Get pipeline objects |
|
||||
| `PipelineV2DownloadLogs()` | PipelineV2ReadService | Download pipeline logs |
|
||||
| `PipelineV2GetDashboardUrl()` | PipelineV2ReadService | Get monitoring dashboard URL |
|
||||
|
||||
### 2.3 Pipelines Service (Legacy)
|
||||
**File:** `src/modules/pipelines/pipelines.service.ts`
|
||||
|
||||
**Uses:** `PipelinesClientService`
|
||||
|
||||
**Methods:**
|
||||
- `getPipelineStatus()` - Get pipeline execution status
|
||||
- `runPipeline()` - Trigger pipeline execution
|
||||
|
||||
### 2.4 Pipelines Client Service (Legacy)
|
||||
**File:** `src/modules/pipelines/client.service.ts`
|
||||
|
||||
**gRPC Methods Called:**
|
||||
| Method | Service | Description |
|
||||
|--------|---------|-------------|
|
||||
| `getPipelineStatus()` | PipelineService | Get pipeline status |
|
||||
| `triggerPipeline()` | PipelineService | Trigger pipeline run |
|
||||
|
||||
---
|
||||
|
||||
## 3. NESTJS MODULES (Module Registration)
|
||||
|
||||
| Module | File | Client Configuration | Exports |
|
||||
|--------|------|---------------------|---------|
|
||||
| CatalogModule | `src/modules/catalog/catalog.module.ts` | CatalogClientConfiguration | CatalogService |
|
||||
| PipelinesV2Module | `src/modules/pipelinesV2/pipelines.module.ts` | PipelinesClientConfiguration | PipelinesService |
|
||||
| PipelinesModule | `src/modules/pipelines/pipelines.module.ts` | PipelinesClientConfiguration | PipelinesService, PipelinesClientService |
|
||||
|
||||
**App Module Registration:** `src/app.module.ts` (Lines 20, 23, 25, 53-54, 59)
|
||||
|
||||
---
|
||||
|
||||
## 4. REST CONTROLLERS (API Endpoints to Migrate)
|
||||
|
||||
### 4.1 Catalog Controller
|
||||
**File:** `src/modules/catalog/catalog.controller.ts`
|
||||
**Base Path:** `/catalog`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| GET | `/catalog` | Search data assets |
|
||||
| GET | `/catalog/download` | Download assets as CSV |
|
||||
| GET | `/catalog/data-asset` | Get asset by pipeline/object |
|
||||
| GET | `/catalog/data-asset/:id` | Get single data asset |
|
||||
| GET | `/catalog/data-asset/rls/:id` | Get data asset RLS info |
|
||||
| GET | `/catalog/data-asset/:id/columns-metadata` | Get column metadata |
|
||||
| GET | `/catalog/data-asset/:id/preview` | Get data preview |
|
||||
| GET | `/catalog/data-asset/:id/docs` | Get documentation |
|
||||
| GET | `/catalog/tags` | Get all tags |
|
||||
| PUT | `/catalog/data-asset/:id` | Update data asset |
|
||||
| PUT | `/catalog/data-asset/:id/manage-permissions` | Manage permissions |
|
||||
| PUT | `/catalog/data-asset/:id/revoke-permissions` | Revoke permissions |
|
||||
| POST | `/catalog` | Create data asset |
|
||||
| POST | `/catalog/data-asset/:id/docs` | Create documentation |
|
||||
| POST | `/catalog/data-asset/:id/comment` | Add comment |
|
||||
| POST | `/catalog/dataset-catalog-task` | Trigger catalog task |
|
||||
| POST | `/catalog/rls-rule` | Add RLS rule |
|
||||
| POST | `/catalog/register-dataset` | Register dataset with metadata |
|
||||
| DELETE | `/catalog/data-asset/:id` | Delete data asset |
|
||||
| DELETE | `/catalog/data-asset/:id/comment` | Delete comment |
|
||||
| DELETE | `/catalog/rls-rule/:id` | Remove RLS rule |
|
||||
| DELETE | `/catalog/rls-rule` | Batch remove RLS rules |
|
||||
| GET | `/catalog/dataset-catalog-task/:session` | Get catalog task status |
|
||||
| GET | `/catalog/rls-rule/:id` | Get single RLS rule |
|
||||
| GET | `/catalog/rls-rule` | Get RLS rules |
|
||||
| GET | `/catalog/nimbus-dashboards` | Get Nimbus dashboards |
|
||||
| GET | `/catalog/pii-reporter` | Get PII report |
|
||||
|
||||
**Total: 27 endpoints**
|
||||
|
||||
### 4.2 Pipelines V2 Controller
|
||||
**File:** `src/modules/pipelinesV2/pipelines.controller.ts`
|
||||
**Base Path:** `/pipelinesV2`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| GET | `/pipelinesV2/monitoring-dashboard` | Get monitoring dashboard URL |
|
||||
| GET | `/pipelinesV2` | List all pipelines |
|
||||
| GET | `/pipelinesV2/download-logs` | Download pipeline logs |
|
||||
| GET | `/pipelinesV2/:id` | Get single pipeline |
|
||||
| GET | `/pipelinesV2/:id/config` | Get pipeline properties |
|
||||
| GET | `/pipelinesV2/:id/objects` | Get pipeline objects |
|
||||
| GET | `/pipelinesV2/:id/status` | Get pipeline status (legacy) |
|
||||
| POST | `/pipelinesV2` | Create pipeline |
|
||||
| POST | `/pipelinesV2/init-upload` | Initialize file upload |
|
||||
| POST | `/pipelinesV2/complete-upload` | Complete file upload |
|
||||
| POST | `/pipelinesV2/file` | Upload file pipeline |
|
||||
| POST | `/pipelinesV2/start/:id` | Start pipeline |
|
||||
| PATCH | `/pipelinesV2/:id` | Update pipeline |
|
||||
| PUT | `/pipelinesV2/:id` | Update pipeline (deprecated) |
|
||||
| DELETE | `/pipelinesV2/:id` | Delete pipeline |
|
||||
|
||||
**Total: 15 endpoints**
|
||||
|
||||
### 4.3 Pipelines Controller (Legacy)
|
||||
**File:** `src/modules/pipelines/pipelines.controller.ts`
|
||||
**Base Path:** `/pipelines`
|
||||
|
||||
| HTTP Method | Endpoint | Description |
|
||||
|-------------|----------|-------------|
|
||||
| POST | `/pipelines/start/:id` | Start pipeline (deprecated) |
|
||||
| GET | `/pipelines/:id/status` | Get pipeline status (deprecated) |
|
||||
|
||||
**Total: 2 endpoints (deprecated)**
|
||||
|
||||
---
|
||||
|
||||
## 5. CONFIGURATION FILES
|
||||
|
||||
### 5.1 Helm Chart Values (Production)
|
||||
**File:** `deploy/helm-chart/values.yaml`
|
||||
```yaml
|
||||
# Line 40
|
||||
maestro:
|
||||
pi_factory_url: pi-factory.dadosfera.ai
|
||||
```
|
||||
|
||||
### 5.2 Helm Chart Values (Staging)
|
||||
**File:** `deploy/helm-chart/values-stg.yaml`
|
||||
```yaml
|
||||
# Line 4
|
||||
maestro:
|
||||
pi_factory_url: pi-factory.stg.dadosfera.ai
|
||||
```
|
||||
|
||||
### 5.3 Helmfiles (Production)
|
||||
**File:** `deploy/helmfiles/prd.yaml`
|
||||
```yaml
|
||||
# Lines 14, 38
|
||||
maestro.pi_factory_url: pi-factory.dadosfera.ai
|
||||
```
|
||||
|
||||
### 5.4 Deployment Template
|
||||
**File:** `deploy/helm-chart/templates/deployment.yaml`
|
||||
```yaml
|
||||
# Line 85
|
||||
- name: PIFACTORY_URL
|
||||
value: {{ .Values.maestro.pi_factory_url }}
|
||||
```
|
||||
|
||||
### 5.5 Environment Type Definition
|
||||
**File:** `environment.d.ts`
|
||||
```typescript
|
||||
// Line 10
|
||||
PIFACTORY_URL: string;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PROTO PACKAGE DEPENDENCIES
|
||||
|
||||
### 6.1 Package.json
|
||||
**File:** `package.json`
|
||||
```json
|
||||
{
|
||||
"@dadosfera/protospack": "2.5.3", // Legacy pipelines
|
||||
"@dadosfera/protospack-v2": "3.38.0-beta.14" // PipelineV2, Catalog, PlatformInterfaces
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Proto Imports from `@dadosfera/protospack-v2` (PI-Factory related)
|
||||
|
||||
| Import Path | Used In |
|
||||
|-------------|---------|
|
||||
| `Catalog` | catalog/catalog-client.ts, catalog/catalog.service.ts |
|
||||
| `Catalog/interfaces/messages` | catalog/catalog.service.ts, catalog/catalog.controller.ts, catalog/dtos |
|
||||
| `PlatformInterfaces` | catalog/catalog-client.ts, catalog/catalog.service.ts |
|
||||
| `PipelineV2` | pipelinesV2/pipelines-client.ts, pipelinesV2/pipelines.service.ts |
|
||||
| `PipelineV2/interfaces/messages` | pipelinesV2/pipelines.service.ts, pipelinesV2/pipelines.controller.ts |
|
||||
|
||||
### 6.3 Proto Imports from `@dadosfera/protospack` (Legacy)
|
||||
|
||||
| Import Path | Used In |
|
||||
|-------------|---------|
|
||||
| `PipelinePackages` | pipelines/pipelines-client.ts |
|
||||
| `PipelineProtoFilePath` | pipelines/pipelines-client.ts |
|
||||
| `PipelineServicesNames` | pipelines/client.service.ts |
|
||||
| `PipelinesServiceInterface` | pipelines/client.service.ts |
|
||||
|
||||
---
|
||||
|
||||
## 7. FILES TO MODIFY/DELETE (Summary)
|
||||
|
||||
### High Priority - Core Integration Files
|
||||
```
|
||||
src/modules/catalog/catalog-client.ts → DELETE or REPLACE
|
||||
src/modules/catalog/catalog.service.ts → REPLACE with local implementation
|
||||
src/modules/pipelinesV2/pipelines-client.ts → DELETE or REPLACE
|
||||
src/modules/pipelinesV2/pipelines.service.ts → REPLACE with local implementation
|
||||
src/modules/pipelines/pipelines-client.ts → DELETE or REPLACE
|
||||
src/modules/pipelines/client.service.ts → REPLACE with local implementation
|
||||
src/modules/pipelines/pipelines.service.ts → REPLACE with local implementation
|
||||
```
|
||||
|
||||
### Medium Priority - Module Registration
|
||||
```
|
||||
src/modules/catalog/catalog.module.ts → UPDATE imports
|
||||
src/modules/pipelinesV2/pipelines.module.ts → UPDATE imports
|
||||
src/modules/pipelines/pipelines.module.ts → UPDATE imports
|
||||
src/app.module.ts → UPDATE if module structure changes
|
||||
```
|
||||
|
||||
### Low Priority - Configuration
|
||||
```
|
||||
deploy/helm-chart/values.yaml → REMOVE pi_factory_url
|
||||
deploy/helm-chart/values-stg.yaml → REMOVE pi_factory_url
|
||||
deploy/helmfiles/prd.yaml → REMOVE pi_factory_url references
|
||||
deploy/helm-chart/templates/deployment.yaml → REMOVE PIFACTORY_URL env var
|
||||
environment.d.ts → REMOVE PIFACTORY_URL type
|
||||
README.md → UPDATE architecture diagram
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. DEPENDENCY COUNT BY MODULE
|
||||
|
||||
| Module | Files | gRPC Calls | REST Endpoints |
|
||||
|--------|-------|------------|----------------|
|
||||
| Catalog | 3 | 24+ | 27 |
|
||||
| PipelinesV2 | 3 | 10 | 15 |
|
||||
| Pipelines (Legacy) | 3 | 2 | 2 |
|
||||
| **TOTAL** | **9** | **36+** | **44** |
|
||||
|
||||
---
|
||||
|
||||
## 9. COMPARISON: PI-Factory vs In-Factory
|
||||
|
||||
| Aspect | PI-Factory | In-Factory |
|
||||
|--------|------------|------------|
|
||||
| Environment Variable | `PIFACTORY_URL` | `INFACTORY_URL` |
|
||||
| Modules | 3 | 6 |
|
||||
| gRPC Calls | 36+ | 27+ |
|
||||
| REST Endpoints | 44 | 25 |
|
||||
| Complexity | HIGH | MEDIUM-HIGH |
|
||||
| Domain | Pipelines, Catalog | Connections, Connectors, Inputs |
|
||||
|
||||
---
|
||||
|
||||
## 10. DATA MODELS (Proto Messages Used)
|
||||
|
||||
### Catalog Messages
|
||||
- `CreateDataAssetRequest` / `CreateDataAssetResponse`
|
||||
- `GetAllDataAssetsRequest` / `GetAllDataAssetsResponse`
|
||||
- `GetOneDataAssetRequest` / Response
|
||||
- `UpdateDataAssetRequest` / Response
|
||||
- `DeleteDataAssetRequest` / Response
|
||||
- `ManagePermissionRequest` / Response
|
||||
- `RevokePermissionRequest` / Response
|
||||
- `MakeACommentRequest` / Response
|
||||
- `UpdateACommentRequest` / Response
|
||||
- `TriggerDatasetCatalogingRequest` / Response
|
||||
- `GetDatasetCatalogTaskRequest` / Response
|
||||
- `AddRlsRuleRequest` / Response
|
||||
- `RemoveRlsRuleRequest` / Response
|
||||
- `GetRlsRulesRequest` / Response
|
||||
- `GetNimbusDashboardsRequest` / Response
|
||||
- `PiiMetadata`
|
||||
- `RegisterDatasetWithMetatadaRequest`
|
||||
|
||||
### PipelineV2 Messages
|
||||
- `PipelineV2CreateRequest` / `PipelineV2CreateResponse`
|
||||
- `PipelineV2FindAllRequest` / `PipelineV2FindAllResponse`
|
||||
- `PipelineV2FindOneRequest` / `PipelineV2FindOneResponse`
|
||||
- `PipelineV2UpdateRequest` / `PipelineV2UpdateResponse`
|
||||
- `PipelineV2RemoveRequest` / Response
|
||||
- `PipelineV2UploadFileRequest` / Response
|
||||
- `PipelineV2CompleteUploadFileRequest` / Response
|
||||
- `PipelineV2FindObjectsRequest` / Response
|
||||
- `PipelineV2DownloadLogsRequest` / Response
|
||||
- `PipelineV2GetDashboardUrlRequest` / Response
|
||||
|
||||
### Platform Interfaces Messages
|
||||
- `CatalogDataAssetsRequest` / Response
|
||||
|
||||
---
|
||||
|
||||
## 11. MIGRATION STRATEGY OPTIONS
|
||||
|
||||
### Option A: Move PI-Factory functionality INTO Maestro
|
||||
**Pros:**
|
||||
- Single service to maintain
|
||||
- No network latency for these operations
|
||||
- Simpler deployment
|
||||
|
||||
**Cons:**
|
||||
- Significantly increases Maestro's responsibility
|
||||
- Requires database access from Maestro
|
||||
- Large refactoring effort (44 endpoints)
|
||||
|
||||
### Option B: Create REST API wrapper in PI-Factory
|
||||
**Pros:**
|
||||
- Minimal changes to Maestro
|
||||
- Can migrate incrementally
|
||||
|
||||
**Cons:**
|
||||
- Still maintains dependency
|
||||
- Additional REST→gRPC translation layer
|
||||
|
||||
### Option C: Direct database access from Maestro
|
||||
**Pros:**
|
||||
- Removes runtime dependency
|
||||
- Better performance
|
||||
|
||||
**Cons:**
|
||||
- Shared database coupling
|
||||
- Complex migration
|
||||
|
||||
---
|
||||
|
||||
## 12. RECOMMENDED MIGRATION ORDER
|
||||
|
||||
Given the complexity, we recommend migrating in this order:
|
||||
|
||||
1. **Legacy Pipelines** (2 endpoints, deprecated) - Lowest risk
|
||||
2. **PipelinesV2** (15 endpoints) - Core pipeline functionality
|
||||
3. **Catalog** (27 endpoints) - Most complex, migrate last
|
||||
|
||||
### Phase 1: Legacy Pipelines (Deprecated)
|
||||
- Remove `/pipelines/start/:id`
|
||||
- Remove `/pipelines/:id/status`
|
||||
- Update all clients to use `/pipelinesV2/*` endpoints
|
||||
|
||||
### Phase 2: PipelinesV2
|
||||
- Migrate pipeline CRUD operations
|
||||
- Migrate file upload functionality
|
||||
- Migrate monitoring dashboard
|
||||
|
||||
### Phase 3: Catalog
|
||||
- Migrate data asset CRUD
|
||||
- Migrate permissions management
|
||||
- Migrate RLS rules
|
||||
- Migrate dataset registration
|
||||
- Migrate PII reporting
|
||||
|
||||
---
|
||||
|
||||
## NEXT STEPS
|
||||
|
||||
1. **Decide on migration strategy** (Option A, B, or C)
|
||||
2. **Deprecate legacy pipelines module** first
|
||||
3. **Create database schema** if moving to Option A
|
||||
4. **Implement local services** one module at a time
|
||||
5. **Update tests** for each migrated module
|
||||
6. **Update deployment configs** to remove PIFACTORY_URL
|
||||
7. **Coordinate with PI-Factory team** for data migration
|
||||
@@ -2,7 +2,6 @@
|
||||
<image src="./assets/maestro.svg" style="width:10rem">
|
||||
</p>
|
||||
|
||||
|
||||
# Maestro
|
||||
|
||||
Maestro é a API principal da Dadosfera. É responsável pela comunicação do Frontend com nossos microsserviços.
|
||||
|
||||
@@ -104,6 +104,10 @@ spec:
|
||||
value: {{ .Values.maestro.redis_host }}
|
||||
- name: REDIS_PORT
|
||||
value: "{{ .Values.maestro.redis_port }}"
|
||||
- name: REDIS_TLS
|
||||
value: "{{ .Values.maestro.redis_tls }}"
|
||||
- name: PLATFORM_API_URL
|
||||
value: {{ .Values.maestro.platform_api_url }}
|
||||
- name: JWT_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -124,3 +128,14 @@ spec:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: AWS_DEFAULT_REGION
|
||||
# Elasticsearch
|
||||
- name: ELASTICSEARCH_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: ELASTICSEARCH_URL
|
||||
- name: ELASTICSEARCH_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: prd-{{ .Values.app_name }}
|
||||
key: ELASTICSEARCH_API_KEY
|
||||
|
||||
@@ -38,3 +38,15 @@ spec:
|
||||
version: "AWSCURRENT"
|
||||
property: token
|
||||
|
||||
- secretKey: ELASTICSEARCH_URL
|
||||
remoteRef:
|
||||
key: {{ .Values.maestro.env }}/microservices/elasticsearch
|
||||
version: "AWSCURRENT"
|
||||
property: ELASTICSEARCH_URL
|
||||
|
||||
- secretKey: ELASTICSEARCH_API_KEY
|
||||
remoteRef:
|
||||
key: {{ .Values.maestro.env }}/microservices/elasticsearch
|
||||
version: "AWSCURRENT"
|
||||
property: ELASTICSEARCH_API_KEY
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ maestro:
|
||||
open_group_id: e3f98a2f-7748-4981-8505-7695c8ca8218
|
||||
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
|
||||
redis_database: "1"
|
||||
platform_api_url: https://xs2hkhq07k.execute-api.us-east-1.amazonaws.com
|
||||
|
||||
hostname: maestro.stg.dadosfera.ai
|
||||
|
||||
|
||||
@@ -43,11 +43,13 @@ maestro:
|
||||
upload_file_agent_connection: cbc2f881-58c4-4d60-8003-0979b0b5b911
|
||||
open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e
|
||||
open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9
|
||||
platform_api_url: https://oz8v2zid1e.execute-api.us-east-1.amazonaws.com
|
||||
dedicated_proxy: ""
|
||||
restricted_ip: ""
|
||||
redis_host: "aaapzppmlyamkocqwstpo7zvopczyyiyuy6xzm2g6c5k4mq3a66be4a-0.redis.sa-saopaulo-1.oci.oraclecloud.com"
|
||||
redis_port: "6379"
|
||||
redis_database: "0"
|
||||
redis_tls: "true"
|
||||
cookie_secret: "13cc5e136d3074bcc05bec8697092ec1f5f376bf"
|
||||
autoscaling:
|
||||
enabled: false
|
||||
|
||||
+925
@@ -3877,6 +3877,36 @@
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID do usuário owner para filtrar data assets",
|
||||
"example": "user-id-1,user-id-2",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "catalog_date_from",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Data inicial para filtro de catálogo (formato: YYYY-MM-DD)",
|
||||
"example": "2025-01-01",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "catalog_date_to",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Data final para filtro de catálogo (formato: YYYY-MM-DD)",
|
||||
"example": "2025-12-31",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4006,6 +4036,36 @@
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID do usuário owner para filtrar data assets",
|
||||
"example": "user-id-1,user-id-2",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "catalog_date_from",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Data inicial para filtro de catálogo (formato: YYYY-MM-DD)",
|
||||
"example": "2025-01-01",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "catalog_date_to",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Data final para filtro de catálogo (formato: YYYY-MM-DD)",
|
||||
"example": "2025-12-31",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4471,6 +4531,14 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "asset_type",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -6811,6 +6879,863 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/platform/pipeline": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_createPipeline",
|
||||
"summary": "Create a new pipeline",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipelines": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipelines",
|
||||
"summary": "List all pipelines for customer",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/{pipelineId}": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipeline",
|
||||
"summary": "Get pipeline by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "PlatformApiController_updatePipeline",
|
||||
"summary": "Update pipeline by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "PlatformApiController_deletePipeline",
|
||||
"summary": "Delete pipeline by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/execute": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_executePipeline",
|
||||
"summary": "Execute a pipeline",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/pause": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_pausePipeline",
|
||||
"summary": "Pause a pipeline",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/unpause": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_unpausePipeline",
|
||||
"summary": "Unpause a pipeline",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/{pipelineId}/memory": {
|
||||
"put": {
|
||||
"operationId": "PlatformApiController_updatePipelineMemory",
|
||||
"summary": "Update pipeline memory configuration",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/{pipelineId}/metadata": {
|
||||
"put": {
|
||||
"operationId": "PlatformApiController_updatePipelineMetadata",
|
||||
"summary": "Update pipeline metadata",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipelines/metadata": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipelinesMetadata",
|
||||
"summary": "Get all pipelines metadata",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/pipeline_run": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_createPipelineRun",
|
||||
"summary": "Create a pipeline run",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/{pipelineId}/pipeline_run": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipelineRuns",
|
||||
"summary": "Get pipeline runs for a pipeline",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/{pipelineId}/pipeline_run/{runId}": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipelineRun",
|
||||
"summary": "Get specific pipeline run",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pipelineId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/pipeline/pipeline_run/{runId}/logs": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getPipelineRunLogs",
|
||||
"summary": "Get pipeline run logs",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "runId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/{jobId}/input": {
|
||||
"put": {
|
||||
"operationId": "PlatformApiController_updateJobInput",
|
||||
"summary": "Update job input columns",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "PlatformApiController_patchJobInput",
|
||||
"summary": "Partial update job input columns",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/{jobId}/memory": {
|
||||
"put": {
|
||||
"operationId": "PlatformApiController_updateJobMemory",
|
||||
"summary": "Update job memory configuration",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/{jobId}/reset-state": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_resetJobState",
|
||||
"summary": "Reset job state",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/jdbc/{jobId}": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getJdbcJob",
|
||||
"summary": "Get JDBC job details",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/jdbc/{jobId}/sync-mode": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_updateJdbcSyncMode",
|
||||
"summary": "Update JDBC job sync mode",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/jdbc/configs/allowed_datatypes": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getJdbcAllowedDatatypes",
|
||||
"summary": "Get allowed datatypes for JDBC",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/singer/{jobId}": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getSingerJob",
|
||||
"summary": "Get Singer job details",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/singer/{jobId}/sync-mode": {
|
||||
"post": {
|
||||
"operationId": "PlatformApiController_updateSingerSyncMode",
|
||||
"summary": "Update Singer job sync mode",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/jobs/s3/{jobId}": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_getS3Job",
|
||||
"summary": "Get S3 job details",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "jobId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/platform/health": {
|
||||
"get": {
|
||||
"operationId": "PlatformApiController_healthCheck",
|
||||
"summary": "Platform API health check",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform API"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"access-token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"operationId": "HealthController_check",
|
||||
|
||||
Vendored
+1
@@ -16,6 +16,7 @@ declare global {
|
||||
OPEN_CUSTOMER_ID: string;
|
||||
DEDICATED_PROXY: string;
|
||||
COOKIE_SECRET: string;
|
||||
REDIS_TLS?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1933
-335
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -27,10 +27,15 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
"@aws-sdk/client-dynamodb": "^3.414.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.414.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.940.0",
|
||||
"@aws-sdk/signature-v4": "^3.370.0",
|
||||
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
||||
"@dadosfera/protospack": "2.5.3",
|
||||
"@dadosfera/protospack-v2": "3.38.0-beta.14",
|
||||
"@dadosfera/protospack-v2": "3.38.0-beta.16",
|
||||
"@grpc/grpc-js": "^1.9.3",
|
||||
"@grpc/proto-loader": "^0.7.9",
|
||||
"@nestjs/cli": "^9.5.0",
|
||||
|
||||
@@ -33,6 +33,7 @@ import { NetworkPolicyModule } from './modules/network-policy/network-policy.mod
|
||||
import { AssignModule } from './modules/assign/assign.module';
|
||||
import { ShareMetadataModule } from './modules/share-metadata/share-metadata.module';
|
||||
import { ApiKeyModule } from './modules/api-key/api-key.module';
|
||||
import { PlatformApiModule } from './modules/platform-api/platform-api.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -73,6 +74,7 @@ import { ApiKeyModule } from './modules/api-key/api-key.module';
|
||||
ApiKeyModule,
|
||||
IdentityProviderModule,
|
||||
NetworkPolicyModule,
|
||||
PlatformApiModule,
|
||||
//Always leave HealthModule last, so it is on the bottom of swagger
|
||||
HealthModule,
|
||||
],
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Req,
|
||||
Param,
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiHeaders,
|
||||
@@ -102,14 +103,13 @@ export class AuthController {
|
||||
const data = await this.authClient.signIn({ username, password, totp }, metadata);
|
||||
|
||||
if (data.tokens) {
|
||||
this.addTokenInCookie(res, {
|
||||
this.authClient.writeAuthSession(res, {
|
||||
accessToken: data.tokens.accessToken,
|
||||
refreshToken: data.tokens.refreshToken,
|
||||
userId: data.user.id
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return res.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error('/auth - SignIn - ERROR', error);
|
||||
@@ -126,27 +126,8 @@ export class AuthController {
|
||||
) {
|
||||
try {
|
||||
this.logger.info('/auth - SignOut');
|
||||
const exp = 1000 * 60 * 3;
|
||||
|
||||
res.cookie('ddf-auth', '', {
|
||||
domain: 'dadosfera.local',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
res.cookie('ddf-refresh-auth', '', {
|
||||
domain: 'dadosfera.local',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
this.logger.info('Clean cookie sessions');
|
||||
|
||||
this.authClient.cleanUpAuthSession(res);
|
||||
|
||||
return res.send();
|
||||
} catch (error) {
|
||||
@@ -175,8 +156,9 @@ export class AuthController {
|
||||
|
||||
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
||||
|
||||
this.addTokenInCookie(res, {
|
||||
this.authClient.writeAuthSession(res, {
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
userId
|
||||
});
|
||||
|
||||
@@ -492,85 +474,36 @@ export class AuthController {
|
||||
@Get('me')
|
||||
async getMe(@Req() req: Request, @Res() res: Response) {
|
||||
this.logger.info('GET /auth/me ')
|
||||
// Lê cookies
|
||||
|
||||
// Get token and headers
|
||||
const accessToken = req.cookies['ddf-auth'];
|
||||
const userId = req.cookies['ddf-user-id'];
|
||||
const refreshToken = req.cookies['ddf-refresh-auth'];
|
||||
const userId = req.cookies['ddf-user-id'];
|
||||
const resourceHost = req.headers["host"]
|
||||
|
||||
const hasUserSession = Boolean(accessToken) && Boolean(userId);
|
||||
this.logger.info('Has User Session: ' + hasUserSession);
|
||||
|
||||
if (!hasUserSession) {
|
||||
throw new UnauthorizedException()
|
||||
}
|
||||
|
||||
this.logger.info('Has cookie: ' + Boolean(accessToken))
|
||||
try {
|
||||
// Decodifica e valida o JWT de acesso
|
||||
const userDto = await this.authClient.extractUserFrom(accessToken);
|
||||
const userDto = await this.authClient.validateUserSession(accessToken, resourceHost);
|
||||
return res.status(200).json(userDto);
|
||||
} catch (err) {
|
||||
this.logger.error(err.message);
|
||||
const refreshToken = req.cookies['ddf-refresh-auth'];
|
||||
} catch (error) {
|
||||
|
||||
this.logger.info('Token is invalid')
|
||||
this.logger.info('Has Refresh Token: '+ Boolean(refreshToken))
|
||||
// Se access token inválido, tenta refresh
|
||||
if (!refreshToken || !userId) {
|
||||
if (!refreshToken) {
|
||||
this.logger.error('Invalid refresh token or customer name');
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
try {
|
||||
// Chama refreshAccessToken
|
||||
const metadata = PackTheMetadata({
|
||||
});
|
||||
this.logger.info('Call Refresh Token')
|
||||
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
||||
this.logger.info('Finish Refresh Token')
|
||||
// Retorna novo access token e dados mínimos
|
||||
this.addTokenInCookie(res, {
|
||||
accessToken: data.accessToken,
|
||||
userId
|
||||
});
|
||||
// Decodifica novo token
|
||||
const userDto = await this.authClient.extractUserFrom(accessToken);
|
||||
return res.status(200).json(userDto);
|
||||
} catch (refreshErr) {
|
||||
this.logger.error(refreshErr)
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new UnauthorizedException("Invalid refresh token or customer name");
|
||||
};
|
||||
|
||||
private addTokenInCookie(res: Response, data: CookiesValues) {
|
||||
let exp = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
if (data.accessToken) {
|
||||
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
|
||||
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
|
||||
|
||||
this.logger.info('Set Cookie ddf-auth')
|
||||
res.cookie('ddf-auth', data.accessToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.refreshToken) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth')
|
||||
res.cookie('ddf-refresh-auth', data.refreshToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.userId) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth')
|
||||
res.cookie('ddf-user-id', data.userId, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
const {
|
||||
authSession,
|
||||
user
|
||||
} = await this.authClient.refreshUserSession(refreshToken, userId, resourceHost);
|
||||
this.authClient.writeAuthSession(res, authSession);
|
||||
return res.status(200).json(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { OnModuleInit, Inject, Injectable, ForbiddenException } from '@nestjs/common';
|
||||
import {
|
||||
OnModuleInit,
|
||||
Inject,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||
import { AuthProtoService as AuthServiceInterface, IdentityProviderProtoService, UsersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import {
|
||||
AuthProtoService as AuthServiceInterface,
|
||||
UsersProtoService,
|
||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||
import {
|
||||
AuthSnowflakeSignInRequest,
|
||||
AuthSignInRequest,
|
||||
@@ -22,12 +32,18 @@ import {
|
||||
import { DucClient } from '../duc/client.config';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { BulkEditResponse, UserDTO } from './dtos/login';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
type AuthSession = {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AuthClientService implements OnModuleInit {
|
||||
|
||||
logger: DadosferaLogger;
|
||||
|
||||
private authService: AuthServiceInterface;
|
||||
@@ -63,11 +79,11 @@ export class AuthClientService implements OnModuleInit {
|
||||
return lastValueFrom(this.authService.AuthSnowflakeSignIn(input));
|
||||
}
|
||||
|
||||
checkDedicatedProxy({
|
||||
customer
|
||||
}: AuthSignInResponse) {
|
||||
checkDedicatedProxy({ customer }: AuthSignInResponse) {
|
||||
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
||||
this.logger.info('SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY);
|
||||
this.logger.info(
|
||||
'SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY,
|
||||
);
|
||||
this.logger.info('Customer ID: ' + customer.id);
|
||||
|
||||
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== customer.id) {
|
||||
@@ -75,7 +91,9 @@ export class AuthClientService implements OnModuleInit {
|
||||
}
|
||||
|
||||
// Bloquear o customer de acesso o maestro publico
|
||||
this.logger.info('Check if customer have network policy: ' + customer.modules);
|
||||
this.logger.info(
|
||||
'Check if customer have network policy: ' + customer.modules,
|
||||
);
|
||||
const hasNetworkPolicyModule = customer.modules.includes('network-policy');
|
||||
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
||||
throw new ForbiddenException();
|
||||
@@ -94,7 +112,6 @@ export class AuthClientService implements OnModuleInit {
|
||||
result = await lastValueFrom(
|
||||
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('SignIn - Error during sign-in');
|
||||
this.logger.error(error);
|
||||
@@ -105,7 +122,7 @@ export class AuthClientService implements OnModuleInit {
|
||||
this.checkDedicatedProxy(result);
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
async refreshAccessToken(
|
||||
@@ -115,7 +132,10 @@ export class AuthClientService implements OnModuleInit {
|
||||
this.logger.info('RefreshAccessToken');
|
||||
|
||||
return lastValueFrom(
|
||||
this.authService.AuthRefreshAccessToken({ refreshToken, userId }, metadata),
|
||||
this.authService.AuthRefreshAccessToken(
|
||||
{ refreshToken, userId },
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -284,50 +304,189 @@ export class AuthClientService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async getUser(id: string, metadata: Metadata) {
|
||||
this.logger.info('getUser');
|
||||
return await lastValueFrom(
|
||||
this.userService.UserFindOneById({ id }, metadata),
|
||||
);
|
||||
public async validateUserSession(accessToken: any, resourceHost: string) {
|
||||
const payload = await this.validateJwtToken(accessToken);
|
||||
|
||||
const userDto = await this.getUserfromPayload(payload);
|
||||
|
||||
this.validateResourceAccess(resourceHost, userDto);
|
||||
return userDto;
|
||||
}
|
||||
|
||||
public async extractUserFrom(token: string) {
|
||||
const decoded: any = token && jwt.decode(token, { complete: true });
|
||||
if (!decoded) throw new Error('Invalid token');
|
||||
public async refreshUserSession(
|
||||
refreshToken: string,
|
||||
userId: string,
|
||||
originHeader: string,
|
||||
): Promise<{
|
||||
user: UserDTO;
|
||||
authSession: AuthSession;
|
||||
}> {
|
||||
const metadata = PackTheMetadata({});
|
||||
|
||||
const { kid } = decoded.header;
|
||||
// Busca a chave pública
|
||||
const { keys } = await this.getPublicKeys();
|
||||
const pemValue = keys.find((k) => k.kid === kid)?.pem;
|
||||
if (!pemValue)
|
||||
throw new Error('Public key not found');
|
||||
jwt.verify(token, pemValue);
|
||||
this.logger.info('Call Refresh Token');
|
||||
const refreshCredentials = await this.refreshAccessToken(
|
||||
{ refreshToken, userId },
|
||||
metadata,
|
||||
);
|
||||
this.logger.info('Finish Refresh Token');
|
||||
|
||||
const payload = decoded.payload;
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: payload.customer_id
|
||||
})
|
||||
const userDto = await this.validateUserSession(
|
||||
refreshCredentials.accessToken,
|
||||
originHeader,
|
||||
);
|
||||
return {
|
||||
user: userDto,
|
||||
authSession: {
|
||||
accessToken: refreshCredentials.accessToken,
|
||||
refreshToken: refreshCredentials.refreshToken,
|
||||
userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
user
|
||||
} = await this.getUser(
|
||||
payload.user_id,
|
||||
metadata
|
||||
);
|
||||
public writeAuthSession(res: Response, data: AuthSession) {
|
||||
let exp = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
const userDto: UserDTO = {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
jobTitle: user?.jobTitle || null,
|
||||
department: user?.department || null,
|
||||
hierarchy: user?.hierarchy || null,
|
||||
customer: {
|
||||
id: payload.customer_id,
|
||||
name: payload.customer_name,
|
||||
tier: payload.customer_tier,
|
||||
}
|
||||
};
|
||||
if (data.accessToken) {
|
||||
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
|
||||
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
|
||||
|
||||
return userDto;
|
||||
this.logger.info('Set Cookie ddf-auth');
|
||||
res.cookie('ddf-auth', data.accessToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.refreshToken) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth');
|
||||
res.cookie('ddf-refresh-auth', data.refreshToken, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
|
||||
if (data.userId) {
|
||||
this.logger.info('Set Cookie ddf-refresh-auth');
|
||||
res.cookie('ddf-user-id', data.userId, {
|
||||
domain: '.dadosfera.ai',
|
||||
maxAge: exp,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public cleanUpAuthSession(res: Response) {
|
||||
const exp = 1000 * 60 * 3;
|
||||
|
||||
res.cookie('ddf-auth', '', {
|
||||
domain: 'dadosfera.ai',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
res.cookie('ddf-refresh-auth', '', {
|
||||
domain: 'dadosfera.ai',
|
||||
maxAge: Date.now() - exp,
|
||||
expires: new Date(),
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||
});
|
||||
|
||||
this.logger.info('Clean cookie sessions');
|
||||
}
|
||||
|
||||
private async validateJwtToken(token: string) {
|
||||
const decoded: any = token && jwt.decode(token, { complete: true });
|
||||
if (!decoded) throw new Error('Invalid token');
|
||||
|
||||
const { kid } = decoded.header;
|
||||
// Busca a chave pública
|
||||
const { keys } = await this.getPublicKeys();
|
||||
const pemValue = keys.find((k) => k.kid === kid)?.pem;
|
||||
if (!pemValue) throw new Error('Public key not found');
|
||||
jwt.verify(token, pemValue);
|
||||
|
||||
return decoded.payload;
|
||||
}
|
||||
|
||||
private async getUserfromPayload(payload: JwtPayload): Promise<UserDTO> {
|
||||
this.logger.info('getUser');
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id: payload.customer_id,
|
||||
});
|
||||
|
||||
const { user } = await lastValueFrom(
|
||||
this.userService.UserFindOneById({ id: payload.user_id }, metadata),
|
||||
);
|
||||
|
||||
const userDto: UserDTO = {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
jobTitle: user?.jobTitle || null,
|
||||
department: user?.department || null,
|
||||
hierarchy: user?.hierarchy || null,
|
||||
customer: {
|
||||
id: payload.customer_id,
|
||||
name: payload.customer_name,
|
||||
tier: payload.customer_tier,
|
||||
},
|
||||
};
|
||||
|
||||
return userDto;
|
||||
}
|
||||
|
||||
private validateResourceAccess(host: string, user: UserDTO) {
|
||||
this.logger.info(
|
||||
"Validate whether the source URL is a resource belonging to the user's client",
|
||||
);
|
||||
this.logger.info('Host: ' + host);
|
||||
this.logger.info('Customer: ' + user.customer.name);
|
||||
|
||||
const hostParts = host.split('.');
|
||||
const domain = hostParts[0];
|
||||
const isResouceStg = hostParts[1] === 'stg';
|
||||
|
||||
const notFoundCustomerInDomain = !domain.includes('-')
|
||||
|
||||
if (notFoundCustomerInDomain) {
|
||||
this.logger.info(`Not found Customer Name in domain`);
|
||||
return;
|
||||
}
|
||||
|
||||
const domainParts = domain.split('-');
|
||||
|
||||
const customerInDomain = domainParts[domainParts.length - 1];
|
||||
|
||||
if (isResouceStg && process.env.ENV !== 'stg') {
|
||||
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
||||
throw new HttpException(
|
||||
`Customer ${user.customer.name} cannot access ${host}`,
|
||||
HttpStatus.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if (customerInDomain != user.customer.name) {
|
||||
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
||||
throw new HttpException(
|
||||
`Customer ${user.customer.name} cannot access ${host}`,
|
||||
HttpStatus.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +423,7 @@ export class CatalogController {
|
||||
@User() user: RequestUser,
|
||||
@Language() language: LanguageEnum,
|
||||
@Param('id') id: string,
|
||||
@Query('asset_type') asset_type: string,
|
||||
): Promise<IDocsResponse> {
|
||||
const { customer_name, customer_id, user_id, username } = user;
|
||||
|
||||
@@ -439,7 +440,7 @@ export class CatalogController {
|
||||
language,
|
||||
});
|
||||
|
||||
const docs = await this.catalogService.getDataDocs(id, metadata);
|
||||
const docs = await this.catalogService.getDataDocs(id, asset_type, metadata);
|
||||
|
||||
return { docs };
|
||||
}
|
||||
@@ -487,21 +488,32 @@ export class CatalogController {
|
||||
@Headers() headers,
|
||||
@Param('id') table_id: string,
|
||||
@Body('docs') docs: string,
|
||||
@Query('asset_type') asset_type: string,
|
||||
) {
|
||||
const { user_id, customer_name } = user;
|
||||
const { user_id, customer_name, customer_id, username } = user;
|
||||
|
||||
const metadata = PackTheMetadata({
|
||||
customer_id,
|
||||
customer_name,
|
||||
user_id,
|
||||
username,
|
||||
});
|
||||
|
||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
||||
this.logger.info(`/catalog - ON POST DATA DOCS ROUTE`, {
|
||||
user_id,
|
||||
customer_name,
|
||||
});
|
||||
|
||||
const res = await this.catalogService.createDataDocs({
|
||||
const body = {
|
||||
table_id,
|
||||
docs,
|
||||
asset_type,
|
||||
info: {
|
||||
customer: customer_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const res = await this.catalogService.createDataDocs(body, metadata);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ import { CatalogClientConfiguration } from './catalog-client';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { RolesService } from '../roles/roles.service';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||
import {
|
||||
AssetReporter,
|
||||
BatchRemoveRlsRulesRequest,
|
||||
CreateDataDocsDTO,
|
||||
IUpdateDataRequest,
|
||||
TriggerCatalogReq,
|
||||
} from './dtos';
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
import { TypeParser } from 'src/utils/FileParser/parser-types';
|
||||
import { ParserBuilder } from 'src/utils/FileParser/parser.builder';
|
||||
|
||||
|
||||
class CatalogService implements OnModuleInit {
|
||||
catalogReadService: ReadService.CatalogReadServices;
|
||||
catalogWriteService: WriteService.CatalogWriteServices;
|
||||
@@ -55,6 +58,7 @@ class CatalogService implements OnModuleInit {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
|
||||
onModuleInit() {
|
||||
this.catalogReadService =
|
||||
this.grpcClient.getService<ReadService.CatalogReadServices>(
|
||||
@@ -70,20 +74,24 @@ class CatalogService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
_getNimbusUrl(body) {
|
||||
this.logger.debug(`Body: ${JSON.stringify(body)}`);
|
||||
const customer = body.info.customer.toLowerCase();
|
||||
|
||||
|
||||
if (process.env.ENV === 'prd') {
|
||||
return `https://nimbus-${customer}.dadosfera.ai`;
|
||||
}
|
||||
|
||||
|
||||
return `https://nimbus-${customer}.${process.env.ENV.replace(
|
||||
'local',
|
||||
'stg',
|
||||
)}.dadosfera.ai`;
|
||||
}
|
||||
|
||||
|
||||
async getPiiReporter(metadata: Metadata, type: TypeParser) {
|
||||
this.logger.info('getPiiReporter: ' + type)
|
||||
try {
|
||||
@@ -94,8 +102,10 @@ class CatalogService implements OnModuleInit {
|
||||
)
|
||||
this.logger.info("Finish grpc call")
|
||||
|
||||
|
||||
const parser = ParserBuilder.build<PiiMetadata>(type);
|
||||
|
||||
|
||||
this.logger.info('parser file to: ' + type)
|
||||
const file = await parser.parse(data)
|
||||
this.logger.info('finish parser')
|
||||
@@ -105,9 +115,11 @@ class CatalogService implements OnModuleInit {
|
||||
'pdf': 'application/pdf'
|
||||
}
|
||||
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `relatorio-pii-${timestamp}.${type}`;
|
||||
|
||||
|
||||
return {
|
||||
file,
|
||||
filename: filename,
|
||||
@@ -118,20 +130,25 @@ class CatalogService implements OnModuleInit {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
if (!data.embed) data.embed = undefined;
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.CreateDataAsset(data, metadata),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async managePermissions(data: Messages.ManagePermissionRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.ManagePermission(data, metadata),
|
||||
).catch((err) => {
|
||||
@@ -142,9 +159,11 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async revokePermissions(data: Messages.RevokePermissionRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.RevokePermission(data, metadata),
|
||||
).catch((err) => {
|
||||
@@ -155,9 +174,11 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async commentOnDataAsset(data: Messages.MakeACommentRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.MakeAComment(data, metadata),
|
||||
).catch((err) => {
|
||||
@@ -168,9 +189,11 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async deleteComment(data: Messages.UpdateACommentRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.UpdateAComment(data, metadata),
|
||||
).catch((err) => {
|
||||
@@ -181,9 +204,11 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async deleteDataAsset(data: Messages.DeleteDataAssetRequest, metadata) {
|
||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||
|
||||
|
||||
return lastValueFrom(
|
||||
this.catalogWriteService.DeleteDataAsset(data, metadata),
|
||||
).catch((err) => {
|
||||
@@ -194,16 +219,20 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async getUserRolesIds(userId: string) {
|
||||
const result = await this.userService.findOneById(userId).catch(() => null);
|
||||
|
||||
|
||||
if (result) {
|
||||
return result.user.roles.map((role) => role.id);
|
||||
}
|
||||
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
async searchDataAssets(
|
||||
query: Record<string, any>,
|
||||
metadata: Metadata,
|
||||
@@ -211,6 +240,7 @@ class CatalogService implements OnModuleInit {
|
||||
) {
|
||||
this.logger.info('CatalogService - searchDataAssets');
|
||||
|
||||
|
||||
const { search, page, size, sort_by, order, ...filters } = query;
|
||||
|
||||
const { data_assets, total } = await lastValueFrom(
|
||||
@@ -227,16 +257,20 @@ class CatalogService implements OnModuleInit {
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
const result = JSON.parse(data_assets);
|
||||
|
||||
|
||||
const response = await this.getAssetsUsersAndRoles(
|
||||
result.data_assets,
|
||||
customer_id,
|
||||
);
|
||||
|
||||
|
||||
return { data_assets: response, total };
|
||||
}
|
||||
|
||||
|
||||
async downloadAssets(
|
||||
query: Record<string, any>,
|
||||
metadata: Metadata,
|
||||
@@ -244,6 +278,7 @@ class CatalogService implements OnModuleInit {
|
||||
) {
|
||||
const data = await this.searchDataAssets(query, metadata, customer_id);
|
||||
|
||||
|
||||
const formatData = data.data_assets.map(asset => ({
|
||||
id: asset.id,
|
||||
display_name: asset.display_name,
|
||||
@@ -252,19 +287,24 @@ class CatalogService implements OnModuleInit {
|
||||
tags: '[' + asset.tags.join(', ') + ']'
|
||||
}))
|
||||
|
||||
|
||||
const parser = ParserBuilder.build<AssetReporter>('csv');
|
||||
|
||||
|
||||
const file = await parser.parse(formatData);
|
||||
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `dadosfera_assets_${timestamp}.csv`;
|
||||
|
||||
|
||||
return {
|
||||
file,
|
||||
filename
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getOneDataAsset(data: {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
@@ -285,9 +325,11 @@ class CatalogService implements OnModuleInit {
|
||||
};
|
||||
asset = await this.getAssetsUsersAndRoles([asset], customer_id);
|
||||
|
||||
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
|
||||
async getOneDataAssetByPipelineAndObject(data: {
|
||||
customer_id: string;
|
||||
pipeline: string;
|
||||
@@ -309,9 +351,11 @@ class CatalogService implements OnModuleInit {
|
||||
};
|
||||
asset = await this.getAssetsUsersAndRoles([asset], customer_id);
|
||||
|
||||
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
|
||||
async updateOneDataAsset(data: {
|
||||
data_asset_id: string;
|
||||
customer_id: string;
|
||||
@@ -320,6 +364,7 @@ class CatalogService implements OnModuleInit {
|
||||
}) {
|
||||
const { body, customer_id, data_asset_id, metadata } = data;
|
||||
|
||||
|
||||
const { data_asset } = await lastValueFrom(
|
||||
this.catalogWriteService.UpdateDataAsset(
|
||||
{ id: data_asset_id, changes: JSON.stringify(body) },
|
||||
@@ -334,18 +379,20 @@ class CatalogService implements OnModuleInit {
|
||||
};
|
||||
asset = await this.getAssetsUsersAndRoles([asset], customer_id);
|
||||
|
||||
|
||||
return { data_asset: asset[0] };
|
||||
}
|
||||
|
||||
async getDataDocs(id: string, metadata: Metadata) {
|
||||
async getDataDocs(id: string, assetType: string, metadata: Metadata) {
|
||||
const { documentation } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
||||
this.catalogReadService.GetDatasetDoc({ id }, metadata),
|
||||
);
|
||||
|
||||
const docs = JSON.parse(documentation);
|
||||
return docs;
|
||||
}
|
||||
|
||||
|
||||
async getDatasetPreview(id: string, metadata: Metadata) {
|
||||
const { preview } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetPreview(
|
||||
@@ -357,6 +404,7 @@ class CatalogService implements OnModuleInit {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
async getDatasetColumnsMetadata(id: string, metadata: Metadata) {
|
||||
const { columns_metadata } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetColumnsMetadata(
|
||||
@@ -368,7 +416,16 @@ class CatalogService implements OnModuleInit {
|
||||
return result;
|
||||
}
|
||||
|
||||
async createDataDocs(body) {
|
||||
async createDataDocs(body: CreateDataDocsDTO, metadata: Metadata) {
|
||||
if (body.asset_type === 'table' || body.asset_type === 'view') {
|
||||
return this.createDataDocsViaNimbus(body);
|
||||
}
|
||||
|
||||
return this.createDataDocsViaGrpc(body, metadata);
|
||||
}
|
||||
|
||||
private async createDataDocsViaNimbus(body: CreateDataDocsDTO) {
|
||||
this.logger.info('Creating data docs via Nimbus for table/view');
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
const { data } = await axios.post(
|
||||
`${nimbusUrl}/api/catalog/data-docs/`,
|
||||
@@ -377,9 +434,35 @@ class CatalogService implements OnModuleInit {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
private async createDataDocsViaGrpc(body: CreateDataDocsDTO, metadata: Metadata) {
|
||||
this.logger.info('Creating data docs via gRPC for other asset types');
|
||||
try {
|
||||
const response: any = await lastValueFrom(
|
||||
this.catalogWriteService.UpdateDataAssetDoc(
|
||||
{
|
||||
id: body.table_id,
|
||||
docs: body.docs,
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Error creating data asset docs:', error);
|
||||
throw new HttpException(
|
||||
'Failed to create data asset documentation',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findAllTags(data, metadata) {
|
||||
this.logger.info('CatalogService - findAllCustomerTags');
|
||||
|
||||
|
||||
const response = await lastValueFrom(
|
||||
this.catalogReadService.GetCustomerTags(data, metadata),
|
||||
)
|
||||
@@ -392,6 +475,7 @@ class CatalogService implements OnModuleInit {
|
||||
throw new Error(err);
|
||||
});
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
async getAssetsUsersAndRoles(data_assets: Array<any>, customer_id: string) {
|
||||
@@ -404,7 +488,8 @@ class CatalogService implements OnModuleInit {
|
||||
return data_assets.map((data_asset) => {
|
||||
const owner = customer_users.find(
|
||||
(u) => u.id === data_asset.owner,
|
||||
)?.username;
|
||||
)?.email;
|
||||
|
||||
|
||||
const roles = [];
|
||||
const users = [];
|
||||
@@ -414,10 +499,11 @@ class CatalogService implements OnModuleInit {
|
||||
if (role) roles.push({ id: role.id, name: role.name });
|
||||
}
|
||||
|
||||
|
||||
const data_asset_users = data_asset?.users || []
|
||||
for (const user_id of data_asset_users) {
|
||||
const user = customer_users.find((r) => r.id === user_id);
|
||||
if (user) users.push({ id: user.id, username: user.username });
|
||||
if (user) users.push({ id: user.id, email: user.email });
|
||||
}
|
||||
return {
|
||||
...data_asset,
|
||||
@@ -428,6 +514,7 @@ class CatalogService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async triggerCatalog(data: TriggerCatalogReq, metadata: Metadata) {
|
||||
const { session } = await lastValueFrom(
|
||||
this.catalogWriteService.TriggerDatasetCataloging(data, metadata),
|
||||
@@ -441,6 +528,7 @@ class CatalogService implements OnModuleInit {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
async addRlsRule(data: AddRlsRuleRequest, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogWriteService.AddRlsRule(data, metadata),
|
||||
@@ -448,6 +536,7 @@ class CatalogService implements OnModuleInit {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
async removeRlsRule(id: number, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogWriteService.RemoveRlsRule({ id }, metadata),
|
||||
@@ -455,12 +544,14 @@ class CatalogService implements OnModuleInit {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
async batchRemoveRlsRule(
|
||||
query: BatchRemoveRlsRulesRequest,
|
||||
metadata: Metadata,
|
||||
) {
|
||||
const { id_rls, nimbus_dashboard_id } = query;
|
||||
|
||||
|
||||
if (id_rls && nimbus_dashboard_id) {
|
||||
throw new BadRequestException(
|
||||
"You can't delete using both parameters. Choose either 'id_rls' or 'nimbus_dashboard_id'",
|
||||
@@ -481,6 +572,7 @@ class CatalogService implements OnModuleInit {
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
|
||||
async getRlsRules(data: GetRlsRulesRequest, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetRlsRules(data, metadata),
|
||||
@@ -488,6 +580,7 @@ class CatalogService implements OnModuleInit {
|
||||
return res.rls_rules;
|
||||
}
|
||||
|
||||
|
||||
async getOneRlsRule(id: number, metadata: Metadata) {
|
||||
const res = await lastValueFrom(
|
||||
this.catalogReadService.GetOneRlsRule({ id }, metadata),
|
||||
@@ -495,6 +588,7 @@ class CatalogService implements OnModuleInit {
|
||||
return res.rls_rule;
|
||||
}
|
||||
|
||||
|
||||
async getNimbusDashboards(
|
||||
data: GetNimbusDashboardsRequest,
|
||||
metadata: Metadata,
|
||||
@@ -505,19 +599,24 @@ class CatalogService implements OnModuleInit {
|
||||
return res.dashboards;
|
||||
}
|
||||
|
||||
|
||||
async createTableMetadata(body: any): Promise<number> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, {...body.logMetadata});
|
||||
|
||||
|
||||
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/`;
|
||||
|
||||
|
||||
this.logger.info(`Creating table metadata for table ${body.table_metadata.table_name}`, {...body.logMetadata});
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||
this.logger.debug(`Payload: ${JSON.stringify(body.table_metadata)}`, {...body.logMetadata});
|
||||
|
||||
|
||||
try {
|
||||
const { data, status } = await axios.post(endpoint, {...body.table_metadata});
|
||||
|
||||
|
||||
this.logger.info(
|
||||
`Table metadata created successfully with status ${status} for table ${body.table_metadata.table_name}`,
|
||||
{...body.logMetadata},
|
||||
@@ -532,19 +631,23 @@ class CatalogService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async createColumnMetadata(body: any): Promise<number[]> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, body.logMetadata);
|
||||
const endpoint = `${nimbusUrl}/api/catalog/column-metadata/`;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
this.logger.info(`Creating column metadata for table ${body.column_metadata.table_name}`, {...body.logMetadata});
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||
this.logger.debug(`Payload: ${JSON.stringify(body.column_metadata)}`, {...body.logMetadata});
|
||||
const { data, status } = await axios.post(endpoint, body.column_metadata);
|
||||
|
||||
|
||||
this.logger.info(
|
||||
`Column metadata created successfully with status ${status} for table ${body.column_metadata.table_name}`,
|
||||
{...body.logMetadata},
|
||||
@@ -559,18 +662,22 @@ class CatalogService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async createDataPreview(body: any): Promise<number> {
|
||||
const nimbusUrl = this._getNimbusUrl(body);
|
||||
this.logger.info(`Nimbus URL: ${nimbusUrl}`, {...body.logMetadata});
|
||||
const endpoint = `${nimbusUrl}/api/catalog/data-preview/`;
|
||||
|
||||
|
||||
this.logger.info(`Creating data preview for table ${body.data_preview.table_name}`, {...body.logMetadata});
|
||||
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||
this.logger.debug(`Payload: ${JSON.stringify(body.data_preview)}`, {...body.logMetadata});
|
||||
|
||||
|
||||
try {
|
||||
const { data, status } = await axios.post(endpoint, body.data_preview);
|
||||
|
||||
|
||||
this.logger.info(
|
||||
`Data preview created successfully with status ${status} for table ${body.data_preview.table_name}`,
|
||||
{...body.logMetadata},
|
||||
@@ -587,9 +694,11 @@ class CatalogService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async catalogDatasetItem(table_metadata_id: number, metadata: Metadata) {
|
||||
const customer_name_raw = metadata.get('customer_name');
|
||||
|
||||
|
||||
const customer_name = customer_name_raw?.[0]?.toString();
|
||||
if (!customer_name) {
|
||||
throw new BadRequestException('Customer name not found in metadata');
|
||||
@@ -612,4 +721,5 @@ class CatalogService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
export { CatalogService };
|
||||
|
||||
export { CatalogService };
|
||||
|
||||
@@ -147,6 +147,24 @@ export class ICatalogAllRequest {
|
||||
description: 'Tipo de ordenação - `asc`: crescente; `desc`: decrescente ',
|
||||
})
|
||||
order?: OrderEnum;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'ID do usuário owner para filtrar data assets',
|
||||
example: 'user-id-1,user-id-2',
|
||||
})
|
||||
owner?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Data inicial para filtro de catálogo (formato: YYYY-MM-DD)',
|
||||
example: '2025-01-01',
|
||||
})
|
||||
catalog_date_from?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Data final para filtro de catálogo (formato: YYYY-MM-DD)',
|
||||
example: '2025-12-31',
|
||||
})
|
||||
catalog_date_to?: string;
|
||||
}
|
||||
|
||||
export class ICatalogAllResponse {
|
||||
@@ -328,3 +346,10 @@ export type AssetReporter = {
|
||||
created_at: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export type CreateDataDocsDTO = {
|
||||
table_id: string;
|
||||
docs: string;
|
||||
asset_type: string;
|
||||
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export class ShareService implements OnModuleInit {
|
||||
});
|
||||
|
||||
const { documentation } = await lastValueFrom(
|
||||
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
||||
this.catalogReadService.GetDatasetDoc({ id }, metadata),
|
||||
);
|
||||
console.log(documentation);
|
||||
const docs = JSON.parse(documentation);
|
||||
@@ -180,7 +180,7 @@ export class ShareService implements OnModuleInit {
|
||||
return data_assets.map((data_asset) => {
|
||||
const owner = customer_users.find(
|
||||
(u) => u.id === data_asset.owner,
|
||||
)?.username;
|
||||
)?.email;
|
||||
|
||||
const roles = [];
|
||||
const users = [];
|
||||
@@ -190,7 +190,7 @@ export class ShareService implements OnModuleInit {
|
||||
}
|
||||
for (const user_id of data_asset.users) {
|
||||
const user = customer_users.find((r) => r.id === user_id);
|
||||
if (user) users.push({ id: user.id, username: user.username });
|
||||
if (user) users.push({ id: user.id, email: user.email });
|
||||
}
|
||||
return {
|
||||
...data_asset,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const PLATFORM_API_CONFIG = {
|
||||
getUrl: (): string => {
|
||||
const url = process.env.PLATFORM_API_URL;
|
||||
if (!url) {
|
||||
throw new Error('PLATFORM_API_URL environment variable is not set');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
timeout: parseInt(process.env.PLATFORM_API_TIMEOUT || '30000', 10),
|
||||
};
|
||||
@@ -0,0 +1,977 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
Inject,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import {
|
||||
Authenticated,
|
||||
RequireAllPermissions,
|
||||
} from '../../decorators/authentication.decorator';
|
||||
import { User, RequestUser } from '../../decorators/user.decorator';
|
||||
import { PlatformApiService } from './platform-api.service';
|
||||
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
|
||||
import { ElasticsearchService } from '../../services/elasticsearch';
|
||||
import { DynamoDBService, ReferenceColumn } from '../../services/dynamodb';
|
||||
import { CustomersService } from '../customers/customers.service';
|
||||
import { validateCronAgainstScheduleLimit } from '../../utils/cron-validation';
|
||||
|
||||
@ApiTags('Platform API')
|
||||
@Controller('platform')
|
||||
export class PlatformApiController {
|
||||
private logger: any;
|
||||
|
||||
constructor(
|
||||
private readonly platformApiService: PlatformApiService,
|
||||
private readonly elasticsearchService: ElasticsearchService,
|
||||
private readonly dynamoDBService: DynamoDBService,
|
||||
private readonly customersService: CustomersService,
|
||||
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize pipeline ID to match Platform-API format.
|
||||
* Platform-API replaces '-' with '_' in pipeline IDs.
|
||||
*/
|
||||
private normalizePipelineId(id: string): string {
|
||||
return id?.replace(/-/g, '_') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalize ID back to UUID format (replace _ with -).
|
||||
* Used when we receive a normalized ID but need the original UUID.
|
||||
*/
|
||||
private denormalizeId(id: string): string {
|
||||
return id?.replace(/_/g, '-') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize job ID to match Platform-API format.
|
||||
* Platform-API replaces '-' with '_' in job IDs.
|
||||
*
|
||||
* Example: "2ccf5481-59f5-4036-8a94-7d5f28f4f899-0" -> "2ccf5481_59f5_4036_8a94_7d5f28f4f899_0"
|
||||
*/
|
||||
private normalizeJobId(jobId: string): string {
|
||||
return jobId?.replace(/-/g, '_') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the pipeline ID (base UUID) from a job ID.
|
||||
* Job IDs have format "uuid-suffix" where suffix is the job index (e.g., "0", "1").
|
||||
* Handles both hyphenated and underscored formats, always returns hyphenated UUID for ES.
|
||||
*
|
||||
* Examples:
|
||||
* - "2ccf5481-59f5-4036-8a94-7d5f28f4f899-0" -> "2ccf5481-59f5-4036-8a94-7d5f28f4f899"
|
||||
* - "2ccf5481_59f5_4036_8a94_7d5f28f4f899_0" -> "2ccf5481-59f5-4036-8a94-7d5f28f4f899"
|
||||
*/
|
||||
private extractPipelineIdFromJobId(jobId: string): string {
|
||||
if (!jobId) return '';
|
||||
|
||||
// Determine the separator used in the jobId
|
||||
const hasUnderscores = jobId.includes('_');
|
||||
const separator = hasUnderscores ? '_' : '-';
|
||||
|
||||
const parts = jobId.split(separator);
|
||||
// UUID has 5 parts (8-4-4-4-12), job suffix is the 6th part
|
||||
if (parts.length >= 6) {
|
||||
// Always return hyphenated format for Elasticsearch lookup
|
||||
return parts.slice(0, 5).join('-');
|
||||
}
|
||||
// If no suffix found, return the ID in hyphenated format
|
||||
return hasUnderscores ? jobId.replace(/_/g, '-') : jobId;
|
||||
}
|
||||
|
||||
private readonly VALID_CONNECTORS = ['jdbc', 'singer', 's3'];
|
||||
private readonly MAX_MEMORY_MB = 12000; // 12GB maximum memory per pipeline/job
|
||||
|
||||
/**
|
||||
* Validate that connector is provided and is a valid type.
|
||||
*/
|
||||
private validateConnector(connector: string | undefined): void {
|
||||
if (!connector || !this.VALID_CONNECTORS.includes(connector)) {
|
||||
throw new BadRequestException(
|
||||
`connector is required in job input and must be one of: ${this.VALID_CONNECTORS.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate memory allocation against maximum limit.
|
||||
*/
|
||||
private validateMemory(memoryMb: number): void {
|
||||
if (memoryMb > this.MAX_MEMORY_MB) {
|
||||
throw new BadRequestException(
|
||||
`Memory limit exceeded. Maximum allowed: ${this.MAX_MEMORY_MB}MB (12GB)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate cron expression against customer's schedule limit.
|
||||
* Fetches current scheduleLimit from DUC to ensure up-to-date configuration.
|
||||
*/
|
||||
private async validateScheduleLimit(cron: string, customerId: string): Promise<void> {
|
||||
if (!cron) return;
|
||||
|
||||
const { customer } = await this.customersService.getCustomer(customerId);
|
||||
const scheduleLimit = customer?.scheduleLimit || 'day';
|
||||
|
||||
const result = validateCronAgainstScheduleLimit(cron, scheduleLimit);
|
||||
if (!result.valid) {
|
||||
throw new BadRequestException(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map connector type to DynamoDB type.
|
||||
* jdbc -> 'database', singer -> 'application', s3 -> 'file'
|
||||
*/
|
||||
private mapConnectorToDynamoType(connector: string): string {
|
||||
switch (connector) {
|
||||
case 'jdbc':
|
||||
return 'database';
|
||||
case 'singer':
|
||||
return 'application';
|
||||
case 's3':
|
||||
return 'file';
|
||||
default:
|
||||
return connector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and transform tables from jobs for DynamoDB input.
|
||||
* Maps connector-specific fields to a common table format.
|
||||
*
|
||||
* - JDBC: load_type, table_name, column_include_list (columns), incremental_column_name/type (reference_column object)
|
||||
* - Singer: type maps replication_method (FULL_TABLE -> full_load, INCREMENTAL -> incremental), no columns
|
||||
* - S3: same mapping as Singer, no columns
|
||||
*/
|
||||
private extractTablesFromJobs(jobs: any[], connector: string): Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn;
|
||||
}> {
|
||||
if (!jobs || jobs.length === 0) return [];
|
||||
|
||||
const tables: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn;
|
||||
}> = [];
|
||||
|
||||
for (const job of jobs) {
|
||||
const input = job.input;
|
||||
if (!input) continue;
|
||||
|
||||
if (connector === 'jdbc') {
|
||||
// JDBC: table_name, load_type, column_include_list, incremental_column_name/type
|
||||
const table: {
|
||||
name: string;
|
||||
type: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn;
|
||||
} = {
|
||||
name: input.table_name || '',
|
||||
type: input.load_type || 'full_load',
|
||||
};
|
||||
if (input.column_include_list && input.column_include_list.length > 0) {
|
||||
table.columns = input.column_include_list;
|
||||
}
|
||||
if (input.incremental_column_name) {
|
||||
// reference_column is stored as an object with name and type
|
||||
table.reference_column = {
|
||||
name: input.incremental_column_name,
|
||||
type: input.incremental_column_type || 'unknown',
|
||||
};
|
||||
}
|
||||
tables.push(table);
|
||||
} else if (connector === 'singer' || connector === 's3') {
|
||||
// Singer/S3: replication_method -> type mapping, no columns
|
||||
let type = 'full_load';
|
||||
if (input.replication_method === 'INCREMENTAL') {
|
||||
type = 'incremental';
|
||||
} else if (input.replication_method === 'FULL_TABLE') {
|
||||
type = 'full_load';
|
||||
}
|
||||
|
||||
tables.push({
|
||||
name: input.table_name || '',
|
||||
type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build properties object for Elasticsearch based on connector type.
|
||||
* Different connectors have different property structures.
|
||||
*
|
||||
* Note: In pi-factory flow, properties come pre-built from frontend.
|
||||
* In Maestro proxy flow, we reconstruct from job input fields.
|
||||
*/
|
||||
private buildPipelineProperties(jobInput: any): Record<string, any> {
|
||||
if (!jobInput) return {};
|
||||
|
||||
const connector = jobInput.connector;
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
// Determine credentials_type
|
||||
if (jobInput.auth_parameters?.credentials_type) {
|
||||
properties.credentials_type = jobInput.auth_parameters.credentials_type;
|
||||
} else {
|
||||
// Default based on connector type
|
||||
// S3 connector typically uses iam_user, others use basic_auth
|
||||
properties.credentials_type = connector === 's3' ? 'iam_user' : 'basic_auth';
|
||||
}
|
||||
|
||||
if (connector === 'jdbc') {
|
||||
// JDBC connectors: schema comes from table_schema
|
||||
if (jobInput.table_schema) {
|
||||
properties.schema = jobInput.table_schema;
|
||||
}
|
||||
} else if (connector === 'singer') {
|
||||
// Singer connectors: merge config fields (dates, selected_competitions, etc.)
|
||||
if (jobInput.config) {
|
||||
Object.assign(properties, jobInput.config);
|
||||
}
|
||||
} else if (connector === 's3') {
|
||||
// S3 connector
|
||||
if (jobInput.engine) properties.engine = jobInput.engine;
|
||||
if (jobInput.source_bucket) properties.source_bucket = jobInput.source_bucket;
|
||||
if (jobInput.source_prefix) properties.source_prefix = jobInput.source_prefix;
|
||||
if (jobInput.file_format_params) properties.file_format_params = jobInput.file_format_params;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync job input changes to DynamoDB for a specific connector type.
|
||||
* Extracts pipeline ID from job ID, fetches ES document to find input ID,
|
||||
* then updates the table entry in DynamoDB.
|
||||
*
|
||||
* Job ID transformations:
|
||||
* - Raw format (from endpoint): "2ccf5481-59f5-4036-8a94-7d5f28f4f899-0"
|
||||
* - Platform API format: "2ccf5481_59f5_4036_8a94_7d5f28f4f899_0" (underscores)
|
||||
* - Elasticsearch pipeline ID: "2ccf5481-59f5-4036-8a94-7d5f28f4f899" (UUID only, hyphens)
|
||||
*
|
||||
* @param connectorType - The connector type ('jdbc', 'singer', 's3') for the Platform API endpoint
|
||||
*/
|
||||
private async syncJobInputToDynamoDB(
|
||||
jobId: string,
|
||||
body: any,
|
||||
user: RequestUser,
|
||||
connectorType: 'jdbc' | 'singer' | 's3',
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Normalize job ID for Platform API GET (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
// Get job details using connector-specific endpoint to find table_name
|
||||
const jobResult = await this.platformApiService.proxy(
|
||||
'GET',
|
||||
`/jobs/${connectorType}/${normalizedJobId}`,
|
||||
user,
|
||||
);
|
||||
|
||||
// Extract the pipeline ID (base UUID) from the raw job ID for ES lookup
|
||||
const esPipelineId = this.extractPipelineIdFromJobId(jobId);
|
||||
const tableName = body.table_name || jobResult.source_config?.table_name;
|
||||
|
||||
if (!esPipelineId || !tableName) {
|
||||
this.logger.warn('Cannot sync job input: missing pipeline_id or table_name', {
|
||||
jobId,
|
||||
esPipelineId,
|
||||
tableName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get pipeline from ES to find input ID (stored in config.tables)
|
||||
const pipeline = await this.elasticsearchService.getPipeline(
|
||||
user.customer_name,
|
||||
esPipelineId,
|
||||
);
|
||||
|
||||
const inputId = pipeline?.config?.tables;
|
||||
if (!inputId) {
|
||||
this.logger.warn('Cannot sync job input: no input ID in ES', {
|
||||
jobId,
|
||||
esPipelineId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Build changes for DynamoDB table entry
|
||||
// reference_column is stored as an object with name and type
|
||||
const changes: {
|
||||
type?: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn | null;
|
||||
} = {};
|
||||
|
||||
|
||||
if ('target_load_type' in body) {
|
||||
changes.type = body.target_load_type;
|
||||
}
|
||||
if ('column_include_list' in body) {
|
||||
changes.columns = body.column_include_list;
|
||||
}
|
||||
if ('incremental_column_name' in body) {
|
||||
// reference_column is stored as an object with name and type
|
||||
if (body.incremental_column_name) {
|
||||
changes.reference_column = {
|
||||
name: body.incremental_column_name,
|
||||
type: body.incremental_column_type || 'unknown',
|
||||
};
|
||||
} else {
|
||||
changes.reference_column = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update DynamoDB if there are changes
|
||||
if (Object.keys(changes).length > 0) {
|
||||
await this.dynamoDBService.updateInputTable(
|
||||
user.customer_id,
|
||||
inputId,
|
||||
tableName,
|
||||
changes,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to sync job input to DynamoDB', {
|
||||
jobId,
|
||||
connectorType,
|
||||
error: error.message,
|
||||
});
|
||||
// Don't throw - Platform API update succeeded, just log the sync error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync sync-mode changes to DynamoDB for JDBC connectors.
|
||||
* Always passes both target_load_type and incremental_column_name to ensure proper sync.
|
||||
*/
|
||||
private async syncJdbcSyncModeToDynamoDB(
|
||||
jobId: string,
|
||||
body: any,
|
||||
user: RequestUser,
|
||||
): Promise<void> {
|
||||
// JDBC sync mode uses target_load_type field
|
||||
const changes: any = {};
|
||||
|
||||
if ('target_load_type' in body) {
|
||||
changes.target_load_type = body.target_load_type;
|
||||
}
|
||||
|
||||
// Handle incremental_column_name:
|
||||
// - If provided in body, use that value
|
||||
// - If changing to full_load, explicitly clear it
|
||||
if ('incremental_column_name' in body) {
|
||||
changes.incremental_column_name = body.incremental_column_name;
|
||||
changes.incremental_column_type = body.incremental_column_type;
|
||||
} else if (body.target_load_type === 'full_load') {
|
||||
// Changing to full_load without specifying incremental_column - clear it
|
||||
changes.incremental_column_name = null;
|
||||
}
|
||||
|
||||
await this.syncJobInputToDynamoDB(jobId, changes, user, 'jdbc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync sync-mode changes to DynamoDB for Singer connectors.
|
||||
*/
|
||||
private async syncSingerSyncModeToDynamoDB(
|
||||
jobId: string,
|
||||
body: any,
|
||||
user: RequestUser,
|
||||
): Promise<void> {
|
||||
// Singer sync mode uses replication_method field
|
||||
// Map to DynamoDB type: FULL_TABLE -> full_load, INCREMENTAL -> incremental
|
||||
if ('replication_method' in body) {
|
||||
const type = body.replication_method === 'INCREMENTAL' ? 'incremental' : 'full_load';
|
||||
await this.syncJobInputToDynamoDB(jobId, { load_type: type }, user, 'singer');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PIPELINE ROUTES ====================
|
||||
|
||||
@Post('pipeline')
|
||||
@ApiOperation({ summary: 'Create a new pipeline' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.CREATE)
|
||||
async createPipeline(@Body() body: any, @User() user: RequestUser) {
|
||||
// Validate that pipeline has jobs
|
||||
if (!body.jobs || body.jobs.length === 0) {
|
||||
throw new BadRequestException('Pipeline must have at least one job');
|
||||
}
|
||||
|
||||
// Validate connector before proceeding
|
||||
const firstJob = body.jobs[0]?.input;
|
||||
this.validateConnector(firstJob?.connector);
|
||||
|
||||
// Validate cron against customer's schedule limit
|
||||
await this.validateScheduleLimit(body.cron, user.customer_id);
|
||||
|
||||
// Inject customer_id (actually customer_name) and normalized ID into body for Platform-API
|
||||
// Note: Platform-API was created before customer_id existed, so it expects customer_name in the customer_id field
|
||||
const enrichedBody = {
|
||||
...body,
|
||||
id: this.normalizePipelineId(body.id),
|
||||
customer_id: user.customer_name,
|
||||
};
|
||||
|
||||
const result = await this.platformApiService.proxy('POST', '/pipeline', user, enrichedBody);
|
||||
|
||||
// Sync to Elasticsearch and DynamoDB
|
||||
try {
|
||||
const plugin = firstJob?.plugin;
|
||||
const connectorType = firstJob?.connector;
|
||||
const connectionId = firstJob?.auth_parameters?.config_id;
|
||||
|
||||
// Build properties based on connector type
|
||||
const properties = this.buildPipelineProperties(firstJob);
|
||||
|
||||
const connector = plugin
|
||||
? await this.elasticsearchService.getConnectorByPlugin(plugin)
|
||||
: null;
|
||||
|
||||
// Extract tables from jobs and create DynamoDB input
|
||||
const tables = this.extractTablesFromJobs(body.jobs, connectorType);
|
||||
let inputId: string | undefined;
|
||||
|
||||
if (tables.length > 0) {
|
||||
const inputDoc = await this.dynamoDBService.createInput(
|
||||
user.customer_id,
|
||||
user.user_id,
|
||||
{
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
plugin: plugin || '',
|
||||
type: this.mapConnectorToDynamoType(connectorType),
|
||||
tables,
|
||||
},
|
||||
);
|
||||
inputId = inputDoc.id;
|
||||
this.logger.info('Created DynamoDB input for tables config', {
|
||||
inputId,
|
||||
pipelineId: body.id,
|
||||
tablesCount: tables.length,
|
||||
});
|
||||
}
|
||||
|
||||
const pipelineType = this.mapConnectorToDynamoType(connectorType);
|
||||
this.logger.info('Syncing pipeline to Elasticsearch', {
|
||||
customerName: user.customer_name,
|
||||
pipelineId: body.id,
|
||||
plugin,
|
||||
connector: connectorType,
|
||||
type: pipelineType,
|
||||
properties,
|
||||
inputId,
|
||||
});
|
||||
|
||||
// Keep original UUID format for Elasticsearch (not normalized)
|
||||
await this.elasticsearchService.createPipeline(
|
||||
user.customer_name,
|
||||
body.id,
|
||||
{
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
customer_id: user.customer_id,
|
||||
plugin,
|
||||
connection_id: connectionId,
|
||||
cron: body.cron,
|
||||
tables: inputId,
|
||||
properties,
|
||||
type: pipelineType,
|
||||
},
|
||||
connector,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to sync pipeline creation to Elasticsearch/DynamoDB', {
|
||||
pipelineId: body.id,
|
||||
customerName: user.customer_name,
|
||||
error: error.message,
|
||||
errorName: error.name,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('pipelines')
|
||||
@ApiOperation({ summary: 'List all pipelines for customer' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipelines(
|
||||
@User() user: RequestUser,
|
||||
@Query() query: Record<string, string>,
|
||||
) {
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
'/pipelines',
|
||||
user,
|
||||
undefined,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('pipeline/:pipelineId')
|
||||
@ApiOperation({ summary: 'Get pipeline by ID' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipeline(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
const normalizedId = this.normalizePipelineId(pipelineId);
|
||||
return this.platformApiService.proxy('GET', `/pipeline/${normalizedId}`, user);
|
||||
}
|
||||
|
||||
@Patch('pipeline/:pipelineId')
|
||||
@ApiOperation({ summary: 'Update pipeline by ID' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updatePipeline(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Validate cron against customer's schedule limit if cron is being updated
|
||||
if (body.cron) {
|
||||
await this.validateScheduleLimit(body.cron, user.customer_id);
|
||||
}
|
||||
|
||||
const normalizedId = this.normalizePipelineId(pipelineId);
|
||||
const result = await this.platformApiService.proxy(
|
||||
'PATCH',
|
||||
`/pipeline/${normalizedId}`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
|
||||
// Sync to Elasticsearch (use original UUID, not normalized)
|
||||
// Only pass fields that are explicitly provided in the request body
|
||||
try {
|
||||
const esChanges: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
cron?: string;
|
||||
status?: string;
|
||||
} = {};
|
||||
|
||||
if ('name' in body) esChanges.name = body.name;
|
||||
if ('description' in body) esChanges.description = body.description;
|
||||
if ('cron' in body) esChanges.cron = body.cron;
|
||||
if ('status' in body) esChanges.status = body.status;
|
||||
|
||||
await this.elasticsearchService.updatePipeline(
|
||||
user.customer_name,
|
||||
pipelineId,
|
||||
esChanges,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to sync pipeline update to Elasticsearch', {
|
||||
pipelineId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete('pipeline/:pipelineId')
|
||||
@ApiOperation({ summary: 'Delete pipeline by ID' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE)
|
||||
async deletePipeline(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
const normalizedId = this.normalizePipelineId(pipelineId);
|
||||
const result = await this.platformApiService.proxy(
|
||||
'DELETE',
|
||||
`/pipeline/${normalizedId}`,
|
||||
user,
|
||||
);
|
||||
|
||||
// Sync to Elasticsearch (use original UUID, not normalized)
|
||||
try {
|
||||
await this.elasticsearchService.deletePipeline(
|
||||
user.customer_name,
|
||||
pipelineId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to sync pipeline deletion to Elasticsearch', {
|
||||
pipelineId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post('pipeline/execute')
|
||||
@ApiOperation({ summary: 'Execute a pipeline' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async executePipeline(@Body() body: any, @User() user: RequestUser) {
|
||||
// Inject customer_id (actually customer_name) into body for Platform-API
|
||||
// Note: Platform-API was created before customer_id existed, so it expects customer_name in the customer_id field
|
||||
const enrichedBody = {
|
||||
...body,
|
||||
customer_id: user.customer_name,
|
||||
};
|
||||
return this.platformApiService.proxy('POST', '/pipeline/execute', user, enrichedBody);
|
||||
}
|
||||
|
||||
@Post('pipeline/pause')
|
||||
@ApiOperation({ summary: 'Pause a pipeline' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async pausePipeline(@Body() body: any, @User() user: RequestUser) {
|
||||
// Inject customer_id (actually customer_name) into body for Platform-API
|
||||
// Note: Platform-API was created before customer_id existed, so it expects customer_name in the customer_id field
|
||||
const enrichedBody = {
|
||||
...body,
|
||||
customer_id: user.customer_name,
|
||||
};
|
||||
return this.platformApiService.proxy('POST', '/pipeline/pause', user, enrichedBody);
|
||||
}
|
||||
|
||||
@Post('pipeline/unpause')
|
||||
@ApiOperation({ summary: 'Unpause a pipeline' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async unpausePipeline(@Body() body: any, @User() user: RequestUser) {
|
||||
// Inject customer_id (actually customer_name) into body for Platform-API
|
||||
// Note: Platform-API was created before customer_id existed, so it expects customer_name in the customer_id field
|
||||
const enrichedBody = {
|
||||
...body,
|
||||
customer_id: user.customer_name,
|
||||
};
|
||||
return this.platformApiService.proxy('POST', '/pipeline/unpause', user, enrichedBody);
|
||||
}
|
||||
|
||||
@Put('pipeline/:pipelineId/memory')
|
||||
@ApiOperation({ summary: 'Update pipeline memory configuration' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updatePipelineMemory(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Validate memory limit
|
||||
if (body.amount) {
|
||||
this.validateMemory(body.amount);
|
||||
}
|
||||
|
||||
return this.platformApiService.proxy(
|
||||
'PUT',
|
||||
`/pipeline/${pipelineId}/memory`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== PIPELINE METADATA ROUTES ====================
|
||||
|
||||
@Put('pipeline/:pipelineId/metadata')
|
||||
@ApiOperation({ summary: 'Update pipeline metadata' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updatePipelineMetadata(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
return this.platformApiService.proxy(
|
||||
'PUT',
|
||||
`/pipeline/${pipelineId}/metadata`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('pipelines/metadata')
|
||||
@ApiOperation({ summary: 'Get all pipelines metadata' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipelinesMetadata(
|
||||
@User() user: RequestUser,
|
||||
@Query() query: Record<string, string>,
|
||||
) {
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
'/pipelines/metadata',
|
||||
user,
|
||||
undefined,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== PIPELINE RUN ROUTES ====================
|
||||
|
||||
@Get('pipeline/:pipelineId/pipeline_run')
|
||||
@ApiOperation({ summary: 'Get pipeline runs for a pipeline' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipelineRuns(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@User() user: RequestUser,
|
||||
@Query() query: Record<string, string>,
|
||||
) {
|
||||
const normalizedId = this.normalizePipelineId(pipelineId);
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
`/pipeline/${normalizedId}/pipeline_run`,
|
||||
user,
|
||||
undefined,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('pipeline/:pipelineId/pipeline_run/:runId')
|
||||
@ApiOperation({ summary: 'Get specific pipeline run' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipelineRun(
|
||||
@Param('pipelineId') pipelineId: string,
|
||||
@Param('runId') runId: string,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
const normalizedPipelineId = this.normalizePipelineId(pipelineId);
|
||||
const normalizedRunId = this.normalizePipelineId(runId);
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
`/pipeline/${normalizedPipelineId}/pipeline_run/${normalizedRunId}`,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('pipeline/pipeline_run/:runId/logs')
|
||||
@ApiOperation({ summary: 'Get pipeline run logs' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getPipelineRunLogs(
|
||||
@Param('runId') runId: string,
|
||||
@User() user: RequestUser,
|
||||
@Query() query: Record<string, string>,
|
||||
) {
|
||||
const normalizedRunId = this.normalizePipelineId(runId);
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
`/pipeline/pipeline_run/${normalizedRunId}/logs`,
|
||||
user,
|
||||
undefined,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== JOBS - COLUMN EDITING ROUTES ====================
|
||||
|
||||
@Put('jobs/:jobId/input')
|
||||
@ApiOperation({ summary: 'Update job input columns' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updateJobInput(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
const result = await this.platformApiService.proxy(
|
||||
'PUT',
|
||||
`/jobs/${normalizedJobId}/input`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
|
||||
// Sync to DynamoDB if connector type is provided
|
||||
const connectorType = body.connector as 'jdbc' | 'singer' | 's3' | undefined;
|
||||
if (connectorType && this.VALID_CONNECTORS.includes(connectorType)) {
|
||||
await this.syncJobInputToDynamoDB(jobId, body, user, connectorType);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Patch('jobs/:jobId/input')
|
||||
@ApiOperation({ summary: 'Partial update job input columns' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async patchJobInput(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
const result = await this.platformApiService.proxy(
|
||||
'PATCH',
|
||||
`/jobs/${normalizedJobId}/input`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
|
||||
// Sync to DynamoDB if connector type is provided
|
||||
const connectorType = body.connector as 'jdbc' | 'singer' | 's3' | undefined;
|
||||
if (connectorType && this.VALID_CONNECTORS.includes(connectorType)) {
|
||||
await this.syncJobInputToDynamoDB(jobId, body, user, connectorType);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put('jobs/:jobId/memory')
|
||||
@ApiOperation({ summary: 'Update job memory configuration' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updateJobMemory(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Validate memory limit
|
||||
if (body.amount) {
|
||||
this.validateMemory(body.amount);
|
||||
}
|
||||
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
return this.platformApiService.proxy(
|
||||
'PUT',
|
||||
`/jobs/${normalizedJobId}/memory`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('jobs/:jobId/reset-state')
|
||||
@ApiOperation({ summary: 'Reset job state' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async resetJobState(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
return this.platformApiService.proxy(
|
||||
'POST',
|
||||
`/jobs/${normalizedJobId}/reset-state`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== JOBS - JDBC SYNC MODE ROUTES ====================
|
||||
|
||||
@Get('jobs/jdbc/:jobId')
|
||||
@ApiOperation({ summary: 'Get JDBC job details' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getJdbcJob(@Param('jobId') jobId: string, @User() user: RequestUser) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
return this.platformApiService.proxy('GET', `/jobs/jdbc/${normalizedJobId}`, user);
|
||||
}
|
||||
|
||||
@Post('jobs/jdbc/:jobId/sync-mode')
|
||||
@ApiOperation({ summary: 'Update JDBC job sync mode' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updateJdbcSyncMode(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
const result = await this.platformApiService.proxy(
|
||||
'POST',
|
||||
`/jobs/jdbc/${normalizedJobId}/sync-mode`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
|
||||
// Sync to DynamoDB (pass raw jobId for pipeline extraction)
|
||||
await this.syncJdbcSyncModeToDynamoDB(jobId, body, user);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('jobs/jdbc/configs/allowed_datatypes')
|
||||
@ApiOperation({ summary: 'Get allowed datatypes for JDBC' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getJdbcAllowedDatatypes(@User() user: RequestUser) {
|
||||
return this.platformApiService.proxy(
|
||||
'GET',
|
||||
'/jobs/jdbc/configs/allowed_datatypes',
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== JOBS - SINGER REPLICATION ROUTES ====================
|
||||
|
||||
@Get('jobs/singer/:jobId')
|
||||
@ApiOperation({ summary: 'Get Singer job details' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getSingerJob(@Param('jobId') jobId: string, @User() user: RequestUser) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
return this.platformApiService.proxy('GET', `/jobs/singer/${normalizedJobId}`, user);
|
||||
}
|
||||
|
||||
@Post('jobs/singer/:jobId/sync-mode')
|
||||
@ApiOperation({ summary: 'Update Singer job sync mode' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
|
||||
async updateSingerSyncMode(
|
||||
@Param('jobId') jobId: string,
|
||||
@Body() body: any,
|
||||
@User() user: RequestUser,
|
||||
) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
|
||||
const result = await this.platformApiService.proxy(
|
||||
'POST',
|
||||
`/jobs/singer/${normalizedJobId}/sync-mode`,
|
||||
user,
|
||||
body,
|
||||
);
|
||||
|
||||
// Sync to DynamoDB (pass raw jobId for pipeline extraction)
|
||||
await this.syncSingerSyncModeToDynamoDB(jobId, body, user);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== JOBS - S3 ROUTES ====================
|
||||
|
||||
@Get('jobs/s3/:jobId')
|
||||
@ApiOperation({ summary: 'Get S3 job details' })
|
||||
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||
async getS3Job(@Param('jobId') jobId: string, @User() user: RequestUser) {
|
||||
// Normalize job ID for Platform API (replace - with _)
|
||||
const normalizedJobId = this.normalizeJobId(jobId);
|
||||
return this.platformApiService.proxy('GET', `/jobs/s3/${normalizedJobId}`, user);
|
||||
}
|
||||
|
||||
// ==================== HEALTH ROUTE ====================
|
||||
|
||||
@Get('health')
|
||||
@ApiOperation({ summary: 'Platform API health check' })
|
||||
@Authenticated()
|
||||
async healthCheck(@User() user: RequestUser) {
|
||||
return this.platformApiService.proxy('GET', '/health', user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import { PlatformApiController } from './platform-api.controller';
|
||||
import { PlatformApiService } from './platform-api.service';
|
||||
import { ElasticsearchModule } from '../../services/elasticsearch';
|
||||
import { DynamoDBModule } from '../../services/dynamodb';
|
||||
import { CustomersModule } from '../customers/customers.module';
|
||||
|
||||
@Module({
|
||||
imports: [ElasticsearchModule, DynamoDBModule, CustomersModule],
|
||||
controllers: [PlatformApiController],
|
||||
providers: [PlatformApiService, DadosferaLogger],
|
||||
exports: [PlatformApiService],
|
||||
})
|
||||
export class PlatformApiModule {}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Injectable, Inject, HttpException } from '@nestjs/common';
|
||||
import { SignatureV4 } from '@aws-sdk/signature-v4';
|
||||
import { Sha256 } from '@aws-crypto/sha256-js';
|
||||
import { defaultProvider } from '@aws-sdk/credential-provider-node';
|
||||
import axios, { AxiosResponse, Method } from 'axios';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
|
||||
import { RequestUser } from '../../decorators/user.decorator';
|
||||
import { PLATFORM_API_CONFIG } from './platform-api.config';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformApiService {
|
||||
private signer: SignatureV4;
|
||||
private logger: any;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
this.signer = new SignatureV4({
|
||||
service: 'execute-api',
|
||||
region: PLATFORM_API_CONFIG.region,
|
||||
credentials: defaultProvider(),
|
||||
sha256: Sha256,
|
||||
});
|
||||
}
|
||||
|
||||
async proxy(
|
||||
method: string,
|
||||
path: string,
|
||||
user: RequestUser,
|
||||
body?: any,
|
||||
query?: Record<string, string>,
|
||||
): Promise<any> {
|
||||
const baseUrl = PLATFORM_API_CONFIG.getUrl();
|
||||
const url = new URL(`${baseUrl}${path}`);
|
||||
|
||||
// Add query params
|
||||
if (query) {
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
host: url.hostname,
|
||||
'content-type': 'application/json',
|
||||
// Forward user context headers
|
||||
// Note: platform-api expects customer_name in the 'customer_id' header (contract inconsistency)
|
||||
'customer_id': user.customer_name || '',
|
||||
'customer_name': user.customer_name || '',
|
||||
'x-user-id': user.user_id || '',
|
||||
'x-username': user.username || '',
|
||||
'x-customer-tier': user.customer_tier || '',
|
||||
'x-customer-id': user.customer_id || '',
|
||||
};
|
||||
|
||||
const requestToSign = {
|
||||
method: method.toUpperCase(),
|
||||
protocol: url.protocol,
|
||||
hostname: url.hostname,
|
||||
port: url.port ? parseInt(url.port, 10) : undefined,
|
||||
path: url.pathname + url.search,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
};
|
||||
|
||||
this.logger.info('Proxying request to platform-api', {
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
customer_id: user.customer_id,
|
||||
user_id: user.user_id,
|
||||
});
|
||||
|
||||
try {
|
||||
// Sign with IAM v4
|
||||
const signedRequest = await this.signer.sign(requestToSign);
|
||||
|
||||
const response: AxiosResponse = await axios({
|
||||
method: method as Method,
|
||||
url: url.href,
|
||||
headers: signedRequest.headers as Record<string, string>,
|
||||
data: body,
|
||||
timeout: PLATFORM_API_CONFIG.timeout,
|
||||
validateStatus: () => true, // Don't throw on non-2xx
|
||||
});
|
||||
|
||||
// Propagate non-2xx responses as HttpExceptions
|
||||
if (response.status >= 400) {
|
||||
throw new HttpException(response.data, response.status);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Platform API proxy error', {
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
path,
|
||||
method: method.toUpperCase(),
|
||||
});
|
||||
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
throw new HttpException(error.response.data, error.response.status);
|
||||
}
|
||||
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
throw new HttpException('Platform API service unavailable', 503);
|
||||
}
|
||||
|
||||
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
||||
throw new HttpException('Platform API request timeout', 504);
|
||||
}
|
||||
|
||||
throw new HttpException('Internal server error', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const DYNAMODB_CONFIG = {
|
||||
region: () => process.env.AWS_REGION || 'us-east-1',
|
||||
inputsTable: () => process.env.INPUTS_DB || 'dadosfera-inputs-prd',
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { DynamoDBService } from './dynamodb.service';
|
||||
|
||||
@Module({
|
||||
providers: [DynamoDBService, DadosferaLogger],
|
||||
exports: [DynamoDBService],
|
||||
})
|
||||
export class DynamoDBModule {}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
||||
import {
|
||||
DynamoDBDocumentClient,
|
||||
GetCommand,
|
||||
PutCommand,
|
||||
DeleteCommand,
|
||||
TranslateConfig,
|
||||
} from '@aws-sdk/lib-dynamodb';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DYNAMODB_CONFIG } from './dynamodb.config';
|
||||
|
||||
export interface ReferenceColumn {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface InputDocument {
|
||||
id: string;
|
||||
client_id: string;
|
||||
user_id: string;
|
||||
created_at: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
plugin: string;
|
||||
type: string;
|
||||
tables?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn;
|
||||
}>;
|
||||
credentials?: Record<string, any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DynamoDBService {
|
||||
private documentClient: DynamoDBDocumentClient;
|
||||
private logger: any;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
|
||||
const dynamoConfig = { region: DYNAMODB_CONFIG.region() };
|
||||
const marshallOptions: TranslateConfig = {
|
||||
marshallOptions: {
|
||||
removeUndefinedValues: true,
|
||||
},
|
||||
};
|
||||
|
||||
const dynamoDb = new DynamoDBClient(dynamoConfig);
|
||||
this.documentClient = DynamoDBDocumentClient.from(dynamoDb, marshallOptions);
|
||||
}
|
||||
|
||||
async createInput(
|
||||
clientId: string,
|
||||
userId: string,
|
||||
data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
plugin: string;
|
||||
type: string;
|
||||
tables?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn;
|
||||
}>;
|
||||
},
|
||||
): Promise<InputDocument> {
|
||||
const tableName = DYNAMODB_CONFIG.inputsTable();
|
||||
const id = uuid();
|
||||
const created_at = new Date().toISOString();
|
||||
|
||||
const item: InputDocument = {
|
||||
id,
|
||||
client_id: clientId,
|
||||
user_id: userId,
|
||||
created_at,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
plugin: data.plugin,
|
||||
type: data.type,
|
||||
tables: data.tables,
|
||||
};
|
||||
|
||||
this.logger.info('DynamoDB: Creating input', {
|
||||
tableName,
|
||||
inputId: id,
|
||||
plugin: data.plugin,
|
||||
});
|
||||
|
||||
const putCommand = new PutCommand({
|
||||
TableName: tableName,
|
||||
Item: item,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.documentClient.send(putCommand);
|
||||
this.logger.info('DynamoDB: Input created successfully', { inputId: id });
|
||||
return item;
|
||||
} catch (error) {
|
||||
this.logger.error('DynamoDB: Failed to create input', {
|
||||
tableName,
|
||||
inputId: id,
|
||||
region: DYNAMODB_CONFIG.region(),
|
||||
error: error.message,
|
||||
errorName: error.name,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findInput(clientId: string, inputId: string): Promise<InputDocument | null> {
|
||||
const tableName = DYNAMODB_CONFIG.inputsTable();
|
||||
|
||||
const getCommand = new GetCommand({
|
||||
TableName: tableName,
|
||||
Key: {
|
||||
id: inputId,
|
||||
client_id: clientId,
|
||||
},
|
||||
});
|
||||
|
||||
const { Item } = await this.documentClient.send(getCommand);
|
||||
|
||||
return Item as InputDocument | null;
|
||||
}
|
||||
|
||||
async deleteInput(clientId: string, inputId: string): Promise<void> {
|
||||
const tableName = DYNAMODB_CONFIG.inputsTable();
|
||||
|
||||
this.logger.info('DynamoDB: Deleting input', {
|
||||
tableName,
|
||||
inputId,
|
||||
});
|
||||
|
||||
const deleteCommand = new DeleteCommand({
|
||||
TableName: tableName,
|
||||
Key: {
|
||||
id: inputId,
|
||||
client_id: clientId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.documentClient.send(deleteCommand);
|
||||
|
||||
this.logger.info('DynamoDB: Input deleted successfully', { inputId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific table entry in the input document.
|
||||
* Fetches the current document, updates the matching table, and saves.
|
||||
*/
|
||||
async updateInputTable(
|
||||
clientId: string,
|
||||
inputId: string,
|
||||
tableName: string,
|
||||
changes: {
|
||||
type?: string;
|
||||
columns?: string[];
|
||||
reference_column?: ReferenceColumn | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
const dynamoTableName = DYNAMODB_CONFIG.inputsTable();
|
||||
|
||||
this.logger.info('DynamoDB: Updating input table', {
|
||||
inputId,
|
||||
tableName,
|
||||
changes: Object.keys(changes),
|
||||
});
|
||||
|
||||
// Get current document
|
||||
const current = await this.findInput(clientId, inputId);
|
||||
if (!current) {
|
||||
this.logger.warn('DynamoDB: Input not found for update', { inputId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and update the matching table
|
||||
const tables = current.tables || [];
|
||||
const tableIndex = tables.findIndex((t) => t.name === tableName);
|
||||
|
||||
if (tableIndex === -1) {
|
||||
this.logger.warn('DynamoDB: Table not found in input', {
|
||||
inputId,
|
||||
tableName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge changes into the table entry
|
||||
const updatedTable = { ...tables[tableIndex] };
|
||||
if ('type' in changes) updatedTable.type = changes.type;
|
||||
if ('columns' in changes) updatedTable.columns = changes.columns;
|
||||
if ('reference_column' in changes) {
|
||||
if (changes.reference_column === null) {
|
||||
delete updatedTable.reference_column;
|
||||
} else {
|
||||
updatedTable.reference_column = changes.reference_column;
|
||||
}
|
||||
}
|
||||
|
||||
tables[tableIndex] = updatedTable;
|
||||
|
||||
// Save updated document
|
||||
const putCommand = new PutCommand({
|
||||
TableName: dynamoTableName,
|
||||
Item: {
|
||||
...current,
|
||||
tables,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await this.documentClient.send(putCommand);
|
||||
this.logger.info('DynamoDB: Input table updated successfully', {
|
||||
inputId,
|
||||
tableName,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('DynamoDB: Failed to update input table', {
|
||||
inputId,
|
||||
tableName,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './dynamodb.service';
|
||||
export * from './dynamodb.module';
|
||||
export * from './dynamodb.config';
|
||||
@@ -0,0 +1,5 @@
|
||||
export const ELASTICSEARCH_CONFIG = {
|
||||
getUrl: () => process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
|
||||
getApiKey: () => process.env.ELASTICSEARCH_API_KEY || '',
|
||||
timeout: 10000, // 10 seconds
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { ElasticsearchService } from './elasticsearch.service';
|
||||
|
||||
@Module({
|
||||
providers: [ElasticsearchService, DadosferaLogger],
|
||||
exports: [ElasticsearchService],
|
||||
})
|
||||
export class ElasticsearchModule {}
|
||||
@@ -0,0 +1,382 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||
import { ELASTICSEARCH_CONFIG } from './elasticsearch.config';
|
||||
|
||||
interface MultiLang {
|
||||
'en-us': string;
|
||||
'pt-br': string;
|
||||
'es-es': string;
|
||||
}
|
||||
|
||||
interface MultiLangArray {
|
||||
'en-us': string[];
|
||||
'pt-br': string[];
|
||||
'es-es': string[];
|
||||
}
|
||||
|
||||
interface ConnectorInfo {
|
||||
plugin: string;
|
||||
name: MultiLang;
|
||||
image: string;
|
||||
version: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface PipelineDocument {
|
||||
id: string;
|
||||
name: MultiLang;
|
||||
description: MultiLang;
|
||||
customer_id: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_status_updated: string;
|
||||
tags: string[];
|
||||
// Connector metadata
|
||||
connection_id: string;
|
||||
connector_name: string;
|
||||
connector_plugin: string;
|
||||
connector_version: string;
|
||||
image_url: string;
|
||||
// Config
|
||||
config: {
|
||||
cron: string;
|
||||
tables?: string;
|
||||
};
|
||||
properties: string;
|
||||
type: string;
|
||||
in_use: number;
|
||||
keywords: MultiLangArray;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ElasticsearchService {
|
||||
private client: AxiosInstance;
|
||||
private logger: any;
|
||||
|
||||
constructor(
|
||||
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
|
||||
) {
|
||||
this.logger = dadosferaLogger.logger;
|
||||
this.client = axios.create({
|
||||
baseURL: ELASTICSEARCH_CONFIG.getUrl(),
|
||||
headers: {
|
||||
Authorization: `ApiKey ${ELASTICSEARCH_CONFIG.getApiKey()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: ELASTICSEARCH_CONFIG.timeout,
|
||||
});
|
||||
}
|
||||
|
||||
private getIndex(customerName: string): string {
|
||||
return `${customerName}_pipelines`;
|
||||
}
|
||||
|
||||
private formatMultiLang(value: string): MultiLang {
|
||||
return {
|
||||
'en-us': value,
|
||||
'pt-br': value,
|
||||
'es-es': value,
|
||||
};
|
||||
}
|
||||
|
||||
private formatMultiLangArray(value: string[] = []): MultiLangArray {
|
||||
return {
|
||||
'en-us': value,
|
||||
'pt-br': value,
|
||||
'es-es': value,
|
||||
};
|
||||
}
|
||||
|
||||
buildPipelineDocument(
|
||||
pipelineId: string,
|
||||
data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
customer_id: string;
|
||||
plugin: string;
|
||||
connection_id: string;
|
||||
cron?: string;
|
||||
tables?: string;
|
||||
properties?: Record<string, any>;
|
||||
type?: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
keywords?: string[];
|
||||
},
|
||||
connector: ConnectorInfo | null,
|
||||
): PipelineDocument {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
id: pipelineId,
|
||||
connection_id: data.connection_id,
|
||||
connector_name: connector?.name?.['en-us'] || '',
|
||||
connector_plugin: connector?.plugin || data.plugin,
|
||||
connector_version: connector?.version || '1.0.0',
|
||||
created_at: data.created_at || now,
|
||||
updated_at: now,
|
||||
customer_id: data.customer_id,
|
||||
description: this.formatMultiLang(data.description || ''),
|
||||
image_url: connector?.image || '',
|
||||
keywords: this.formatMultiLangArray(data.keywords),
|
||||
name: this.formatMultiLang(data.name || ''),
|
||||
status: data.status || 'CREATED',
|
||||
user_id: data.user_id,
|
||||
username: data.username,
|
||||
config: {
|
||||
cron: data.cron,
|
||||
tables: data.tables,
|
||||
},
|
||||
properties: data.properties ? JSON.stringify(data.properties) : '{}',
|
||||
type: data.type,
|
||||
in_use: 1,
|
||||
last_status_updated: now,
|
||||
tags: connector?.tags || [],
|
||||
};
|
||||
}
|
||||
|
||||
async getConnectorByPlugin(plugin: string): Promise<ConnectorInfo | null> {
|
||||
this.logger.info('Elasticsearch: Looking up connector', { plugin });
|
||||
|
||||
try {
|
||||
const response = await this.client.post('/connectors/_search', {
|
||||
query: {
|
||||
term: { plugin: plugin },
|
||||
},
|
||||
size: 1,
|
||||
});
|
||||
|
||||
const hits = response.data.hits?.hits || [];
|
||||
if (hits.length === 0) {
|
||||
this.logger.warn('Elasticsearch: Connector not found', { plugin });
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = hits[0]._source;
|
||||
return {
|
||||
plugin: source.plugin,
|
||||
name: source.name,
|
||||
image: source.image,
|
||||
version: source.version,
|
||||
tags: source.tags || [],
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError('getConnectorByPlugin', error, { plugin });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createPipeline(
|
||||
customerName: string,
|
||||
pipelineId: string,
|
||||
data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
customer_id: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
plugin: string;
|
||||
connection_id: string;
|
||||
cron?: string;
|
||||
tables?: string;
|
||||
properties?: Record<string, any>;
|
||||
type?: string;
|
||||
},
|
||||
connector: ConnectorInfo | null,
|
||||
): Promise<any> {
|
||||
const index = this.getIndex(customerName);
|
||||
const document = this.buildPipelineDocument(pipelineId, data, connector);
|
||||
|
||||
this.logger.info('Elasticsearch: Creating pipeline', {
|
||||
index,
|
||||
pipelineId,
|
||||
plugin: document.connector_plugin,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.client.post(
|
||||
`${index}/_doc/${pipelineId}`,
|
||||
document,
|
||||
{ params: { refresh: 'wait_for' } },
|
||||
);
|
||||
|
||||
this.logger.info('Elasticsearch: Pipeline created successfully', {
|
||||
pipelineId,
|
||||
result: response.data.result,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.handleError('createPipeline', error, { pipelineId, index });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updatePipeline(
|
||||
customerName: string,
|
||||
pipelineId: string,
|
||||
changes: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
cron?: string;
|
||||
status?: string;
|
||||
tags?: string[];
|
||||
},
|
||||
): Promise<any> {
|
||||
const index = this.getIndex(customerName);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
this.logger.info('Elasticsearch: Updating pipeline', {
|
||||
index,
|
||||
pipelineId,
|
||||
fields: Object.keys(changes),
|
||||
});
|
||||
|
||||
try {
|
||||
// Fetch current document
|
||||
const currentDoc = await this.client.get(`${index}/_doc/${pipelineId}`);
|
||||
const current = currentDoc.data._source;
|
||||
|
||||
// Build updated document, preserving existing values
|
||||
const updated: Record<string, any> = {
|
||||
...current,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
if ('name' in changes) {
|
||||
updated.name = this.formatMultiLang(changes.name);
|
||||
}
|
||||
|
||||
if ('description' in changes) {
|
||||
updated.description = this.formatMultiLang(changes.description);
|
||||
}
|
||||
|
||||
if ('cron' in changes) {
|
||||
updated.config = {
|
||||
...current.config,
|
||||
cron: changes.cron,
|
||||
};
|
||||
}
|
||||
|
||||
if ('status' in changes) {
|
||||
updated.status = changes.status;
|
||||
updated.last_status_updated = now;
|
||||
}
|
||||
|
||||
if ('tags' in changes) {
|
||||
updated.tags = changes.tags;
|
||||
}
|
||||
|
||||
const response = await this.client.post(
|
||||
`${index}/_doc/${pipelineId}`,
|
||||
updated,
|
||||
{ params: { refresh: 'wait_for' } },
|
||||
);
|
||||
|
||||
this.logger.info('Elasticsearch: Pipeline updated successfully', {
|
||||
pipelineId,
|
||||
result: response.data.result,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.handleError('updatePipeline', error, { pipelineId, index });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getPipeline(
|
||||
customerName: string,
|
||||
pipelineId: string,
|
||||
): Promise<PipelineDocument | null> {
|
||||
const index = this.getIndex(customerName);
|
||||
|
||||
this.logger.info('Elasticsearch: Getting pipeline', {
|
||||
index,
|
||||
pipelineId,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`${index}/_doc/${pipelineId}`);
|
||||
return response.data._source as PipelineDocument;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
this.logger.warn('Elasticsearch: Pipeline not found', {
|
||||
pipelineId,
|
||||
index,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
this.handleError('getPipeline', error, { pipelineId, index });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deletePipeline(
|
||||
customerName: string,
|
||||
pipelineId: string,
|
||||
): Promise<any> {
|
||||
const index = this.getIndex(customerName);
|
||||
|
||||
this.logger.info('Elasticsearch: Deleting pipeline', {
|
||||
index,
|
||||
pipelineId,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.client.delete(
|
||||
`${index}/_doc/${pipelineId}`,
|
||||
{ params: { refresh: 'wait_for' } },
|
||||
);
|
||||
|
||||
this.logger.info('Elasticsearch: Pipeline deleted successfully', {
|
||||
pipelineId,
|
||||
result: response.data.result,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// If document not found, log warning but don't throw
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
this.logger.warn('Elasticsearch: Pipeline not found for deletion', {
|
||||
pipelineId,
|
||||
index,
|
||||
});
|
||||
return { result: 'not_found' };
|
||||
}
|
||||
|
||||
this.handleError('deletePipeline', error, { pipelineId, index });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(
|
||||
operation: string,
|
||||
error: any,
|
||||
context: Record<string, any>,
|
||||
): void {
|
||||
if (error instanceof AxiosError) {
|
||||
this.logger.error(`Elasticsearch: ${operation} failed`, {
|
||||
...context,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
errorData: error.response?.data,
|
||||
message: error.message,
|
||||
});
|
||||
} else {
|
||||
this.logger.error(`Elasticsearch: ${operation} failed`, {
|
||||
...context,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './elasticsearch.module';
|
||||
export * from './elasticsearch.service';
|
||||
export * from './elasticsearch.config';
|
||||
@@ -14,7 +14,7 @@ import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
keyPrefix: 'maestro:sso',
|
||||
}
|
||||
|
||||
if (process.env.ENV !== 'local') {
|
||||
if (process.env.REDIS_TLS === 'true') {
|
||||
baseRedisConfig['tls'] = {
|
||||
servername: process.env.REDIS_HOST,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import CronParser from 'cron-parser';
|
||||
|
||||
export enum ScheduleLimits {
|
||||
MINUTE = 'minute',
|
||||
HOUR = 'hour',
|
||||
DAY = 'day',
|
||||
UNLIMITED = 'unlimited',
|
||||
}
|
||||
|
||||
const SECONDS_IN_MINUTE = 60;
|
||||
const SECONDS_IN_HOUR = 3600;
|
||||
const SECONDS_IN_DAY = 86400;
|
||||
|
||||
/**
|
||||
* Airflow preset schedules mapped to cron expressions.
|
||||
* @once is special - it means run only once (no recurring schedule).
|
||||
*/
|
||||
const AIRFLOW_PRESETS: Record<string, string | null> = {
|
||||
'@once': null, // No recurring schedule - always valid
|
||||
'@hourly': '0 * * * *', // Every hour
|
||||
'@daily': '0 0 * * *', // Every day at midnight
|
||||
'@weekly': '0 0 * * 0', // Every week on Sunday
|
||||
'@monthly': '0 0 1 * *', // First day of every month
|
||||
'@yearly': '0 0 1 1 *', // First day of every year
|
||||
'@annually': '0 0 1 1 *', // Same as @yearly
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert Airflow preset to cron expression.
|
||||
* Returns null for @once (no recurring schedule).
|
||||
* Returns original string if not an Airflow preset.
|
||||
*/
|
||||
export function convertAirflowPresetToCron(schedule: string): string | null {
|
||||
const preset = AIRFLOW_PRESETS[schedule.toLowerCase()];
|
||||
if (preset !== undefined) {
|
||||
return preset;
|
||||
}
|
||||
return schedule;
|
||||
}
|
||||
|
||||
export function getMinimumIntervalSeconds(scheduleLimit: string): number {
|
||||
switch (scheduleLimit) {
|
||||
case ScheduleLimits.MINUTE:
|
||||
return SECONDS_IN_MINUTE;
|
||||
case ScheduleLimits.HOUR:
|
||||
return SECONDS_IN_HOUR;
|
||||
case ScheduleLimits.DAY:
|
||||
return SECONDS_IN_DAY;
|
||||
case ScheduleLimits.UNLIMITED:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCronIntervalSeconds(cron: string): number {
|
||||
const interval = CronParser.parseExpression(cron);
|
||||
const nextDate = interval.next().toDate();
|
||||
const afterNextDate = interval.next().toDate();
|
||||
return Math.floor((afterNextDate.getTime() - nextDate.getTime()) / 1000);
|
||||
}
|
||||
|
||||
export function validateCronAgainstScheduleLimit(
|
||||
cron: string,
|
||||
scheduleLimit: string,
|
||||
): { valid: boolean; message?: string } {
|
||||
if (!cron) return { valid: true };
|
||||
|
||||
// Convert Airflow presets to cron expressions
|
||||
const cronExpression = convertAirflowPresetToCron(cron);
|
||||
|
||||
// @once returns null - no recurring schedule, always valid
|
||||
if (cronExpression === null) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const cronInterval = getCronIntervalSeconds(cronExpression);
|
||||
const minInterval = getMinimumIntervalSeconds(scheduleLimit);
|
||||
|
||||
if (cronInterval < minInterval) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Schedule interval (${cronInterval}s) is below customer limit (${scheduleLimit}: ${minInterval}s minimum)`,
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
return { valid: false, message: `Invalid cron expression: ${error.message}` };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user