diff --git a/api-contracts/openapi/management-api-openapi.yaml b/api-contracts/openapi/management-api-openapi.yaml deleted file mode 100644 index b746002..0000000 --- a/api-contracts/openapi/management-api-openapi.yaml +++ /dev/null @@ -1,1109 +0,0 @@ -openapi: 3.0.0 -paths: - /: - get: - operationId: AppController_getHello - parameters: [] - responses: - '200': - description: '' - tags: - - App - /admin/gateways: - get: - operationId: GatewaysController_getAdminGateways - parameters: - - name: tenant_id - required: false - in: query - schema: - type: string - responses: - '403': - description: Forbidden - summary: Get all Gateways - tags: - - Admin Gateways - post: - operationId: GatewaysController_addGateway - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AddGatewayRequestDto' - responses: - '404': - description: Tenant not found - '409': - description: Factory_id already registered - summary: Add Gateway to a Tenant - tags: - - Admin Gateways - /admin/tenants: - get: - operationId: TenantsController_getTenants - parameters: [] - responses: - '200': - description: List of tenants - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/TenantsResponseDto' - '401': - description: Unauthorized - '403': - description: Forbidden - summary: Get all tenants - tags: - - Admin Tenants - post: - operationId: TenantsController_createTenant - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateTenantRequestDto' - responses: - '201': - description: Tenant created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/TenantsResponseDto' - '400': - description: Bad Request - '409': - description: Conflict - Tenant with the same name already exists - summary: Create a new tenant - tags: - - Admin Tenants - /admin/tenants/{id}: - patch: - operationId: TenantsController_updateTenant - parameters: - - name: id - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateTenantRequestDto' - responses: - '200': - description: Tenant updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/TenantsResponseDto' - '400': - description: Bad Request - '404': - description: Tenant not found - summary: Update tenant details - tags: - - Admin Tenants - delete: - operationId: TenantsController_deleteTenant - parameters: - - name: id - required: true - in: path - schema: - type: string - responses: - '200': - description: Tenant deleted successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteTenantResponseDto' - '404': - description: Tenant not found - summary: Delete a tenant - tags: - - Admin Tenants - /alerts: - get: - operationId: AlertsController_getAlerts - parameters: - - name: from - required: false - in: query - schema: - type: string - - name: to - required: false - in: query - schema: - type: string - - name: gateway_id - required: false - in: query - schema: - type: string - responses: - '200': - description: '' - summary: Get alerts for the tenant in a time range - tags: - - Alerts - /alerts/config: - get: - operationId: AlertsController_getAlertsConfig - parameters: [] - responses: - '200': - description: '' - summary: Get alert configuration for the tenant - tags: - - Alerts - /alerts/config/default: - put: - operationId: AlertsController_setDefaultAlertsConfig - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SetAlertsConfigDefaultRequestDto' - responses: - '200': - description: '' - summary: Set default alert configuration for the tenant - tags: - - Alerts - /alerts/config/gateway/{gatewayId}: - put: - operationId: AlertsController_setGatewayAlertsConfig - parameters: - - name: gatewayId - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SetGatewayAlertsConfigRequestDto' - responses: - '200': - description: Alert configuration updated successfully - '400': - description: Bad Request - '403': - description: Gateway not associated with tenant of JWT - '404': - description: Gateway not found - summary: Set alert configuration for a specific gateway - tags: - - Alerts - /api-clients: - post: - operationId: ApiClientController_createApiClient - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateApiClientRequestDto' - responses: - '400': - description: Bad Request - '409': - description: API client with the same name already exists - summary: Create a new API client for the tenant - tags: - - ApiClient - get: - operationId: ApiClientController_getApiClients - parameters: [] - responses: - '401': - description: Unauthorized - '403': - description: Forbidden - summary: Get all API clients for the tenant - tags: - - ApiClient - /api-clients/{id}: - delete: - operationId: ApiClientController_deleteApiClient - parameters: - - name: id - required: true - in: path - schema: - type: string - responses: - '404': - description: API client not found - summary: Delete an API client for the tenant - tags: - - ApiClient - /audit: - get: - operationId: AuditLogController_getAuditLogs - parameters: - - name: from - required: true - in: query - schema: - format: date-time - type: string - - name: to - required: true - in: query - schema: - format: date-time - type: string - - name: userId - required: true - in: query - schema: - type: string - - name: action - required: true - in: query - schema: - type: string - responses: - '400': - description: Bad Request - '401': - description: Unauthorized - '403': - description: Forbidden - '404': - description: Audit logs not found - summary: Get audit logs for the tenant - tags: - - AuditLog - /auth/me: - get: - operationId: AuthController_getMe - parameters: [] - responses: - '200': - description: Authenticated user details - '401': - description: Unauthorized - summary: Return authenticated user claims mapped by JwtStrategy - tags: - - auth - /auth/impersonate: - post: - operationId: AuthController_impersonate - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - properties: - user_id: - type: string - responses: - '200': - description: Impersonation token - content: - application/json: - schema: - properties: - access_token: - type: string - expires_in: - type: number - summary: Impersonate a tenant user (admin only) - tags: - - auth - /cmd/{gatewayId}/config: - post: - operationId: CommandController_sendConfig - parameters: - - name: gatewayId - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SendConfigRequestDto' - responses: - '202': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CommandResponseDto' - summary: Send configuration to a gateway - tags: - - Commands - /cmd/{gatewayId}/firmware: - post: - operationId: CommandController_sendFirmware - parameters: - - name: gatewayId - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SendFirmwareRequestDto' - responses: - '202': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CommandResponseDto' - summary: Send firmware update to a gateway - tags: - - Commands - /cmd/{gatewayId}/status/{commandId}: - get: - operationId: CommandController_getStatus - parameters: - - name: gatewayId - required: true - in: path - schema: - type: string - - name: commandId - required: true - in: path - schema: - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CommandStatusResponseDto' - summary: Get command execution status - tags: - - Commands - /costs: - get: - operationId: CostsController_getTenantCost - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CostResponseDto' - summary: Get current costs for the tenant - tags: - - Costs - /gateways: - get: - operationId: GatewaysController_getGateways - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/GatewayResponseDto' - summary: Get all gateways for the tenant - tags: - - Gateways - /gateways/{id}: - get: - operationId: GatewaysController_getGatewayById - parameters: - - name: id - required: true - in: path - schema: - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/GatewayResponseDto' - summary: Get gateway by id - tags: - - Gateways - patch: - operationId: GatewaysController_updateGateway - parameters: - - name: id - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateGatewayRequestDto' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateGatewayResponseDto' - summary: Update gateway details - tags: - - Gateways - delete: - operationId: GatewaysController_deleteGateway - parameters: - - name: id - required: true - in: path - schema: - type: string - responses: - '200': - description: Gateway deleted - summary: Delete a gateway - tags: - - Gateways - /keys: - get: - operationId: KeysController_getKeys - parameters: - - name: id - required: true - in: query - schema: - type: string - responses: - '200': - description: List of keys - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/KeysResponseDto' - '401': - description: Unauthorized - '403': - description: Forbidden - '404': - description: Gateway not found - summary: Get all keys for a gateway - tags: - - Keys - /thresholds: - get: - operationId: ThresholdsController_getThresholds - parameters: [] - responses: - '200': - description: '' - tags: - - Thresholds - /thresholds/default: - put: - operationId: ThresholdsController_setDefaultThreshold - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SetThresholdDefaultTypeRequestDto' - responses: - '200': - description: '' - tags: - - Thresholds - /thresholds/sensor/{sensorId}: - put: - operationId: ThresholdsController_setSensorThreshold - parameters: - - name: sensorId - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SetThresholdSensorRequestDto' - responses: - '200': - description: '' - tags: - - Thresholds - delete: - operationId: ThresholdsController_deleteSensorThreshold - parameters: - - name: sensorId - required: true - in: path - schema: - type: string - responses: - '200': - description: '' - tags: - - Thresholds - /thresholds/type/{sensorType}: - delete: - operationId: ThresholdsController_deleteThresholdType - parameters: - - name: sensorType - required: true - in: path - schema: - type: string - responses: - '200': - description: '' - tags: - - Thresholds - /users/{id}: - get: - operationId: UsersController_getUserById - parameters: - - name: id - required: true - in: path - schema: - type: string - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UserResponseDto' - summary: Get user details by id - tags: - - Users - patch: - operationId: UsersController_updateUser - parameters: - - name: id - required: true - in: path - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserRequestDto' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserResponseDto' - summary: Update user in tenant - tags: - - Users - /users: - get: - operationId: UsersController_getUsers - parameters: [] - responses: - '200': - description: '' - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/UserResponseDto' - summary: Get users for tenant - tags: - - Users - post: - operationId: UsersController_createUser - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserRequestDto' - responses: - '201': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserResponseDto' - summary: Create user in tenant - tags: - - Users - /users/bulk-delete: - post: - operationId: UsersController_deleteUsers - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteUserRequestDto' - responses: - '200': - description: '' - content: - application/json: - schema: - $ref: '#/components/schemas/DeleteUserResponseDto' - summary: Delete users by ids - tags: - - Users -info: - title: NoTIP Management API - description: NoTIP Management API OpenAPI specification - version: 1.2.0 - contact: {} -tags: [] -servers: [] -components: - securitySchemes: - bearer: - scheme: bearer - bearerFormat: JWT - type: http - schemas: - AddGatewayRequestDto: - type: object - properties: - factory_id: - type: string - tenant_id: - type: string - factory_key_hash: - type: string - required: - - factory_id - - tenant_id - - factory_key_hash - TenantsResponseDto: - type: object - properties: - id: - type: string - name: - type: string - status: - type: string - enum: - - active - - suspended - created_at: - format: date-time - type: string - required: - - id - - name - - status - - created_at - CreateTenantRequestDto: - type: object - properties: - name: - type: string - admin_email: - type: string - admin_name: - type: string - admin_password: - type: string - example: Temp_1234_A! - required: - - name - - admin_email - - admin_name - - admin_password - UpdateTenantRequestDto: - type: object - properties: - name: - type: string - status: - type: string - enum: - - active - - suspended - suspension_interval_days: - type: number - required: - - name - - status - - suspension_interval_days - DeleteTenantResponseDto: - type: object - properties: - message: - type: string - required: - - message - SetAlertsConfigDefaultRequestDto: - type: object - properties: - tenant_unreachable_timeout_ms: - type: number - required: - - tenant_unreachable_timeout_ms - SetGatewayAlertsConfigRequestDto: - type: object - properties: - gateway_unreachable_timeout_ms: - type: number - required: - - gateway_unreachable_timeout_ms - CreateApiClientRequestDto: - type: object - properties: {} - SendConfigRequestDto: - type: object - properties: - send_frequency_ms: - type: number - status: - type: string - CommandResponseDto: - type: object - properties: - command_id: - type: string - status: - type: string - enum: - - queued - - ack - - nack - - expired - - timeout - issued_at: - type: string - format: date-time - required: - - command_id - - status - - issued_at - SendFirmwareRequestDto: - type: object - properties: - firmware_version: - type: string - download_url: - type: string - required: - - firmware_version - - download_url - CommandStatusResponseDto: - type: object - properties: - command_id: - type: string - status: - type: string - enum: - - queued - - ack - - nack - - expired - - timeout - timestamp: - type: string - format: date-time - required: - - command_id - - status - - timestamp - CostResponseDto: - type: object - properties: - storage_gb: - type: number - bandwidth_gb: - type: number - required: - - storage_gb - - bandwidth_gb - GatewayResponseDto: - type: object - properties: - id: - type: string - description: Unique identifier of the gateway - example: 550e8400-e29b-41d4-a716-446655440000 - name: - type: string - description: User-defined name of the gateway - example: Main Entrance Gateway - status: - type: string - description: Current connectivity status - enum: - - gateway_online - - gateway_offline - - gateway_suspended - example: gateway_online - last_seen_at: - type: string - description: Timestamp of the last heart-beat received from the gateway - example: '2024-03-24T10:00:00Z' - format: date-time - provisioned: - type: boolean - description: Whether the gateway has been provisioned/activated - example: true - firmware_version: - type: string - description: Current firmware version installed on the device - example: 1.2.3 - send_frequency_ms: - type: number - description: Configured data sending frequency in milliseconds - example: 30000 - required: - - id - - name - - status - - last_seen_at - - provisioned - - firmware_version - - send_frequency_ms - UpdateGatewayRequestDto: - type: object - properties: - name: - type: string - required: - - name - UpdateGatewayResponseDto: - type: object - properties: - id: - type: string - name: - type: string - status: - type: string - enum: - - gateway_online - - gateway_offline - - gateway_suspended - updated_at: - type: string - format: date-time - required: - - id - - name - - status - - updated_at - KeysResponseDto: - type: object - properties: - gateway_id: - type: string - key_material: - type: string - format: byte - key_version: - type: number - required: - - gateway_id - - key_material - - key_version - SetThresholdDefaultTypeRequestDto: - type: object - properties: - sensor_type: - type: string - min_value: - type: number - max_value: - type: number - required: - - sensor_type - - min_value - - max_value - SetThresholdSensorRequestDto: - type: object - properties: - min_value: - type: number - max_value: - type: number - sensor_type: - type: string - required: - - min_value - - max_value - - sensor_type - UserResponseDto: - type: object - properties: - id: - type: string - name: - type: string - email: - type: string - role: - type: string - enum: - - system_admin - - tenant_admin - - tenant_user - last_access: - type: string - format: date-time - nullable: true - required: - - id - - name - - email - - role - - last_access - CreateUserRequestDto: - type: object - properties: - name: - type: string - email: - type: string - role: - type: string - enum: - - system_admin - - tenant_admin - - tenant_user - password: - type: string - required: - - name - - email - - role - - password - CreateUserResponseDto: - type: object - properties: - id: - type: string - name: - type: string - email: - type: string - role: - type: string - enum: - - system_admin - - tenant_admin - - tenant_user - created_at: - type: string - format: date-time - required: - - id - - name - - email - - role - - created_at - UpdateUserRequestDto: - type: object - properties: - name: - type: string - email: - type: string - role: - type: string - enum: - - system_admin - - tenant_admin - - tenant_user - permissions: - type: array - items: - type: string - required: - - name - - email - - role - - permissions - UpdateUserResponseDto: - type: object - properties: - id: - type: string - name: - type: string - email: - type: string - role: - type: string - enum: - - system_admin - - tenant_admin - - tenant_user - updated_at: - type: string - format: date-time - required: - - id - - name - - email - - role - - updated_at - DeleteUserRequestDto: - type: object - properties: - ids: - type: array - items: - type: string - required: - - ids - DeleteUserResponseDto: - type: object - properties: - deleted: - type: number - failed: - type: array - items: - type: string - required: - - deleted - - failed diff --git a/package-lock.json b/package-lock.json index 834f39a..9f33bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.0.0", "@nestjs/typeorm": "^11.0.0", + "class-validator": "^0.14.4", + "nats": "^2.29.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "typeorm": "^0.3.28" @@ -115,13 +118,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", - "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.22.tgz", + "integrity": "sha512-tvfu5jhem1o8qidVxvXe5KfCij65ioMLCOFA947DD+zb3yTl5pJyDm2dqzbOehuQw0fmH4XPQukRJsCUy+UwaA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.22", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -134,14 +137,14 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", - "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.22.tgz", + "integrity": "sha512-6BvkxDz4nV8B6Ha4n/pYZ503vXgLxMaEpcKsFDao1sl0iSwrIOphlIS1yWprlGdCThIM3aJref1JU13ZvEcBCA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/core": "19.2.22", + "@angular-devkit/schematics": "19.2.22", "@inquirer/prompts": "7.3.2", "ansi-colors": "4.1.3", "symbol-observable": "4.0.0", @@ -157,13 +160,13 @@ } }, "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.22.tgz", + "integrity": "sha512-OqN/Ded+ZKypPZN5+qUFwtnKGl7FKpxJXYO2Vts5vLBojY5goCZd9SGW1CyXeuPnisRUW+vjqBQbWYuEUh36Tw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", @@ -214,23 +217,6 @@ } } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -249,6 +235,24 @@ } } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics-cli/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -262,6 +266,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -283,13 +303,13 @@ } }, "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.22.tgz", + "integrity": "sha512-OqN/Ded+ZKypPZN5+qUFwtnKGl7FKpxJXYO2Vts5vLBojY5goCZd9SGW1CyXeuPnisRUW+vjqBQbWYuEUh36Tw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", @@ -310,23 +330,6 @@ } } }, - "node_modules/@angular-devkit/schematics/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@angular-devkit/schematics/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -345,6 +348,24 @@ } } }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -358,6 +379,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -629,37 +666,14 @@ } }, "node_modules/@asyncapi/generator-components": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@asyncapi/generator-components/-/generator-components-0.5.0.tgz", - "integrity": "sha512-yZo/zF4jE5KzvFpC5sYxI8vBkBZiU9hw68H7nUeghnuDTFOuAPoMesPTC/TiGBHn1AxFnyoq+ALhpFjPQuqSYQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@asyncapi/generator-components/-/generator-components-1.0.0.tgz", + "integrity": "sha512-6yf1yUjmSJEwKLiDurwWnY7CQz2RfbdH7gmGpTPJWyqBjqklxyyWMqKLDbluNABwwHdttsDqK56TyJ/hHsR0Zw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@asyncapi/generator-helpers": "*", - "@asyncapi/generator-react-sdk": "*", - "@asyncapi/modelina": "^5.10.1" - } - }, - "node_modules/@asyncapi/generator-components/node_modules/@asyncapi/modelina": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/@asyncapi/modelina/-/modelina-5.10.1.tgz", - "integrity": "sha512-mvk77+ls2ia+w3uQftJ7s6/Yid4lO+1IgbTkJ94mGSV9Qqk1n+ln5dz2snccARI5ubdy3ofKb3QP2Dq/OGeH8A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.1.0", - "@apidevtools/swagger-parser": "^10.1.0", - "@asyncapi/multi-parser": "^2.2.0", - "@asyncapi/parser": "^3.4.0", - "alterschema": "^1.1.2", - "change-case": "^4.1.2", - "fast-xml-parser": "^5.3.0", - "js-yaml": "^4.1.0", - "openapi-types": "^12.1.3", - "typescript-json-schema": "^0.58.1" - }, - "engines": { - "node": ">=18" + "@asyncapi/generator-react-sdk": "^1.1.2", + "@asyncapi/modelina": "^4.0.0-next.62" } }, "node_modules/@asyncapi/generator-helpers": { @@ -918,9 +932,9 @@ } }, "node_modules/@asyncapi/modelina-cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -5741,9 +5755,9 @@ } }, "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -6176,15 +6190,15 @@ } }, "node_modules/@nestjs/cli": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", - "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.17.tgz", + "integrity": "sha512-tOMgoB9k+Zb2WdKYPhbhceROLcDR1BFQZWfkBOGMRgBTo8rnC125E65UvThEA77vp4w+zKjqiSIv0leT+wdpHg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", - "@angular-devkit/schematics-cli": "19.2.19", + "@angular-devkit/core": "19.2.22", + "@angular-devkit/schematics": "19.2.22", + "@angular-devkit/schematics-cli": "19.2.22", "@inquirer/prompts": "7.10.1", "@nestjs/schematics": "^11.0.1", "ansis": "4.2.0", @@ -6192,13 +6206,13 @@ "cli-table3": "0.6.5", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.1.0", - "glob": "13.0.0", + "glob": "13.0.6", "node-emoji": "1.11.0", "ora": "5.4.1", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0", "typescript": "5.9.3", - "webpack": "5.104.1", + "webpack": "5.105.4", "webpack-node-externals": "3.0.0" }, "bin": { @@ -6221,13 +6235,13 @@ } }, "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.22.tgz", + "integrity": "sha512-OqN/Ded+ZKypPZN5+qUFwtnKGl7FKpxJXYO2Vts5vLBojY5goCZd9SGW1CyXeuPnisRUW+vjqBQbWYuEUh36Tw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", @@ -6248,30 +6262,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nestjs/cli/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@nestjs/cli/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -6316,53 +6306,6 @@ "node": ">= 6" } }, - "node_modules/@nestjs/cli/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@nestjs/cli/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nestjs/cli/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@nestjs/cli/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -6400,44 +6343,6 @@ "tslib": "^2.1.0" } }, - "node_modules/@nestjs/cli/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@nestjs/cli/node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@nestjs/cli/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -6448,55 +6353,6 @@ "node": ">= 8" } }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/@nestjs/common": { "version": "11.1.17", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", @@ -6528,6 +6384,33 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz", + "integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==", + "license": "MIT", + "dependencies": { + "dotenv": "17.2.3", + "dotenv-expand": "12.0.3", + "lodash": "4.17.23" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@nestjs/core": { "version": "11.1.17", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz", @@ -6611,15 +6494,15 @@ } }, "node_modules/@nestjs/schematics": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", - "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.10.tgz", + "integrity": "sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.17", - "@angular-devkit/schematics": "19.2.17", - "comment-json": "4.4.1", + "@angular-devkit/core": "19.2.23", + "@angular-devkit/schematics": "19.2.23", + "comment-json": "4.6.2", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, @@ -6628,16 +6511,16 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", - "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "version": "19.2.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.23.tgz", + "integrity": "sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -6656,13 +6539,13 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", - "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "version": "19.2.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.23.tgz", + "integrity": "sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.17", + "@angular-devkit/core": "19.2.23", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -6674,23 +6557,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@nestjs/schematics/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@nestjs/schematics/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -6709,10 +6575,28 @@ } } }, + "node_modules/@nestjs/schematics/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nestjs/schematics/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6722,6 +6606,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@nestjs/schematics/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nestjs/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -7127,9 +7027,9 @@ } }, "node_modules/@npmcli/arborist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7259,9 +7159,9 @@ } }, "node_modules/@npmcli/config/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7570,9 +7470,9 @@ } }, "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7786,9 +7686,9 @@ } }, "node_modules/@oclif/core/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8610,9 +8510,9 @@ } }, "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -13517,6 +13417,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -13740,9 +13646,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15221,9 +15127,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -15416,9 +15322,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -15799,6 +15705,17 @@ "dev": true, "license": "MIT" }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -16140,14 +16057,13 @@ } }, "node_modules/comment-json": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", - "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", "dev": true, "license": "MIT", "dependencies": { "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", "esprima": "^4.0.1" }, "engines": { @@ -17096,6 +17012,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/driver.js": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", @@ -18319,9 +18250,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -19266,18 +19197,18 @@ "license": "ISC" }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -19314,9 +19245,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19327,13 +19258,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -19485,9 +19416,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19895,9 +19826,9 @@ } }, "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -21155,9 +21086,9 @@ } }, "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -21327,9 +21258,9 @@ } }, "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -21391,9 +21322,9 @@ } }, "node_modules/jest-message-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -21560,9 +21491,9 @@ } }, "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -21704,9 +21635,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -22291,6 +22222,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -23474,6 +23411,18 @@ "url": "https://opencollective.com/napi-postinstall" } }, + "node_modules/nats": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz", + "integrity": "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==", + "license": "Apache-2.0", + "dependencies": { + "nkeys.js": "1.1.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -23602,6 +23551,18 @@ "lodash.topath": "^4.5.2" } }, + "node_modules/nkeys.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz", + "integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==", + "license": "Apache-2.0", + "dependencies": { + "tweetnacl": "1.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -24101,9 +24062,9 @@ } }, "node_modules/npm-packlist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -26936,9 +26897,9 @@ } }, "node_modules/oclif/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27955,9 +27916,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -28922,9 +28883,9 @@ } }, "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -32092,9 +32053,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -32511,6 +32472,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -32747,9 +32714,9 @@ } }, "node_modules/typeorm/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -33532,6 +33499,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -33704,7 +33680,6 @@ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -33773,8 +33748,7 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", @@ -33782,7 +33756,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -33797,7 +33770,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -33808,7 +33780,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -33819,7 +33790,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -33833,7 +33803,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index 973d365..df928f0 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,13 @@ }, "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.0.0", "@nestjs/typeorm": "^11.0.0", + "class-validator": "^0.14.4", + "nats": "^2.29.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "typeorm": "^0.3.28" diff --git a/src/app.module.ts b/src/app.module.ts index 949355c..ec2cae0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,52 +1,41 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MeasureModule } from './data-api/measure.module'; import { SensorModule } from './data-api/sensor.module'; +import { validate } from './env.validation'; -function parseSslFlag(value?: string): boolean { - return value === 'true' || value === '1'; -} - -function buildTypeOrmOptions(): TypeOrmModuleOptions { - const { - MEASURES_DB_HOST, - MEASURES_DB_PORT, - MEASURES_DB_USER, - MEASURES_DB_PASSWORD, - MEASURES_DB_NAME, - DB_SSL, - } = process.env; - - if ( - !MEASURES_DB_HOST || - !MEASURES_DB_PORT || - !MEASURES_DB_USER || - !MEASURES_DB_NAME - ) { - throw new Error( - 'Missing TypeORM database configuration. Set MEASURES_DB_HOST, MEASURES_DB_PORT, MEASURES_DB_USER and MEASURES_DB_NAME.', - ); - } - - return { - type: 'postgres' as const, - host: MEASURES_DB_HOST, - port: Number.parseInt(MEASURES_DB_PORT, 10), - username: MEASURES_DB_USER, - password: MEASURES_DB_PASSWORD, - database: MEASURES_DB_NAME, - ssl: parseSslFlag(DB_SSL), - autoLoadEntities: true, - }; -} +const databaseImports = + process.env.NODE_ENV === 'test' + ? [] + : [ + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService): TypeOrmModuleOptions => { + return { + type: 'postgres', + host: configService.get('MEASURES_DB_HOST'), + port: configService.get('MEASURES_DB_PORT'), + username: configService.get('MEASURES_DB_USER'), + password: configService.get('MEASURES_DB_PASSWORD'), + database: configService.get('MEASURES_DB_NAME'), + ssl: configService.get('DB_SSL'), + autoLoadEntities: true, + }; + }, + }), + ]; @Module({ imports: [ - TypeOrmModule.forRootAsync({ - useFactory: () => buildTypeOrmOptions(), + ConfigModule.forRoot({ + isGlobal: true, + validate, + expandVariables: true, }), + ...databaseImports, MeasureModule, SensorModule, ], diff --git a/src/data-api/controller/measure.controller.spec.ts b/src/data-api/controller/measure.controller.spec.ts index 16036e1..456c135 100644 --- a/src/data-api/controller/measure.controller.spec.ts +++ b/src/data-api/controller/measure.controller.spec.ts @@ -7,6 +7,7 @@ import { PaginatedQueryModel } from '../models/paginated-query.model'; import { QueryResponseDto } from '../dto/query.response.dto'; import { EncryptedEnvelopeModel } from '../models/encrypted-envelope.model'; import { EncryptedEnvelopeDto } from '../dto/encrypted-envelope.dto'; +import { firstValueFrom, of } from 'rxjs'; describe('MeasureController', () => { let controller: MeasureController; @@ -123,7 +124,7 @@ describe('MeasureController', () => { expect(result).toEqual([queryResponseDto]); }); - it('should use default limit = 1000 when limit is not provided', async () => { + it('should use default limit = 999 when limit is not provided', async () => { const from = '2024-01-01T00:00:00Z'; const to = '2024-01-02T00:00:00Z'; @@ -159,7 +160,7 @@ describe('MeasureController', () => { expect(serviceQueryMock).toHaveBeenCalledWith({ from, to, - limit: 1000, + limit: 999, gatewayId: undefined, sensorId: undefined, sensorType: undefined, @@ -308,4 +309,94 @@ describe('MeasureController', () => { }); }); }); + + describe('stream', () => { + it('should pass filters, since and JWT context to the stream listener', async () => { + const payload = Buffer.from( + JSON.stringify({ + tenantId: 'tenant-1', + exp: Math.floor( + new Date('2026-03-23T10:05:00.000Z').getTime() / 1000, + ), + }), + ).toString('base64url'); + + mockStreamListenerService.stream.mockReturnValue( + of({ + kind: 'data', + data: { + gatewayId: 'gw-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }, + }), + ); + + const result = await firstValueFrom( + controller.stream( + { + headers: { + authorization: `Bearer header.${payload}.signature`, + }, + } as never, + 'gw-1' as never, + 'sensor-1' as never, + 'temperature' as never, + '2026-03-23T09:50:00.000Z', + ), + ); + + expect(mockStreamListenerService.stream).toHaveBeenCalledWith({ + gatewayId: ['gw-1'], + sensorId: ['sensor-1'], + sensorType: ['temperature'], + since: '2026-03-23T09:50:00.000Z', + tenantId: 'tenant-1', + tokenExpiresAt: new Date('2026-03-23T10:05:00.000Z').getTime(), + }); + expect(result).toEqual({ + data: { + gatewayId: 'gw-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }, + }); + }); + + it('should map token_expired to an SSE error event', async () => { + mockStreamListenerService.stream.mockReturnValue( + of({ + kind: 'error', + reason: 'token_expired', + }), + ); + + const result = await firstValueFrom( + controller.stream( + { headers: {} } as never, + undefined, + undefined, + undefined, + undefined, + ), + ); + + expect(result).toEqual({ + data: { + type: 'error', + reason: 'token_expired', + }, + }); + }); + }); }); diff --git a/src/data-api/controller/measure.controller.ts b/src/data-api/controller/measure.controller.ts index 97de5fa..6813d4e 100644 --- a/src/data-api/controller/measure.controller.ts +++ b/src/data-api/controller/measure.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Query, Sse, MessageEvent } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Query, Sse, MessageEvent, Req } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { MeasureService } from './../services/measure.service'; import { QueryResponseDto } from './../dto/query.response.dto'; import { EncryptedEnvelopeDto } from './../dto/encrypted-envelope.dto'; @@ -8,14 +8,33 @@ import { QueryInput } from './../interfaces/query.input'; import { ExportInput } from './../interfaces/export.input'; import { Observable, map } from 'rxjs'; import { StreamInput } from '../interfaces/stream.input'; -import { StreamListenerService } from '../services/stream-listener.service'; -import { EncryptedEnvelopeModel } from '../models/encrypted-envelope.model'; +import { + StreamEmission, + StreamListenerService, +} from '../services/stream-listener.service'; +import type { Request } from 'express'; import { ApiMeasureExportDocs, ApiMeasureQueryDocs, ApiMeasureStreamDocs, } from '../openapi.decorators'; +const DEFAULT_QUERY_LIMIT = 999; + +function normalizeLimit(value?: number | string): number { + if (value === undefined || value === null || value === '') { + return DEFAULT_QUERY_LIMIT; + } + + if (typeof value === 'number') { + return value; + } + + const parsed = Number.parseInt(value, 10); + + return Number.isNaN(parsed) ? DEFAULT_QUERY_LIMIT : parsed; +} + function normalizeArrayParam(value?: string | string[]): string[] | undefined { if (value === undefined) { return undefined; @@ -24,6 +43,66 @@ function normalizeArrayParam(value?: string | string[]): string[] | undefined { return Array.isArray(value) ? value : [value]; } +function parseBearerToken(authorization?: string): string | undefined { + if (!authorization?.startsWith('Bearer ')) { + return undefined; + } + + return authorization.slice('Bearer '.length); +} + +function decodeBase64Url(value: string): string | undefined { + const normalized = value.replaceAll('-', '+').replaceAll('_', '/'); + const padding = (4 - (normalized.length % 4)) % 4; + + try { + return Buffer.from(normalized + '='.repeat(padding), 'base64').toString( + 'utf8', + ); + } catch { + return undefined; + } +} + +function extractJwtContext(authorization?: string): { + tenantId?: string; + tokenExpiresAt?: number; +} { + const token = parseBearerToken(authorization); + + if (!token) { + return {}; + } + + const [, payload] = token.split('.'); + + if (!payload) { + return {}; + } + + const decoded = decodeBase64Url(payload); + + if (!decoded) { + return {}; + } + + try { + const parsed = JSON.parse(decoded) as { + tenantId?: string; + tenant_id?: string; + exp?: number; + }; + + return { + tenantId: parsed.tenantId ?? parsed.tenant_id, + tokenExpiresAt: + typeof parsed.exp === 'number' ? parsed.exp * 1000 : undefined, + }; + } catch { + return {}; + } +} + @ApiTags('measures') @Controller('measures') export class MeasureController { @@ -33,11 +112,30 @@ export class MeasureController { ) {} @ApiMeasureQueryDocs() + @ApiOperation({ summary: 'Query encrypted measures with pagination' }) + @ApiResponse({ + status: 200, + description: 'Paginated query results', + type: QueryResponseDto, + }) + @ApiResponse({ + status: 400, + description: + 'Bad Request - windows larger than 24h or limit greater than or equal to 1000', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden', + }) @Get('query') async query( @Query('from') from: string, @Query('to') to: string, - @Query('limit') limit: string, + @Query('limit') limit?: number | string, @Query('gatewayId') gatewayId?: string | string[], @Query('sensorId') sensorId?: string | string[], @Query('sensorType') sensorType?: string | string[], @@ -46,7 +144,7 @@ export class MeasureController { const input: QueryInput = { from, to, - limit: limit ? Number(limit) : 1000, + limit: normalizeLimit(limit), gatewayId: normalizeArrayParam(gatewayId), sensorId: normalizeArrayParam(sensorId), sensorType: normalizeArrayParam(sensorType), @@ -59,26 +157,73 @@ export class MeasureController { @ApiMeasureStreamDocs() @Sse('stream') + @ApiOperation({ summary: 'Stream encrypted measures in real-time' }) + @ApiResponse({ + status: 200, + description: 'Stream of encrypted measures', + type: EncryptedEnvelopeDto, + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden', + }) stream( + @Req() request: Request, @Query('gatewayId') gatewayId?: string | string[], @Query('sensorId') sensorId?: string | string[], @Query('sensorType') sensorType?: string | string[], + @Query('since') since?: string, ): Observable { + const jwtContext = extractJwtContext(request.headers.authorization); const input: StreamInput = { gatewayId: normalizeArrayParam(gatewayId), sensorId: normalizeArrayParam(sensorId), sensorType: normalizeArrayParam(sensorType), + since, + tenantId: jwtContext.tenantId, + tokenExpiresAt: jwtContext.tokenExpiresAt, }; return this.sl.stream(input).pipe( - map((model: EncryptedEnvelopeModel) => ({ - data: MeasureMapper.toEncryptedEnvelopeDto(model), - })), + map((event: StreamEmission) => + event.kind === 'error' + ? { + data: { + type: 'error', + reason: event.reason, + }, + } + : { + data: MeasureMapper.toEncryptedEnvelopeDto(event.data), + }, + ), ); } @ApiMeasureExportDocs() @Get('export') + @ApiOperation({ summary: 'Export encrypted measures without pagination' }) + @ApiResponse({ + status: 200, + description: 'Exported encrypted measures', + type: [EncryptedEnvelopeDto], + }) + @ApiResponse({ + status: 400, + description: 'Bad Request - windows larger than 24h', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden', + }) async export( @Query('from') from: string, @Query('to') to: string, diff --git a/src/data-api/controller/sensor.controller.ts b/src/data-api/controller/sensor.controller.ts index e7c674a..f338ab8 100644 --- a/src/data-api/controller/sensor.controller.ts +++ b/src/data-api/controller/sensor.controller.ts @@ -1,18 +1,36 @@ import { Controller, Query, Get } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SensorService } from '../services/sensor.service'; import { SensorDto } from '../dto/sensor.dto'; import { MeasureMapper } from '../measure.mapper'; import { GetSensorsInput } from '../interfaces/get-sensors.input'; import { ApiSensorListDocs } from '../openapi.decorators'; -@ApiTags('sensor') +@ApiTags('sensors') @Controller('sensor') export class SensorController { constructor(private readonly ss: SensorService) {} @ApiSensorListDocs() @Get() + @ApiOperation({ summary: 'Get list of sensors with optional gateway filter' }) + @ApiResponse({ + status: 200, + description: 'List of sensors', + type: [SensorDto], + }) + @ApiResponse({ + status: 400, + description: 'Bad Request - invalid gatewayId format', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized', + }) + @ApiResponse({ + status: 403, + description: 'Forbidden', + }) async getSensors( @Query('gatewayId') gatewayId?: string, ): Promise { diff --git a/src/data-api/dto/error-response.dto.ts b/src/data-api/dto/error-response.dto.ts index b5186e7..c075b9a 100644 --- a/src/data-api/dto/error-response.dto.ts +++ b/src/data-api/dto/error-response.dto.ts @@ -15,7 +15,7 @@ export class ErrorResponseDto { @ApiPropertyOptional({ description: 'Human-readable error message', - example: 'limit must be less than or equal to 1000', + example: 'limit must be less than 1000', }) message?: string; diff --git a/src/data-api/interfaces/np-query-persistence.input.ts b/src/data-api/interfaces/np-query-persistence.input.ts index 878841d..fdd180c 100644 --- a/src/data-api/interfaces/np-query-persistence.input.ts +++ b/src/data-api/interfaces/np-query-persistence.input.ts @@ -1,4 +1,5 @@ export interface NpQueryPersistenceInput { + tenantId?: string; gatewayId?: string[]; sensorId?: string[]; sensorType?: string[]; diff --git a/src/data-api/interfaces/stream.input.ts b/src/data-api/interfaces/stream.input.ts index 71dd037..a72b2ce 100644 --- a/src/data-api/interfaces/stream.input.ts +++ b/src/data-api/interfaces/stream.input.ts @@ -2,4 +2,7 @@ export interface StreamInput { gatewayId?: string[]; sensorId?: string[]; sensorType?: string[]; + since?: string; + tenantId?: string; + tokenExpiresAt?: number; } diff --git a/src/data-api/measure.module.ts b/src/data-api/measure.module.ts index 9e38620..996e645 100644 --- a/src/data-api/measure.module.ts +++ b/src/data-api/measure.module.ts @@ -4,10 +4,17 @@ import { MeasureController } from './controller/measure.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MeasureEntity } from './entity/measure.entity'; import { MeasurePersistenceService } from './services/measure.persistence.service'; +import { StreamListenerService } from './services/stream-listener.service'; +import { TelemetryStreamBridgeService } from './services/telemetry-stream-bridge.service'; @Module({ imports: [TypeOrmModule.forFeature([MeasureEntity])], controllers: [MeasureController], - providers: [MeasureService, MeasurePersistenceService], + providers: [ + MeasureService, + MeasurePersistenceService, + StreamListenerService, + TelemetryStreamBridgeService, + ], }) export class MeasureModule {} diff --git a/src/data-api/openapi.decorators.ts b/src/data-api/openapi.decorators.ts index 47aa9c7..48cb114 100644 --- a/src/data-api/openapi.decorators.ts +++ b/src/data-api/openapi.decorators.ts @@ -115,8 +115,8 @@ export function ApiMeasureQueryDocs() { ApiQuery({ name: 'limit', required: false, - description: 'Page size. Defaults to 1000 and cannot exceed 1000.', - schema: { type: 'integer', minimum: 1, maximum: 1000, default: 1000 }, + description: 'Page size. Defaults to 999 and must be less than 1000.', + schema: { type: 'integer', minimum: 1, maximum: 999, default: 999 }, example: 100, }), apiMeasureFilterQueries('query'), @@ -146,6 +146,14 @@ export function ApiMeasureStreamDocs() { 'Streams matching encrypted measures as server-sent events using the text/event-stream media type.', }), ApiProduces('text/event-stream'), + ApiQuery({ + name: 'since', + required: false, + description: + 'Optional timestamp used to replay historical measures before switching to real-time events', + schema: { type: 'string', format: 'date-time' }, + example: '2026-03-23T09:50:00.000Z', + }), apiMeasureFilterQueries('stream'), ApiOkResponse({ description: 'Server-sent event stream containing encrypted measures', @@ -153,7 +161,7 @@ export function ApiMeasureStreamDocs() { 'text/event-stream': { schema: { type: 'string' }, example: - 'data: {"gatewayId":"gw-1","sensorId":"sensor-1","sensorType":"temperature","timestamp":"2026-03-23T09:58:00.000Z","encryptedData":"enc-3","iv":"iv-3","authTag":"tag-3","keyVersion":1}\n\n', + 'data: {"gatewayId":"gw-1","sensorId":"sensor-1","sensorType":"temperature","timestamp":"2026-03-23T09:58:00.000Z","encryptedData":"enc-3","iv":"iv-3","authTag":"tag-3","keyVersion":1}\n\ndata: {"type":"error","reason":"token_expired"}\n\n', }, }, }), diff --git a/src/data-api/services/measure.persistence.service.spec.ts b/src/data-api/services/measure.persistence.service.spec.ts index e302128..eb0cbfb 100644 --- a/src/data-api/services/measure.persistence.service.spec.ts +++ b/src/data-api/services/measure.persistence.service.spec.ts @@ -69,17 +69,25 @@ describe('MeasurePersistenceService', () => { }); expect(repository.createQueryBuilder).toHaveBeenCalledWith('m'); - expect(qb.andWhere).toHaveBeenNthCalledWith(1, 'm.gatewayId = :gatewayId', { - gatewayId: ['gw-1'], - }); - expect(qb.andWhere).toHaveBeenNthCalledWith(2, 'm.sensorId = :sensorId', { - sensorId: ['sensor-1'], - }); + expect(qb.andWhere).toHaveBeenNthCalledWith( + 1, + 'm.gatewayId IN (:...gatewayIds)', + { + gatewayIds: ['gw-1'], + }, + ); + expect(qb.andWhere).toHaveBeenNthCalledWith( + 2, + 'm.sensorId IN (:...sensorIds)', + { + sensorIds: ['sensor-1'], + }, + ); expect(qb.andWhere).toHaveBeenNthCalledWith( 3, - 'm.sensorType = :sensorType', + 'm.sensorType IN (:...sensorTypes)', { - sensorType: ['temperature'], + sensorTypes: ['temperature'], }, ); expect(qb.andWhere).toHaveBeenNthCalledWith(4, 'm.time >= :from', { @@ -100,6 +108,47 @@ describe('MeasurePersistenceService', () => { }); }); + it('builds a non paginated query with array filters', async () => { + const qb = createQueryBuilder(); + qb.getMany.mockResolvedValue([]); + + const repository = { + createQueryBuilder: jest.fn().mockReturnValue(qb), + }; + + const service = new MeasurePersistenceService(repository as never); + + await service.nonPaginatedQuery({ + gatewayId: ['gw-1', 'gw-2'], + sensorId: ['sensor-1'], + sensorType: ['temperature', 'humidity'], + from: '2026-03-23T09:50:00.000Z', + to: '2026-03-23T10:00:00.000Z', + }); + + expect(qb.andWhere).toHaveBeenNthCalledWith( + 1, + 'm.gatewayId IN (:...gatewayIds)', + { + gatewayIds: ['gw-1', 'gw-2'], + }, + ); + expect(qb.andWhere).toHaveBeenNthCalledWith( + 2, + 'm.sensorId IN (:...sensorIds)', + { + sensorIds: ['sensor-1'], + }, + ); + expect(qb.andWhere).toHaveBeenNthCalledWith( + 3, + 'm.sensorType IN (:...sensorTypes)', + { + sensorTypes: ['temperature', 'humidity'], + }, + ); + }); + it('builds a non paginated query without optional filters', async () => { const qb = createQueryBuilder(); const rows: MeasureEntity[] = [ diff --git a/src/data-api/services/measure.persistence.service.ts b/src/data-api/services/measure.persistence.service.ts index 71733cb..1e82230 100644 --- a/src/data-api/services/measure.persistence.service.ts +++ b/src/data-api/services/measure.persistence.service.ts @@ -3,10 +3,40 @@ import { InjectRepository } from '@nestjs/typeorm'; import { MeasureEntity } from './../entity/measure.entity'; import { NpQueryPersistenceInput } from './../interfaces/np-query-persistence.input'; import { PQueryPersistenceInput } from './../interfaces/p-query-persistence.input'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { PaginatedQuery } from './../interfaces/paginated-query'; import { NpQueryPersistenceService } from '../interfaces/np-query-persistence.service'; +function applyScalarFilter( + qb: SelectQueryBuilder, + column: string, + parameterName: string, + value?: string, +): void { + if (!value) { + return; + } + + qb.andWhere(`${column} = :${parameterName}`, { + [parameterName]: value, + }); +} + +function applyArrayFilter( + qb: SelectQueryBuilder, + column: string, + parameterName: string, + values?: string[], +): void { + if (!values?.length) { + return; + } + + qb.andWhere(`${column} IN (:...${parameterName})`, { + [parameterName]: values, + }); +} + @Injectable() export class MeasurePersistenceService implements NpQueryPersistenceService { constructor( @@ -17,17 +47,10 @@ export class MeasurePersistenceService implements NpQueryPersistenceService { async paginatedQuery(p: PQueryPersistenceInput): Promise { const qb = this.r.createQueryBuilder('m'); - if (p.gatewayId) { - qb.andWhere('m.gatewayId = :gatewayId', { gatewayId: p.gatewayId }); - } - - if (p.sensorId) { - qb.andWhere('m.sensorId = :sensorId', { sensorId: p.sensorId }); - } - - if (p.sensorType) { - qb.andWhere('m.sensorType = :sensorType', { sensorType: p.sensorType }); - } + applyScalarFilter(qb, 'm.tenantId', 'tenantId', p.tenantId); + applyArrayFilter(qb, 'm.gatewayId', 'gatewayIds', p.gatewayId); + applyArrayFilter(qb, 'm.sensorId', 'sensorIds', p.sensorId); + applyArrayFilter(qb, 'm.sensorType', 'sensorTypes', p.sensorType); qb.andWhere('m.time >= :from', { from: p.from }); qb.andWhere('m.time <= :to', { to: p.to }); @@ -57,17 +80,10 @@ export class MeasurePersistenceService implements NpQueryPersistenceService { ): Promise { const qb = this.r.createQueryBuilder('m'); - if (n.gatewayId) { - qb.andWhere('m.gatewayId = :gatewayId', { gatewayId: n.gatewayId }); - } - - if (n.sensorId) { - qb.andWhere('m.sensorId = :sensorId', { sensorId: n.sensorId }); - } - - if (n.sensorType) { - qb.andWhere('m.sensorType = :sensorType', { sensorType: n.sensorType }); - } + applyScalarFilter(qb, 'm.tenantId', 'tenantId', n.tenantId); + applyArrayFilter(qb, 'm.gatewayId', 'gatewayIds', n.gatewayId); + applyArrayFilter(qb, 'm.sensorId', 'sensorIds', n.sensorId); + applyArrayFilter(qb, 'm.sensorType', 'sensorTypes', n.sensorType); qb.andWhere('m.time >= :from', { from: n.from }); qb.andWhere('m.time <= :to', { to: n.to }); diff --git a/src/data-api/services/measure.service.spec.ts b/src/data-api/services/measure.service.spec.ts index b0aa770..5ea344c 100644 --- a/src/data-api/services/measure.service.spec.ts +++ b/src/data-api/services/measure.service.spec.ts @@ -97,16 +97,16 @@ describe('MeasureService', () => { expect(result).toEqual([mappedResult]); }); - it('should throw BadRequestException when limit is greater than 1000', async () => { + it('should throw BadRequestException when limit is greater than or equal to 1000', async () => { await expect( service.query({ ...input, - limit: 1001, + limit: 1000, }), ).rejects.toEqual( new BadRequestException({ code: 'QUERY_LIMIT_EXCEEDED', - message: 'limit must be less than or equal to 1000', + message: 'limit must be less than 1000', }), ); diff --git a/src/data-api/services/measure.service.ts b/src/data-api/services/measure.service.ts index 07c73cf..f5b92d2 100644 --- a/src/data-api/services/measure.service.ts +++ b/src/data-api/services/measure.service.ts @@ -13,7 +13,7 @@ import { PQueryPersistenceInput } from './../interfaces/p-query-persistence.inpu import { MeasureMapper } from './../measure.mapper'; import { NpQueryPersistenceInput } from './../interfaces/np-query-persistence.input'; -const MAX_QUERY_LIMIT = 1000; +const MAX_QUERY_LIMIT = 999; const MAX_WINDOW_MS = 24 * 60 * 60 * 1000; type ServiceError = { @@ -174,7 +174,7 @@ export class MeasureService { if (input.limit > MAX_QUERY_LIMIT) { throw new BadRequestException({ code: 'QUERY_LIMIT_EXCEEDED', - message: 'limit must be less than or equal to 1000', + message: 'limit must be less than 1000', }); } diff --git a/src/data-api/services/stream-listener.service.spec.ts b/src/data-api/services/stream-listener.service.spec.ts index 030a5d4..b21541a 100644 --- a/src/data-api/services/stream-listener.service.spec.ts +++ b/src/data-api/services/stream-listener.service.spec.ts @@ -1,55 +1,148 @@ import { firstValueFrom, take } from 'rxjs'; import { StreamListenerService } from './stream-listener.service'; +import { MeasurePersistenceService } from './measure.persistence.service'; +import { MeasureEntity } from '../entity/measure.entity'; describe('StreamListenerService', () => { let service: StreamListenerService; + let persistence: jest.Mocked; + + const historicalMeasure: MeasureEntity = { + time: '2026-03-23T09:58:00.000Z', + tenantId: 'tenant-1', + gatewayId: 'gw-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + encryptedData: 'enc-1', + iv: 'iv-1', + authTag: 'tag-1', + keyVersion: 1, + }; beforeEach(() => { jest.useFakeTimers(); jest.setSystemTime(new Date('2026-03-23T10:00:00.000Z')); - service = new StreamListenerService(); + + persistence = { + paginatedQuery: jest.fn(), + nonPaginatedQuery: jest.fn(), + } as unknown as jest.Mocked; + + service = new StreamListenerService(persistence); }); afterEach(() => { jest.useRealTimers(); }); - it('emits sample events that match the requested filters', async () => { - const eventPromise = firstValueFrom( - service - .stream({ + it('replays historical measures when since is provided', async () => { + persistence.nonPaginatedQuery.mockResolvedValue([historicalMeasure]); + + await expect( + firstValueFrom( + service.stream({ + tenantId: 'tenant-1', + since: '2026-03-23T09:50:00.000Z', gatewayId: ['gw-1'], - sensorId: ['sensor-1'], - sensorType: ['temperature'], - }) - .pipe(take(1)), + }), + ), + ).resolves.toEqual({ + kind: 'data', + data: { + gatewayId: 'gw-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T09:58:00.000Z', + encryptedData: 'enc-1', + iv: 'iv-1', + authTag: 'tag-1', + keyVersion: 1, + }, + }); + + expect(persistence.nonPaginatedQuery.mock.calls[0]?.[0]).toEqual({ + tenantId: 'tenant-1', + gatewayId: ['gw-1'], + sensorId: undefined, + sensorType: undefined, + from: '2026-03-23T09:50:00.000Z', + to: '2026-03-23T10:00:00.000Z', + }); + }); + + it('fan-outs live measures only to the matching tenant stream', async () => { + persistence.nonPaginatedQuery.mockResolvedValue([]); + + const eventPromise = firstValueFrom( + service.stream({ tenantId: 'tenant-1' }).pipe(take(1)), ); - jest.advanceTimersByTime(1000); + service.publishLiveMeasure('tenant-2', { + gatewayId: 'gw-2', + sensorId: 'sensor-2', + sensorType: 'humidity', + timestamp: '2026-03-23T10:00:01.000Z', + encryptedData: 'enc-2', + iv: 'iv-2', + authTag: 'tag-2', + keyVersion: 2, + }); - await expect(eventPromise).resolves.toEqual({ + service.publishLiveMeasure('tenant-1', { gatewayId: 'gw-1', sensorId: 'sensor-1', sensorType: 'temperature', - timestamp: '2026-03-23T10:00:01.000Z', - encryptedData: 'encrypted', - iv: 'iv', - authTag: 'tag', - keyVersion: 1, + timestamp: '2026-03-23T10:00:02.000Z', + encryptedData: 'enc-live', + iv: 'iv-live', + authTag: 'tag-live', + keyVersion: 3, + }); + + await expect(eventPromise).resolves.toEqual({ + kind: 'data', + data: { + gatewayId: 'gw-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:02.000Z', + encryptedData: 'enc-live', + iv: 'iv-live', + authTag: 'tag-live', + keyVersion: 3, + }, }); }); - it('filters out events that do not match the requested filters', () => { - const next = jest.fn(); - const subscription = service - .stream({ - gatewayId: ['gw-2'], - }) - .subscribe(next); + it('emits token_expired immediately when the JWT is already expired', async () => { + await expect( + firstValueFrom( + service.stream({ + tenantId: 'tenant-1', + tokenExpiresAt: Date.now() - 1, + }), + ), + ).resolves.toEqual({ + kind: 'error', + reason: 'token_expired', + }); + }); + + it('emits token_expired when the JWT expires during the live stream', async () => { + const eventPromise = firstValueFrom( + service + .stream({ + tenantId: 'tenant-1', + tokenExpiresAt: Date.now() + 1000, + }) + .pipe(take(1)), + ); jest.advanceTimersByTime(1000); - expect(next).not.toHaveBeenCalled(); - subscription.unsubscribe(); + await expect(eventPromise).resolves.toEqual({ + kind: 'error', + reason: 'token_expired', + }); }); }); diff --git a/src/data-api/services/stream-listener.service.ts b/src/data-api/services/stream-listener.service.ts index edac07e..ec77e72 100644 --- a/src/data-api/services/stream-listener.service.ts +++ b/src/data-api/services/stream-listener.service.ts @@ -1,19 +1,141 @@ import { Injectable } from '@nestjs/common'; -import { Observable, interval, map, filter } from 'rxjs'; +import { + concat, + EMPTY, + from, + map, + merge, + mergeMap, + Observable, + of, + Subject, + filter, + take, + takeUntil, + timer, +} from 'rxjs'; import { EncryptedEnvelopeModel } from '../models/encrypted-envelope.model'; import { StreamInput } from '../interfaces/stream.input'; +import { MeasurePersistenceService } from './measure.persistence.service'; +import { MeasureMapper } from '../measure.mapper'; + +export type StreamEmission = + | { + kind: 'data'; + data: EncryptedEnvelopeModel; + } + | { + kind: 'error'; + reason: 'token_expired'; + }; @Injectable() export class StreamListenerService { - stream(input: StreamInput): Observable { - return this.listenToSource().pipe( - filter((event) => this.matchesFilters(event, input)), + private readonly tenantStreams = new Map< + string, + Subject + >(); + + constructor(private readonly mps: MeasurePersistenceService) {} + + stream(input: StreamInput): Observable { + const baseStream = concat( + this.replayHistorical(input), + this.listenToSource(input).pipe( + filter( + (event) => + event.kind === 'error' || this.matchesFilters(event.data, input), + ), + ), ); + + if (this.isTokenExpired(input.tokenExpiresAt)) { + return of({ + kind: 'error', + reason: 'token_expired', + }); + } + + if (!input.tokenExpiresAt) { + return baseStream; + } + + const expiration$ = timer( + Math.max(input.tokenExpiresAt - Date.now(), 0), + ).pipe(take(1)); + + return merge( + baseStream.pipe(takeUntil(expiration$)), + expiration$.pipe( + map( + () => + ({ + kind: 'error', + reason: 'token_expired', + }) satisfies StreamEmission, + ), + ), + ); + } + + publishLiveMeasure(tenantId: string, event: EncryptedEnvelopeModel): void { + this.getTenantStream(tenantId).next(event); } - private listenToSource(): Observable { - return interval(1000).pipe(map(() => this.createSampleEvent())); + private replayHistorical(input: StreamInput): Observable { + if (!input.since) { + return EMPTY; + } + + return from( + this.mps.nonPaginatedQuery({ + tenantId: input.tenantId, + gatewayId: input.gatewayId, + sensorId: input.sensorId, + sensorType: input.sensorType, + from: input.since, + to: new Date().toISOString(), + }), + ).pipe( + map((entities) => MeasureMapper.toEncryptedEnvelopeModels(entities)), + mergeMap((models) => from(models)), + map((model) => ({ kind: 'data', data: model }) satisfies StreamEmission), + ); + } + + private listenToSource(input: StreamInput): Observable { + return new Observable((subscriber) => { + const tenantStream = this.getTenantStream(input.tenantId); + const subscription = tenantStream.subscribe((event) => { + subscriber.next({ + kind: 'data', + data: event, + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }); + } + + private getTenantStream( + tenantId = 'anonymous', + ): Subject { + const existing = this.tenantStreams.get(tenantId); + + if (existing) { + return existing; + } + + const subject = new Subject(); + this.tenantStreams.set(tenantId, subject); + return subject; + } + + private isTokenExpired(tokenExpiresAt?: number): boolean { + return tokenExpiresAt !== undefined && tokenExpiresAt <= Date.now(); } private matchesFilters( @@ -31,17 +153,4 @@ export class StreamListenerService { return matchesGateway && matchesSensor && matchesSensorType; } - - private createSampleEvent(): EncryptedEnvelopeModel { - return { - gatewayId: 'gw-1', - sensorId: 'sensor-1', - sensorType: 'temperature', - timestamp: new Date().toISOString(), - encryptedData: 'encrypted', - iv: 'iv', - authTag: 'tag', - keyVersion: 1, - }; - } } diff --git a/src/data-api/services/telemetry-stream-bridge.service.spec.ts b/src/data-api/services/telemetry-stream-bridge.service.spec.ts new file mode 100644 index 0000000..021161b --- /dev/null +++ b/src/data-api/services/telemetry-stream-bridge.service.spec.ts @@ -0,0 +1,371 @@ +import { Logger } from '@nestjs/common'; +import type { ConnectionOptions, Subscription } from 'nats'; +import { TelemetryStreamBridgeService } from './telemetry-stream-bridge.service'; +import { StreamListenerService } from './stream-listener.service'; + +type TestableTelemetryBridgeService = { + logger: Logger; + buildConnectionOptions: () => ConnectionOptions; + consumeMessages: (subscription: Subscription) => Promise; + extractTenantId: (subject: string) => string | undefined; + parseEnvelope: (data: Uint8Array) => unknown; +}; + +function asTestableService( + service: TelemetryStreamBridgeService, +): TestableTelemetryBridgeService { + return service as unknown as TestableTelemetryBridgeService; +} + +describe('TelemetryStreamBridgeService', () => { + const originalEnv = process.env; + + function loadServiceClass(): typeof TelemetryStreamBridgeService { + let ServiceClass!: typeof TelemetryStreamBridgeService; + jest.isolateModules(() => { + const moduleExports = jest.requireActual< + typeof import('./telemetry-stream-bridge.service') + >('./telemetry-stream-bridge.service'); + ServiceClass = moduleExports.TelemetryStreamBridgeService; + }); + + return ServiceClass; + } + + afterEach(() => { + process.env = originalEnv; + jest.resetModules(); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('subscribes to telemetry subjects and forwards envelopes to the stream listener', async () => { + process.env = { + ...originalEnv, + NODE_ENV: 'development', + NATS_URL: 'nats://localhost:4222', + }; + + let messageConsumed!: () => void; + const consumed = new Promise((resolve) => { + messageConsumed = resolve; + }); + + const receivedMessages = [ + { + subject: 'telemetry.data.tenant-1.gateway-1', + data: Buffer.from( + JSON.stringify({ + gatewayId: 'gateway-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }), + ), + }, + ]; + + const subscription = { + unsubscribe: jest.fn(), + [Symbol.asyncIterator]: async function* () { + await Promise.resolve(); + yield* receivedMessages; + messageConsumed(); + }, + } as unknown as Subscription; + + const subscribe = jest.fn().mockReturnValue(subscription); + const drain = jest.fn().mockResolvedValue(undefined); + const close = jest.fn().mockResolvedValue(undefined); + + jest.doMock('nats', () => ({ + connect: jest.fn().mockResolvedValue({ + subscribe, + drain, + close, + }), + })); + + const publishLiveMeasure = jest.fn(); + const streamListener = { + publishLiveMeasure, + } as unknown as StreamListenerService; + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass(streamListener); + await service.onModuleInit(); + await consumed; + + expect(subscribe).toHaveBeenCalledWith('telemetry.data.*.*'); + expect(publishLiveMeasure).toHaveBeenCalledWith('tenant-1', { + gatewayId: 'gateway-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }); + + await service.onModuleDestroy(); + expect(drain).toHaveBeenCalled(); + expect(close).toHaveBeenCalled(); + }); + + it('skips the NATS subscription bootstrap in test mode', async () => { + process.env = { + ...originalEnv, + NODE_ENV: 'test', + }; + + const connect = jest.fn(); + jest.doMock('nats', () => ({ connect })); + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass({ + publishLiveMeasure: jest.fn(), + } as unknown as StreamListenerService); + + await service.onModuleInit(); + + expect(connect).not.toHaveBeenCalled(); + }); + + it('builds connection options with TLS and token authentication', () => { + process.env = { + ...originalEnv, + NODE_ENV: 'development', + NATS_SERVERS: ' nats://one:4222, nats://two:4222 ', + NATS_CLIENT_NAME: 'custom-client', + NATS_TOKEN: ' token-value ', + NATS_TLS_CA: '/tmp/ca.pem', + NATS_TLS_CERT: '/tmp/cert.pem', + NATS_TLS_KEY: '/tmp/key.pem', + }; + + const readFileSync = jest + .fn() + .mockReturnValueOnce(Buffer.from('ca')) + .mockReturnValueOnce(Buffer.from('cert')) + .mockReturnValueOnce(Buffer.from('key')); + + jest.doMock('node:fs', () => { + const actualFs = jest.requireActual('node:fs'); + return { + ...actualFs, + readFileSync, + }; + }); + jest.doMock('nats', () => ({ + connect: jest.fn(), + })); + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass({ + publishLiveMeasure: jest.fn(), + } as unknown as StreamListenerService); + const testableService = asTestableService(service); + + // Access private method for test + expect(testableService.buildConnectionOptions()).toEqual({ + servers: ['nats://one:4222', 'nats://two:4222'], + name: 'custom-client', + token: 'token-value', + tls: { + ca: [Buffer.from('ca')], + cert: Buffer.from('cert'), + key: Buffer.from('key'), + }, + }); + expect(readFileSync).toHaveBeenCalledTimes(3); + }); + + it('falls back to localhost and user/password auth, logging TLS load errors', () => { + process.env = { + ...originalEnv, + NODE_ENV: 'development', + NATS_USER: ' demo-user ', + NATS_PASSWORD: ' demo-pass ', + NATS_TLS_CA: '/tmp/ca.pem', + NATS_TLS_CERT: '/tmp/cert.pem', + NATS_TLS_KEY: '/tmp/key.pem', + }; + + const readFileSync = jest.fn(() => { + throw new Error('missing cert'); + }); + + jest.doMock('node:fs', () => { + const actualFs = jest.requireActual('node:fs'); + return { + ...actualFs, + readFileSync, + }; + }); + jest.doMock('nats', () => ({ + connect: jest.fn(), + })); + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass({ + publishLiveMeasure: jest.fn(), + } as unknown as StreamListenerService); + const testableService = asTestableService(service); + // Access logger safely + const logger = testableService.logger; + const errorSpy = jest.spyOn(logger, 'error').mockImplementation(); + + expect(testableService.buildConnectionOptions()).toEqual({ + servers: ['nats://localhost:4222'], + name: 'data-api', + user: 'demo-user', + pass: 'demo-pass', + }); + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to load NATS TLS certificates: missing cert', + ); + }); + + it('ignores invalid subjects and malformed envelopes during consumption', async () => { + const publishLiveMeasure = jest.fn(); + jest.doMock('nats', () => ({ + connect: jest.fn(), + })); + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass({ + publishLiveMeasure, + } as unknown as StreamListenerService); + const testableService = asTestableService(service); + // Access logger safely + const logger = testableService.logger; + const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + + const invalidMessages = [ + { + subject: 'telemetry.data.tenant-only', + data: Buffer.from( + JSON.stringify({ + gatewayId: 'gateway-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }), + ), + }, + { + subject: 'telemetry.data.tenant-1.gateway-1', + data: Buffer.from( + JSON.stringify({ + gatewayId: 'gateway-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + }), + ), + }, + { + subject: 'telemetry.data.tenant-1.gateway-1', + data: Buffer.from('{invalid'), + }, + ]; + + const subscription = { + [Symbol.asyncIterator]: async function* () { + // Add await to satisfy require-await + await Promise.resolve(); + yield* invalidMessages; + }, + } as unknown as Subscription; + + await testableService.consumeMessages(subscription); + + expect(publishLiveMeasure).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith( + 'Ignoring telemetry message with invalid envelope', + ); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Ignoring telemetry message with invalid JSON:'), + ); + expect( + testableService.extractTenantId('telemetry.data.tenant-1.gateway-1'), + ).toBe('tenant-1'); + expect( + testableService.extractTenantId('telemetry.data.tenant-1'), + ).toBeUndefined(); + expect( + testableService.parseEnvelope(Buffer.from('{invalid')), + ).toBeUndefined(); + }); + + it('logs bootstrap failures and message processing failures', async () => { + process.env = { + ...originalEnv, + NODE_ENV: 'development', + NATS_URL: 'nats://localhost:4222', + }; + + const connect = jest.fn().mockRejectedValue(new Error('nats unavailable')); + jest.doMock('nats', () => ({ + connect, + })); + + const ServiceClass = loadServiceClass(); + const service = new ServiceClass({ + publishLiveMeasure: jest.fn(() => { + throw new Error('stream down'); + }), + } as unknown as StreamListenerService); + const testableService = asTestableService(service); + // Access logger safely + const logger = testableService.logger; + const errorSpy = jest.spyOn(logger, 'error').mockImplementation(); + + await service.onModuleInit(); + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to initialize telemetry NATS bridge', + expect.any(Error), + ); + + const subscription = { + [Symbol.asyncIterator]: async function* () { + // Add await to satisfy require-await + await Promise.resolve(); + yield { + subject: 'telemetry.data.tenant-1.gateway-1', + data: Buffer.from( + JSON.stringify({ + gatewayId: 'gateway-1', + sensorId: 'sensor-1', + sensorType: 'temperature', + timestamp: '2026-03-23T10:00:00.000Z', + encryptedData: 'enc', + iv: 'iv', + authTag: 'tag', + keyVersion: 1, + }), + ), + }; + }, + } as unknown as Subscription; + + await testableService.consumeMessages(subscription); + + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to process telemetry message on telemetry.data.tenant-1.gateway-1', + expect.any(Error), + ); + }); +}); diff --git a/src/data-api/services/telemetry-stream-bridge.service.ts b/src/data-api/services/telemetry-stream-bridge.service.ts new file mode 100644 index 0000000..9bbca8c --- /dev/null +++ b/src/data-api/services/telemetry-stream-bridge.service.ts @@ -0,0 +1,185 @@ +import { + Injectable, + Logger, + OnModuleDestroy, + OnModuleInit, +} from '@nestjs/common'; +import * as fs from 'node:fs'; +import { + type ConnectionOptions, + connect, + type NatsConnection, + type Subscription, +} from 'nats'; +import { EncryptedEnvelopeModel } from '../models/encrypted-envelope.model'; +import { StreamListenerService } from './stream-listener.service'; + +const TELEMETRY_SUBJECT = 'telemetry.data.*.*'; + +@Injectable() +export class TelemetryStreamBridgeService + implements OnModuleInit, OnModuleDestroy +{ + private readonly logger = new Logger(TelemetryStreamBridgeService.name); + private connection: NatsConnection | null = null; + private subscription: Subscription | null = null; + + constructor(private readonly streamListener: StreamListenerService) {} + + async onModuleInit(): Promise { + if (process.env.NODE_ENV === 'test') { + return; + } + + await this.connectAndSubscribe(); + } + + async onModuleDestroy(): Promise { + this.subscription?.unsubscribe(); + this.subscription = null; + + if (this.connection) { + await this.connection.drain(); + await this.connection.close(); + this.connection = null; + } + } + + private async connectAndSubscribe(): Promise { + try { + this.connection = await connect(this.buildConnectionOptions()); + this.subscription = this.connection.subscribe(TELEMETRY_SUBJECT); + + void this.consumeMessages(this.subscription); + this.logger.log(`Subscribed to telemetry subject ${TELEMETRY_SUBJECT}`); + } catch (error) { + this.logger.error( + 'Failed to initialize telemetry NATS bridge', + error as Error, + ); + } + } + + private async consumeMessages(subscription: Subscription): Promise { + for await (const message of subscription) { + try { + const tenantId = this.extractTenantId(message.subject); + const payload = this.parseEnvelope(message.data); + + if (!tenantId || !payload) { + continue; + } + + this.streamListener.publishLiveMeasure(tenantId, payload); + } catch (error) { + this.logger.error( + `Failed to process telemetry message on ${message.subject}`, + error as Error, + ); + } + } + } + + private buildConnectionOptions(): ConnectionOptions { + const options: ConnectionOptions = { + servers: this.resolveServers(), + name: process.env.NATS_CLIENT_NAME ?? 'data-api', + }; + + const caFile = process.env.NATS_TLS_CA; + const certFile = process.env.NATS_TLS_CERT; + const keyFile = process.env.NATS_TLS_KEY; + + if (caFile && certFile && keyFile) { + try { + (options as { tls: any }).tls = { + ca: [fs.readFileSync(caFile)], + cert: fs.readFileSync(certFile), + key: fs.readFileSync(keyFile), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error(`Failed to load NATS TLS certificates: ${message}`); + } + } + + const token = process.env.NATS_TOKEN?.trim(); + const user = process.env.NATS_USER?.trim(); + const pass = process.env.NATS_PASSWORD?.trim(); + + if (token) { + options.token = token; + return options; + } + + if (user && pass) { + options.user = user; + options.pass = pass; + } + + return options; + } + + private resolveServers(): string[] { + const raw = process.env.NATS_SERVERS ?? process.env.NATS_URL; + + if (!raw) { + return ['nats://localhost:4222']; + } + + return raw + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + } + + private extractTenantId(subject: string): string | undefined { + const parts = subject.split('.'); + + if (parts.length !== 4) { + return undefined; + } + + return parts[2]; + } + + private parseEnvelope(data: Uint8Array): EncryptedEnvelopeModel | undefined { + try { + const parsed = JSON.parse( + Buffer.from(data).toString('utf8'), + ) as Partial; + + if ( + !parsed.gatewayId || + !parsed.sensorId || + !parsed.sensorType || + !parsed.timestamp || + !parsed.encryptedData || + !parsed.iv || + !parsed.authTag || + typeof parsed.keyVersion !== 'number' + ) { + this.logger.warn('Ignoring telemetry message with invalid envelope'); + return undefined; + } + + return { + gatewayId: parsed.gatewayId, + sensorId: parsed.sensorId, + sensorType: parsed.sensorType, + timestamp: parsed.timestamp, + encryptedData: parsed.encryptedData, + iv: parsed.iv, + authTag: parsed.authTag, + keyVersion: parsed.keyVersion, + }; + } catch (error) { + this.logger.warn( + `Ignoring telemetry message with invalid JSON: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + return undefined; + } + } +} diff --git a/src/env.validation.spec.ts b/src/env.validation.spec.ts new file mode 100644 index 0000000..f1f7391 --- /dev/null +++ b/src/env.validation.spec.ts @@ -0,0 +1,94 @@ +import { validate } from './env.validation'; + +describe('validate', () => { + it('parses numeric and boolean environment values outside test mode', () => { + expect( + validate({ + NODE_ENV: 'development', + DATA_API_PORT: '3001', + MEASURES_DB_HOST: 'db.example.local', + MEASURES_DB_PORT: '6543', + MEASURES_DB_USER: 'postgres', + MEASURES_DB_PASSWORD: 'secret', + MEASURES_DB_NAME: 'measures', + DB_SSL: '1', + NATS_URL: 'nats://localhost:4222', + }), + ).toEqual({ + DATA_API_PORT: 3001, + MEASURES_DB_HOST: 'db.example.local', + MEASURES_DB_PORT: 6543, + MEASURES_DB_USER: 'postgres', + MEASURES_DB_PASSWORD: 'secret', + MEASURES_DB_NAME: 'measures', + DB_SSL: true, + NATS_URL: 'nats://localhost:4222', + NATS_SERVERS: undefined, + NATS_TOKEN: undefined, + NATS_USER: undefined, + NATS_PASSWORD: undefined, + NATS_TLS_CA: undefined, + NATS_TLS_CERT: undefined, + NATS_TLS_KEY: undefined, + }); + }); + + it('throws when required database environment variables are missing outside test mode', () => { + expect(() => + validate({ + NODE_ENV: 'development', + MEASURES_DB_PORT: '5432', + MEASURES_DB_USER: 'postgres', + MEASURES_DB_NAME: 'measures', + }), + ).toThrow('Missing environment variable: MEASURES_DB_HOST'); + }); + + it('throws when numeric variables are invalid', () => { + expect(() => + validate({ + NODE_ENV: 'development', + DATA_API_PORT: 'abc', + MEASURES_DB_HOST: 'localhost', + MEASURES_DB_PORT: '5432', + MEASURES_DB_USER: 'postgres', + MEASURES_DB_NAME: 'measures', + }), + ).toThrow('Invalid numeric environment variable: DATA_API_PORT'); + + expect(() => + validate({ + NODE_ENV: 'development', + MEASURES_DB_HOST: 'localhost', + MEASURES_DB_PORT: 'abc', + MEASURES_DB_USER: 'postgres', + MEASURES_DB_NAME: 'measures', + }), + ).toThrow('Invalid numeric environment variable: MEASURES_DB_PORT'); + }); + + it('uses test defaults when database environment variables are omitted', () => { + expect( + validate({ + NODE_ENV: 'test', + DB_SSL: 'false', + }), + ).toEqual({ + DATA_API_PORT: undefined, + MEASURES_DB_HOST: 'localhost', + MEASURES_DB_PORT: 5432, + MEASURES_DB_USER: 'test', + MEASURES_DB_PASSWORD: undefined, + MEASURES_DB_NAME: 'test', + DB_SSL: false, + NATS_URL: undefined, + NATS_SERVERS: undefined, + NATS_TOKEN: undefined, + NATS_USER: undefined, + NATS_PASSWORD: undefined, + NATS_TLS_CA: undefined, + NATS_TLS_CERT: undefined, + NATS_TLS_KEY: undefined, + }); + }); +}); diff --git a/src/env.validation.ts b/src/env.validation.ts new file mode 100644 index 0000000..8475614 --- /dev/null +++ b/src/env.validation.ts @@ -0,0 +1,75 @@ +type DataApiEnv = { + DATA_API_PORT?: number; + MEASURES_DB_HOST: string; + MEASURES_DB_PORT: number; + MEASURES_DB_USER: string; + MEASURES_DB_PASSWORD?: string; + MEASURES_DB_NAME: string; + DB_SSL: boolean; + NATS_URL?: string; + NATS_SERVERS?: string; + NATS_TOKEN?: string; + NATS_USER?: string; + NATS_PASSWORD?: string; + NATS_TLS_CA?: string; + NATS_TLS_CERT?: string; + NATS_TLS_KEY?: string; +}; + +function parseNumber(value: string | undefined, name: string): number { + if (!value) { + throw new TypeError(`Missing environment variable: ${name}`); + } + + const parsed = Number.parseInt(value, 10); + + if (Number.isNaN(parsed)) { + throw new TypeError(`Invalid numeric environment variable: ${name}`); + } + + return parsed; +} + +function parseBoolean(value: string | undefined): boolean { + return value === 'true' || value === '1'; +} + +export function validate(config: NodeJS.ProcessEnv): DataApiEnv { + const isTestEnvironment = config.NODE_ENV === 'test'; + const required = [ + 'MEASURES_DB_HOST', + 'MEASURES_DB_PORT', + 'MEASURES_DB_USER', + 'MEASURES_DB_NAME', + ] as const; + + if (!isTestEnvironment) { + for (const key of required) { + if (!config[key]) { + throw new Error(`Missing environment variable: ${key}`); + } + } + } + + return { + DATA_API_PORT: config.DATA_API_PORT + ? parseNumber(config.DATA_API_PORT, 'DATA_API_PORT') + : undefined, + MEASURES_DB_HOST: config.MEASURES_DB_HOST ?? 'localhost', + MEASURES_DB_PORT: config.MEASURES_DB_PORT + ? parseNumber(config.MEASURES_DB_PORT, 'MEASURES_DB_PORT') + : 5432, + MEASURES_DB_USER: config.MEASURES_DB_USER ?? 'test', + MEASURES_DB_PASSWORD: config.MEASURES_DB_PASSWORD, + MEASURES_DB_NAME: config.MEASURES_DB_NAME ?? 'test', + DB_SSL: parseBoolean(config.DB_SSL), + NATS_URL: config.NATS_URL, + NATS_SERVERS: config.NATS_SERVERS, + NATS_TOKEN: config.NATS_TOKEN, + NATS_USER: config.NATS_USER, + NATS_PASSWORD: config.NATS_PASSWORD, + NATS_TLS_CA: config.NATS_TLS_CA, + NATS_TLS_CERT: config.NATS_TLS_CERT, + NATS_TLS_KEY: config.NATS_TLS_KEY, + }; +} diff --git a/src/generated/openapi/management-api-openapi.ts b/src/generated/openapi/management-api-openapi.ts deleted file mode 100644 index 25f6020..0000000 --- a/src/generated/openapi/management-api-openapi.ts +++ /dev/null @@ -1,1570 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - "/": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["AppController_getHello"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/gateways": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all Gateways */ - get: operations["GatewaysController_getAdminGateways"]; - put?: never; - /** Add Gateway to a Tenant */ - post: operations["GatewaysController_addGateway"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/tenants": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all tenants */ - get: operations["TenantsController_getTenants"]; - put?: never; - /** Create a new tenant */ - post: operations["TenantsController_createTenant"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/tenants/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Delete a tenant */ - delete: operations["TenantsController_deleteTenant"]; - options?: never; - head?: never; - /** Update tenant details */ - patch: operations["TenantsController_updateTenant"]; - trace?: never; - }; - "/alerts": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get alerts for the tenant in a time range */ - get: operations["AlertsController_getAlerts"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/alerts/config": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get alert configuration for the tenant */ - get: operations["AlertsController_getAlertsConfig"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/alerts/config/default": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Set default alert configuration for the tenant */ - put: operations["AlertsController_setDefaultAlertsConfig"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/alerts/config/gateway/{gatewayId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Set alert configuration for a specific gateway */ - put: operations["AlertsController_setGatewayAlertsConfig"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api-clients": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all API clients for the tenant */ - get: operations["ApiClientController_getApiClients"]; - put?: never; - /** Create a new API client for the tenant */ - post: operations["ApiClientController_createApiClient"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api-clients/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Delete an API client for the tenant */ - delete: operations["ApiClientController_deleteApiClient"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/audit": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get audit logs for the tenant */ - get: operations["AuditLogController_getAuditLogs"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Return authenticated user claims mapped by JwtStrategy */ - get: operations["AuthController_getMe"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/auth/impersonate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Impersonate a tenant user (admin only) */ - post: operations["AuthController_impersonate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/cmd/{gatewayId}/config": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Send configuration to a gateway */ - post: operations["CommandController_sendConfig"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/cmd/{gatewayId}/firmware": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Send firmware update to a gateway */ - post: operations["CommandController_sendFirmware"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/cmd/{gatewayId}/status/{commandId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get command execution status */ - get: operations["CommandController_getStatus"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/costs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get current costs for the tenant */ - get: operations["CostsController_getTenantCost"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/gateways": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all gateways for the tenant */ - get: operations["GatewaysController_getGateways"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/gateways/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get gateway by id */ - get: operations["GatewaysController_getGatewayById"]; - put?: never; - post?: never; - /** Delete a gateway */ - delete: operations["GatewaysController_deleteGateway"]; - options?: never; - head?: never; - /** Update gateway details */ - patch: operations["GatewaysController_updateGateway"]; - trace?: never; - }; - "/keys": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all keys for a gateway */ - get: operations["KeysController_getKeys"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/thresholds": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["ThresholdsController_getThresholds"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/thresholds/default": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put: operations["ThresholdsController_setDefaultThreshold"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/thresholds/sensor/{sensorId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put: operations["ThresholdsController_setSensorThreshold"]; - post?: never; - delete: operations["ThresholdsController_deleteSensorThreshold"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/thresholds/type/{sensorType}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete: operations["ThresholdsController_deleteThresholdType"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get user details by id */ - get: operations["UsersController_getUserById"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update user in tenant */ - patch: operations["UsersController_updateUser"]; - trace?: never; - }; - "/users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get users for tenant */ - get: operations["UsersController_getUsers"]; - put?: never; - /** Create user in tenant */ - post: operations["UsersController_createUser"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/users/bulk-delete": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Delete users by ids */ - post: operations["UsersController_deleteUsers"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - AddGatewayRequestDto: { - factory_id: string; - tenant_id: string; - factory_key_hash: string; - }; - TenantsResponseDto: { - id: string; - name: string; - /** @enum {string} */ - status: "active" | "suspended"; - /** Format: date-time */ - created_at: string; - }; - CreateTenantRequestDto: { - name: string; - admin_email: string; - admin_name: string; - /** @example Temp_1234_A! */ - admin_password: string; - }; - UpdateTenantRequestDto: { - name: string; - /** @enum {string} */ - status: "active" | "suspended"; - suspension_interval_days: number; - }; - DeleteTenantResponseDto: { - message: string; - }; - SetAlertsConfigDefaultRequestDto: { - tenant_unreachable_timeout_ms: number; - }; - SetGatewayAlertsConfigRequestDto: { - gateway_unreachable_timeout_ms: number; - }; - CreateApiClientRequestDto: Record; - SendConfigRequestDto: { - send_frequency_ms?: number; - status?: string; - }; - CommandResponseDto: { - command_id: string; - /** @enum {string} */ - status: "queued" | "ack" | "nack" | "expired" | "timeout"; - /** Format: date-time */ - issued_at: string; - }; - SendFirmwareRequestDto: { - firmware_version: string; - download_url: string; - }; - CommandStatusResponseDto: { - command_id: string; - /** @enum {string} */ - status: "queued" | "ack" | "nack" | "expired" | "timeout"; - /** Format: date-time */ - timestamp: string; - }; - CostResponseDto: { - storage_gb: number; - bandwidth_gb: number; - }; - GatewayResponseDto: { - /** - * @description Unique identifier of the gateway - * @example 550e8400-e29b-41d4-a716-446655440000 - */ - id: string; - /** - * @description User-defined name of the gateway - * @example Main Entrance Gateway - */ - name: string; - /** - * @description Current connectivity status - * @example gateway_online - * @enum {string} - */ - status: "gateway_online" | "gateway_offline" | "gateway_suspended"; - /** - * Format: date-time - * @description Timestamp of the last heart-beat received from the gateway - * @example 2024-03-24T10:00:00Z - */ - last_seen_at: string; - /** - * @description Whether the gateway has been provisioned/activated - * @example true - */ - provisioned: boolean; - /** - * @description Current firmware version installed on the device - * @example 1.2.3 - */ - firmware_version: string; - /** - * @description Configured data sending frequency in milliseconds - * @example 30000 - */ - send_frequency_ms: number; - }; - UpdateGatewayRequestDto: { - name: string; - }; - UpdateGatewayResponseDto: { - id: string; - name: string; - /** @enum {string} */ - status: "gateway_online" | "gateway_offline" | "gateway_suspended"; - /** Format: date-time */ - updated_at: string; - }; - KeysResponseDto: { - gateway_id: string; - /** Format: byte */ - key_material: string; - key_version: number; - }; - SetThresholdDefaultTypeRequestDto: { - sensor_type: string; - min_value: number; - max_value: number; - }; - SetThresholdSensorRequestDto: { - min_value: number; - max_value: number; - sensor_type: string; - }; - UserResponseDto: { - id: string; - name: string; - email: string; - /** @enum {string} */ - role: "system_admin" | "tenant_admin" | "tenant_user"; - /** Format: date-time */ - last_access: string | null; - }; - CreateUserRequestDto: { - name: string; - email: string; - /** @enum {string} */ - role: "system_admin" | "tenant_admin" | "tenant_user"; - password: string; - }; - CreateUserResponseDto: { - id: string; - name: string; - email: string; - /** @enum {string} */ - role: "system_admin" | "tenant_admin" | "tenant_user"; - /** Format: date-time */ - created_at: string; - }; - UpdateUserRequestDto: { - name: string; - email: string; - /** @enum {string} */ - role: "system_admin" | "tenant_admin" | "tenant_user"; - permissions: string[]; - }; - UpdateUserResponseDto: { - id: string; - name: string; - email: string; - /** @enum {string} */ - role: "system_admin" | "tenant_admin" | "tenant_user"; - /** Format: date-time */ - updated_at: string; - }; - DeleteUserRequestDto: { - ids: string[]; - }; - DeleteUserResponseDto: { - deleted: number; - failed: string[]; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - AppController_getHello: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - GatewaysController_getAdminGateways: { - parameters: { - query?: { - tenant_id?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - GatewaysController_addGateway: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AddGatewayRequestDto"]; - }; - }; - responses: { - /** @description Tenant not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Factory_id already registered */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - TenantsController_getTenants: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of tenants */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TenantsResponseDto"][]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - TenantsController_createTenant: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateTenantRequestDto"]; - }; - }; - responses: { - /** @description Tenant created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TenantsResponseDto"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Conflict - Tenant with the same name already exists */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - TenantsController_deleteTenant: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Tenant deleted successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteTenantResponseDto"]; - }; - }; - /** @description Tenant not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - TenantsController_updateTenant: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTenantRequestDto"]; - }; - }; - responses: { - /** @description Tenant updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TenantsResponseDto"]; - }; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Tenant not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AlertsController_getAlerts: { - parameters: { - query?: { - from?: string; - to?: string; - gateway_id?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AlertsController_getAlertsConfig: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AlertsController_setDefaultAlertsConfig: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetAlertsConfigDefaultRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AlertsController_setGatewayAlertsConfig: { - parameters: { - query?: never; - header?: never; - path: { - gatewayId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetGatewayAlertsConfigRequestDto"]; - }; - }; - responses: { - /** @description Alert configuration updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Gateway not associated with tenant of JWT */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Gateway not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ApiClientController_getApiClients: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ApiClientController_createApiClient: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateApiClientRequestDto"]; - }; - }; - responses: { - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description API client with the same name already exists */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ApiClientController_deleteApiClient: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description API client not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuditLogController_getAuditLogs: { - parameters: { - query: { - from: string; - to: string; - userId: string; - action: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Audit logs not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_getMe: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authenticated user details */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_impersonate: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": { - user_id?: string; - }; - }; - }; - responses: { - /** @description Impersonation token */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - access_token?: string; - expires_in?: number; - }; - }; - }; - }; - }; - CommandController_sendConfig: { - parameters: { - query?: never; - header?: never; - path: { - gatewayId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SendConfigRequestDto"]; - }; - }; - responses: { - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CommandResponseDto"]; - }; - }; - }; - }; - CommandController_sendFirmware: { - parameters: { - query?: never; - header?: never; - path: { - gatewayId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SendFirmwareRequestDto"]; - }; - }; - responses: { - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CommandResponseDto"]; - }; - }; - }; - }; - CommandController_getStatus: { - parameters: { - query?: never; - header?: never; - path: { - gatewayId: string; - commandId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CommandStatusResponseDto"]; - }; - }; - }; - }; - CostsController_getTenantCost: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CostResponseDto"]; - }; - }; - }; - }; - GatewaysController_getGateways: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GatewayResponseDto"][]; - }; - }; - }; - }; - GatewaysController_getGatewayById: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GatewayResponseDto"]; - }; - }; - }; - }; - GatewaysController_deleteGateway: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Gateway deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - GatewaysController_updateGateway: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateGatewayRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UpdateGatewayResponseDto"]; - }; - }; - }; - }; - KeysController_getKeys: { - parameters: { - query: { - id: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of keys */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["KeysResponseDto"][]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Gateway not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ThresholdsController_getThresholds: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ThresholdsController_setDefaultThreshold: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetThresholdDefaultTypeRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ThresholdsController_setSensorThreshold: { - parameters: { - query?: never; - header?: never; - path: { - sensorId: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetThresholdSensorRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ThresholdsController_deleteSensorThreshold: { - parameters: { - query?: never; - header?: never; - path: { - sensorId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - ThresholdsController_deleteThresholdType: { - parameters: { - query?: never; - header?: never; - path: { - sensorType: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_getUserById: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserResponseDto"]; - }; - }; - }; - }; - UsersController_updateUser: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateUserRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UpdateUserResponseDto"]; - }; - }; - }; - }; - UsersController_getUsers: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["UserResponseDto"][]; - }; - }; - }; - }; - UsersController_createUser: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateUserRequestDto"]; - }; - }; - responses: { - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreateUserResponseDto"]; - }; - }; - }; - }; - UsersController_deleteUsers: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteUserRequestDto"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteUserResponseDto"]; - }; - }; - }; - }; -} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index bbee27b..76b3253 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -107,17 +107,17 @@ describe('Data API integration', () => { ]); }); - it('/measures/query (GET) rejects requests with limit greater than 1000', async () => { + it('/measures/query (GET) rejects requests with limit greater than or equal to 1000', async () => { await expect( measureController.query( '2026-03-23T09:50:00.000Z', '2026-03-23T10:00:00.000Z', - '1001', + '1000', ), ).rejects.toMatchObject({ response: { code: 'QUERY_LIMIT_EXCEEDED', - message: 'limit must be less than or equal to 1000', + message: 'limit must be less than 1000', }, status: 400, }); @@ -128,7 +128,7 @@ describe('Data API integration', () => { measureController.query( '2026-03-22T09:59:59.000Z', '2026-03-23T10:00:00.000Z', - '1000', + '999', ), ).rejects.toMatchObject({ response: { diff --git a/test/mocks/in-memory-measure-persistence.service.ts b/test/mocks/in-memory-measure-persistence.service.ts index 3a4b284..ff12e55 100644 --- a/test/mocks/in-memory-measure-persistence.service.ts +++ b/test/mocks/in-memory-measure-persistence.service.ts @@ -75,6 +75,8 @@ export class InMemoryMeasurePersistenceService { input: NpQueryPersistenceInput & { cursor?: string }, ): MeasureEntity[] { return MEASURES.filter((measure) => { + const matchesTenant = + !input.tenantId || input.tenantId === measure.tenantId; const matchesGateway = !input.gatewayId?.length || input.gatewayId.includes(measure.gatewayId); const matchesSensor = @@ -87,6 +89,7 @@ export class InMemoryMeasurePersistenceService { const matchesCursor = !input.cursor || measure.time < input.cursor; return ( + matchesTenant && matchesGateway && matchesSensor && matchesSensorType && diff --git a/test/mocks/mock-stream-listener.service.ts b/test/mocks/mock-stream-listener.service.ts index c764342..eeea018 100644 --- a/test/mocks/mock-stream-listener.service.ts +++ b/test/mocks/mock-stream-listener.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { EncryptedEnvelopeModel } from '../../src/data-api/models/encrypted-envelope.model'; +import { StreamEmission } from '../../src/data-api/services/stream-listener.service'; const STREAM_EVENT: EncryptedEnvelopeModel = { gatewayId: 'gw-1', @@ -15,7 +16,10 @@ const STREAM_EVENT: EncryptedEnvelopeModel = { @Injectable() export class MockStreamListenerService { - stream(): Observable { - return of(STREAM_EVENT); + stream(): Observable { + return of({ + kind: 'data', + data: STREAM_EVENT, + }); } } diff --git a/tsconfig.json b/tsconfig.json index 5156b43..e0ec288 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ "resolvePackageJsonExports": true, /* Build Performance & Output */ "outDir": "./dist", - "baseUrl": "./", "sourceMap": true, "declaration": true, "incremental": true,