mirror of
https://github.com/dadosfera/maestro.git
synced 2026-08-31 19:58:21 +00:00
Merge pull request #515 from dadosfera/release/2026-08-27-beta
Release/2026 08 27 beta
This commit is contained in:
@@ -71,6 +71,11 @@ jobs:
|
|||||||
sudo mv helmfile /usr/local/bin/
|
sudo mv helmfile /usr/local/bin/
|
||||||
helmfile --version
|
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
|
- name: Debug Helm env
|
||||||
run: |
|
run: |
|
||||||
helm env
|
helm env
|
||||||
@@ -102,4 +107,5 @@ jobs:
|
|||||||
- name: Run Helmfile Diff
|
- name: Run Helmfile Diff
|
||||||
env:
|
env:
|
||||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||||
|
HELM_PLUGINS: /home/runner/.local/share/helm/plugins
|
||||||
run: helmfile -f deploy/helmfiles/${ENV}.yaml diff
|
run: helmfile -f deploy/helmfiles/${ENV}.yaml diff
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ spec:
|
|||||||
value: "{{ .Values.maestro.redis_tls }}"
|
value: "{{ .Values.maestro.redis_tls }}"
|
||||||
- name: PLATFORM_API_URL
|
- name: PLATFORM_API_URL
|
||||||
value: {{ .Values.maestro.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
|
- name: STORAGE_EXPLORER_API_URL
|
||||||
value: {{ .Values.maestro.storage_explorer_api_url | quote }}
|
value: {{ .Values.maestro.storage_explorer_api_url | quote }}
|
||||||
- name: FIREBASE_BASE_URL
|
- name: FIREBASE_BASE_URL
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ maestro:
|
|||||||
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
|
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
|
||||||
redis_database: "1"
|
redis_database: "1"
|
||||||
platform_api_url: https://xs2hkhq07k.execute-api.us-east-1.amazonaws.com
|
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"
|
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
|
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/stg
|
||||||
|
|
||||||
|
|||||||
+135
@@ -4714,6 +4714,62 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/platform/pipelines/{pipelineId}/pipeline_run/{runId}/jobs": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "PlatformApiController_getPipelineRunJobs",
|
||||||
|
"summary": "Get pipeline run jobs",
|
||||||
|
"description": "Proxies platform-api DB-backed job runs and returns `{ jobs: [...] }`.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "pipelineId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "runId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "DB-backed job runs for the selected pipeline run.",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"jobs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"jobs"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Platform API"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/platform/jobs/{jobId}/input": {
|
"/platform/jobs/{jobId}/input": {
|
||||||
"put": {
|
"put": {
|
||||||
"operationId": "PlatformApiController_updateJobInput",
|
"operationId": "PlatformApiController_updateJobInput",
|
||||||
@@ -5662,6 +5718,48 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/catalog/custom-properties": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "CatalogController_getCustomPropertyDefinitions",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "dadosfera-lang",
|
||||||
|
"in": "header",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"enum": [
|
||||||
|
"pt-br",
|
||||||
|
"en-us"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Catalog"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/catalog/data-asset/{id}": {
|
"/catalog/data-asset/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "CatalogController_getDataAsset",
|
"operationId": "CatalogController_getDataAsset",
|
||||||
@@ -11223,6 +11321,37 @@
|
|||||||
"docs"
|
"docs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"CustomPropertyDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"text",
|
||||||
|
"number",
|
||||||
|
"date",
|
||||||
|
"boolean"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"emoji": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"key",
|
||||||
|
"value",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
"IUpdateDataRequest": {
|
"IUpdateDataRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -11251,6 +11380,12 @@
|
|||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"custom_properties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CustomPropertyDto"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const config: Config.InitialOptions = {
|
|||||||
'<rootDir>/node_modules/',
|
'<rootDir>/node_modules/',
|
||||||
'.*\\.module\\.[jt]s$',
|
'.*\\.module\\.[jt]s$',
|
||||||
],
|
],
|
||||||
|
setupFiles: ['<rootDir>/jest.setup.ts'],
|
||||||
// moduleDirectories: ['node_modules'], // default is already 'node_modules'
|
// moduleDirectories: ['node_modules'], // default is already 'node_modules'
|
||||||
// rootDir: '.', //No need
|
// rootDir: '.', //No need
|
||||||
// testEnvironment: 'node', //Defaults to 'node'
|
// testEnvironment: 'node', //Defaults to 'node'
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
process.env.DUC_URL="duc:50051"
|
||||||
|
process.env.INFACTORY_URL="in-factory:50052"
|
||||||
|
process.env.PIFACTORY_URL="pi-factory:50053"
|
||||||
Generated
+4
-4
@@ -16,7 +16,7 @@
|
|||||||
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
||||||
"@aws-sdk/signature-v4": "^3.370.0",
|
"@aws-sdk/signature-v4": "^3.370.0",
|
||||||
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
||||||
"@dadosfera/protospack-v2": "^3.40.0-beta.10",
|
"@dadosfera/protospack-v2": "^3.40.0-beta.14",
|
||||||
"@grpc/grpc-js": "^1.9.3",
|
"@grpc/grpc-js": "^1.9.3",
|
||||||
"@grpc/proto-loader": "^0.7.9",
|
"@grpc/proto-loader": "^0.7.9",
|
||||||
"@nestjs/cli": "^9.5.0",
|
"@nestjs/cli": "^9.5.0",
|
||||||
@@ -1735,9 +1735,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@dadosfera/protospack-v2": {
|
"node_modules/@dadosfera/protospack-v2": {
|
||||||
"version": "3.40.0-beta.10",
|
"version": "3.40.0-beta.14",
|
||||||
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.40.0-beta.10.tgz",
|
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.40.0-beta.14.tgz",
|
||||||
"integrity": "sha512-F45dSEIKG+gwwDMYHayA242bFwhFTJbZm26KesQbGhf4I45ur6hYZD8dK0voNuVQInLMQhtm6+7th6/jJ8xpTQ==",
|
"integrity": "sha512-pv3pxq0x1XcBgf3ajD6QOFRLOduh8iEozKFA3AKlIW4gid+gT4iL0GcU2M+O7h0QFeO4JIzRZe/nEMN82nqk7A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.3",
|
"@grpc/grpc-js": "^1.9.3",
|
||||||
"rxjs": "^7.5.5"
|
"rxjs": "^7.5.5"
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
"@aws-sdk/lib-dynamodb": "^3.414.0",
|
||||||
"@aws-sdk/signature-v4": "^3.370.0",
|
"@aws-sdk/signature-v4": "^3.370.0",
|
||||||
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
||||||
"@dadosfera/protospack-v2": "^3.40.0-beta.10",
|
"@dadosfera/protospack-v2": "^3.40.0-beta.14",
|
||||||
"@grpc/grpc-js": "^1.9.3",
|
"@grpc/grpc-js": "^1.9.3",
|
||||||
"@grpc/proto-loader": "^0.7.9",
|
"@grpc/proto-loader": "^0.7.9",
|
||||||
"@nestjs/cli": "^9.5.0",
|
"@nestjs/cli": "^9.5.0",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
RequireAllPermissions,
|
RequireAllPermissions,
|
||||||
} from 'src/decorators/authentication.decorator';
|
} from 'src/decorators/authentication.decorator';
|
||||||
import { AuthClientService } from './auth.service';
|
import { AuthClientService } from './auth.service';
|
||||||
|
import { UserDTO } from './dtos/login';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter';
|
import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
@@ -486,7 +487,7 @@ export class AuthController {
|
|||||||
this.logger.info('Authenticating via X-Api-key header');
|
this.logger.info('Authenticating via X-Api-key header');
|
||||||
const { api_key } = await this.apiKeyService.get(apiKey);
|
const { api_key } = await this.apiKeyService.get(apiKey);
|
||||||
|
|
||||||
const userDto = {
|
const userDto: UserDTO = {
|
||||||
id: api_key.user_id,
|
id: api_key.user_id,
|
||||||
name: api_key.username,
|
name: api_key.username,
|
||||||
email: api_key.username,
|
email: api_key.username,
|
||||||
@@ -494,7 +495,8 @@ export class AuthController {
|
|||||||
id: api_key.customer_id,
|
id: api_key.customer_id,
|
||||||
name: api_key.customer_name,
|
name: api_key.customer_name,
|
||||||
tier: api_key.customer_tier,
|
tier: api_key.customer_tier,
|
||||||
}
|
},
|
||||||
|
permissions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.status(200).json(userDto);
|
return res.status(200).json(userDto);
|
||||||
|
|||||||
@@ -447,6 +447,9 @@ export class AuthClientService implements OnModuleInit {
|
|||||||
name: payload.customer_name,
|
name: payload.customer_name,
|
||||||
tier: payload.customer_tier,
|
tier: payload.customer_tier,
|
||||||
},
|
},
|
||||||
|
// Raw permission seqids from the JWT. Consumers own the seqid->meaning
|
||||||
|
// mapping (e.g. Orchest's auth-server); Maestro reports them as-is.
|
||||||
|
permissions: payload.permissions ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return userDto;
|
return userDto;
|
||||||
|
|||||||
@@ -152,5 +152,6 @@ export type UserDTO = {
|
|||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
tier: string,
|
tier: string,
|
||||||
}
|
},
|
||||||
|
permissions: number[],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,23 @@ export class CatalogController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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')
|
@Get('data-asset/:id')
|
||||||
@RequireSomePermission(
|
@RequireSomePermission(
|
||||||
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||||
@@ -1056,4 +1073,4 @@ export class CatalogController {
|
|||||||
this.logger.error(error.message);
|
this.logger.error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ class CatalogService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCustomPropertyDefinitions(metadata: Metadata) {
|
||||||
|
return lastValueFrom(this.catalogReadService.GetCustomPropertyDefinitions({}, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
||||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||||
if (!data.embed) data.embed = undefined;
|
if (!data.embed) data.embed = undefined;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional, PickType } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional, PickType } from '@nestjs/swagger';
|
||||||
import { IsEnum } from 'class-validator';
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
import { CreateDataAssetRequest } from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
import { CreateDataAssetRequest } from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||||
|
|
||||||
export enum DataAssetShareType {
|
export enum DataAssetShareType {
|
||||||
@@ -198,6 +203,27 @@ export class IData {
|
|||||||
day_opening: number;
|
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 {
|
export class IUpdateDataRequest {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
name: string;
|
name: string;
|
||||||
@@ -211,6 +237,8 @@ export class IUpdateDataRequest {
|
|||||||
share_type?: DataAssetShareType;
|
share_type?: DataAssetShareType;
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
docs?: string;
|
docs?: string;
|
||||||
|
@ApiPropertyOptional({ type: [CustomPropertyDto] })
|
||||||
|
custom_properties?: CustomPropertyDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IUpdateCertificationStatusRequest {
|
export class IUpdateCertificationStatusRequest {
|
||||||
@@ -372,4 +400,4 @@ export type CreateDataDocsDTO = {
|
|||||||
docs: string;
|
docs: string;
|
||||||
asset_type: string;
|
asset_type: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import {
|
|||||||
ConnectionTestListTablesRes,
|
ConnectionTestListTablesRes,
|
||||||
GetTableMetadataRes,
|
GetTableMetadataRes,
|
||||||
GetTableMetadataReq,
|
GetTableMetadataReq,
|
||||||
|
RefreshCatalogReq,
|
||||||
|
RefreshCatalogRes,
|
||||||
|
RefreshCatalogStatusReq,
|
||||||
} from './dto/connection-test';
|
} from './dto/connection-test';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { Authenticated, RequireModule } from 'src/decorators/authentication.decorator';
|
import { Authenticated, RequireModule } from 'src/decorators/authentication.decorator';
|
||||||
@@ -88,7 +91,7 @@ export class ConnectionTestController {
|
|||||||
});
|
});
|
||||||
return this.connectionTestService.connectionTestListSchemas(
|
return this.connectionTestService.connectionTestListSchemas(
|
||||||
body,
|
body,
|
||||||
user.customer_name,
|
user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ export class ConnectionTestController {
|
|||||||
});
|
});
|
||||||
return this.connectionTestService.connectionTestListTables(
|
return this.connectionTestService.connectionTestListTables(
|
||||||
body,
|
body,
|
||||||
user.customer_name,
|
user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +125,38 @@ export class ConnectionTestController {
|
|||||||
});
|
});
|
||||||
return this.connectionTestService.getTableMetadata(
|
return this.connectionTestService.getTableMetadata(
|
||||||
body,
|
body,
|
||||||
user.customer_name,
|
user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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,10 +5,17 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
|||||||
import { ClientsModule } from '@nestjs/microservices';
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
import { ConnectionTestClientConfiguration } from './connection-test-client.config';
|
import { ConnectionTestClientConfiguration } from './connection-test-client.config';
|
||||||
import { ConnectionModule } from '../connection/connection.module';
|
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();
|
const client = new ConnectionTestClientConfiguration();
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ConnectionTestController],
|
controllers: [ConnectionTestController],
|
||||||
providers: [ConnectionTestService, DadosferaLogger],
|
providers: [ConnectionTestService, DadosferaLogger],
|
||||||
imports: [ClientsModule.register([client.providerOptions]), ConnectionModule],
|
imports: [
|
||||||
|
ClientsModule.register([client.providerOptions]),
|
||||||
|
ConnectionModule,
|
||||||
|
ConnectionsApiModule,
|
||||||
|
PlatformApiModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ConnectionTestModule {}
|
export class ConnectionTestModule {}
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
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 { Inject, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||||
import { ClientGrpc } from '@nestjs/microservices';
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
import { ConnectionTest } from '@dadosfera/protospack-v2';
|
import { ConnectionTest } from '@dadosfera/protospack-v2';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
ConnectionTestPingRes,
|
ConnectionTestPingRes,
|
||||||
GetTableMetadataReq,
|
GetTableMetadataReq,
|
||||||
GetTableMetadataRes,
|
GetTableMetadataRes,
|
||||||
|
RefreshCatalogReq,
|
||||||
|
RefreshCatalogRes,
|
||||||
|
RefreshCatalogStatusReq,
|
||||||
} from './dto/connection-test';
|
} from './dto/connection-test';
|
||||||
import { ConnectionClientService } from '../connection/client.service';
|
import { ConnectionClientService } from '../connection/client.service';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +24,8 @@ import {
|
|||||||
} from '../connection/dtos/connection';
|
} from '../connection/dtos/connection';
|
||||||
import { RequestUser } from 'src/decorators/user.decorator';
|
import { RequestUser } from 'src/decorators/user.decorator';
|
||||||
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
import { ConnectionsApiService } from '../connections-api/connections-api.service';
|
||||||
|
import { PlatformApiService } from '../platform-api/platform-api.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConnectionTestService {
|
export class ConnectionTestService {
|
||||||
@@ -28,6 +33,8 @@ export class ConnectionTestService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject('ConnectionTestGrpcClient') private readonly grpcClient: ClientGrpc,
|
@Inject('ConnectionTestGrpcClient') private readonly grpcClient: ClientGrpc,
|
||||||
private connectionsService: ConnectionClientService,
|
private connectionsService: ConnectionClientService,
|
||||||
|
private connectionsApiService: ConnectionsApiService,
|
||||||
|
private platformApiService: PlatformApiService,
|
||||||
) {
|
) {
|
||||||
this.connectionTestReadClient =
|
this.connectionTestReadClient =
|
||||||
grpcClient.getService<ConnectionTest.ReadService.ConnectionTestReadServices>(
|
grpcClient.getService<ConnectionTest.ReadService.ConnectionTestReadServices>(
|
||||||
@@ -147,45 +154,137 @@ export class ConnectionTestService {
|
|||||||
}
|
}
|
||||||
async connectionTestListSchemas(
|
async connectionTestListSchemas(
|
||||||
body: ConnectionTestListSchemasReq,
|
body: ConnectionTestListSchemasReq,
|
||||||
customer_name: string,
|
user: RequestUser,
|
||||||
): Promise<ConnectionTestListSchemasRes> {
|
): Promise<ConnectionTestListSchemasRes> {
|
||||||
const { connection_id, plugin } = body;
|
const result = await this.connectionsApiService.proxy(
|
||||||
return lastValueFrom(
|
'GET',
|
||||||
this.connectionTestReadClient.ListSchemas({
|
`/connection_catalog/${encodeURIComponent(body.connection_id)}/schemas`,
|
||||||
connection_id,
|
user,
|
||||||
customer_name,
|
|
||||||
plugin,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
operation_result: true,
|
||||||
|
schema_list: result.schemas.map((schema) => schema.schema_name),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectionTestListTables(
|
async connectionTestListTables(
|
||||||
body: ConnectionTestListTablesReq,
|
body: ConnectionTestListTablesReq,
|
||||||
customer_name: string,
|
user: RequestUser,
|
||||||
): Promise<ConnectionTestListTablesRes> {
|
): Promise<ConnectionTestListTablesRes> {
|
||||||
const { connection_id, plugin, schema } = body;
|
const result = await this.connectionsApiService.proxy(
|
||||||
return lastValueFrom(
|
'GET',
|
||||||
this.connectionTestReadClient.ListTables({
|
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
|
||||||
connection_id,
|
`/schemas/${encodeURIComponent(body.schema)}/tables`,
|
||||||
customer_name,
|
user,
|
||||||
plugin,
|
|
||||||
schema,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
operation_result: true,
|
||||||
|
table_list: result.tables.map((table) => table.table_name),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTableMetadata(
|
async getTableMetadata(
|
||||||
body: GetTableMetadataReq,
|
body: GetTableMetadataReq,
|
||||||
customer_name: string,
|
user: RequestUser,
|
||||||
): Promise<GetTableMetadataRes> {
|
): Promise<GetTableMetadataRes> {
|
||||||
const { schema, plugin, table_list, connection_id } = body;
|
const tables_metadata = await Promise.all(
|
||||||
return lastValueFrom(
|
body.table_list.map(async (table_name) => {
|
||||||
this.connectionTestReadClient.GetTableMetadata({
|
const result = await this.connectionsApiService.proxy(
|
||||||
connection_id,
|
'GET',
|
||||||
customer_name,
|
`/connection_catalog/${encodeURIComponent(body.connection_id)}` +
|
||||||
plugin,
|
`/schemas/${encodeURIComponent(body.schema)}` +
|
||||||
schema,
|
`/tables/${encodeURIComponent(table_name)}/columns`,
|
||||||
table_list,
|
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: [],
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
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 { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
|
||||||
import { IsString, IsOptional } from 'class-validator';
|
import { IsIn, IsString, IsOptional } from 'class-validator';
|
||||||
import { DatabaseConnectionPropertiesDto } from 'src/modules/connection/dtos/connection';
|
import { DatabaseConnectionPropertiesDto } from 'src/modules/connection/dtos/connection';
|
||||||
import { CreateConnectionDto } from 'src/modules/connection/dtos/connection';
|
import { CreateConnectionDto } from 'src/modules/connection/dtos/connection';
|
||||||
export class ColumnDto {
|
export class ColumnDto {
|
||||||
@@ -7,6 +7,8 @@ export class ColumnDto {
|
|||||||
name: string;
|
name: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
type: string;
|
type: string;
|
||||||
|
@ApiProperty()
|
||||||
|
is_primary_key: boolean;
|
||||||
}
|
}
|
||||||
export class TableMetadataDto {
|
export class TableMetadataDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -131,3 +133,37 @@ export class GetTableMetadataRes {
|
|||||||
@ApiProperty({ type: [TableMetadataDto] })
|
@ApiProperty({ type: [TableMetadataDto] })
|
||||||
tables_metadata: 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ClientsModule } from '@nestjs/microservices';
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
// import { DucClient } from 'src/clients/duc/client.config';
|
// import { DucClient } from '../duc/client.config'
|
||||||
import { PermissionsController } from './permissions.controller';
|
import { PermissionsController } from './permissions.controller';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { PermissionsService } from './permissions.service';
|
import { PermissionsService } from './permissions.service';
|
||||||
@@ -22,7 +22,7 @@ describe.skip('PermissionsController', () => {
|
|||||||
provide: DadosferaLogger,
|
provide: DadosferaLogger,
|
||||||
useValue: { logger },
|
useValue: { logger },
|
||||||
},
|
},
|
||||||
PermissionsService,
|
PermissionsService
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiOkResponse } from '@nestjs/swagger';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -74,6 +74,10 @@ export class PlatformApiController {
|
|||||||
return id?.replace(/-/g, '_') || '';
|
return id?.replace(/-/g, '_') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private decodePathParam(value: string): string {
|
||||||
|
return value ? decodeURIComponent(value) : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Denormalize ID back to UUID format (replace _ with -).
|
* Denormalize ID back to UUID format (replace _ with -).
|
||||||
* Used when we receive a normalized ID but need the original UUID.
|
* Used when we receive a normalized ID but need the original UUID.
|
||||||
@@ -834,6 +838,40 @@ export class PlatformApiController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('pipelines/:pipelineId/pipeline_run/:runId/jobs')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get pipeline run jobs',
|
||||||
|
description: 'Proxies platform-api DB-backed job runs and returns `{ jobs: [...] }`.',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: 'DB-backed job runs for the selected pipeline run.',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
jobs: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['jobs'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.PIPELINE.permissions.GET)
|
||||||
|
async getPipelineRunJobs(
|
||||||
|
@Param('pipelineId') pipelineId: string,
|
||||||
|
@Param('runId') runId: string,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
const normalizedPipelineId = this.normalizePipelineId(pipelineId);
|
||||||
|
const decodedRunId = this.decodePathParam(runId);
|
||||||
|
|
||||||
|
return this.platformApiService.proxy(
|
||||||
|
'GET',
|
||||||
|
`/pipeline/${normalizedPipelineId}/pipeline_run/${decodedRunId}/jobs`,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== JOBS - COLUMN EDITING ROUTES ====================
|
// ==================== JOBS - COLUMN EDITING ROUTES ====================
|
||||||
|
|
||||||
@Put('jobs/:jobId/input')
|
@Put('jobs/:jobId/input')
|
||||||
|
|||||||
@@ -5,11 +5,21 @@ import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
|||||||
|
|
||||||
describe('ReleaseNoteController', () => {
|
describe('ReleaseNoteController', () => {
|
||||||
let controller: ReleaseNoteController;
|
let controller: ReleaseNoteController;
|
||||||
|
const logger = {
|
||||||
|
info: (...args) => args,
|
||||||
|
error: (...args) => args,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [ReleaseNoteController],
|
controllers: [ReleaseNoteController],
|
||||||
providers: [ReleaseNoteService, DadosferaLogger],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DadosferaLogger,
|
||||||
|
useValue: { logger },
|
||||||
|
},
|
||||||
|
ReleaseNoteService
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<ReleaseNoteController>(ReleaseNoteController);
|
controller = module.get<ReleaseNoteController>(ReleaseNoteController);
|
||||||
|
|||||||
@@ -4,10 +4,20 @@ import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
|||||||
|
|
||||||
describe('ReleaseNoteService', () => {
|
describe('ReleaseNoteService', () => {
|
||||||
let service: ReleaseNoteService;
|
let service: ReleaseNoteService;
|
||||||
|
const logger = {
|
||||||
|
info: (...args) => args,
|
||||||
|
error: (...args) => args,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [ReleaseNoteService, DadosferaLogger],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DadosferaLogger,
|
||||||
|
useValue: { logger },
|
||||||
|
},
|
||||||
|
ReleaseNoteService
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<ReleaseNoteService>(ReleaseNoteService);
|
service = module.get<ReleaseNoteService>(ReleaseNoteService);
|
||||||
|
|||||||
Reference in New Issue
Block a user