Compare commits

..
Author SHA1 Message Date
Claude e0bfe03898 DOCS: Map maestro-pifactory integration points for migration
Add comprehensive documentation mapping all integration points between
maestro and pi-factory services to facilitate removing the dependency.

Includes:
- 3 gRPC client configurations using PIFACTORY_URL
- 36+ gRPC method calls across Catalog, PipelineV2, and legacy Pipeline services
- 44 REST endpoints that proxy to pi-factory
- Configuration files and environment variables
- Proto package dependencies (protospack and protospack-v2)
- Migration strategy options and recommendations
- Comparison with in-factory migration scope
2025-12-17 18:21:38 +00:00
Claude 177ec6368c DOCS: Map maestro-infactory integration points for migration
Add comprehensive documentation mapping all integration points between
maestro and in-factory services to facilitate removing the dependency.

Includes:
- All 6 gRPC client configurations using INFACTORY_URL
- 27+ gRPC method calls across 6 services
- 25 REST endpoints that proxy to in-factory
- Configuration files and environment variables
- Proto package dependencies
- Migration strategy options and recommendations
2025-12-17 17:09:50 +00:00
92 changed files with 4375 additions and 10470 deletions
-12
View File
@@ -1,12 +0,0 @@
node_modules
dist
.git
*.log
npm-debug.log*
.DS_Store
.env
.env.*
coverage
.nyc_output
*.tgz
!protospack.tgz
+3 -12
View File
@@ -66,21 +66,13 @@ jobs:
- name: Install Helmfile
run: |
curl -fsSLO https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
wget 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 --version v3.9.3
helm diff version
- name: Debug Helm env
run: |
helm env
echo "HOME=$HOME"
ls -R $HOME/.local/share/helm || true
- name: Install Helm Diff Plugin
run: helm plugin install https://github.com/databus23/helm-diff || true
- name: Authenticate with OKE cluster
env:
@@ -107,5 +99,4 @@ jobs:
- name: Run Helmfile Diff
env:
ENV: ${{ needs.extract_environment.outputs.environment }}
HELM_PLUGINS: /home/runner/.local/share/helm/plugins
run: helmfile -f deploy/helmfiles/${ENV}.yaml diff
+3 -4
View File
@@ -1,5 +1,4 @@
FROM node:20-alpine AS base_image
RUN npm install -g npm@10.8.2
FROM node:18.17-alpine AS base_image
FROM base_image AS build_base
WORKDIR /app
@@ -22,7 +21,7 @@ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
# run aws cli without mounting secret, because CI already has AWS credentials
FROM build_base AS ci_image
RUN aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
RUN npm ci --ignore-scripts
RUN npm ci
COPY . .
@@ -37,7 +36,7 @@ FROM build_base AS dev
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
# flag --build-from-source is required to force-build sqlite3
RUN npm ci --ignore-scripts
RUN npm ci
COPY . .
ENTRYPOINT npm run start:dev
-47
View File
@@ -1,47 +0,0 @@
FROM node:22-alpine AS base_image
RUN npm install -g npm@latest
FROM base_image AS build_base
WORKDIR /app
RUN apk update
RUN apk add --no-cache \
aws-cli \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont
COPY package*.json ./
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
# Local build with secrets
FROM build_base AS build
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
FROM base_image
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package*.json ./
RUN apk update
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENTRYPOINT ["npm", "run", "start:prod"]
+482
View File
@@ -0,0 +1,482 @@
# 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
+320
View File
@@ -0,0 +1,320 @@
# 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?
+486
View File
@@ -0,0 +1,486 @@
# 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
+1 -1
View File
@@ -2,8 +2,8 @@
<image src="./assets/maestro.svg" style="width:10rem">
</p>
# Maestro
# Maestro
Maestro é a API principal da Dadosfera. É responsável pela comunicação do Frontend com nossos microsserviços.
@@ -48,9 +48,6 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
env:
# Auth Provider Configuration (cognito or keycloak)
- name: AUTH_PROVIDER
value: {{ .Values.maestro.auth_provider | default "cognito" | quote }}
- name: AWS_IDENTITY_POOL_ID
value: {{ .Values.maestro.aws_identity_pool_id }}
- name: AWS_REGION
@@ -107,16 +104,6 @@ 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: CONNECTIONS_API_URL
value: {{ .Values.maestro.connections_api_url | default "" | quote }}
- name: STORAGE_EXPLORER_API_URL
value: {{ .Values.maestro.storage_explorer_api_url | quote }}
- name: FIREBASE_BASE_URL
value: {{ .Values.maestro.firebase_base_url }}
- name: JWT_PRIVATE_KEY
valueFrom:
secretKeyRef:
@@ -137,14 +124,3 @@ 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
-12
View File
@@ -38,15 +38,3 @@ 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
-4
View File
@@ -8,10 +8,6 @@ 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
connections_api_url: https://iy40eans64.execute-api.us-east-1.amazonaws.com
storage_explorer_api_url: "http://storage-explorer-{customer}.data-apps.svc.cluster.local:8000/api"
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/stg
hostname: maestro.stg.dadosfera.ai
-7
View File
@@ -27,9 +27,6 @@ resources:
cpu: 2000m
memory: 2Gi
maestro:
# Auth provider: "cognito" (default) or "keycloak"
# Note: maestro doesn't connect to Keycloak directly, only duc does
auth_provider: "cognito"
aws_identity_pool_id: "us-east-1_Mrezsw9Sn"
duc_url: duc.dadosfera.ai
in_factory_url: in-factory.dadosfera.ai
@@ -46,16 +43,12 @@ 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
storage_explorer_api_url: "https://storage-explorer-{customer}.dadosfera.ai/api"
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"
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/prd
autoscaling:
enabled: false
minReplicas: 1
+674 -2747
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -16,7 +16,6 @@ declare global {
OPEN_CUSTOMER_ID: string;
DEDICATED_PROXY: string;
COOKIE_SECRET: string;
REDIS_TLS?: string;
}
}
}
+1793 -2594
View File
File diff suppressed because it is too large Load Diff
+5 -19
View File
@@ -10,7 +10,7 @@
},
"scripts": {
"co:login": "aws codeartifact login --tool npm --namespace @dadosfera --repository dadosfera-npm --domain dadosfera --domain-owner 611330257153 --region us-east-1",
"proto-update": "npm i @dadosfera/protospack-v2@v3.40.0-beta.1 --save-exact",
"proto-update": "npm i @dadosfera/protospack-v2@latest --save-exact",
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@@ -27,14 +27,10 @@
"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/credential-provider-node": "^3.940.0",
"@aws-sdk/lib-dynamodb": "^3.414.0",
"@aws-sdk/signature-v4": "^3.370.0",
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
"@dadosfera/protospack-v2": "^3.40.0-beta.14",
"@dadosfera/protospack": "2.5.3",
"@dadosfera/protospack-v2": "3.38.0-beta.14",
"@grpc/grpc-js": "^1.9.3",
"@grpc/proto-loader": "^0.7.9",
"@nestjs/cli": "^9.5.0",
@@ -48,7 +44,7 @@
"@nestjs/schematics": "^9.2.0",
"@nestjs/swagger": "^6.3.0",
"@nestjs/testing": "^9.4.3",
"axios": "0.30.3",
"axios": "^0.27.2",
"cache-manager": "^5.1.4",
"cache-manager-ioredis-yet": "^1.1.0",
"class-transformer": "^0.5.1",
@@ -64,7 +60,6 @@
"jwk-to-pem": "^2.0.5",
"mixpanel": "^0.17.0",
"ms": "^3.0.0-canary.1",
"multer": "^2.0.2",
"openid-client": "^5.7.1",
"passport": "^0.6.0",
"passport-facebook": "^3.0.0",
@@ -80,13 +75,7 @@
"swagger-ui-express": "^4.6.3"
},
"overrides": {
"axios": "0.30.3",
"form-data": "^4.0.4",
"body-parser": "^1.20.3",
"cross-spawn": "^7.0.5",
"glob": "^10.5.0",
"path-to-regexp": "^3.3.0",
"semver": "^7.5.2"
"multer": "1.4.5-lts.1"
},
"devDependencies": {
"@types/cache-manager": "^4.0.6",
@@ -117,8 +106,5 @@
"ts-node": "^10.9.1",
"tsconfig-paths": "^3.14.2",
"typescript": "^4.9.5"
},
"resolutions": {
"axios": "0.30.3"
}
}
+2 -7
View File
@@ -17,6 +17,7 @@ import { ConnectionTestModule } from './modules/connection-test/connection-test.
import { NetworkConfigModule } from './modules/network-config/network-config.module';
import { InputsModule } from './modules/inputs/inputs.module';
import { OauthModule } from './modules/oauth/oauth.module';
import { PipelinesModule } from './modules/pipelines/pipelines.module';
import { TransformationsModule } from './modules/transformations/transformations.module';
import { HealthModule } from './modules/health/health.module';
import { CatalogModule } from './modules/catalog/catalog.module';
@@ -32,10 +33,6 @@ 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';
import { StorageExplorerModule } from './modules/storage-explorer/storage-explorer.module';
import { ReleaseNoteModule } from './modules/release_note/release_note.module';
@Module({
providers: [
@@ -59,6 +56,7 @@ import { ReleaseNoteModule } from './modules/release_note/release_note.module';
PermissionsModule,
TermsOfUseModule,
ConnectionTestModule,
PipelinesModule,
TransformationsModule,
UsersModule,
RolesModule,
@@ -75,11 +73,8 @@ import { ReleaseNoteModule } from './modules/release_note/release_note.module';
ApiKeyModule,
IdentityProviderModule,
NetworkPolicyModule,
PlatformApiModule,
StorageExplorerModule,
//Always leave HealthModule last, so it is on the bottom of swagger
HealthModule,
ReleaseNoteModule,
],
})
export class AppModule {}
@@ -153,7 +153,6 @@ export class AuthenticationGuard
user_id: accessTokenPayload.user_id,
username: accessTokenPayload.username,
permissions: accessTokenPayload.permissions,
roles: accessTokenPayload.roles,
customer_id: accessTokenPayload.customer_id,
customer_name: accessTokenPayload.customer_name,
customer_tier: accessTokenPayload.customer_tier,
-1
View File
@@ -11,7 +11,6 @@ export function extractUserFrom(aRawJwt: string) {
user_id: payload.user_id,
username: payload.username,
permissions: payload.permissions,
roles: payload.roles,
customer_id: payload.customer_id,
customer_name: payload.customer_name,
customer_tier: payload.customer_tier,
-39
View File
@@ -357,16 +357,6 @@ export const PERMISSIONS_GROUPS = {
'es-es': 'Crear y editar atributos en el catálogo',
},
},
CERTIFY: {
seqid: 53,
claim: 'catalog:certify',
usage: PermissionUsages.PUBLIC,
name: {
'pt-br': 'Alterar o status de certificação dos Ativos',
'en-us': "Change Assets' certification status",
'es-es': 'Cambiar el estado de certificación de los Activos',
},
},
DELETE: {
seqid: 1,
claim: 'catalog:delete',
@@ -678,35 +668,6 @@ export const PERMISSIONS_GROUPS = {
},
},
},
STORAGE_EXPLORER: {
title: {
'pt-br': 'Storage Explorer',
'en-us': 'Storage Explorer',
'es-es': 'Storage Explorer',
},
permissions: {
READ: {
seqid: 51,
claim: 'storage-explorer:read',
usage: PermissionUsages.PUBLIC,
name: {
'pt-br': 'Ler dados do Storage Explorer',
'en-us': 'Read Storage Explorer data',
'es-es': 'Leer datos del Storage Explorer',
},
},
WRITE: {
seqid: 52,
claim: 'storage-explorer:write',
usage: PermissionUsages.PUBLIC,
name: {
'pt-br': 'Escrever dados no Storage Explorer',
'en-us': 'Write Storage Explorer data',
'es-es': 'Escribir datos en Storage Explorer',
},
},
},
},
};
export interface DadosferaModule {
name: string;
-1
View File
@@ -12,7 +12,6 @@ export interface RequestUser {
customer_tier: string;
access_token: string;
customer_modules: string[];
roles: string[];
}
export const User: (options?: { required?: boolean }) => ParameterDecorator =
-65
View File
@@ -1,65 +0,0 @@
import {
BadRequestException,
CanActivate,
ExecutionContext,
Inject,
Injectable,
OnModuleInit,
} from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { map, Observable } from 'rxjs';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import {
ReadService,
ProtoServices,
} from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
import { PipelinesClientConfiguration } from 'src/modules/pipelinesV2/pipelines-client';
import { PlatformApiService } from 'src/modules/platform-api/platform-api.service';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
@Injectable()
export class PipelineExecutionGuard implements CanActivate {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private readonly platformApiService: PlatformApiService,
) {
this.logger = dadosferaLogger.logger;
}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
this.logger.info(
'PipelineExecutionGuard: Checking if pipeline can be executed...',
);
const request = context.switchToHttp().getRequest();
const pipelineId = request.params.pipelineId;
const user = request.user;
const idRegex = /[^0-9a-zA-Z_$]+/g;
const convertedId = pipelineId.replace(idRegex, '_');
const status = await this.platformApiService.proxy(
'GET',
`/pipeline/${convertedId}/pipeline_run`,
user,
);
const currentStatus = status[status.length - 1]
this.logger.info('Pipeline current status response:' + JSON.stringify(currentStatus));
if (currentStatus.last_status.toLowerCase() === 'running') {
this.logger.error('Pipeline is running, cannot update input now');
throw new BadRequestException('Pipeline is running, cannot update input now');
} else {
return true;
}
} catch (error) {
this.logger.error('Error in PipelineExecutionGuard: ' + error.message);
throw new BadRequestException('Error checking pipeline status: ' + error.message);
}
}
}
-1
View File
@@ -111,4 +111,3 @@ function configureSwagger(app: INestApplication) {
);
}
bootstrap();
+98 -56
View File
@@ -13,7 +13,6 @@ import {
Req,
Param,
Res,
UnauthorizedException,
} from '@nestjs/common';
import {
ApiHeaders,
@@ -55,7 +54,6 @@ import jwt, { JwtPayload } from 'jsonwebtoken';
import { LanguageEnum } from 'src/utils/languages.enum';
import { Language } from 'src/decorators/language.decorator';
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
type CookiesValues = {
accessToken?: string;
@@ -75,7 +73,6 @@ export class AuthController {
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private authClient: AuthClientService,
private apiKeyService: ApiKeyService,
) {
this.logger = dadosferaLogger.logger;
@@ -105,13 +102,14 @@ export class AuthController {
const data = await this.authClient.signIn({ username, password, totp }, metadata);
if (data.tokens) {
this.authClient.writeAuthSession(res, {
this.addTokenInCookie(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);
@@ -128,8 +126,27 @@ export class AuthController {
) {
try {
this.logger.info('/auth - SignOut');
this.authClient.cleanUpAuthSession(res);
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');
return res.send();
} catch (error) {
@@ -158,9 +175,8 @@ export class AuthController {
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
this.authClient.writeAuthSession(res, {
this.addTokenInCookie(res, {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
userId
});
@@ -192,14 +208,13 @@ export class AuthController {
) {
this.logger.info('/auth - change-password');
const { oldPassword, newPassword, totpCode } = body;
const { oldPassword, newPassword } = body;
const { authorization: accessToken } = headers;
return this.authClient.changePassword({
accessToken,
oldPassword,
newPassword,
totpCode,
});
}
@@ -217,8 +232,7 @@ export class AuthController {
const { username } = body;
await this.authClient.resetPassword({ username }, metadata);
return { authProvider: process.env.AUTH_PROVIDER || 'cognito' };
return this.authClient.resetPassword({ username }, metadata);
}
@ApiInternalOnlyEndpoint()
@@ -478,57 +492,85 @@ export class AuthController {
@Get('me')
async getMe(@Req() req: Request, @Res() res: Response) {
this.logger.info('GET /auth/me ')
this.logger.info(JSON.stringify(req.headers));
// Check for API key header first
const apiKey = req.get('X-Api-key');
if (apiKey) {
this.logger.info('Authenticating via X-Api-key header');
const { api_key } = await this.apiKeyService.get(apiKey);
const userDto = {
id: api_key.user_id,
name: api_key.username,
email: api_key.username,
customer: {
id: api_key.customer_id,
name: api_key.customer_name,
tier: api_key.customer_tier,
}
};
return res.status(200).json(userDto);
}
// Get token and headers
// Lê cookies
const accessToken = req.cookies['ddf-auth'];
const refreshToken = req.cookies['ddf-refresh-auth'];
const userId = req.cookies['ddf-user-id'];
const resourceHost = req.headers["x-original-url"] as string || "" ;
const hasUserSession = Boolean(accessToken) && Boolean(userId);
this.logger.info('Has User Session: ' + hasUserSession);
this.logger.info('Has cookie: ' + Boolean(accessToken))
try {
// Decodifica e valida o JWT de acesso
const userDto = await this.authClient.extractUserFrom(accessToken);
return res.status(200).json(userDto);
} catch (err) {
this.logger.error(err.message);
const refreshToken = req.cookies['ddf-refresh-auth'];
if (!hasUserSession) {
throw new UnauthorizedException()
this.logger.info('Token is invalid')
this.logger.info('Has Refresh Token: '+ Boolean(refreshToken))
// Se access token inválido, tenta refresh
if (!refreshToken || !userId) {
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' });
}
}
}
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
});
}
try {
const userDto = await this.authClient.validateUserSession(accessToken, resourceHost);
return res.status(200).json(userDto);
} catch (error) {
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 (!refreshToken) {
this.logger.error('Invalid refresh token or customer name');
throw new UnauthorizedException("Invalid refresh token or customer name");
};
const {
authSession,
user
} = await this.authClient.refreshUserSession(refreshToken, userId, resourceHost);
this.authClient.writeAuthSession(res, authSession);
return res.status(200).json(user);
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
});
}
}
}
+1 -2
View File
@@ -8,11 +8,10 @@ import { AuthClientService } from './auth.service';
import { DucClient } from '../duc/client.config';
import { GoogleLoginStrategy } from './passport-strategies/google-strategy';
import { getOauthSecrets } from 'src/utils/OauthSecrets';
import { ApiKeyModule } from '../api-key/api-key.module';
const client = new DucClient();
@Module({
imports: [ClientsModule.register([client.providerOptions]), ApiKeyModule],
imports: [ClientsModule.register([client.providerOptions])],
controllers: [AuthController],
providers: [
AuthClientService,
+48 -210
View File
@@ -1,20 +1,10 @@
import {
OnModuleInit,
Inject,
Injectable,
ForbiddenException,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { OnModuleInit, Inject, Injectable, ForbiddenException } 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,
UsersProtoService,
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import { AuthProtoService as AuthServiceInterface, IdentityProviderProtoService, UsersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import {
AuthSnowflakeSignInRequest,
AuthSignInRequest,
@@ -32,18 +22,12 @@ import {
import { DucClient } from '../duc/client.config';
import { Metadata } from '@grpc/grpc-js';
import { BulkEditResponse, UserDTO } from './dtos/login';
import jwt, { JwtPayload } from 'jsonwebtoken';
import jwt 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;
@@ -79,11 +63,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) {
@@ -91,9 +75,7 @@ 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();
@@ -112,6 +94,7 @@ 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);
@@ -122,7 +105,7 @@ export class AuthClientService implements OnModuleInit {
this.checkDedicatedProxy(result);
}
return result;
return result
}
async refreshAccessToken(
@@ -132,10 +115,7 @@ export class AuthClientService implements OnModuleInit {
this.logger.info('RefreshAccessToken');
return lastValueFrom(
this.authService.AuthRefreshAccessToken(
{ refreshToken, userId },
metadata,
),
this.authService.AuthRefreshAccessToken({ refreshToken, userId }, metadata),
);
}
@@ -143,7 +123,6 @@ export class AuthClientService implements OnModuleInit {
accessToken,
oldPassword,
newPassword,
totpCode,
}: AuthChangePasswordRequest) {
this.logger.info('ChangePassword');
@@ -152,7 +131,6 @@ export class AuthClientService implements OnModuleInit {
accessToken,
oldPassword,
newPassword,
totpCode,
}),
);
}
@@ -306,190 +284,50 @@ export class AuthClientService implements OnModuleInit {
}
}
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 refreshUserSession(
refreshToken: string,
userId: string,
originHeader: string,
): Promise<{
user: UserDTO;
authSession: AuthSession;
}> {
const metadata = PackTheMetadata({});
this.logger.info('Call Refresh Token');
const refreshCredentials = await this.refreshAccessToken(
{ refreshToken, userId },
metadata,
);
this.logger.info('Finish Refresh Token');
const userDto = await this.validateUserSession(
refreshCredentials.accessToken,
originHeader,
);
return {
user: userDto,
authSession: {
accessToken: refreshCredentials.accessToken,
refreshToken: refreshCredentials.refreshToken,
userId,
},
};
}
public writeAuthSession(res: Response, data: AuthSession) {
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
});
}
}
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> {
private async getUser(id: string, metadata: Metadata) {
this.logger.info('getUser');
const metadata = PackTheMetadata({
customer_id: payload.customer_id,
});
const { user } = await lastValueFrom(
this.userService.UserFindOneById({ id: payload.user_id }, metadata),
return await lastValueFrom(
this.userService.UserFindOneById({ id }, metadata),
);
const userDto: UserDTO = {
id: user.id,
name: user.name,
email: user.email,
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);
public async extractUserFrom(token: string) {
const decoded: any = token && jwt.decode(token, { complete: true });
if (!decoded) throw new Error('Invalid token');
const hostParts = host.split('.');
const domain = hostParts[0];
const isResouceStg = hostParts[1] === 'stg';
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);
const notFoundCustomerInDomain = !domain.includes('-')
const payload = decoded.payload;
const metadata = PackTheMetadata({
customer_id: payload.customer_id
})
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
const {
user
} = await this.getUser(
payload.user_id,
metadata
);
}
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
);
}
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;
}
return userDto;
}
}
-1
View File
@@ -144,7 +144,6 @@ export interface BulkEditResponse {
export type UserDTO = {
id: string,
name: string,
email: string,
jobTitle?: string,
department?: string,
hierarchy?: string,
+6 -94
View File
@@ -17,7 +17,6 @@ import {
HttpStatus,
Res,
} from '@nestjs/common';
import { ValidationPipe } from '../../pipes/object-validation.pipe';
import {
ApiCreatedResponse,
ApiHeaders,
@@ -47,7 +46,6 @@ import {
IMakeAComment,
IOneDataAsset,
IPreviewResponse,
IUpdateCertificationStatusRequest,
IUpdateDataRequest,
TriggerCatalogReq,
TriggerCatalogRes,
@@ -243,51 +241,6 @@ export class CatalogController {
return res;
}
@Get('schemas')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
async findSchemas(@User() user: RequestUser) {
const { username, user_id, customer_id, customer_name } = user;
this.logger.info(`/catalog - ON FIND SCHEMAS ROUTE`, {
username,
customer_name,
});
const metadata = PackTheMetadata({
username,
user_id,
customer_id,
customer_name,
});
try {
const res = await this.catalogService.findSchemas(metadata);
return res;
} catch (error) {
throw new HttpException(error.message, HttpStatus.NOT_FOUND);
}
}
@Get('custom-properties')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
async getCustomPropertyDefinitions(@User() user: RequestUser) {
const { customer_id, customer_name, user_id, username } = user;
const metadata = PackTheMetadata({
customer_id,
customer_name,
user_id,
username,
});
return this.catalogService.getCustomPropertyDefinitions(metadata);
}
@Get('data-asset/:id')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
@@ -470,7 +423,6 @@ 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;
@@ -487,7 +439,7 @@ export class CatalogController {
language,
});
const docs = await this.catalogService.getDataDocs(id, asset_type, metadata);
const docs = await this.catalogService.getDataDocs(id, metadata);
return { docs };
}
@@ -512,8 +464,6 @@ export class CatalogController {
language,
});
delete (body as any).certification_status;
const result = await this.catalogService.updateOneDataAsset({
body,
data_asset_id,
@@ -527,33 +477,6 @@ export class CatalogController {
return result;
}
@Put('data-asset/:id/certification-status')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.CERTIFY,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
async updateDataAssetCertificationStatus(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@Param('id') data_asset_id: string,
@Body(new ValidationPipe()) body: IUpdateCertificationStatusRequest,
): Promise<IUpdateCertificationStatusRequest> {
const { customer_id, customer_name, user_id, username } = user;
const metadata = PackTheMetadata({
customer_id,
customer_name,
user_id,
username,
language,
});
return this.catalogService.updateCertificationStatus({
body,
data_asset_id,
metadata,
});
}
@Post('data-asset/:id/docs')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
@@ -564,32 +487,21 @@ 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, customer_id, username } = user;
const metadata = PackTheMetadata({
customer_id,
customer_name,
user_id,
username,
});
const { user_id, customer_name } = user;
this.logger.info(`/catalog - ON POST DATA DOCS ROUTE`, {
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
user_id,
customer_name,
});
const body = {
const res = await this.catalogService.createDataDocs({
table_id,
docs,
asset_type,
info: {
customer: customer_name,
},
}
const res = await this.catalogService.createDataDocs(body, metadata);
});
return res;
}
@@ -1019,4 +931,4 @@ export class CatalogController {
this.logger.error(error.message);
}
}
}
}
+3
View File
@@ -5,17 +5,20 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { CatalogController } from './catalog.controller';
import { CatalogClientConfiguration } from './catalog-client';
import { ClientsModule } from '@nestjs/microservices';
import { PipelinesModule as OldPipelineModule } from 'src/modules/pipelines/pipelines.module';
import { UsersModule } from '../users/users.module';
import { RolesModule } from '../roles/roles.module';
import { CustomersModule } from '../customers/customers.module';
import { ShareModule } from './share/share.module';
import { CatalogService } from './catalog.service';
import { MixpanelModule } from '../mixpanel/mixpanel.module';
const client = new CatalogClientConfiguration();
@Module({
imports: [
ClientsModule.register([client.providerOptions]),
OldPipelineModule,
UsersModule,
RolesModule,
CustomersModule,
+52 -282
View File
@@ -24,12 +24,9 @@ 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,
IUpdateCertificationStatusRequest,
IUpdateDataRequest,
TriggerCatalogReq,
} from './dtos';
@@ -88,23 +85,25 @@ class CatalogService implements OnModuleInit {
}
async getPiiReporter(metadata: Metadata, type: TypeParser) {
this.logger.info('getPiiReporter: ' + type);
this.logger.info('getPiiReporter: ' + type)
try {
const { data } = await lastValueFrom(
this.catalogWriteService.GetPiiReporter({}, metadata),
);
this.logger.info('Finish grpc call');
const {
data
} = await lastValueFrom(
this.catalogWriteService.GetPiiReporter({}, metadata)
)
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');
this.logger.info('parser file to: ' + type)
const file = await parser.parse(data)
this.logger.info('finish parser')
const mimeTypes: Record<TypeParser, string> = {
csv: 'text/csv',
html: 'text/html',
pdf: 'application/pdf',
};
'csv': 'text/csv',
'html': 'text/html',
'pdf': 'application/pdf'
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `relatorio-pii-${timestamp}.${type}`;
@@ -112,16 +111,13 @@ class CatalogService implements OnModuleInit {
return {
file,
filename: filename,
type: mimeTypes[type],
};
type: mimeTypes[type]
}
} catch (error) {
this.logger.error(error.message);
throw error;
}
}
async getCustomPropertyDefinitions(metadata: Metadata) {
return lastValueFrom(this.catalogReadService.GetCustomPropertyDefinitions({}, metadata));
}
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
@@ -213,81 +209,10 @@ class CatalogService implements OnModuleInit {
metadata: Metadata,
customer_id: string,
) {
this.logger.info('CatalogService - searchDataAssets', { query });
this.logger.info('CatalogService - searchDataAssets');
const { search, page, size, sort_by, order, ...filters } = query;
this.logger.debug('Extracted filters:', { filters });
if (
filters.manually !== undefined &&
filters.manually !== null &&
filters.manually !== ''
) {
filters.manually = Number(filters.manually); // 1 ou 0
} else {
delete filters.manually;
}
if (filters.owner) {
const { users: customer_users } =
await this.userService.findAllUsersByCustomerId(customer_id);
this.logger.info('Available users in database count:', {
count: customer_users.length,
});
this.logger.info('First 5 users:', {
users: customer_users
.slice(0, 5)
.map((u) => ({ id: u.id, email: u.email, name: u.name })),
});
const ownerValues = Array.isArray(filters.owner)
? filters.owner
: typeof filters.owner === 'string' && filters.owner.includes(',')
? filters.owner.split(',').map((o: string) => o.trim())
: [filters.owner];
this.logger.info('Owner values to convert:', {
ownerValues,
ownerFiltersOriginal: filters.owner,
});
const ownerIds = ownerValues
.map((ownerValue: string) => {
const normalizedOwner = ownerValue.replace(/\s/g, '+');
const user = customer_users.find((u) => {
const isIdMatch = u.id === ownerValue;
const isEmailMatch =
u.email === ownerValue || u.email === normalizedOwner;
const isNameMatch =
u.name === ownerValue || u.name === normalizedOwner;
this.logger.info('Comparing:', {
userId: u.id,
userEmail: u.email,
userName: u.name,
filterValue: ownerValue,
normalizedFilter: normalizedOwner,
idMatch: isIdMatch,
emailMatch: isEmailMatch,
nameMatch: isNameMatch,
});
return isIdMatch || isEmailMatch || isNameMatch;
});
this.logger.info('Looking for owner result:', {
ownerValue,
found: !!user,
userId: user?.id,
});
return user?.id || ownerValue;
})
.filter((id: string) => id);
if (ownerIds.length > 0) {
filters.owner = ownerIds;
}
}
const { data_assets, total } = await lastValueFrom(
this.catalogReadService.GetAllDataAssets(
{
@@ -302,8 +227,6 @@ class CatalogService implements OnModuleInit {
),
);
console.log('MAESTRO RECEBEU RESPOSTA DO PI-FACTORY');
const result = JSON.parse(data_assets);
const response = await this.getAssetsUsersAndRoles(
@@ -321,13 +244,13 @@ class CatalogService implements OnModuleInit {
) {
const data = await this.searchDataAssets(query, metadata, customer_id);
const formatData = data.data_assets.map((asset) => ({
const formatData = data.data_assets.map(asset => ({
id: asset.id,
display_name: asset.display_name,
data_asset_type: asset.data_asset_type,
created_at: asset.created_at,
tags: '[' + asset.tags.join(', ') + ']',
}));
tags: '[' + asset.tags.join(', ') + ']'
}))
const parser = ParserBuilder.build<AssetReporter>('csv');
@@ -338,8 +261,8 @@ class CatalogService implements OnModuleInit {
return {
file,
filename,
};
filename
}
}
async getOneDataAsset(data: {
@@ -389,28 +312,6 @@ class CatalogService implements OnModuleInit {
return { data_asset: asset[0] };
}
async updateCertificationStatus(data: {
data_asset_id: string;
body: IUpdateCertificationStatusRequest;
metadata: Metadata;
}) {
const { body, data_asset_id, metadata } = data;
await lastValueFrom(
this.catalogWriteService.UpdateDataAsset(
{
id: data_asset_id,
changes: JSON.stringify({
certification_status: body.certification_status,
}),
},
metadata,
),
);
return { certification_status: body.certification_status };
}
async updateOneDataAsset(data: {
data_asset_id: string;
customer_id: string;
@@ -436,11 +337,11 @@ class CatalogService implements OnModuleInit {
return { data_asset: asset[0] };
}
async getDataDocs(id: string, assetType: string, metadata: Metadata) {
async getDataDocs(id: string, metadata: Metadata) {
const { documentation } = await lastValueFrom(
this.catalogReadService.GetDatasetDoc({ id }, metadata),
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
);
const docs = JSON.parse(documentation);
return docs;
}
@@ -467,16 +368,7 @@ class CatalogService implements OnModuleInit {
return result;
}
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');
async createDataDocs(body) {
const nimbusUrl = this._getNimbusUrl(body);
const { data } = await axios.post(
`${nimbusUrl}/api/catalog/data-docs/`,
@@ -485,29 +377,6 @@ 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');
@@ -525,23 +394,6 @@ class CatalogService implements OnModuleInit {
return response;
}
async findSchemas(metadata: Metadata) {
this.logger.info('CatalogService - findSchemas');
try {
const response = await lastValueFrom(
this.catalogReadService.GetSchemas({}, metadata),
);
return response;
} catch (error) {
this.logger.error('Error fetching schemas:', error);
throw error;
}
}
async getAssetsUsersAndRoles(data_assets: Array<any>, customer_id: string) {
const { users: customer_users } =
await this.userService.findAllUsersByCustomerId(customer_id);
@@ -552,20 +404,20 @@ class CatalogService implements OnModuleInit {
return data_assets.map((data_asset) => {
const owner = customer_users.find(
(u) => u.id === data_asset.owner,
)?.email;
)?.username;
const roles = [];
const users = [];
const data_asset_roles = data_asset?.roles || [];
const data_asset_roles = data_asset?.roles || []
for (const role_id of data_asset_roles) {
const role = customer_roles.find((r) => r.id === role_id);
if (role) roles.push({ id: role.id, name: role.name });
}
const data_asset_users = data_asset?.users || [];
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, email: user.email });
if (user) users.push({ id: user.id, username: user.username });
}
return {
...data_asset,
@@ -655,36 +507,27 @@ class CatalogService implements OnModuleInit {
async createTableMetadata(body: any): Promise<number> {
const nimbusUrl = this._getNimbusUrl(body);
this.logger.info(`Nimbus URL: ${nimbusUrl}`, { ...body.logMetadata });
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,
});
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,
});
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 },
{...body.logMetadata},
);
return data.id;
} catch (error) {
this.logger.error(
`Failed to create table metadata for table ${body.table_metadata.table_name} failed with status ${
error.response?.status
} because of ${JSON.stringify(error.response?.data) || error.message}`,
{ ...body.logMetadata },
);
} because of ${JSON.stringify(error.response?.data) || error.message}`, {...body.logMetadata});
throw new Error(error.response?.data?.message || error.message);
}
}
@@ -694,58 +537,43 @@ class CatalogService implements OnModuleInit {
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(`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 },
{...body.logMetadata},
);
return data.map((column) => column.id);
} catch (error) {
this.logger.error(
`Failed to create column metadata failed with status for table ${body.column_metadata.table_name} ${
error.response?.status
} because of ${error.response?.data || error.message}`,
{ ...body.logMetadata },
);
} because of ${error.response?.data || error.message}`, {...body.logMetadata});
throw new Error(error.response?.data?.message || error.message);
}
}
async createDataPreview(body: any): Promise<number> {
const nimbusUrl = this._getNimbusUrl(body);
this.logger.info(`Nimbus URL: ${nimbusUrl}`, { ...body.logMetadata });
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 },
);
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 },
{...body.logMetadata},
);
return data.id;
} catch (error) {
@@ -753,70 +581,12 @@ class CatalogService implements OnModuleInit {
`Failed to create data preview for table ${body.data_preview.table_name} failed with status ${
error.response?.status
} because of ${error.response?.data || error.message}`,
{ ...body.logMetadata },
{...body.logMetadata},
);
throw new Error(error.response?.data?.message || error.message);
}
}
async renameTableOnNimbus(
nimbusUrl: string,
nimbusId: number,
changes: { table_name?: string; table_schema?: string; display_name?: string },
): Promise<void> {
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/${nimbusId}`;
this.logger.info(`Renaming table-metadata ${nimbusId} on Nimbus`, { endpoint, changes });
await axios.patch(endpoint, changes);
}
async renameColumnMetadataOnNimbus(
nimbusUrl: string,
databaseName: string,
oldTableName: string,
oldTableSchema: string,
newTableName: string,
newTableSchema: string,
): Promise<void> {
const listEndpoint = `${nimbusUrl}/api/catalog/column-metadata/?database_name=${encodeURIComponent(databaseName)}&table_name=${encodeURIComponent(oldTableName)}&table_schema=${encodeURIComponent(oldTableSchema)}`;
this.logger.info(`Fetching column-metadata records to rename`, { listEndpoint });
const { data: columns } = await axios.get(listEndpoint);
const filtered = Array.isArray(columns) ? columns : [];
for (const column of filtered) {
const patchEndpoint = `${nimbusUrl}/api/catalog/column-metadata/${column.id}`;
await axios.patch(patchEndpoint, {
table_name: newTableName,
table_schema: newTableSchema,
});
}
this.logger.info(`Renamed ${filtered.length} column-metadata records on Nimbus`);
}
async renameDataPreviewOnNimbus(
nimbusUrl: string,
databaseName: string,
oldTableName: string,
oldTableSchema: string,
newTableName: string,
newTableSchema: string,
): Promise<void> {
const listEndpoint = `${nimbusUrl}/api/catalog/data-preview/?database_name=${encodeURIComponent(databaseName)}&table_name=${encodeURIComponent(oldTableName)}&table_schema=${encodeURIComponent(oldTableSchema)}`;
this.logger.info(`Fetching data-preview records to rename`, { listEndpoint });
const { data: previews } = await axios.get(listEndpoint);
const filtered = Array.isArray(previews) ? previews : [];
for (const preview of filtered) {
const patchEndpoint = `${nimbusUrl}/api/catalog/data-preview/${preview.id}`;
await axios.patch(patchEndpoint, {
table_name: newTableName,
table_schema: newTableSchema,
});
}
this.logger.info(`Renamed ${filtered.length} data-preview records on Nimbus`);
}
async catalogDatasetItem(table_metadata_id: number, metadata: Metadata) {
const customer_name_raw = metadata.get('customer_name');
@@ -842,4 +612,4 @@ class CatalogService implements OnModuleInit {
}
}
export { CatalogService };
export { CatalogService };
-73
View File
@@ -1,10 +1,4 @@
import { ApiProperty, ApiPropertyOptional, PickType } from '@nestjs/swagger';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
} from 'class-validator';
import { CreateDataAssetRequest } from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
export enum DataAssetShareType {
@@ -12,12 +6,6 @@ export enum DataAssetShareType {
public = 'public',
private = 'private',
}
export enum CertificationStatus {
draft = 'draft',
in_review = 'in_review',
approved = 'approved',
deprecated = 'deprecated',
}
export enum OrderEnum {
asc = 'asc',
desc = 'desc',
@@ -110,8 +98,6 @@ export class IDataAsset {
embed?: EmbedObject;
@ApiPropertyOptional({ enum: DataAssetShareType })
share_type?: DataAssetShareType;
@ApiPropertyOptional()
docs?: string;
}
export class IOneDataAsset {
@@ -161,24 +147,6 @@ 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 {
@@ -203,27 +171,6 @@ export class IData {
day_opening: number;
}
export enum CustomPropertyType {
TEXT = 'text',
NUMBER = 'number',
DATE = 'date',
BOOLEAN = 'boolean',
}
export class CustomPropertyDto {
@ApiProperty()
key: string;
@ApiProperty()
value: string;
@ApiProperty({ enum: CustomPropertyType })
type: CustomPropertyType;
@ApiPropertyOptional()
color?: string;
@ApiPropertyOptional()
emoji?: string;
}
export class IUpdateDataRequest {
@ApiProperty()
name: string;
@@ -235,18 +182,7 @@ export class IUpdateDataRequest {
embed: EmbedObject;
@ApiPropertyOptional({ enum: DataAssetShareType })
share_type?: DataAssetShareType;
@ApiPropertyOptional()
docs?: string;
@ApiPropertyOptional({ type: [CustomPropertyDto] })
custom_properties?: CustomPropertyDto[];
}
export class IUpdateCertificationStatusRequest {
@ApiProperty({ enum: CertificationStatus })
@IsEnum(CertificationStatus)
certification_status: CertificationStatus;
}
export class ICreateDataAsset implements CreateDataAssetRequest {
@ApiProperty()
display_name: string;
@@ -260,8 +196,6 @@ export class ICreateDataAsset implements CreateDataAssetRequest {
location: string;
@ApiPropertyOptional()
embed: EmbedObject;
@ApiPropertyOptional()
docs: string;
}
export class IPreview {
@@ -394,10 +328,3 @@ export type AssetReporter = {
created_at: string;
tags: string;
}
export type CreateDataDocsDTO = {
table_id: string;
docs: string;
asset_type: string;
}
+3 -4
View File
@@ -160,7 +160,7 @@ export class ShareService implements OnModuleInit {
});
const { documentation } = await lastValueFrom(
this.catalogReadService.GetDatasetDoc({ id }, metadata),
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, 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,
)?.email;
)?.username;
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, email: user.email });
if (user) users.push({ id: user.id, username: user.username });
}
return {
...data_asset,
@@ -262,7 +262,6 @@ export class ShareService implements OnModuleInit {
user_id: accessTokenPayload.user_id,
username: accessTokenPayload.username,
permissions: accessTokenPayload.permissions,
roles: accessTokenPayload.roles,
customer_id: accessTokenPayload.customer_id,
customer_name: accessTokenPayload.customer_name,
customer_tier: accessTokenPayload.customer_tier,
@@ -22,9 +22,6 @@ import {
ConnectionTestListTablesRes,
GetTableMetadataRes,
GetTableMetadataReq,
RefreshCatalogReq,
RefreshCatalogRes,
RefreshCatalogStatusReq,
} from './dto/connection-test';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { Authenticated } from 'src/decorators/authentication.decorator';
@@ -87,7 +84,7 @@ export class ConnectionTestController {
});
return this.connectionTestService.connectionTestListSchemas(
body,
user,
user.customer_name,
);
}
@@ -104,7 +101,7 @@ export class ConnectionTestController {
});
return this.connectionTestService.connectionTestListTables(
body,
user,
user.customer_name,
);
}
@@ -121,38 +118,7 @@ export class ConnectionTestController {
});
return this.connectionTestService.getTableMetadata(
body,
user,
user.customer_name,
);
}
@Post('refresh-catalog')
@ApiOkResponse({ type: RefreshCatalogRes })
@HttpCode(HttpStatus.ACCEPTED)
async refreshCatalog(
@User() user: RequestUser,
@Body(new ValidationPipe()) body: RefreshCatalogReq,
) {
this.logger.info('/connection-test/refresh-catalog', {
user: user.user_id,
customer: user.customer_name,
connection: body.connection_id,
});
return this.connectionTestService.refreshCatalog(body, user);
}
@Post('refresh-catalog/status')
@ApiOkResponse({ type: RefreshCatalogRes })
@HttpCode(HttpStatus.OK)
async refreshCatalogStatus(
@User() user: RequestUser,
@Body(new ValidationPipe()) body: RefreshCatalogStatusReq,
) {
this.logger.info('/connection-test/refresh-catalog/status', {
user: user.user_id,
customer: user.customer_name,
connection: body.connection_id,
session: body.session_id,
});
return this.connectionTestService.refreshCatalogStatus(body, user);
}
}
@@ -5,17 +5,10 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { ClientsModule } from '@nestjs/microservices';
import { ConnectionTestClientConfiguration } from './connection-test-client.config';
import { ConnectionModule } from '../connection/connection.module';
import { ConnectionsApiModule } from '../connections-api/connections-api.module';
import { PlatformApiModule } from '../platform-api/platform-api.module';
const client = new ConnectionTestClientConfiguration();
@Module({
controllers: [ConnectionTestController],
providers: [ConnectionTestService, DadosferaLogger],
imports: [
ClientsModule.register([client.providerOptions]),
ConnectionModule,
ConnectionsApiModule,
PlatformApiModule,
],
imports: [ClientsModule.register([client.providerOptions]), ConnectionModule],
})
export class ConnectionTestModule {}
@@ -1,205 +0,0 @@
import { ConnectionTestService } from './connection-test.service';
import { RequestUser } from 'src/decorators/user.decorator';
describe('ConnectionTestService catalog cache', () => {
const user: RequestUser = {
user_id: 'user-id',
username: 'user@example.com',
permissions: [],
customer_id: 'customer-id',
customer_name: 'customer-name',
customer_tier: 'standard',
access_token: 'token',
customer_modules: [],
roles: [],
};
const grpcClient = { getService: jest.fn().mockReturnValue({}) };
const connectionsService = {};
const connectionsApiService = { proxy: jest.fn() };
const platformApiService = { proxy: jest.fn() };
let service: ConnectionTestService;
beforeEach(() => {
jest.clearAllMocks();
service = new ConnectionTestService(
grpcClient as any,
connectionsService as any,
connectionsApiService as any,
platformApiService as any,
);
});
it('keeps the existing schemas response contract', async () => {
connectionsApiService.proxy.mockResolvedValue({
schemas: [{ schema_name: 'analytics' }, { schema_name: 'public' }],
});
await expect(
service.connectionTestListSchemas(
{ connection_id: 'config-id', plugin: 'postgresql' },
user,
),
).resolves.toEqual({
operation_result: true,
schema_list: ['analytics', 'public'],
});
});
it('keeps the existing tables response contract', async () => {
connectionsApiService.proxy.mockResolvedValue({
tables: [{ table_name: 'customers' }, { table_name: 'orders' }],
});
await expect(
service.connectionTestListTables(
{
connection_id: 'config-id',
plugin: 'postgresql',
schema: 'public',
},
user,
),
).resolves.toEqual({
operation_result: true,
table_list: ['customers', 'orders'],
});
});
it('maps cached columns to the existing table metadata contract', async () => {
connectionsApiService.proxy.mockResolvedValue({
columns: [
{
column_name: 'id',
data_type: 'bigint',
is_primary_key: true,
},
],
});
await expect(
service.getTableMetadata(
{
connection_id: 'config-id',
plugin: 'postgresql',
schema: 'public',
table_list: ['customers'],
},
user,
),
).resolves.toEqual({
operation_result: true,
tables_metadata: [
{
table_name: 'customers',
columns: [
{
name: 'id',
type: 'bigint',
is_primary_key: true,
},
],
references: [],
},
],
});
expect(connectionsApiService.proxy).toHaveBeenCalledWith(
'GET',
'/connection_catalog/config-id/schemas/public/tables/customers/columns',
user,
);
});
it('submits a catalog refresh without holding the request open', async () => {
platformApiService.proxy.mockResolvedValue({
session_id: 'session-id',
date: '20260731',
});
await expect(
service.refreshCatalog(
{ connection_id: 'config-id', plugin: 'postgresql' },
user,
),
).resolves.toEqual({
operation_result: true,
status: 'PENDING',
session_id: 'session-id',
date: '20260731',
});
expect(platformApiService.proxy).toHaveBeenCalledWith(
'POST',
'/connection_test',
user,
{
customer_id: user.customer_name,
plugin: 'postgresql',
task: {
task_type: 'refresh_catalog',
connection: {
provider: 'connection_manager',
config_id: 'config-id',
},
},
},
);
});
it('keeps polling without changing the catalog pointer while pending', async () => {
platformApiService.proxy.mockResolvedValue({ status: 'PENDING' });
await expect(
service.refreshCatalogStatus(
{
connection_id: 'config-id',
plugin: 'postgresql',
session_id: 'session-id',
date: '20260731',
},
user,
),
).resolves.toEqual({
operation_result: false,
status: 'PENDING',
session_id: 'session-id',
date: '20260731',
});
expect(connectionsApiService.proxy).not.toHaveBeenCalled();
});
it('publishes the catalog pointer after the refresh finishes', async () => {
platformApiService.proxy.mockResolvedValue({ status: 'DONE' });
connectionsApiService.proxy.mockResolvedValue({
last_catalog_refresh_status: 'SUCCESS',
});
await expect(
service.refreshCatalogStatus(
{
connection_id: 'config/id',
plugin: 'postgresql',
session_id: 'session-id',
date: '20260731',
},
user,
),
).resolves.toEqual({
operation_result: true,
status: 'DONE',
session_id: 'session-id',
date: '20260731',
});
expect(connectionsApiService.proxy).toHaveBeenCalledWith(
'PUT',
'/connection_config/config%2Fid/catalog_metadata',
user,
{
last_catalog_refresh_status: 'SUCCESS',
last_catalog_connection_test_date: '20260731',
last_catalog_connection_test_session_id: 'session-id',
},
);
});
});
@@ -1,4 +1,4 @@
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { ConnectionTest } from '@dadosfera/protospack-v2';
import { lastValueFrom } from 'rxjs';
@@ -13,9 +13,6 @@ import {
ConnectionTestPingRes,
GetTableMetadataReq,
GetTableMetadataRes,
RefreshCatalogReq,
RefreshCatalogRes,
RefreshCatalogStatusReq,
} from './dto/connection-test';
import { ConnectionClientService } from '../connection/client.service';
import {
@@ -24,8 +21,6 @@ import {
} from '../connection/dtos/connection';
import { RequestUser } from 'src/decorators/user.decorator';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { ConnectionsApiService } from '../connections-api/connections-api.service';
import { PlatformApiService } from '../platform-api/platform-api.service';
@Injectable()
export class ConnectionTestService {
@@ -33,8 +28,6 @@ export class ConnectionTestService {
constructor(
@Inject('ConnectionTestGrpcClient') private readonly grpcClient: ClientGrpc,
private connectionsService: ConnectionClientService,
private connectionsApiService: ConnectionsApiService,
private platformApiService: PlatformApiService,
) {
this.connectionTestReadClient =
grpcClient.getService<ConnectionTest.ReadService.ConnectionTestReadServices>(
@@ -154,137 +147,45 @@ export class ConnectionTestService {
}
async connectionTestListSchemas(
body: ConnectionTestListSchemasReq,
user: RequestUser,
customer_name: string,
): Promise<ConnectionTestListSchemasRes> {
const result = await this.connectionsApiService.proxy(
'GET',
`/connection_catalog/${encodeURIComponent(body.connection_id)}/schemas`,
user,
const { connection_id, plugin } = body;
return lastValueFrom(
this.connectionTestReadClient.ListSchemas({
connection_id,
customer_name,
plugin,
}),
);
return {
operation_result: true,
schema_list: result.schemas.map((schema) => schema.schema_name),
};
}
async connectionTestListTables(
body: ConnectionTestListTablesReq,
user: RequestUser,
customer_name: string,
): Promise<ConnectionTestListTablesRes> {
const result = await this.connectionsApiService.proxy(
'GET',
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
`/schemas/${encodeURIComponent(body.schema)}/tables`,
user,
const { connection_id, plugin, schema } = body;
return lastValueFrom(
this.connectionTestReadClient.ListTables({
connection_id,
customer_name,
plugin,
schema,
}),
);
return {
operation_result: true,
table_list: result.tables.map((table) => table.table_name),
};
}
async getTableMetadata(
body: GetTableMetadataReq,
user: RequestUser,
customer_name: string,
): Promise<GetTableMetadataRes> {
const tables_metadata = await Promise.all(
body.table_list.map(async (table_name) => {
const result = await this.connectionsApiService.proxy(
'GET',
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
`/schemas/${encodeURIComponent(body.schema)}` +
`/tables/${encodeURIComponent(table_name)}/columns`,
user,
);
return {
table_name,
columns: result.columns.map((column) => ({
name: column.column_name,
type: column.data_type,
is_primary_key: column.is_primary_key,
})),
references: [],
};
const { schema, plugin, table_list, connection_id } = body;
return lastValueFrom(
this.connectionTestReadClient.GetTableMetadata({
connection_id,
customer_name,
plugin,
schema,
table_list,
}),
);
return { operation_result: true, tables_metadata };
}
async refreshCatalog(
body: RefreshCatalogReq,
user: RequestUser,
): Promise<RefreshCatalogRes> {
const task = await this.platformApiService.proxy(
'POST',
'/connection_test',
user,
{
customer_id: user.customer_name,
plugin: body.plugin,
task: {
task_type: 'refresh_catalog',
connection: {
provider: 'connection_manager',
config_id: body.connection_id,
},
},
},
);
if (!task.session_id || !task.date) {
throw new HttpException(
'Platform API did not return a catalog refresh task identifier',
HttpStatus.BAD_GATEWAY,
);
}
return {
operation_result: true,
status: 'PENDING',
session_id: task.session_id,
date: task.date,
};
}
async refreshCatalogStatus(
body: RefreshCatalogStatusReq,
user: RequestUser,
): Promise<RefreshCatalogRes> {
const result = await this.platformApiService.proxy(
'POST',
'/connection_test/status',
user,
{
session_id: body.session_id,
date: body.date,
},
);
if (result.status === 'DONE') {
await this.connectionsApiService.proxy(
'PUT',
`/connection_config/${encodeURIComponent(
body.connection_id,
)}/catalog_metadata`,
user,
{
last_catalog_refresh_status: 'SUCCESS',
last_catalog_connection_test_date: body.date,
last_catalog_connection_test_session_id: body.session_id,
},
);
} else if (result.status === 'ERROR' || result.status === 'EXPIRED') {
throw new HttpException(
`Catalog refresh finished with status ${result.status}`,
HttpStatus.BAD_GATEWAY,
);
}
return {
operation_result: result.status === 'DONE',
status: result.status,
session_id: body.session_id,
date: body.date,
};
}
}
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
import { IsIn, IsString, IsOptional } from 'class-validator';
import { IsString, IsOptional } from 'class-validator';
import { DatabaseConnectionPropertiesDto } from 'src/modules/connection/dtos/connection';
import { CreateConnectionDto } from 'src/modules/connection/dtos/connection';
export class ColumnDto {
@@ -7,8 +7,6 @@ export class ColumnDto {
name: string;
@ApiProperty()
type: string;
@ApiProperty()
is_primary_key: boolean;
}
export class TableMetadataDto {
@ApiProperty()
@@ -133,37 +131,3 @@ export class GetTableMetadataRes {
@ApiProperty({ type: [TableMetadataDto] })
tables_metadata: TableMetadataDto[];
}
export class RefreshCatalogReq {
@ApiProperty()
@IsString()
connection_id: string;
@ApiProperty({ enum: ['oracle', 'mysql', 'postgresql', 'sqlserver'] })
@IsIn(['oracle', 'mysql', 'postgresql', 'sqlserver'])
plugin: string;
}
export class RefreshCatalogStatusReq extends RefreshCatalogReq {
@ApiProperty()
@IsString()
session_id: string;
@ApiProperty()
@IsString()
date: string;
}
export class RefreshCatalogRes {
@ApiProperty()
operation_result: boolean;
@ApiProperty()
status: string;
@ApiProperty()
session_id: string;
@ApiProperty()
date: string;
}
@@ -1,11 +0,0 @@
export const CONNECTIONS_API_CONFIG = {
getUrl: (): string => {
const url = process.env.CONNECTIONS_API_URL;
if (!url) {
throw new Error('CONNECTIONS_API_URL environment variable is not set');
}
return url;
},
region: process.env.AWS_REGION || 'us-east-1',
timeout: parseInt(process.env.CONNECTIONS_API_TIMEOUT || '30000', 10),
};
@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { ConnectionsApiService } from './connections-api.service';
@Module({
providers: [ConnectionsApiService, DadosferaLogger],
exports: [ConnectionsApiService],
})
export class ConnectionsApiModule {}
@@ -1,99 +0,0 @@
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 { CONNECTIONS_API_CONFIG } from './connections-api.config';
@Injectable()
export class ConnectionsApiService {
private signer: SignatureV4;
private logger: any;
constructor(@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger) {
this.logger = dadosferaLogger.logger;
this.signer = new SignatureV4({
service: 'execute-api',
region: CONNECTIONS_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 = CONNECTIONS_API_CONFIG.getUrl();
const url = new URL(`${baseUrl}${path}`);
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',
customer_name: user.customer_name || '',
customer_id: user.customer_id || '',
'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,
};
try {
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: CONNECTIONS_API_CONFIG.timeout,
validateStatus: () => true,
});
if (response.status >= 400) {
throw new HttpException(response.data, response.status);
}
return response.data;
} catch (error) {
this.logger.error('Connections API proxy error', {
error: error.message,
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('Connections API service unavailable', 503);
}
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
throw new HttpException('Connections API request timeout', 504);
}
throw new HttpException('Internal server error', 500);
}
}
}
@@ -136,32 +136,4 @@ export class CustomersController {
const result = await this.customersService.getAccessDashboardUrl(user.customer_name, metadata);
return result;
}
@Get(':id/organization-info')
@Authenticated()
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
@ApiOkResponse({ description: 'Organization information' })
async getOrganizationInfo(@Param('id') id: string) {
this.logger.info('getOrganizationInfo', { id });
return this.customersService.getOrganizationInfo(id);
}
@Put(':id/organization-info')
@Authenticated()
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ description: 'Organization information updated' })
async updateOrganizationInfo(
@Param('id') id: string,
@Body() body: {
companyName: string;
companySite: string;
domain: string;
cnpj: string;
description: string;
},
) {
return this.customersService.updateOrganizationInfo(id, body);
}
}
+11 -64
View File
@@ -9,12 +9,12 @@ import {
} from '@nestjs/common';
import { firstValueFrom, lastValueFrom } from 'rxjs';
import { Link } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities';
import { DucClient } from '../duc/client.config';
import { ClientGrpc } from '@nestjs/microservices';
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
import { CustomerSetLinksRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { CustomerUpdateRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { CustomersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import { CustomerLinksConfig } from './dtos/customers';
import ErrorCodes from 'src/utils/errorCodes';
import jwt from 'jsonwebtoken';
import {
@@ -67,12 +67,12 @@ export class CustomersService implements OnModuleInit {
)
}
async getLinks(customerId: string): Promise<CustomerLinksConfig | null> {
async getLinks(customerId: string) {
try {
const result = await lastValueFrom(
this.customerService.CustomerGetLinks({ customerId }),
this.customerService.CustomerFindOneById({ id: customerId }),
);
return (result.links as CustomerLinksConfig) || null;
return result.customer?.links || [];
} catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
@@ -80,17 +80,17 @@ export class CustomersService implements OnModuleInit {
}
}
async setLinks(customerId: string, links: CustomerLinksConfig) {
async setLinks(customerId: string, links: Link[]) {
if (!customerId || !links) {
throw new HttpException(null, HttpStatus.BAD_REQUEST);
}
try {
return await firstValueFrom(
this.customerService.CustomerSetLinks({
customerId,
links: links as CustomerSetLinksRequest['links'],
}),
this.customerService.CustomerUpdate({
id: customerId,
links,
} as CustomerUpdateRequest),
);
} catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
@@ -223,57 +223,4 @@ export class CustomersService implements OnModuleInit {
})
)
}
async updateOrganizationInfo(
customerId: string,
data: {
companyName: string;
companySite: string;
domain: string;
cnpj: string;
description: string;
},
) {
try {
const result = await lastValueFrom(
this.customerService.OrganizationUpdate({
customerId,
companyName: data.companyName || '',
companySite: data.companySite || '',
domain: data.domain || '',
cnpj: data.cnpj || '',
description: data.description || '',
}),
);
return result;
} catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
else throw err;
}
}
async getOrganizationInfo(customerId: string) {
try {
const customerResponse = await lastValueFrom(
this.customerService.CustomerFindOneById({ id: customerId })
);
const customer = customerResponse.customer;
return {
companyName: customer.companyName || '',
companySite: customer.companySite || '',
domain: customer.domain || '',
cnpj: customer.cnpj || '',
description: customer.description || ''
};
} catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND)
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
else throw err;
}
}
}
}
+9 -52
View File
@@ -1,6 +1,7 @@
import { Link } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CustomerLinkItem {
export class CustomerLink implements Link {
@ApiProperty()
href: string;
@ApiProperty()
@@ -8,59 +9,15 @@ export class CustomerLinkItem {
@ApiProperty()
description: string;
@ApiPropertyOptional()
iconSrc?: string;
iconSrc: string;
}
export class CustomerSidebarLinkItem {
@ApiProperty()
type: 'link';
@ApiProperty({ type: Object })
title: Record<string, string>;
@ApiProperty()
link: string;
@ApiPropertyOptional()
icon?: string;
}
export class CustomerSidebarMenuItem {
@ApiProperty()
type: 'menu';
@ApiProperty({ type: Object })
title: Record<string, string>;
@ApiPropertyOptional()
icon?: string;
@ApiProperty({ type: [CustomerSidebarLinkItem] })
items: CustomerSidebarLinkItem[];
}
export class CustomerSidebarSection {
@ApiProperty({ type: Object })
title: Record<string, string>;
@ApiProperty({
type: 'array',
items: {
oneOf: [
{ $ref: '#/components/schemas/CustomerSidebarMenuItem' },
{ $ref: '#/components/schemas/CustomerSidebarLinkItem' },
],
},
})
items: (CustomerSidebarMenuItem | CustomerSidebarLinkItem)[];
}
export class CustomerLinksConfig {
@ApiPropertyOptional({ type: [CustomerLinkItem] })
home?: CustomerLinkItem[];
@ApiPropertyOptional({ type: [CustomerSidebarSection] })
sidebar?: CustomerSidebarSection[];
}
export class CustomerLinkRequest {
@ApiProperty({ type: CustomerLinksConfig })
links: CustomerLinksConfig;
@ApiProperty({ type: [CustomerLink] })
links: CustomerLink[];
}
export class CustomerLinksResponse {
@ApiPropertyOptional({ type: CustomerLinksConfig })
links?: CustomerLinksConfig;
}
@ApiProperty({ type: [CustomerLink] })
links: CustomerLink[];
}
@@ -1,27 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class OrganizationUpdateRequest {
@ApiProperty()
name: string;
@ApiPropertyOptional()
companySite: string;
@ApiProperty()
domain: string;
@ApiPropertyOptional()
info: string;
@ApiPropertyOptional()
cnpj: string;
}
export class OrganizationResponse {
@ApiProperty()
name: string;
@ApiPropertyOptional()
companySite: string;
@ApiProperty()
domain: string;
@ApiPropertyOptional()
info: string;
@ApiPropertyOptional()
cnpj: string;
}
+1 -10
View File
@@ -11,19 +11,10 @@ export class TableColumns {
name: string;
@ApiProperty()
columns: string[];
@ApiPropertyOptional({ type: [Column] })
@ApiProperty()
references: Column[];
@ApiProperty()
destination: Record<'raw' | 'qualify', {
table_name: string;
table_schema: string;
}> | null;
@ApiProperty()
type: string;
@ApiPropertyOptional({ type: [String] })
identifier_columns?: string[];
@ApiPropertyOptional({ type: Column })
reference_column?: Column;
}
export class AvailableEntity {
@ApiProperty()
+1 -5
View File
@@ -1,8 +1,4 @@
export interface Info {
user_id: string;
customer_id: string;
customer: string;
}
import { Info } from '@dadosfera/protospack/dist/lib/interfaces';
interface Values {
jdbc_user: string;
-1
View File
@@ -99,7 +99,6 @@ export class InputsController {
customer: info.customer,
});
this.logger.info(JSON.stringify(body))
const response = await this.inputService.create({ body, info });
return response;
+11 -50
View File
@@ -17,13 +17,10 @@ import {
InputCreateGenericRequest,
InputCreateS3Request,
InputNewCreateRequest,
InputUpdateResponse,
RollbackInputRequest,
TestConnectionRequest,
} from '@dadosfera/protospack-v2/dist/lib/Input/interfaces/messages';
import { Info } from '@dadosfera/protospack-v2/dist/lib/Input/interfaces/entities';
import { CreateInputReq } from './dtos/input.model';
import { Metadata } from '@grpc/grpc-js';
@Injectable()
@@ -74,10 +71,10 @@ export class InputsService {
objectCamelToSnake(createInputResponse);
return createInputResponse;
},
update: async (updateInputDTO: UpdateInputRequest, metadata: Metadata): Promise<InputUpdateResponse> => {
this.logger.info('InputClientService - Update' + JSON.stringify(updateInputDTO));
update: async (updateInputDTO: UpdateInputRequest) => {
this.logger.info('InputClientService - Update');
const updateInputResponse = await lastValueFrom(
this.inputWriteService.InputUpdate(updateInputDTO, metadata),
this.inputWriteService.InputUpdate(updateInputDTO),
);
return updateInputResponse;
@@ -167,11 +164,6 @@ export class InputsService {
const inputCreateGenericRequest: InputCreateGenericRequest = {
input: {
...body,
tables: (body.tables || []).map((table) => ({
...table,
identifier_columns: table.identifier_columns || [],
reference_column: table.reference_column || table.references?.[0],
})),
},
info,
};
@@ -207,47 +199,24 @@ export class InputsService {
return findOneInputResponse;
}
async update(id: string, data, info: Info, metadata?: Metadata) {
// this.validateCron({ ...data, info });
async update(id: string, data, info: Info) {
this.validateCron({ ...data, info });
try {
const {
tablesUpdate,
dataAssetUpdate,
input
} = await this.OLD_inputClient.update({
const updateInputResponse: any = await this.OLD_inputClient.update({
id,
...data,
info,
}, metadata);
...data,
});
const updateInputResponse = this.adjustInputPayload(
input,
updateInputResponse.input = this.adjustInputPayload(
updateInputResponse?.input,
);
return {
input: updateInputResponse,
tablesUpdate,
dataAssetUpdate
};
return updateInputResponse;
} catch (err) {
throw new HttpException(err.message, HttpStatus.NOT_FOUND);
}
}
async rollbackUpdate(
data: RollbackInputRequest
) {
this.logger.info('PipelinesClientService - rollbackUpdate');
this.logger.info('Rolling back input update with data: ' + JSON.stringify(data));
const updatePipelineResponse = await lastValueFrom(
this.inputWriteService.RollbackInputUpdate(
data
),
);
this.logger.info('Done');
return updatePipelineResponse;
}
async remove(idRequest: IIdRequest) {
return lastValueFrom(this.inputWriteService.InputRemove(idRequest));
}
@@ -289,12 +258,4 @@ export class InputsService {
};
return formatedPayload;
}
async markTableDeleted(data: { input_id: string; table_name: string; info: Info }) {
return lastValueFrom(this.inputWriteService.MarkTableDeleted(data));
}
async unmarkTableDeleted(data: { input_id: string; table_name: string; info: Info }) {
return lastValueFrom((this.inputWriteService as any).UnmarkTableDeleted(data));
}
}
+78
View File
@@ -0,0 +1,78 @@
import { ConflictException, Inject, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import {
PipelineServicesNames,
PipelinesServiceInterface,
} from '@dadosfera/protospack';
import { lastValueFrom } from 'rxjs';
import { IIdRequest } from './interfaces';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { PipelinesClientConfiguration } from './pipelines-client';
export class PipelinesClientService implements OnModuleInit {
private pipelineService: PipelinesServiceInterface;
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
@Inject(PipelinesClientConfiguration.name)
private readonly grpcClient: ClientGrpc,
) {
this.logger = dadosferaLogger.logger;
}
onModuleInit() {
this.pipelineService =
this.grpcClient.getService<PipelinesServiceInterface>(
PipelineServicesNames.PipelineService,
);
}
async getPipelineStatus(data) {
this.logger.info('PipelinesClientService - GetPipelineStatus');
const statusPipelineResponse = await lastValueFrom(
this.pipelineService.getPipelineStatus(data),
)
.then((res) => {
const statusArray =
res.status?.sort((a, b) => {
if (a.id < b.id) {
return 1;
} else {
return -1;
}
}) || [];
return { status: statusArray };
})
.catch((err) => {
this.logger.error(err.message);
throw new Error(err);
});
this.logger.info('Done');
return statusPipelineResponse;
}
async runPipeline({ id, info }: IIdRequest) {
this.logger.info('PipelinesClientService - RunPipeline');
const statusPipelineResponse = await lastValueFrom(
this.pipelineService.triggerPipeline({ id, info }),
).catch((err) => {
this.logger.error(err.message);
throw new Error(err);
});
if (statusPipelineResponse.status == false) {
throw new ConflictException(
'This pipeline is not ready yet to execute, Try again later!',
);
}
this.logger.info('Done');
return statusPipelineResponse;
}
}
+36
View File
@@ -0,0 +1,36 @@
import { Info } from '@dadosfera/protospack/dist/lib/interfaces';
export interface ICreatePipelineDto {
input: IdRequest;
transformations: IdRequest[];
output: IdRequest;
tags: string[];
name: string;
description: string;
info: Info;
}
export interface IdRequest {
id: string;
}
export interface IIdRequest {
id: string;
info: Info;
}
export interface IUpdatePipelineRequest {
input: IdRequest;
transformations: IdRequest[];
output: IdRequest;
tags: string[];
name: string;
description: string;
id: string;
info: Info;
}
export interface IGetPipelineLogsRequest {
id: string;
details: string;
}
+33
View File
@@ -0,0 +1,33 @@
import {
ClientsProviderAsyncOptions,
GrpcOptions,
Transport,
} from '@nestjs/microservices';
import { PipelinePackages, PipelineProtoFilePath } from '@dadosfera/protospack';
import { credentials } from '@grpc/grpc-js';
const isLocalConnection =
process.env.PIFACTORY_URL.startsWith('pi-factory:') ||
process.env.PIFACTORY_URL.includes('0.0.0.0');
export class PipelinesClientConfiguration {
public name = 'PipelinesClientConfiguration';
private config: GrpcOptions = {
transport: Transport.GRPC,
options: {
url: process.env.PIFACTORY_URL,
package: PipelinePackages,
credentials: isLocalConnection ? undefined : credentials.createSsl(),
protoPath: PipelineProtoFilePath,
loader: {
keepCase: true,
enums: String,
defaults: false,
},
},
};
providerOptions: ClientsProviderAsyncOptions = {
name: this.name,
...this.config,
};
}
@@ -0,0 +1,72 @@
import { Body, Controller, Get, Inject, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
AuthenticateCondition,
Authenticated,
RequireSomePermission,
} from 'src/decorators/authentication.decorator';
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
import { PipelinesService } from './pipelines.service';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
@ApiInternalOnlyController()
@ApiTags('Pipelines')
@Controller('pipelines')
@Authenticated()
export class PipelinesController {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private pipelineService: PipelinesService,
) {
this.logger = dadosferaLogger.logger;
}
@Post('start/:id')
@RequireSomePermission(PERMISSIONS_GROUPS.PIPELINE.permissions.CREATE)
@ApiOperation({
deprecated: true,
description:
'This method is deprecated. Please use route /pipelinesV2/start/:id instead',
})
async activate(@Param('id') id: string, @Body() body) {
const { info } = body;
this.logger.info(
process.env.DEV_URL + `/pipeline/start/${id} - ON START PIPELINE ROUTE`,
{
user: body.info.user_id,
customer: body.info.customer,
},
);
const response = await this.pipelineService.runPipeline({ id, info });
return response;
}
@Get(':id/status')
@ApiOperation({
deprecated: true,
description:
'This method is deprecated. Please use route /pipelinesV2/:id/status instead',
})
@RequireSomePermission(PERMISSIONS_GROUPS.IMPORT_FILES.permissions.VIEW, PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
async getPipelineStatus(@Body() body, @Param('id') id: string) {
body.id = id;
this.logger.info(
process.env.DEV_URL + `/pipeline/${id} - ON GET PIPELINE STATUS ROUTE`,
{
user: body.info.user_id,
customer: body.info.customer,
},
);
const response = await this.pipelineService.getPipelineStatus(body);
return response;
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ClientsModule } from '@nestjs/microservices';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { PipelinesController } from './pipelines.controller';
import { PipelinesService } from './pipelines.service';
import { PipelinesClientConfiguration } from './pipelines-client';
import { PipelinesClientService } from './client.service';
const client = new PipelinesClientConfiguration();
@Module({
imports: [ClientsModule.register([client.providerOptions])],
controllers: [PipelinesController],
providers: [PipelinesService, PipelinesClientService, DadosferaLogger],
exports: [PipelinesService],
})
export class PipelinesModule {}
@@ -0,0 +1,33 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PipelinesClientService } from './client.service';
import { IIdRequest } from './interfaces';
import { objectCamelToSnake } from 'src/utils/CaseConverter';
@Injectable()
export class PipelinesService {
constructor(private pipelineClient: PipelinesClientService) {}
async getPipelineStatus(data: IIdRequest) {
try {
const pipelineStatusResponse =
await this.pipelineClient.getPipelineStatus(data);
return objectCamelToSnake(pipelineStatusResponse);
} catch (err) {
throw new HttpException(err.message, HttpStatus.NOT_FOUND);
}
}
async runPipeline({ id, info }: IIdRequest) {
try {
const triggerPipelineResponse = await this.pipelineClient.runPipeline({
id,
info,
});
return objectCamelToSnake(triggerPipelineResponse);
} catch (err) {
throw new HttpException(err.message, HttpStatus.NOT_FOUND);
}
}
}
+1 -42
View File
@@ -1,13 +1,5 @@
import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
export class PipelineInputsDTO {
@ApiProperty()
tables: Array<{
name: string,
type: string,
}>
}
import { Info } from '@dadosfera/protospack/dist/lib/interfaces';
export class IPipelineV2 {
@ApiProperty()
@@ -61,12 +53,6 @@ export interface IIdRequest {
info: Info;
}
export interface Info {
user_id: string;
customer_id: string;
customer: string;
}
export interface IUpdatePipelineRequest {
input: IdRequest;
transformations: IdRequest[];
@@ -137,30 +123,3 @@ export class PipelineFindAllReq {
@ApiPropertyOptional()
type?: string | undefined;
}
export interface UpdateTableDTO {
name: string;
type: string;
columns: string[];
destinations: {
raw: {
table_schema: string;
table_name: string;
};
qualify: {
table_schema: string;
table_name: string;
};
};
identifier_columns: string[];
reference_column: {
name: string;
type: string;
};
memory: number;
}
export interface UpdatePlatformInputRequest {
cron: string;
tables: Array<UpdateTableDTO>;
}
+26 -103
View File
@@ -14,7 +14,7 @@ import {
Patch,
HttpException,
BadRequestException,
UseGuards,
CacheTTL,
} from '@nestjs/common';
import {
ApiCreatedResponse,
@@ -24,6 +24,7 @@ import {
ApiTags,
} from '@nestjs/swagger';
import {
AuthenticateCondition,
RequireAllPermissions,
RequireSomePermission,
} from 'src/decorators/authentication.decorator';
@@ -34,6 +35,7 @@ import { Messages } from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
import { RequestUser, User } from 'src/decorators/user.decorator';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { PipelinesService as OldPipelineService } from 'src/modules/pipelines/pipelines.service';
import {
ICompleteUploadCSVFile,
ICreatePipelineCSVFile,
@@ -41,17 +43,11 @@ import {
IPipelineV2,
IInitUploadCSVFile,
PipelineFindAllReq,
UpdatePlatformInputRequest,
} from './interfaces';
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
import { LanguageEnum } from 'src/utils/languages.enum';
import { Language } from 'src/decorators/language.decorator';
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
import { Info } from '@dadosfera/protospack-v2/dist/lib/Input/interfaces/entities';
import { PipelineExecutionGuard } from 'src/guards/pipeline-execution.guard';
type PipelineTable = { name: string; job_id?: string; is_deleted?: boolean; [key: string]: any };
type PipelineTablesConfig = { input_id?: string; tables: PipelineTable[] };
@ApiTags('PipelinesV2')
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
@@ -63,6 +59,7 @@ export class PipelinesController {
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private pipelinesClientService: PipelinesService,
private oldPipelinesService: OldPipelineService,
) {
this.logger = dadosferaLogger.logger;
}
@@ -190,7 +187,6 @@ export class PipelinesController {
@Get(':id/status')
@RequireSomePermission(PERMISSIONS_GROUPS.IMPORT_FILES.permissions.VIEW, PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
async getPipelineStatus(@Body() body, @Param('id') id: string) {
body.id = id;
this.logger.info(`/pipeline/${id} - ON GET PIPELINE STATUS ROUTE`, {
@@ -198,7 +194,7 @@ export class PipelinesController {
customer: body.info.customer,
});
const response = await this.pipelinesClientService.getPipelineStatus(body);
const response = await this.oldPipelinesService.getPipelineStatus(body);
return response;
}
@@ -222,50 +218,26 @@ export class PipelinesController {
language,
});
const pipelineRes = await this.pipelinesClientService.findOne({ id }, metadata);
const parsed: PipelineTablesConfig = JSON.parse(pipelineRes.pipeline.config.tables);
const input_id = parsed.input_id;
const tables: PipelineTable[] = parsed.tables ?? [];
Object.assign(pipelineRes.pipeline, {
transformations: pipelineRes.pipeline.transformations
? JSON.parse(pipelineRes.pipeline.transformations)
: [],
config: {
cron: pipelineRes.pipeline.config.cron,
tables,
input_id,
},
properties: pipelineRes.pipeline.properties
? JSON.parse(pipelineRes.pipeline.properties)
: {},
});
return pipelineRes;
}
@Get("/:id/data-assets")
@RequireSomePermission(
PERMISSIONS_GROUPS.PIPELINE.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
PERMISSIONS_GROUPS.CATALOG.permissions.GET
)
async findAllDataAssetByPipeline(
@Language() language: LanguageEnum,
@Param('id') id: string,
@User() user: RequestUser,
@Query('object') object: string
) {
const payload = {
pipeline: id,
object: object,
};
this.logger.info(`GET pipelinesV2/:id/data-assets` + JSON.stringify(payload));
const result =
await this.pipelinesClientService.findAllDataAssetByPipeline(payload, user);
const result = await this.pipelinesClientService
.findOne({ id }, metadata)
.then((res) => {
//{pipeline:{tables: {tables: [], input_id: ''}}}
let tables = JSON.parse(res.pipeline.config.tables);
if (tables?.tables) tables = tables.tables;
Object.assign(res.pipeline, {
transformations: res.pipeline.transformations
? JSON.parse(res.pipeline.transformations)
: [],
config: {
cron: res.pipeline.config.cron,
tables,
},
properties: res.pipeline.properties
? JSON.parse(res.pipeline.properties)
: {},
});
return res;
});
return result;
}
@@ -305,40 +277,6 @@ export class PipelinesController {
return response;
}
@Patch('/:pipelineId/inputs/:id')
@RequireSomePermission(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
@UseGuards(PipelineExecutionGuard)
async updatePipelineInput(
@Language() language: LanguageEnum,
@Body() pipelineInputDTO: UpdatePlatformInputRequest,
@Param('id') inputId: string,
@Param('pipelineId') pipelineId: string,
@User() user: RequestUser,
) {
this.logger.info('PipelinesController - update', { user });
const info: Info = {
user_id: user.user_id,
customer: user.customer_name,
customer_id: user.customer_id,
pipeline_id: pipelineId
};
const metadata = PackTheMetadata(user);
const response = await this.pipelinesClientService.updatePipelineInput(
pipelineId,
inputId,
pipelineInputDTO,
info,
user,
metadata,
);
this.logger.info('PipelinesController - update: OK', { user });
return response;
}
@ApiInternalOnlyEndpoint()
@Put('/:id')
@ApiOperation({
@@ -358,21 +296,6 @@ export class PipelinesController {
return response;
}
@Patch('/:id/upgrade')
@RequireSomePermission(PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE)
@HttpCode(HttpStatus.NO_CONTENT)
async upgradeConnector(
@Language() language: LanguageEnum,
@Param('id') id: string,
@User() user: RequestUser
) {
this.logger.info('PipelinesController - upgrade connector');
const metadata = PackTheMetadata(user);
await this.pipelinesClientService.upgrade(id, metadata);
}
@Delete(':id')
@ApiNoContentResponse()
@HttpCode(HttpStatus.NO_CONTENT)
@@ -502,7 +425,7 @@ export class PipelinesController {
},
);
const response = await this.pipelinesClientService.runPipeline({ id, info });
const response = await this.oldPipelinesService.runPipeline({ id, info });
return response;
}
+3 -8
View File
@@ -7,28 +7,23 @@ import { PipelinesService } from './pipelines.service';
import { PipelinesClientConfiguration } from './pipelines-client';
import { PipelinesModule as OldPipelineModule } from 'src/modules/pipelines/pipelines.module';
import { ConnectorModule } from '../connector/connector.module';
import { InputsModule } from '../inputs/inputs.module';
import { TransformationsModule } from '../transformations/transformations.module';
import { PlatformApiModule } from '../platform-api/platform-api.module';
import { NimbusServicesModule } from 'src/services/nimbus/nimbus.module';
import { NimbusService } from 'src/services/nimbus/nimbus.service';
import { CatalogModule } from '../catalog/catalog.module';
const client = new PipelinesClientConfiguration();
@Module({
imports: [
ClientsModule.register([client.providerOptions]),
OldPipelineModule,
ConnectorModule,
InputsModule,
TransformationsModule,
PlatformApiModule,
NimbusServicesModule,
CatalogModule
],
controllers: [PipelinesController],
providers: [PipelinesService, DadosferaLogger, NimbusService],
providers: [PipelinesService, DadosferaLogger],
exports: [PipelinesService],
})
export class PipelinesV2Module {}
+1 -343
View File
@@ -1,7 +1,5 @@
/* eslint-disable no-async-promise-executor */
import {
BadRequestException,
ConflictException,
HttpException,
HttpStatus,
Inject,
@@ -18,7 +16,7 @@ import { lastValueFrom } from 'rxjs';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { PipelinesClientConfiguration } from './pipelines-client';
import { ICreatePipelineV2Req, IIdRequest, UpdatePlatformInputRequest, UpdateTableDTO } from './interfaces';
import { ICreatePipelineV2Req } from './interfaces';
import { PipelineV2CreateRequest } from '@dadosfera/protospack-v2/dist/lib/PipelineV2/interfaces/messages';
import { Metadata } from '@grpc/grpc-js';
import { ConnectorClientService } from '../connector/client.service';
@@ -28,17 +26,6 @@ import { TransformationsService } from '../transformations/transformations.servi
import { getObjValueFromPath, objHasPath } from 'src/utils/ObjValueFromPath';
import ErrorCodes from 'src/utils/errorCodes';
import ErrorBuilder from 'src/utils/ErrorBuilder';
import { PlatformApiService } from '../platform-api/platform-api.service';
import { Info } from '@dadosfera/protospack-v2/dist/lib/Input/interfaces/entities';
import { TableUpdate } from '@dadosfera/protospack-v2/dist/lib/Input/interfaces/messages';
import { AxiosError } from 'axios';
import { NimbusService } from 'src/services/nimbus/nimbus.service';
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { IDataAsset } from '../catalog/dtos';
import { CatalogService } from '../catalog/catalog.service';
type RollbackPromise = () => Promise<any>;
export class PipelinesService implements OnModuleInit {
logger: DadosferaLogger;
@@ -52,9 +39,6 @@ export class PipelinesService implements OnModuleInit {
private readonly connectorService: ConnectorClientService,
private readonly inputsService: InputsService,
private readonly transformationsService: TransformationsService,
private readonly platformAPI: PlatformApiService,
private readonly nimbusService: NimbusService,
private readonly catalogService: CatalogService
) {
this.logger = dadosferaLogger.logger;
}
@@ -154,7 +138,6 @@ export class PipelinesService implements OnModuleInit {
const findOnePipelineResponse = await lastValueFrom(
this.pipelineReadService.PipelineV2FindOne(data, metadata),
);
console.log('pipeline find one response', findOnePipelineResponse);
this.logger.info('Done');
return findOnePipelineResponse;
@@ -177,15 +160,6 @@ export class PipelinesService implements OnModuleInit {
return updatePipelineResponse;
}
async upgrade(id: string, metadata: Metadata) {
await lastValueFrom(
this.pipelineWriteService.Upgrade(
{ id },
metadata,
),
);
}
async remove(data: { id: string; metadata: Metadata; user: RequestUser }) {
const { id, metadata, user } = data;
const info = {
@@ -365,320 +339,4 @@ export class PipelinesService implements OnModuleInit {
return res;
}
async updatePipelineInput(pipelineId: string, inputId: string, updateInputDTO: UpdatePlatformInputRequest, info: Info, user: RequestUser, metadata: Metadata) {
this.logger.info('InputClientService - Update');
const {
input: oldInput
} = await this.inputsService.findOne({
id: inputId,
info: info
});
this.logger.info('Update Dynamo Reference :' + JSON.stringify(oldInput));
const pipelineIdFormat = pipelineId.split('-').join('_');
const rollback: RollbackPromise[] = [];
const updateInputResponse = await this.inputsService.update(
inputId,
updateInputDTO,
info,
metadata
);
const inputRollback = () => {
this.logger.info("exec rollback to input: " + JSON.stringify(oldInput));
return this.inputsService.rollbackUpdate(
{
id: inputId,
dataAssetUpdate: updateInputResponse.dataAssetUpdate,
tables: oldInput.tables,
info
}
) as Promise<any>;
}
rollback.push(inputRollback);
this.logger.info("Input Update Response: " + JSON.stringify(updateInputResponse))
const nimbusUpdates = updateInputResponse?.tablesUpdate || [];
if (user.customer_modules.includes('catalog')) {
nimbusUpdates.forEach(update => {
const nimbusRollback = () => {
return this.nimbusService.renameTable(
info.customer,
update.database,
{
table_name: update.table_name,
table_schema: update.table_schema
},
{
table_name: update.old_table_name,
table_schema: update.old_table_schema
}
);
}
rollback.push(nimbusRollback);
});
try {
await this.updateNimbus(info.customer, nimbusUpdates);
} catch (error) {
this.logger.error(error);
if (error instanceof AxiosError) {
this.logger.error(JSON.stringify(error.response.data));
}
await this.executeRenameRollback(rollback);
throw new Error("Error Nimbus updating tables");
}
}
try {
await this.updatePlatformJobs(
pipelineIdFormat,
updateInputResponse.input.type,
updateInputDTO,
user
);
} catch (error) {
this.logger.error(error);
await this.executeRenameRollback(rollback)
throw new Error("Error Platform API updating jobs");
}
return updateInputResponse;
}
private async executeRenameRollback(request: RollbackPromise[]) {
this.logger.info('rollback steps: ' + request.length)
const result = await Promise.allSettled(request.map(func => func()));
result.forEach(promise => {
this.logger.info("Promise finish with status: " + promise.status)
if (promise.status === "rejected") {
this.logger.error("reject with: " + JSON.stringify(promise.reason || {}))
}
if (promise.status === "fulfilled") {
this.logger.info("success with: " + JSON.stringify(promise.value || {}))
}
});
}
private async updateNimbus(customer: string, changes: TableUpdate[]) {
// throw new Error("teste error nimbus");
this.logger.info('Nimbus Changes: ' + JSON.stringify(changes));
if(!changes || changes.length === 0) return;
const requests = changes.map(change => {
return this.nimbusService.renameTable(customer, change.database, {
table_name: change.old_table_name,
table_schema: change.old_table_schema
}, {
table_name: change.table_name,
table_schema: change.table_schema
});
})
const values = await Promise.allSettled(requests);
const success = values.map(request => request.status === "fulfilled")
this.logger.info("Updates with succes: " + success.length);
values.forEach(promise => {
this.logger.info("Promise finish with status: " + promise.status)
if (promise.status === "rejected") {
this.logger.error("reject with: " + JSON.stringify(promise.reason || {}));
throw new Error(promise.reason );
}
if (promise.status === "fulfilled") {
this.logger.info("success with: " + JSON.stringify(promise.value || {}));
}
});
}
async updatePlatformJobs(pipelineId: string, pipelineType: string, updateInputDTO: UpdatePlatformInputRequest, user: RequestUser) {
const jobsUpdated = [];
for (const [index, table] of updateInputDTO.tables.entries()) {
const jobUpdate = {
job_id: `${pipelineId}_${index}`,
}
if (table.type !== "incremental_with_qualify") {
delete table.destinations?.qualify;
}
if (table.memory) {
jobUpdate["memory"] = {
amount: table.memory * 1000
}
}
this.logger.info('Updating input reference for table: ' + table.name);
let hasUpdateSyncMode = false;
const jobSyncMode = {}
if (table.columns) {
hasUpdateSyncMode = true;
jobSyncMode['column_include_list'] = table.columns;
}
if (table.reference_column) {
hasUpdateSyncMode = true;
jobSyncMode['incremental_column_name'] = table.reference_column.name;
jobSyncMode['incremental_column_type'] = table.reference_column.type;
}
if (table.identifier_columns) {
hasUpdateSyncMode = true;
jobSyncMode['primary_keys'] = table.identifier_columns;
}
if (table.type) {
hasUpdateSyncMode = true;
jobSyncMode['target_load_type'] = table.type;
}
if(hasUpdateSyncMode) {
jobUpdate["sync_mode"] = jobSyncMode;
}
if (Object.keys(table.destinations).length > 1) {
let hasChanges = false
const jobRenameTables = {
raw: {},
qualify: {}
}
if (Object.keys(table.destinations.raw).length > 1) {
hasChanges = true;
jobRenameTables.raw = table.destinations.raw;
}
if (Object.keys(table.destinations.qualify).length > 1) {
hasChanges = true;
jobRenameTables.qualify = table.destinations.qualify;
}
if (hasChanges) {
jobUpdate['rename_tables'] = jobRenameTables;
}
}
jobsUpdated.push(jobUpdate);
}
this.logger.info('Request body:' + JSON.stringify({
jobs_updated: jobsUpdated
}));
const response = await this.platformAPI.proxy(
'PUT',
`/pipeline/${pipelineId}/jobs`,
user,
{
job_updates: jobsUpdated
}
)
this.logger.info('Platform api response: ' + JSON.stringify(response));
}
async findAllDataAssetByPipeline(data: {
pipeline: string,
object?: string
}, user: RequestUser) {
const metadata = PackTheMetadata(user);
const isDataAdmin = user.permissions.includes(
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
);
let has_permission = false;
const {
data_assets: resultString
} = await lastValueFrom(
this.pipelineReadService.FindAllDataAssetByPipeline(data, metadata)
);
const result = JSON.parse(resultString) as any;
const data_assets: IDataAsset[] = []
result.forEach(data_asset => {
if (data_asset?.owner === user.username) has_permission = true;
for (const role of user.roles) {
if (data_asset.roles.includes(role)) has_permission = true;
}
if (data_asset.users.includes(user.user_id)) has_permission = true;
if (isDataAdmin || has_permission) {
delete data_asset.p_roles;
delete data_asset.p_users;
data_assets.push(data_asset as IDataAsset);
}
});
const assets = await this.catalogService.getAssetsUsersAndRoles(data_assets, user.customer_id);
return assets;
}
async getPipelineStatus(data) {
this.logger.info('PipelinesClientService - GetPipelineStatus');
const statusPipelineResponse = await lastValueFrom(
this.pipelineReadService.PipelineV2GetPipelineV2Status(data),
)
.then((res) => {
const statusArray =
res.status?.sort((a, b) => {
if (a.id < b.id) {
return 1;
} else {
return -1;
}
}) || [];
return { status: statusArray };
})
.catch((err) => {
this.logger.error(err.message);
throw new Error(err);
});
this.logger.info('Done');
return statusPipelineResponse;
}
async runPipeline({ id, info }: IIdRequest) {
this.logger.info('PipelinesClientService - RunPipeline');
const statusPipelineResponse = await lastValueFrom(
this.pipelineWriteService.PipelineV2TriggerPipelineV2({ id, info }),
).catch((err) => {
this.logger.error(err.message);
throw new Error(err);
});
if (statusPipelineResponse.status == false) {
throw new ConflictException(
'This pipeline is not ready yet to execute, Try again later!',
);
}
this.logger.info('Done');
return statusPipelineResponse;
}
}
@@ -1,11 +0,0 @@
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),
};
File diff suppressed because it is too large Load Diff
@@ -1,9 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
export class ValidationTableDTO {
@ApiProperty()
tables: Array<{
table_name: string;
table_schema: string;
}>
}
@@ -1,19 +0,0 @@
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';
import { CatalogModule } from '../catalog/catalog.module';
import { InputsModule } from '../inputs/inputs.module';
@Module({
imports: [ElasticsearchModule, DynamoDBModule, CustomersModule, CatalogModule, InputsModule],
controllers: [PlatformApiController],
providers: [PlatformApiService, DadosferaLogger],
exports: [PlatformApiService],
})
export class PlatformApiModule {}
@@ -1,131 +0,0 @@
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) {
this.logger.error('Platform API upstream error' + JSON.stringify({
status: response.status,
data: response.data,
path,
method: method.toUpperCase(),
}));
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(),
});
this.logger.error(error)
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);
}
}
}
@@ -1,14 +0,0 @@
export type ReleaseNoteDTO = {
id: string;
date: string;
tag: string;
title: string;
visible: boolean;
expiryDate: string;
content: string;
showEmojis: boolean;
image?: string;
link?: string;
linkText?: string;
};
@@ -1,20 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReleaseNoteController } from './release_note.controller';
import { ReleaseNoteService } from './release_note.service';
describe('ReleaseNoteController', () => {
let controller: ReleaseNoteController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ReleaseNoteController],
providers: [ReleaseNoteService],
}).compile();
controller = module.get<ReleaseNoteController>(ReleaseNoteController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -1,26 +0,0 @@
import { Controller, Get, Inject } from '@nestjs/common';
import { ReleaseNoteService } from './release_note.service';
import { Authenticated } from 'src/decorators/authentication.decorator';
import { Language } from 'src/decorators/language.decorator';
import { LanguageEnum } from 'src/utils/languages.enum';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
@Controller('release_note')
@Authenticated()
export class ReleaseNoteController {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private readonly releaseNoteService: ReleaseNoteService,
) {
this.logger = dadosferaLogger.logger;
}
@Get()
async getLatestReleaseNote(@Language() language: LanguageEnum) {
this.logger.info(`Fetching latest release note for language: ${language}`);
return await this.releaseNoteService.getLatestReleaseNote(language);
}
}
@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { ReleaseNoteService } from './release_note.service';
import { ReleaseNoteController } from './release_note.controller';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
@Module({
controllers: [ReleaseNoteController],
providers: [ReleaseNoteService, DadosferaLogger]
})
export class ReleaseNoteModule {}
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReleaseNoteService } from './release_note.service';
describe('ReleaseNoteService', () => {
let service: ReleaseNoteService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReleaseNoteService],
}).compile();
service = module.get<ReleaseNoteService>(ReleaseNoteService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -1,46 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';
import { LanguageEnum } from 'src/utils/languages.enum';
import { ReleaseNoteDTO } from './dto/release_note.dto';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
@Injectable()
export class ReleaseNoteService {
client: AxiosInstance;
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
this.client = axios.create({
baseURL: process.env.FIREBASE_BASE_URL,
});
}
async getLatestReleaseNote(lang: LanguageEnum) {
try {
const lng = lang.split('-');
const language = lng[0] + '-' + lng[1].toUpperCase();
const endpoint = `/release_note/${language}.json`;
const {
data,
status,
config
} = await this.client.get<ReleaseNoteDTO>(endpoint)
this.logger.info(`Fetched release note for language: ${lang} with status: ${status}`);
this.logger.info(`Request URL: ${config.baseURL}/${config.url}`);
return data;
} catch (error) {
this.logger.error(`Error fetching release note: ${error.message}`);
if (axios.isAxiosError(error)) {
this.logger.error(`Axios error details: ${error.toJSON()}`);
}
}
}
}
+16 -22
View File
@@ -229,33 +229,27 @@ export class RolesService {
const [roleTreated] = this.getRolesPermissionsName([role.role]);
return { role: roleTreated };
}
getRolesPermissionsName(roles: GetRolesPermissionsName[]): RoleDto[] {
const newRoles: RoleDto[] = [];
for (const role of roles) {
const newRole: RoleDto = this.formatRole(role);
const allPermissions = this.permissionsService.getAllPermissions(
this.language,
);
const newPermissions = role.permissions.map((p) => {
const permission = allPermissions.find((per) => per.seqid === p.seqid);
return {
...p,
name: permission.name,
id: p.seqid,
};
});
const newRole: RoleDto = {
...role,
permissions: newPermissions,
isPublic: role.isPublic,
};
newRoles.push(newRole);
}
return newRoles;
}
private formatRole(role: GetRolesPermissionsName): RoleDto {
const allPermissions = this.permissionsService.getAllPermissions(
this.language
);
const newPermissions = role.permissions.map((p) => {
const permission = allPermissions.find((per) => per.seqid === p.seqid);
return {
...p,
name: permission.name,
id: p.seqid,
};
});
return {
...role,
permissions: newPermissions,
isPublic: role.isPublic,
};;
}
}
@@ -1,13 +0,0 @@
export const STORAGE_EXPLORER_CONFIG = {
getUrl: (customerName: string): string => {
const urlTemplate = process.env.STORAGE_EXPLORER_API_URL;
if (!urlTemplate) {
throw new Error('STORAGE_EXPLORER_API_URL environment variable is not set');
}
// Replace {customer_id} placeholder with actual customer ID
// For local: http://172.17.0.1:8000/api (no placeholder)
// For prod: https://storage-explorer-{customer_id}.dadosfera.ai/api
return urlTemplate.replace('{customer}', customerName);
},
timeout: parseInt(process.env.STORAGE_EXPLORER_TIMEOUT || '30000', 10),
};
@@ -1,383 +0,0 @@
import {
Controller,
Get,
Post,
Put,
Param,
Body,
Query,
Inject,
UseInterceptors,
UploadedFiles,
Headers,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger';
import { FilesInterceptor } from '@nestjs/platform-express';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import FormData from 'form-data';
import {
Authenticated,
RequireAllPermissions,
} from '../../decorators/authentication.decorator';
import { User, RequestUser } from '../../decorators/user.decorator';
import { StorageExplorerService } from './storage-explorer.service';
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
@ApiTags('Storage Explorer')
@Controller('storage-explorer')
@Authenticated()
export class StorageExplorerController {
private logger: any;
constructor(
private readonly storageExplorerService: StorageExplorerService,
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
}
// ============================================
// TABLE OPERATIONS
// ============================================
@ApiOperation({ summary: 'Validate table name in PostgreSQL and Snowflake' })
@Post('tables/validate-name')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async validateTableName(
@Body() body: any,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'POST',
'/tables/validate-name',
user,
body,
);
}
@ApiOperation({ summary: 'Create a new table' })
@Post('tables')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async createTable(
@Body() body: any,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'POST',
'/tables/',
user,
body,
);
}
@ApiOperation({ summary: 'List all tables with pagination' })
@Get('tables')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async listTables(
@Query('page') page: number,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
'/tables/',
user,
undefined,
{ page },
);
}
@ApiOperation({ summary: 'Get table details by ID' })
@Get('tables/:tableId')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getTable(
@Param('tableId') tableId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/tables/${tableId}`,
user,
);
}
@ApiOperation({ summary: 'Link a dataset to a table' })
@Post('tables/:tableId/datasets/:datasetId')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async linkDatasetToTable(
@Param('tableId') tableId: string,
@Param('datasetId') datasetId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'POST',
`/tables/${tableId}/datasets/${datasetId}`,
user,
);
}
@ApiOperation({ summary: 'Get all datasets linked to a table' })
@Get('tables/:tableId/datasets')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getTableDatasets(
@Param('tableId') tableId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/tables/${tableId}/datasets`,
user,
);
}
@ApiOperation({ summary: 'Get table schema' })
@Get('tables/:tableId/schema')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getTableSchema(
@Param('tableId') tableId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/tables/${tableId}/schema`,
user,
);
}
@ApiOperation({ summary: 'Validate schema compatibility between table and dataset' })
@Post('tables/:tableId/validate-compatibility/:datasetId')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async validateSchemaCompatibility(
@Param('tableId') tableId: string,
@Param('datasetId') datasetId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'POST',
`/tables/${tableId}/validate-compatibility/${datasetId}`,
user,
);
}
// ============================================
// DATASET OPERATIONS
// ============================================
@ApiOperation({ summary: 'Get dataset preview data' })
@Get('datasets/:datasetId/preview')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getDatasetPreview(
@Param('datasetId') datasetId: string,
@Query('limit') limit: number,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/datasets/${datasetId}/preview`,
user,
undefined,
{ limit },
);
}
@ApiOperation({ summary: 'Get dataset schema information' })
@Get('datasets/:datasetId/schema')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getDatasetSchema(
@Param('datasetId') datasetId: string,
@Query('force_refresh') forceRefresh: boolean,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/datasets/${datasetId}/schema`,
user,
undefined,
{ force_refresh: forceRefresh },
);
}
@ApiOperation({ summary: 'List all datasets for a specific upload' })
@Get('datasets/upload/:uploadId')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async listDatasetsByUpload(
@Param('uploadId') uploadId: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
`/datasets/upload/${uploadId}`,
user,
);
}
@ApiOperation({ summary: 'Refresh dataset schema with new parsing options (Excel)' })
@Put('datasets/:datasetId/refresh-schema')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async refreshDatasetSchema(
@Param('datasetId') datasetId: string,
@Body() body: any,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'PUT',
`/datasets/${datasetId}/refresh-schema`,
user,
body,
);
}
// ============================================
// STORAGE OPERATIONS
// ============================================
@ApiOperation({ summary: 'List file explorer uploads with pagination' })
@Get('storage/uploads/history')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async listFileExplorerUploads(
@Query('page') page: number,
@Query('limit') limit: number,
@Query('folder_path') folderPath: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
'/storage/uploads/history',
user,
undefined,
{ page, limit, folder_path: folderPath },
);
}
@ApiOperation({ summary: 'Browse folders and files in storage' })
@Get('storage/browse')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async browseStorage(
@Query('path') path: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
'/storage/browse',
user,
undefined,
{ path },
);
}
@ApiOperation({ summary: 'Upload multiple files to storage' })
@Post('storage/upload/batch')
@ApiConsumes('multipart/form-data')
@UseInterceptors(FilesInterceptor('files'))
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async batchUpload(
@UploadedFiles() files: Array<Express.Multer.File>,
@Body('folder_path') folderPath: string,
@User() user: RequestUser,
) {
// Create FormData to forward files to storage-explorer API
const formData = new FormData();
// Add files
if (files && files.length > 0) {
files.forEach((file) => {
formData.append('files', file.buffer, {
filename: file.originalname,
contentType: file.mimetype,
});
});
}
// Add folder_path
if (folderPath) {
formData.append('folder_path', folderPath);
}
return this.storageExplorerService.proxyFormData(
'POST',
'/storage/upload/batch',
user,
formData,
);
}
@ApiOperation({ summary: 'Create a new folder in storage' })
@Post('storage/folder/create')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE)
async createFolder(
@Body() body: any,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'POST',
'/storage/folder/create',
user,
body,
);
}
@ApiOperation({ summary: 'Download a file from storage' })
@Get('storage/download')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async downloadFile(
@Query('file_path') filePath: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
'/storage/download',
user,
undefined,
{ file_path: filePath },
);
}
@ApiOperation({ summary: 'Get detailed file metadata' })
@Get('storage/metadata')
@RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ)
async getFileMetadata(
@Query('file_path') filePath: string,
@User() user: RequestUser,
) {
return this.storageExplorerService.proxy(
'GET',
'/storage/metadata',
user,
undefined,
{ file_path: filePath },
);
}
}
@@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { StorageExplorerController } from './storage-explorer.controller';
import { StorageExplorerService } from './storage-explorer.service';
@Module({
imports: [],
controllers: [StorageExplorerController],
providers: [StorageExplorerService, DadosferaLogger],
exports: [StorageExplorerService],
})
export class StorageExplorerModule {}
@@ -1,178 +0,0 @@
import { Injectable, Inject, HttpException } from '@nestjs/common';
import axios, { AxiosResponse, Method } from 'axios';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { RequestUser } from '../../decorators/user.decorator';
import { STORAGE_EXPLORER_CONFIG } from './storage-explorer.config';
@Injectable()
export class StorageExplorerService {
private logger: any;
constructor(
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
}
async proxy(
method: string,
path: string,
user: RequestUser,
body?: any,
query?: Record<string, any>
): Promise<any> {
// Validate customer_id is present for multi-tenant isolation
if (!user.customer_id) {
throw new HttpException('Customer ID is required for storage operations', 400);
}
// Get customer-specific storage-explorer URL
const baseUrl = STORAGE_EXPLORER_CONFIG.getUrl(user.customer_name);
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> = {
'content-type': 'application/json',
};
this.logger.info('Proxying request to storage-explorer', {
method: method.toUpperCase(),
path,
customer_id: user.customer_id,
storage_url: baseUrl,
user_id: user.user_id,
});
try {
const response: AxiosResponse = await axios({
method: method as Method,
url: url.href,
headers,
data: body,
timeout: STORAGE_EXPLORER_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('Storage Explorer API proxy error', {
error: error.message,
status: error.response?.status,
path,
storage_url: baseUrl,
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('Storage Explorer API service unavailable', 503);
}
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
throw new HttpException('Storage Explorer API request timeout', 504);
}
throw new HttpException('Internal server error', 500);
}
}
/**
* Proxy with file upload support (multipart/form-data)
*/
async proxyFormData(
method: string,
path: string,
user: RequestUser,
formData: any,
query?: Record<string, any>,
): Promise<any> {
// Validate customer_id is present for multi-tenant isolation
if (!user.customer_id) {
throw new HttpException('Customer ID is required for storage operations', 400);
}
// Get customer-specific storage-explorer URL
const baseUrl = STORAGE_EXPLORER_CONFIG.getUrl(user.customer_name);
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> = {
// Let axios set Content-Type for multipart/form-data with boundary
...formData.getHeaders?.(),
};
this.logger.info('Proxying form data request to storage-explorer', {
method: method.toUpperCase(),
path,
customer_id: user.customer_id,
storage_url: baseUrl,
user_id: user.user_id,
});
try {
const response: AxiosResponse = await axios({
method: method as Method,
url: url.href,
headers,
data: formData,
timeout: STORAGE_EXPLORER_CONFIG.timeout,
maxContentLength: Infinity,
maxBodyLength: Infinity,
validateStatus: () => true,
});
if (response.status >= 400) {
throw new HttpException(response.data, response.status);
}
return response.data;
} catch (error) {
this.logger.error('Storage Explorer API form data proxy error', {
error: error.message,
status: error.response?.status,
path,
storage_url: baseUrl,
method: method.toUpperCase(),
});
if (error instanceof HttpException) {
throw error;
}
if (error.response) {
throw new HttpException(error.response.data, error.response.status);
}
throw new HttpException('Internal server error', 500);
}
}
}
+2 -5
View File
@@ -1,8 +1,5 @@
export interface Info {
user_id: string;
customer_id: string;
customer: string;
}
import { Info } from '@dadosfera/protospack/dist/lib/interfaces';
export interface ICreateTransformationsRequest {
transformations: Transformation[];
info: Info;
-14
View File
@@ -38,12 +38,6 @@ export class User {
department?: string;
@ApiProperty()
hierarchy?: string;
@ApiProperty()
bio?: string;
@ApiProperty()
companyName?: string;
@ApiProperty()
personalSite?: string;
@ApiPropertyOptional()
customer?: Customer;
@ApiProperty()
@@ -64,8 +58,6 @@ export class UserNoRolesAndCustomer extends OmitType(UserNoRoles, [
export class IUserByCustomer extends OmitType(User, ['customer']) {
@ApiPropertyOptional()
permissions?: string[];
@ApiPropertyOptional()
authProvider?: string;
}
export class CreateUserReq {
@@ -117,12 +109,6 @@ export class UpdateUserReq {
@ApiPropertyOptional()
hierarchy?: string;
@ApiPropertyOptional()
bio?: string;
@ApiPropertyOptional()
personalSite?: string;
@ApiPropertyOptional()
companyName?: string;
@ApiPropertyOptional()
roleNames?: string[];
}
+1 -12
View File
@@ -266,6 +266,7 @@ export class UsersController {
}
@Patch(':id')
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
@ApiOkResponse({ type: UpdateUserRes })
async updateUser(
@User() user: RequestUser,
@@ -273,18 +274,6 @@ export class UsersController {
@Param('id') id: string,
@Language() language: LanguageEnum,
) {
const isSameUser = user.user_id === id;
const isSuperAdmin = user.permissions.includes(PERMISSIONS_GROUPS.USERS.permissions.ADMIN.seqid)
if (!isSameUser && !isSuperAdmin) {
throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN);
}
if (isSameUser && !isSuperAdmin && body.roleNames) {
// Prevent users from updating their own roles
delete body.roleNames;
}
this.logger.info('updateUser', { user });
this.userService.setLanguage(language);
return await this.userService.updateUser(body, id, user.customer_id);
+7 -11
View File
@@ -125,7 +125,6 @@ export class UsersService implements OnModuleInit {
return { permissions };
});
res.user.permissions = permissions;
res.user.authProvider = process.env.AUTH_PROVIDER || 'cognito';
return res;
}
@@ -149,23 +148,20 @@ export class UsersService implements OnModuleInit {
}
async updateUser(req: UpdateUserReq, id: string, customerId: string) {
const { roleNames, ...updateUserDTO } = req;
if (roleNames && roleNames.length > 0) {
const { department, hierarchy, jobTitle, name, roleNames, email } = req;
if (roleNames) {
await this.setRoles({ roleNames, userId: id }, customerId);
}
const { user } = await lastValueFrom(
this.usersClientService.UserUpdate({
department: updateUserDTO.department,
email: updateUserDTO.email,
hierarchy: updateUserDTO.hierarchy,
jobTitle: updateUserDTO.jobTitle,
name: updateUserDTO.name,
bio: updateUserDTO.bio,
companyName: updateUserDTO.companyName,
personalSite: updateUserDTO.personalSite,
name,
customerId,
id,
department,
hierarchy,
jobTitle,
email,
metabaseUserId: undefined,
}),
);
+1 -4
View File
@@ -14,10 +14,7 @@ export class ValidationPipe implements PipeTransform<any> {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object, {
forbidUnknownValues: false,
whitelist: true,
});
const errors = await validate(object);
if (errors.length > 0) {
const errorMessages = errors.map((err) => err.constraints);
throw new BadRequestException(errorMessages);
-4
View File
@@ -1,4 +0,0 @@
export const DYNAMODB_CONFIG = {
region: () => process.env.AWS_REGION || 'us-east-1',
inputsTable: () => process.env.INPUTS_DB || 'dadosfera-inputs-prd',
};
-9
View File
@@ -1,9 +0,0 @@
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 {}
-238
View File
@@ -1,238 +0,0 @@
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,
},
});
try {
const { Item } = await this.documentClient.send(getCommand);
return Item as InputDocument | null;
} catch (error) {
this.logger.error('DynamoDB: findInput failed', { inputId, clientId, error: error.message });
throw error;
}
}
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;
}
}
}
-3
View File
@@ -1,3 +0,0 @@
export * from './dynamodb.service';
export * from './dynamodb.module';
export * from './dynamodb.config';
@@ -1,5 +0,0 @@
export const ELASTICSEARCH_CONFIG = {
getUrl: () => process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
getApiKey: () => process.env.ELASTICSEARCH_API_KEY || '',
timeout: 10000, // 10 seconds
};
@@ -1,9 +0,0 @@
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 {}
@@ -1,457 +0,0 @@
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 getDataAssetIndex(customerName: string): string {
return `${customerName}_data_assets_catalog`;
}
async findDataAssetByTable(
customerName: string,
tableName: string,
tableSchema: string,
): Promise<{ id: string; nimbus_id: number | null; [key: string]: any } | null> {
const index = this.getDataAssetIndex(customerName);
this.logger.info('Elasticsearch: Searching data asset', {
index,
tableName,
tableSchema,
});
try {
const response = await this.client.post(`/${index}/_search`, {
query: {
bool: {
must: [
{ term: { 'table_name.keyword': tableName.toUpperCase() } },
{ term: { 'table_schema.keyword': tableSchema.toUpperCase() } },
],
},
},
size: 1,
});
const hits = response.data.hits?.hits || [];
if (hits.length === 0) {
this.logger.warn('Elasticsearch: Data asset not found', { tableName, tableSchema, index });
return null;
}
return { ...hits[0]._source, _es_id: hits[0]._id };
} catch (error) {
this.handleError('findDataAssetByTable', error, { tableName, tableSchema, index });
throw error;
}
}
async updateDataAsset(
customerName: string,
assetId: string,
updates: Record<string, any>,
): Promise<any> {
const index = this.getDataAssetIndex(customerName);
this.logger.info('Elasticsearch: Updating data asset', {
index,
assetId,
fields: Object.keys(updates),
});
try {
const response = await this.client.post(
`/${index}/_update/${assetId}`,
{ doc: updates },
{ params: { refresh: 'wait_for' } },
);
this.logger.info('Elasticsearch: Data asset updated', {
assetId,
result: response.data.result,
});
return response.data;
} catch (error) {
this.handleError('updateDataAsset', error, { assetId, 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,
});
}
}
}
-3
View File
@@ -1,3 +0,0 @@
export * from './elasticsearch.module';
export * from './elasticsearch.service';
export * from './elasticsearch.config';
-9
View File
@@ -1,9 +0,0 @@
import { Module } from "@nestjs/common";
import { NimbusService } from "./nimbus.service";
import DadosferaLogger from "@dadosfera/dadosfera-logs";
@Module({
providers: [NimbusService, DadosferaLogger],
exports: [NimbusService],
})
export class NimbusServicesModule {}
-56
View File
@@ -1,56 +0,0 @@
import DadosferaLogger from "@dadosfera/dadosfera-logs";
import { Inject, Injectable } from "@nestjs/common";
import axios from "axios";
type TableUpdate = {
table_schema: string;
table_name: string;
}
@Injectable()
export class NimbusService {
private logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
}
private buildUrl(customerName: string) {
if (process.env.ENV === 'prd') {
return `https://nimbus-${customerName}.dadosfera.ai`;
}
return `https://nimbus-${customerName}.${process.env.ENV.replace(
'local',
'stg',
)}.dadosfera.ai`;
}
async renameTable(customerName: string, database: string, old: TableUpdate, update: TableUpdate) {
const nimbusUrl = this.buildUrl(customerName);
const path = `/api/catalog/rename-tables/?database_name=${encodeURIComponent(database)}&table_name=${encodeURIComponent(old.table_name)}&table_schema=${encodeURIComponent(old.table_schema)}`;
try {
this.logger.info("Request for PATCH " + nimbusUrl + path);
this.logger.info("Payload: " + JSON.stringify(update));
const { data } = await axios.patch(nimbusUrl + path, {
table_name: update.table_name,
table_schema: update.table_schema
})
return data;
} catch (error) {
this.logger.error(error);
return {
message: error.message,
database,
old,
update
}
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ import { redisStore } from 'cache-manager-ioredis-yet';
keyPrefix: 'maestro:sso',
}
if (process.env.REDIS_TLS === 'true') {
if (process.env.ENV !== 'local') {
baseRedisConfig['tls'] = {
servername: process.env.REDIS_HOST,
}
-1
View File
@@ -9,7 +9,6 @@ interface IMetadata {
details?: string;
sensitive?: string;
roles?: string[];
customer_modules?: string[];
is_data_manager?: boolean;
access_token?: string;
host?: string;
-90
View File
@@ -1,90 +0,0 @@
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}` };
}
}