Compare commits

...
Author SHA1 Message Date
marcos-silva-rodrigues f396cdb2d3 FIX: correct env 2025-04-02 14:01:54 -03:00
Guilherme Maioli 1f92510a43 Merge pull request #270 from dadosfera/bugfixes/13-03
Bugfixes/13 03
2025-03-13 17:59:32 -03:00
marcos-silva-rodrigues f7fceb1ea2 Merge branch 'fix/white-label' into bugfixes/13-03 2025-03-13 17:41:12 -03:00
marcos-silva-rodrigues 5c98cc1748 FIX: cors list 2025-02-26 17:13:57 -03:00
marcos-silva-rodrigues 5d014f4669 FIX: correct ip 2025-02-26 15:57:07 -03:00
marcos-silva-rodrigues 38fc9f521b FIX: add ip range and domain list to route /open-data 2025-02-25 17:10:39 -03:00
marcos-silva-rodrigues 9bacd7a0f5 UPDATE: merge with beta branch 2025-02-24 09:28:03 -03:00
Aldemir Humberto Soares Neto c89ac40039 Merge pull request #266 from dadosfera/fixing-build-ecr
FEAT: fixing package-lock
2025-02-22 07:42:46 -03:00
aldemirneto 6d6cf590ee FEAT: fixing package-lock 2025-02-22 07:39:41 -03:00
Aldemir Humberto Soares Neto f063a8f30e Merge pull request #265 from dadosfera/UNIVR-PII-FACILITIES
Univr pii facilities
2025-02-22 07:29:14 -03:00
aldemirneto 03536a2180 FEAT: ensuring compatibility with ^ in the version 2025-02-22 07:28:30 -03:00
aldemirneto 0efd5cc257 FEAT: Changing protospack version
PII enabling
2025-02-22 07:23:34 -03:00
marcos-silva-rodrigues 9439f13b7b UPdATE: new displayName prop 2025-02-21 17:31:49 -03:00
Rafael Santana d936806406 Merge pull request #262 from dadosfera/beta
[PRD] Enable Dataset Registration via API for External Sources
2025-02-20 14:03:56 -03:00
Rafael Santana 9d7ccd4201 Merge pull request #263 from dadosfera/feat/catalog-external-datasets-using-api-call
UPDATE: migrating maestro to backend nodepool
2025-02-20 13:47:44 -03:00
Rafael c8a4c1aae6 UPDATE: migrating maestro to backend nodepool 2025-02-20 13:46:30 -03:00
Rafael Santana 406db7ccff Merge pull request #261 from dadosfera/feat/catalog-external-datasets-using-api-call
UPDATE: improve logging for table metadata
2025-02-20 08:52:15 -03:00
Rafael f48a35c7ce UPDATE: improve logging for table metadata 2025-02-20 08:40:48 -03:00
Rafael Santana 8d25df74b7 Merge pull request #260 from dadosfera/feat/catalog-external-datasets-using-api-call
UPDATE: updating protospack for add table_schema for /catalog/registe…
2025-02-19 12:25:10 -03:00
Rafael 831a03402e UPDATE: updating protospack for add table_schema for /catalog/register-dataset rout 2025-02-19 12:19:59 -03:00
Marcos Rodrigues Silva a631338599 Merge pull request #259 from dadosfera/feat/update-customer-dash
UPDATE: customer dash
2025-02-17 19:02:52 -03:00
Marcos Rodrigues Silva d943721b4a Merge pull request #258 from dadosfera/feat/update-customer-dash
Feat/update customer dash
2025-02-17 18:47:11 -03:00
Rafael Santana 1bbf978cc4 Merge pull request #257 from dadosfera/feat/catalog-external-datasets-using-api-call
FEAT: Enable dataset registration via API for external sources
2025-02-14 16:57:58 -03:00
Rafael 856f913ce6 Merge branch 'feat/catalog-external-datasets-using-api-call' of github.com:dadosfera/maestro into feat/catalog-external-datasets-using-api-call 2025-02-14 15:07:13 -03:00
Rafael dc617b6925 REFACTOR: Improve logging with additional metadata
- Enhanced logs to include more detailed request body information.
- Standardized log calls to include extra metadata, such as service name, environment, customer name, user ID, HTTP method, and request path.
- Improved error logging to provide better debugging insights.

Example log format:
EXTRA[{"service":{"name":"maestro","environment":"stg"},"customer_name":"dadosfera","user_id":"047824d8-0021-709a-bf15-39b8dd069aaf","method":"POST","path":"/catalog/register-dataset"}]

Related tickets or issues: [TECN-9441]
2025-02-14 15:04:22 -03:00
Rafael Santana 3f80062672 Merge branch 'beta' into feat/catalog-external-datasets-using-api-call 2025-02-14 10:14:01 -03:00
Rafael 07eb423814 UPDATE: update version of protospack 2025-02-14 10:12:20 -03:00
Rafael 721232448e FEAT: Enable dataset registration via API for external sources
As part of our hybrid approach, we need to allow dataset cataloging from outside our cloud environment.
Instead of relying on our GRPC APIs, we leveraged an existing REST API to handle this process.

- Updated Maestro to call Nimbus directly for dataset registration.
- Modified protospack to generate the necessary TypeScript interfaces.
- Added the `/catalog/register-dataset` endpoint to the API.

This change enables seamless dataset registration from external sources while maintaining compatibility with our existing infrastructure.

Related tickets or issues: [TECN-9441]
2025-02-14 10:08:45 -03:00
Marcos Rodrigues Silva 0b0d261aae Merge pull request #256 from dadosfera/fix/white-label
UPDATE: remove user agent
2025-02-13 13:56:37 -03:00
marcos-silva-rodrigues 87a8d741d7 UPDATE: remove user agent 2025-02-13 13:55:37 -03:00
marcos-silva-rodrigues d03dd6129a UPDATE: customer dash 2025-02-13 12:20:04 -03:00
Marcos Rodrigues Silva 96c20995a3 Merge pull request #255 from dadosfera/beta
Beta
2025-02-03 18:22:05 -03:00
Marcos Rodrigues Silva 82470553db Merge pull request #254 from dadosfera/fix/white-label
FIX: remove support for svg
2025-02-03 12:29:19 -03:00
marcos-silva-rodrigues 2408505889 FIX: remove support for svg 2025-02-03 12:25:56 -03:00
Marcos Rodrigues Silva 7d85693a1f Merge pull request #253 from dadosfera/fix/white-label
Fix/white label
2025-02-03 09:53:38 -03:00
marcos-silva-rodrigues d86a011bfd Reapply "Merge pull request #250 from dadosfera/beta"
This reverts commit d3f7664116.
2025-02-01 16:49:18 -03:00
Marcos Rodrigues Silva 4d54ac744b Merge pull request #252 from dadosfera/feature/new-monitoring-dash
FEAT: update monitoring dash
2025-01-31 18:32:41 -03:00
marcos-silva-rodrigues 132614b551 FEAT: update monitoring dash 2025-01-31 18:24:08 -03:00
Rafael Santana 17c541548a Merge pull request #251 from dadosfera/rollback-main
Revert "Merge pull request #250 from dadosfera/beta"
2025-01-31 18:20:20 -03:00
29 changed files with 2718 additions and 1500 deletions
-73
View File
@@ -1,73 +0,0 @@
name: Deploy K8S Modifications
on:
push:
branches:
- main
- beta
jobs:
extract_environment:
runs-on: ubuntu-22.04
outputs:
environment: ${{ steps.extract_environment.outputs.environment }}
steps:
- name: Extract Environment
run: |
if [ ${GITHUB_REF} == "refs/heads/main" ]; then
echo "environment=prd" >> $GITHUB_OUTPUT
elif [ ${GITHUB_REF} == "refs/heads/beta" ]; then
echo "environment=stg" >> $GITHUB_OUTPUT
fi
id: extract_environment
helmfile-deploy:
needs: [extract_environment]
runs-on: [self-hosted, "prd-azure"]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Helm
uses: azure/setup-helm@v1
with:
version: 'v3.9.0'
- name: Install Azure ClI
run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
- uses: azure/login@v2
with:
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install Helmfile
run: |
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
mv helmfile /usr/local/bin/
helmfile --version
- name: Install Helm Diff Plugin
run: helm plugin install https://github.com/databus23/helm-diff || true
- name: Setup kubectl
uses: azure/setup-kubectl@v1
with:
version: 'v1.30.1'
- name: Authenticate with cluster
env:
CLUSTER_NAME: platform-${{ needs.extract_environment.outputs.environment }}
run: az aks get-credentials --resource-group dadosfera-prd --name ${CLUSTER_NAME} --overwrite-existing
- name: Run Helmfile Apply
env:
ENV: ${{ needs.extract_environment.outputs.environment }}
run: helmfile -f helmfiles/${ENV}.yaml sync
+1
View File
@@ -2,6 +2,7 @@
<image src="./assets/maestro.svg" style="width:10rem">
</p>
# Maestro
Maestro é a API principal da Dadosfera. É responsável pela comunicação do Frontend com nossos microsserviços.
+217 -2
View File
@@ -4787,6 +4787,41 @@
]
}
},
"/catalog/register-dataset": {
"post": {
"operationId": "CatalogController_registerDatasetWithMetadataRequest",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
}
],
"responses": {
"201": {
"description": ""
}
},
"tags": [
"Catalog"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
]
}
},
"/terms-of-use/token": {
"get": {
"operationId": "TermsOfUseController_getToken",
@@ -5431,6 +5466,125 @@
]
}
},
"/open-data/sharing-ocean-data": {
"post": {
"operationId": "OpenDataController_createUser",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
},
{
"name": "language",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "origin",
"required": true,
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"OpenData"
]
}
},
"/customers/{id}/theme": {
"post": {
"operationId": "ThemeController_saveCustomertheme",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerThemeRequest"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerThemeResponse"
}
}
}
}
},
"tags": [
"Theme"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
]
},
"get": {
"operationId": "ThemeController_getCustomerTheme",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomerThemeResponse"
}
}
}
}
},
"tags": [
"Theme"
]
}
},
"/health": {
"get": {
"operationId": "HealthController_check",
@@ -5497,6 +5651,9 @@
"name": {
"type": "string"
},
"displayName": {
"type": "string"
},
"tier": {
"type": "string"
},
@@ -5508,15 +5665,20 @@
"items": {
"type": "string"
}
},
"themeEnabled": {
"type": "boolean"
}
},
"required": [
"modules",
"id",
"name",
"displayName",
"tier",
"scheduleLimit",
"links"
"links",
"themeEnabled"
]
},
"AuthUser": {
@@ -8234,8 +8396,61 @@
"required": [
"links"
]
},
"CustomerThemeRequest": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"textColor": {
"type": "string"
}
},
"required": [
"displayName",
"backgroundColor",
"textColor"
]
},
"CustomerTheme": {
"type": "object",
"properties": {
"backgroundColor": {
"type": "string"
},
"textColor": {
"type": "string"
},
"logo": {
"type": "string"
},
"logoLogin": {
"type": "string"
}
},
"required": [
"backgroundColor",
"textColor",
"logo",
"logoLogin"
]
},
"CustomerThemeResponse": {
"type": "object",
"properties": {
"theme": {
"$ref": "#/components/schemas/CustomerTheme"
}
},
"required": [
"theme"
]
}
}
}
}
}
}
+2
View File
@@ -12,6 +12,8 @@ declare global {
INTERNAL_SWAGGER: 'true' | 'false';
AWS_REGION: string;
OPEN_GROUP_ID: string;
OPEN_CUSTOMER_ID: string;
}
}
}
+7 -1
View File
@@ -13,4 +13,10 @@ charts:
- name: maestro.in_factory_url
value: in-factory.dadosfera.ai
- name: maestro.tr_factory_url
value: in-factory.dadosfera.ai
value: in-factory.dadosfera.ai
- name: maestro.open_customer_id
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
- name: maestro.open_group_id
value: c0afdcce-c5be-40d0-9d1d-2d271121f14a
- name: replicaCount
value: 2
+11 -5
View File
@@ -5,12 +5,18 @@ charts:
- ../maestro/values.yaml
set:
- name: maestro.duc_url
value: duc-temp.dadosfera.ai
value: duc.stg.dadosfera.ai
- name: hostname
value: maestro-temp.dadosfera.ai
value: maestro.stg.dadosfera.ai
- name: maestro.pi_factory_url
value: pi-factory-temp.dadosfera.ai
value: pi-factory.dadosfera.ai
- name: maestro.in_factory_url
value: in-factory-temp.dadosfera.ai
value: in-factory.dadosfera.ai
- name: maestro.tr_factory_url
value: in-factory-temp.dadosfera.ai
value: in-factory.dadosfera.ai
- name: maestro.open_customer_id
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
- name: maestro.open_group_id
value: e3f98a2f-7748-4981-8505-7695c8ca8218
- name: replicaCount
value: 1
+11 -1
View File
@@ -35,7 +35,13 @@ spec:
- key: application
operator: In
values:
- backend
- general
tolerations:
- key: "kubernetes.azure.com/scalesetpriority"
operator: "Equal"
value: "spot"
effect: "NoSchedule"
containers:
- name: maestro
@@ -90,6 +96,10 @@ spec:
value: {{ .Values.maestro.tr_factory_url }}
- name: UPLOAD_FILE_AGENT_CONNECTION
value: {{ .Values.maestro.upload_file_agent_connection }}
- name: OPEN_CUSTOMER_ID
value: {{ .Values.maestro.open_customer_id }}
- name: OPEN_GROUP_ID
value: {{ .Values.maestro.open_group_id }}
- name: JWT_PRIVATE_KEY
valueFrom:
secretKeyRef:
+28
View File
@@ -0,0 +1,28 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "69.49.241.121/32" # hostgator ip
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/server-snippet: |
underscores_in_headers on;
ignore_invalid_headers on;
generation: 1
labels:
app: maestro
name: open-data
namespace: applications
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.hostname }}
http:
paths:
- backend:
service:
name: maestro
port:
number: {{ .Values.ingress.port }}
path: /open-data/sharing-ocean-data
pathType: Prefix
+3 -2
View File
@@ -3,7 +3,7 @@
# Declare variables to be passed into your templates.
replicaCount: 3
hostname: maestro-temp.dadosfera.ai
hostname: maestro.dadosfera.ai
image:
repository: 611330257153.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_prd
pullPolicy: IfNotPresent
@@ -40,7 +40,8 @@ maestro:
sm_oauth_path: prd/root/oauth_applications
tr_factory_url: in-factory.dadosfera.ai
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
autoscaling:
enabled: false
minReplicas: 1
+1623 -1405
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -30,7 +30,7 @@
"@aws-sdk/client-secrets-manager": "^3.414.0",
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
"@dadosfera/protospack": "2.5.3",
"@dadosfera/protospack-v2": "3.34.0",
"@dadosfera/protospack-v2": "^3.37.0-beta.10 ",
"@grpc/grpc-js": "^1.9.3",
"@grpc/proto-loader": "^0.7.9",
"@nestjs/cli": "^9.5.0",
@@ -74,7 +74,7 @@
"@types/jest": "27.0.2",
"@types/jsonwebtoken": "^8.5.9",
"@types/jwk-to-pem": "^2.0.1",
"@types/multer": "^1.4.7",
"@types/multer": "^1.4.12",
"@types/node": "^16.18.52",
"@types/passport-facebook": "^2.1.11",
"@types/passport-google-oauth20": "^2.0.11",
+5 -2
View File
@@ -26,9 +26,10 @@ import { PipelinesV2Module } from './modules/pipelinesV2/pipelines.module';
import { ProductboardModule } from './modules/productboard/productboard.module';
import { MixpanelModule } from './modules/mixpanel/mixpanel.module';
import { CustomersModule } from './modules/customers/customers.module';
import { OpenDataModule } from './modules/open-data/open-data.module';
import { ThemeModule } from './modules/theme/theme.module';
@Module({
controllers: [],
providers: [
DadosferaLogger,
{
@@ -58,8 +59,10 @@ import { CustomersModule } from './modules/customers/customers.module';
ProductboardModule,
MixpanelModule,
CustomersModule,
OpenDataModule,
ThemeModule,
//Always leave HealthModule last, so it is on the bottom of swagger
HealthModule,
HealthModule
],
})
export class AppModule {}
+4
View File
@@ -72,11 +72,15 @@ export class AuthCustomer {
@ApiProperty()
name: string;
@ApiProperty()
displayName: string;
@ApiProperty()
tier: string;
@ApiProperty()
scheduleLimit: string;
@ApiProperty()
links: Link[];
@ApiProperty()
themeEnabled: boolean;
}
export class AuthSignInReq implements AuthSignInRequest {
+3
View File
@@ -5,6 +5,7 @@ import {
} from '@nestjs/microservices';
import { credentials } from '@grpc/grpc-js';
import { Catalog } from '@dadosfera/protospack-v2';
import { PlatformInterfaces } from '@dadosfera/protospack-v2';
const isLocalConnection =
process.env.PIFACTORY_URL.startsWith('pi-factory:') ||
@@ -19,11 +20,13 @@ export class CatalogClientConfiguration {
package: [
Catalog.ProtoPackages.ReadPackage,
Catalog.ProtoPackages.WritePackage,
PlatformInterfaces.ProtoPackages.WritePackage
],
credentials: isLocalConnection ? undefined : credentials.createSsl(),
protoPath: [
Catalog.ProtoPaths.ReadFilePath,
Catalog.ProtoPaths.WriteFilePath,
PlatformInterfaces.ProtoPaths.WriteFilePath
],
loader: {
keepCase: true,
+94
View File
@@ -13,6 +13,8 @@ import {
Put,
Query,
UseFilters,
HttpException,
HttpStatus,
} from '@nestjs/common';
import {
ApiCreatedResponse,
@@ -30,6 +32,7 @@ import { CatalogService } from './catalog.service';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
import { RequestUser, User } from 'src/decorators/user.decorator';
import { lastValueFrom } from 'rxjs';
import {
BatchRemoveRlsRulesRequest,
GetDatasetCatalogTaskRes,
@@ -54,6 +57,7 @@ import {
AddRlsRuleRequest,
GetNimbusDashboardsRequest,
GetRlsRulesRequest,
RegisterDatasetWithMetatadaRequest,
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
@ApiTags('Catalog')
@@ -743,4 +747,94 @@ export class CatalogController {
return JSON.parse(dashboards);
}
@Post('register-dataset')
@RequireSomePermission(
PERMISSIONS_GROUPS.CATALOG.permissions.CREATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER
)
async registerDatasetWithMetadataRequest(
@Body() body: RegisterDatasetWithMetatadaRequest,
@User() user: RequestUser,
) {
const { customer_id, customer_name, user_id, username } = user;
const logMetadata = {
customer_name: customer_name,
user_id: user_id,
method: 'POST',
path: '/catalog/register-dataset',
};
try {
const metadata = PackTheMetadata({
customer_id,
customer_name,
user_id,
username,
});
this.logger.log(`Request from user ${user_id} for customer ${customer_name}`, logMetadata);
// Create table metadata
const tableMetadataBody = {
table_metadata: body.table_metadata,
info: {
customer: customer_name,
},
logMetadata: logMetadata
};
const table_metadata_id = await this.catalogService.createTableMetadata(
tableMetadataBody,
);
this.logger.info(`table_metadata_id: ${table_metadata_id}`, logMetadata);
// Create column metadata
const columnMetadataBody = {
column_metadata: body.column_metadata,
info: {
customer: customer_name,
},
};
this.logger.info(`Creating column metadata for table ${table_metadata_id}`, logMetadata);
const column_metadata_ids = await this.catalogService.createColumnMetadata(columnMetadataBody)
// Create data preview
const dataPreviewBody = {
data_preview: body.data_preview,
info: {
customer: customer_name,
},
};
this.logger.debug(`Creating data preview for table ${table_metadata_id}`, logMetadata);
const data_preview_id = await this.catalogService.createDataPreview(
dataPreviewBody,
);
// Catalog dataset item
this.logger.info(`Cataloging dataset item for table ${table_metadata_id}`, logMetadata);
await this.catalogService.catalogDatasetItem(table_metadata_id, metadata);
this.logger.info(`Dataset registration completed successfully for table ${table_metadata_id}`, logMetadata);
return {
message: 'Dataset registered successfully',
table_metadata_id: table_metadata_id,
column_metadata_ids: column_metadata_ids,
data_preview_id: data_preview_id,
};
} catch (error) {
this.logger.error(`Failed to register dataset. The following error occurred: ${error.response.data}`, logMetadata);
throw new HttpException(
{
message: 'Ocorreu um erro ao registrar o dataset',
error: error.message,
code: 'REGISTRATION_FAILED',
details: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
+117
View File
@@ -5,6 +5,11 @@ import {
ProtoServices,
Messages,
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
import {
Messages as PlatformInterfaceMessages,
WriteService as PlatformInterfaceWriteService,
ProtoServices as PlatformInterfacesProtoServices,
} from '@dadosfera/protospack-v2/dist/lib/PlatformInterfaces';
import {
BadRequestException,
HttpException,
@@ -33,6 +38,7 @@ import {
class CatalogService implements OnModuleInit {
catalogReadService: ReadService.CatalogReadServices;
catalogWriteService: WriteService.CatalogWriteServices;
platformWriteService: PlatformInterfaceWriteService.PlatformInterfacesWriteServices;
logger: any;
constructor(
@Inject(DadosferaLogger)
@@ -54,9 +60,14 @@ class CatalogService implements OnModuleInit {
this.grpcClient.getService<WriteService.CatalogWriteServices>(
ProtoServices.CatalogWriteServices,
);
this.platformWriteService =
this.grpcClient.getService<PlatformInterfaceWriteService.PlatformInterfacesWriteServices>(
PlatformInterfacesProtoServices.PlatformInterfacesWriteServices,
);
}
_getNimbusUrl(body) {
this.logger.debug(`Body: ${JSON.stringify(body)}`);
const customer = body.info.customer.toLowerCase();
if (process.env.ENV === 'prd') {
@@ -420,6 +431,112 @@ class CatalogService implements OnModuleInit {
);
return res.dashboards;
}
async createTableMetadata(body: any): Promise<number> {
const nimbusUrl = this._getNimbusUrl(body);
this.logger.info(`Nimbus URL: ${nimbusUrl}`, {...body.logMetadata});
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/`;
this.logger.info(`Creating table metadata for table ${body.table_metadata.table_name}`, {...body.logMetadata});
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
this.logger.debug(`Payload: ${JSON.stringify(body.table_metadata)}`, {...body.logMetadata});
try {
const { data, status } = await axios.post(endpoint, {...body.table_metadata});
this.logger.info(
`Table metadata created successfully with status ${status} for table ${body.table_metadata.table_name}`,
{...body.logMetadata},
);
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});
throw new Error(error.response?.data?.message || error.message);
}
}
async createColumnMetadata(body: any): Promise<number[]> {
const nimbusUrl = this._getNimbusUrl(body);
this.logger.info(`Nimbus URL: ${nimbusUrl}`, body.logMetadata);
const endpoint = `${nimbusUrl}/api/catalog/column-metadata/`;
try {
this.logger.info(`Creating column metadata for table ${body.column_metadata.table_name}`, {...body.logMetadata});
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
this.logger.debug(`Payload: ${JSON.stringify(body.column_metadata)}`, {...body.logMetadata});
const { data, status } = await axios.post(endpoint, body.column_metadata);
this.logger.info(
`Column metadata created successfully with status ${status} for table ${body.column_metadata.table_name}`,
{...body.logMetadata},
);
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});
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});
const endpoint = `${nimbusUrl}/api/catalog/data-preview/`;
this.logger.info(`Creating data preview for table ${body.data_preview.table_name}`, {...body.logMetadata});
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
this.logger.debug(`Payload: ${JSON.stringify(body.data_preview)}`, {...body.logMetadata});
try {
const { data, status } = await axios.post(endpoint, body.data_preview);
this.logger.info(
`Data preview created successfully with status ${status} for table ${body.data_preview.table_name}`,
{...body.logMetadata},
);
return data.id;
} catch (error) {
this.logger.error(
`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},
);
throw new Error(error.response?.data?.message || error.message);
}
}
async catalogDatasetItem(table_metadata_id: number, metadata: Metadata) {
const customer_name_raw = metadata.get('customer_name');
const customer_name = customer_name_raw?.[0]?.toString();
if (!customer_name) {
throw new BadRequestException('Customer name not found in metadata');
}
const res = await lastValueFrom(
this.platformWriteService.CatalogDataAssets(
{
data_assets: [
{
data_asset_id: table_metadata_id.toString(),
customer_name: customer_name,
data_asset_type: 'dataset',
},
],
},
metadata,
),
);
return res;
}
}
export { CatalogService };
@@ -1,5 +1,4 @@
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { IdResponse } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import {
Body,
Controller,
@@ -18,7 +17,6 @@ import {
Authenticated,
RequireAllPermissions,
} from 'src/decorators/authentication.decorator';
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
import { CustomersService } from './customers.service';
import { CustomerLinkRequest, CustomerLinksResponse } from './dtos/customers';
@@ -28,7 +26,6 @@ import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
@ApiTags('Customers')
@Controller('customers')
@Authenticated()
@UseFilters(GrpcToHttpExceptionFilter)
export class CustomersController {
logger: DadosferaLogger;
@@ -42,6 +39,7 @@ export class CustomersController {
}
@Get(':id/links')
@Authenticated()
@ApiOkResponse({ type: CustomerLinksResponse })
async getCustomerLinks(@Param('id') id: string) {
this.logger.info('getCustomerLinks', { id });
@@ -50,6 +48,7 @@ export class CustomersController {
}
@Put(':id/links')
@Authenticated()
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
@ApiOkResponse()
@HttpCode(HttpStatus.OK)
@@ -63,6 +62,7 @@ export class CustomersController {
}
@Get('token')
@Authenticated()
@RequireAllPermissions(PERMISSIONS_GROUPS.AUTH.permissions.GENERATE_TOKEN)
@ApiProduces('text/plain')
async getCustomerToken(
@@ -79,6 +79,7 @@ export class CustomersController {
}
@Get('monitoring-dashboard')
@Authenticated()
@RequireAllPermissions(
PERMISSIONS_GROUPS.CUSTOMER.permissions.MONITORING_DASHBOARD,
)
+8 -3
View File
@@ -28,19 +28,24 @@ import {
} from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
import { Metadata } from '@grpc/grpc-js';
import { PipelinesClientConfiguration } from '../pipelinesV2/pipelines-client';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
// This function will accept any string, which may result in a bug.
@Injectable()
export class CustomersService implements OnModuleInit {
private customerService: CustomersProtoService;
private logger: DadosferaLogger;
private pipelineReadService: ReadService.PipelineV2ReadService;
constructor(
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
@Inject(PipelinesClientConfiguration.name)
private readonly pipelinesGrpcClient: ClientGrpc,
) {}
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
}
onModuleInit() {
this.customerService = this.grpcClient.getService<CustomersProtoService>(
@@ -140,7 +145,7 @@ export class CustomersService implements OnModuleInit {
const res = await lastValueFrom(
this.pipelineReadService.PipelineV2GetDashboardUrl(
{
dashboard_id: '45',
dashboard_id: '98',
exp: '15m',
metabase_customer_name: 'dadosferatech',
},
+1
View File
@@ -20,3 +20,4 @@ export class CustomerLinksResponse {
@ApiProperty({ type: [CustomerLink] })
links: CustomerLink[];
}
+2
View File
@@ -29,6 +29,8 @@ export class DucClient {
objects: true,
arrays: true,
},
maxSendMessageLength: 15 * 1024 * 1024, // 15 MB por mensagem
maxReceiveMessageLength: 15 * 1024 * 1024,
},
};
@@ -0,0 +1,48 @@
import { ApiProperty } from "@nestjs/swagger";
export class CreateUserOpenDataDTO {
@ApiProperty()
firstName: string;
@ApiProperty()
lastName: string;
@ApiProperty()
email: string;
@ApiProperty()
organization: string;
@ApiProperty()
enquiryType: string;
}
type FormField = {
id: string;
type: string;
title: string;
value: string;
raw_value: string;
required: string;
};
type MetaData = {
title: string;
value: string;
};
export type WordpressForm = {
form: {
id: string;
name: string;
};
fields: {
[key: string]: FormField;
};
meta: {
date: MetaData;
time: MetaData;
page_url: MetaData;
user_agent: MetaData;
remote_ip: MetaData;
credit: MetaData;
};
};
@@ -0,0 +1,101 @@
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import { Body, Controller, ForbiddenException, Header, Headers, HttpCode, HttpException, Inject, Param, Post, Query, Req, Res, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
import { ApiCreatedResponse, ApiHeaders, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
import { LanguageEnum } from 'src/utils/languages.enum';
import { UsersService } from '../users/users.service';
import { Language } from 'src/decorators/language.decorator';
import { OpenDataService } from './open-data.service';
import { CreateUserOpenDataDTO, WordpressForm } from './dto/wordpres-form';
import { Metadata } from '@grpc/grpc-js';
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
import { request } from 'http';
import { Request } from 'express';
@Controller('open-data')
@ApiInternalOnlyController()
@ApiTags('OpenData')
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
@UseFilters(GrpcToHttpExceptionFilter)
export class OpenDataController {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private openDataService: OpenDataService,
) {
this.logger = dadosferaLogger.logger;
}
@Post("/sharing-ocean-data")
@HttpCode(200)
@Header('content-type', 'application/json')
@ApiOkResponse()
async createUser(
@Body()
body: WordpressForm,
@Query('language')
language: string,
@Req()
request: Request,
@Headers('origin')
origin: string
) {
this.logger.info('createUser for open data' + JSON.stringify(request.headers));
const corslist = ["https://devsbm.dadosfera.io", "https://sharingoceandata.com"];
if (!corslist.includes(origin)) {
this.logger.info('block request by cors list: '+ origin);
throw new ForbiddenException();
}
const OPENDATA_CUSTOMER_ID = process.env.OPEN_CUSTOMER_ID;
const OPENDATA_GROUP_ID = process.env.OPEN_GROUP_ID;
const roles = [process.env.OPEN_GROUP_ID];
const metadata = PackTheMetadata({
language: language || 'en-us'
});
const data = {}
try {
Object.keys(body.fields)
.filter(key => body.fields[key].required === "1")
.forEach(key => {
const field = body.fields[key]
data[field.id] = field.value
});
} catch (e) {
this.logger.error('user data ' + e.message);
}
const user: CreateUserOpenDataDTO = {
email: data["email"],
enquiryType: data["enquiry_type"],
firstName: data["first_name"],
lastName: data["last_name"],
organization: data["organization"]
}
this.logger.info(`user request to group ${OPENDATA_CUSTOMER_ID} with role ${OPENDATA_GROUP_ID}`);
try {
const id = await this.openDataService.createUser(OPENDATA_CUSTOMER_ID, user, roles, metadata);
this.logger.info('user created with id: '+ id);
return {
success: true,
status: 'success',
message: 'user created with succesfull'
}
} catch (e) {
this.logger.error('failed with exception: ' + e.message);
return {
success: false,
status: 'failed',
message: e.message
};
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { OpenDataController } from './open-data.controller';
import { UsersService } from '../users/users.service';
import { ClientsModule } from '@nestjs/microservices'
import { DucClient } from '../duc/client.config';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import { RolesModule } from '../roles/roles.module';
import { PermissionsModule } from '../permissions/permissions.module';
import { OpenDataService } from './open-data.service';
const client = new DucClient();
@Module({
controllers: [OpenDataController],
imports: [
ClientsModule.register([client.providerOptions]),
RolesModule,
// PermissionsModule,
],
providers: [DadosferaLogger, UsersService, OpenDataService]
})
export class OpenDataModule {}
@@ -0,0 +1,51 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { lastValueFrom } from 'rxjs';
import { CreateUserOpenDataDTO } from './dto/wordpres-form';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import { UsersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import { DucClient } from '../duc/client.config';
import { ClientGrpc } from '@nestjs/microservices';
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
import { Metadata } from '@grpc/grpc-js';
@Injectable()
export class OpenDataService implements OnModuleInit {
logger: DadosferaLogger;
private usersClientService: UsersProtoService;
constructor(
@Inject(DadosferaLogger)
private dadosferaLogger: DadosferaLogger,
@Inject(DucClient.name)
private readonly grpcClient: ClientGrpc,
) {
this.logger = dadosferaLogger.logger;
}
onModuleInit() {
this.usersClientService = this.grpcClient.getService(
ProtoServices.UsersProtoService,
);
}
async createUser(customerId: string, data: CreateUserOpenDataDTO, roleIds: string[], metadata: Metadata) {
const body = {
email: data.email,
name: data.firstName + " " + data.lastName,
department: data.organization,
jobTitle: data.enquiryType,
customerId: customerId,
roleIds: roleIds
}
try {
const { user } = await lastValueFrom(
this.usersClientService.SimpleUserCreate(body, metadata),
);
return user.id;
} catch(err) {
return err;
}
}
}
+1 -1
View File
@@ -328,7 +328,7 @@ export class PipelinesService implements OnModuleInit {
const res = await lastValueFrom(
this.pipelineReadService.PipelineV2GetDashboardUrl(
{
dashboard_id: '83',
dashboard_id: '95',
exp: '15m',
metabase_customer_name: 'dadosferatech',
},
+47
View File
@@ -0,0 +1,47 @@
import { Link, Theme } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CustomerLink implements Link {
@ApiProperty()
href: string;
@ApiProperty()
name: string;
@ApiProperty()
description: string;
@ApiPropertyOptional()
iconSrc: string;
}
export class CustomerLinkRequest {
@ApiProperty({ type: [CustomerLink] })
links: CustomerLink[];
}
export class CustomerLinksResponse {
@ApiProperty({ type: [CustomerLink] })
links: CustomerLink[];
}
export class CustomerTheme implements Theme {
@ApiProperty()
backgroundColor: string;
@ApiProperty()
textColor: string;
@ApiProperty()
logo: string;
@ApiProperty()
logoLogin: string;
}
export class CustomerThemeResponse {
@ApiProperty()
theme: CustomerTheme;
}
export class CustomerThemeRequest {
@ApiProperty()
displayName: string;
@ApiProperty()
backgroundColor: string;
@ApiProperty()
textColor: string;
}
+127
View File
@@ -0,0 +1,127 @@
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Inject,
Param,
Post,
Put,
Query,
UploadedFiles,
UseFilters,
UseInterceptors,
HttpCode
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import {
Authenticated,
RequireAllPermissions,
} from 'src/decorators/authentication.decorator';
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
import { CustomerThemeRequest, CustomerThemeResponse } from './dtos/customers';
import ErrorCodes from 'src/utils/errorCodes';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { ThemeService } from './theme.service';
@ApiTags('Theme')
@Controller('customers')
@UseFilters(GrpcToHttpExceptionFilter)
export class ThemeController {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private themeService: ThemeService,
) {
this.logger = dadosferaLogger.logger;
}
@Post('/:id/theme')
@Authenticated()
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
@ApiOkResponse({ type: CustomerThemeResponse })
@UseInterceptors(AnyFilesInterceptor())
@HttpCode(HttpStatus.OK)
async saveCustomertheme(
@Param('id') id: string,
@Body() data: CustomerThemeRequest,
@UploadedFiles() files: Array<Express.Multer.File>
) {
this.logger.info('saveCustomertheme' + JSON.stringify({
id,
}));
const logo = files.find(file => file.fieldname === 'logo');
const logoLogin = files.find(file => file.fieldname === 'logoLogin');
this.validFileSize(logo);
this.validFileSize(logoLogin);
this.validMimeType(logo);
this.validMimeType(logoLogin);
try {
const theme = await this.themeService.createThemeByCustomer(id, {
...data,
logo,
logoLogin
});
this.logger.info('saveCustomertheme' + JSON.stringify(theme));
return theme;
} catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
this.logger.error('Error - saveCustomertheme - Expect CUSTOMER.NOT_FOUND');
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
} else {
this.logger.error('Error - saveCustomertheme Unknown Error:' + err?.message);
return { theme: null };
};
}
}
private validFileSize(file: Express.Multer.File) {
const maxFileSize = 10 * 1024 * 1024; // 10MB
if (file && file.size > maxFileSize) {
throw new HttpException(`O Arquivo ${file.filename} possui mais de 10MB`, HttpStatus.BAD_REQUEST);
}
}
private validMimeType(file: Express.Multer.File) {
const mimeTypesValid = ['image/jpeg', 'image/jpg', 'image/png'];
if (file && !mimeTypesValid.includes(file.mimetype)) {
throw new HttpException(`O Arquivo ${file.fieldname} deve ser jpeg, jpg, ou png`, HttpStatus.BAD_REQUEST);
}
}
@Get('/:id/theme')
@ApiOkResponse({ type: CustomerThemeResponse })
async getCustomerTheme(@Param('id') id: string) {
this.logger.info('getCustomerTheme with id' + id);
try {
const data = await this.themeService.getThemeByCustomer(id);
this.logger.info('Success - getCustomerTheme'+ JSON.stringify(data));
if (data?.theme) return data;
return { theme: null };
}catch (err) {
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
this.logger.error('Error - getCustomerTheme - Expect CUSTOMER.NOT_FOUND');
throw new HttpException(err.details, HttpStatus.NOT_FOUND);
} else {
this.logger.error('Error - getCustomerTheme Unknown Error:' + err?.message);
return { theme: null };
};
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { ClientsModule } from '@nestjs/microservices';
import { ThemeController } from './theme.controller';
import { ThemeService } from './theme.service';
import { DucClient } from '../duc/client.config';
const ducClient = new DucClient();
@Module({
imports: [
ClientsModule.register([
ducClient.providerOptions,
]),
],
controllers: [ThemeController],
providers: [ThemeService, DadosferaLogger],
exports: [ThemeService],
})
export class ThemeModule {}
+158
View File
@@ -0,0 +1,158 @@
import {
OnModuleInit,
Inject,
Injectable,
HttpException,
HttpStatus,
InternalServerErrorException,
} from '@nestjs/common';
import { firstValueFrom, lastValueFrom, ReplaySubject } 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 { CustomerUpdateRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { ThemeProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import { ThemeRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { CustomerThemeRequest, CustomerThemeResponse } from './dtos/customers';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import { resolve } from 'path';
import { Readable } from 'stream';
type Files = {
logo: Express.Multer.File,
logoLogin: Express.Multer.File,
}
@Injectable()
export class ThemeService implements OnModuleInit {
private themeService: ThemeProtoService;
private logger: DadosferaLogger;
constructor(
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
) {
this.logger = dadosferaLogger.logger;
}
onModuleInit() {
this.themeService = this.grpcClient.getService<ThemeProtoService>(
ProtoServices.ThemeProtoService,
);
}
async createThemeByCustomer(id: string, theme: CustomerThemeRequest & Files) {
if (!id) {
this.logger.error('Error - saveCustomertheme - not found id:' + id);
throw new HttpException(null, HttpStatus.BAD_REQUEST);
}
const customerThemeRequest$ = new ReplaySubject<ThemeRequest>();
customerThemeRequest$.next({
customerId: id,
displayName: theme.displayName,
backgroundColor: theme.backgroundColor,
textColor: theme.textColor,
isMetadata: true,
filename: '',
chunk: Buffer.alloc(0)
})
if(theme.logo) {
await this.sendFile(theme.logo, customerThemeRequest$);
}
if(theme.logoLogin) {
await this.sendFile(theme.logoLogin, customerThemeRequest$);
}
customerThemeRequest$.complete();
const stream = this.themeService.CustomerCreateTheme(customerThemeRequest$);
return lastValueFrom(stream);
}
async getThemeByCustomer(id: string): Promise<CustomerThemeResponse> {
if (!id) {
this.logger.error('Error - getCustomerTheme - not found id:' + id);
throw new HttpException(null, HttpStatus.BAD_REQUEST);
}
const { theme } = await firstValueFrom(
this.themeService.CustomerGetTheme({
id
}),
);
return {
theme
}
}
private async sendFile(file: Express.Multer.File, stream$: ReplaySubject<ThemeRequest>) {
const chunkSize = 4 * 1024 * 1024;
const bufferStream = new CustomBufferStream(file.buffer, chunkSize);
const parseMimitypeForExtension = {
'image/jpeg': '.jpeg',
'image/jpg': '.jpg',
'image/png': '.png',
'image/svg+xml': '.svg',
}
const extension = parseMimitypeForExtension[file.mimetype];
return new Promise((resolve, reject) => {
bufferStream.on('data', (chunk) => {
const filename = file.fieldname.concat(extension);
stream$.next({
customerId: '',
displayName: '',
backgroundColor: '',
textColor: '',
isMetadata: false,
filename: filename,
chunk: chunk
});
});
bufferStream.on('end', () => {
resolve(file.filename)
});
bufferStream.on('error', (err) => {
reject(err);
});
});
}
}
class CustomBufferStream extends Readable {
buffer: Buffer;
offset: number;
chunkSize: number;
constructor(buffer: Buffer, chunkSize: number) {
super({ highWaterMark: chunkSize }); // Configura o tamanho do chunk
this.buffer = buffer;
this.offset = 0;
this.chunkSize = chunkSize;
}
_read() {
if (this.offset < this.buffer.length) {
const end = Math.min(this.offset + this.chunkSize, this.buffer.length);
const copiedBuf = Uint8Array.prototype.slice.call(this.buffer);
const chunk = copiedBuf.slice(this.offset, end);
this.offset = end;
this.push(chunk);
} else {
this.push(null);
}
}
}