diff --git a/libs/common/src/common.module.ts b/libs/common/src/common.module.ts index d156de3..de0780a 100644 --- a/libs/common/src/common.module.ts +++ b/libs/common/src/common.module.ts @@ -7,14 +7,24 @@ import { ConfigModule } from '@nestjs/config'; import config from './config'; import { EmailService } from './util/email.service'; import { ErrorMessageService } from 'src/error-message/error-message.service'; +import { TuyaService } from './integrations/tuya/services/tuya.service'; +import { SceneDeviceRepository } from './modules/scene-device/repositories'; @Module({ - providers: [CommonService, EmailService, ErrorMessageService], + providers: [ + CommonService, + EmailService, + ErrorMessageService, + TuyaService, + SceneDeviceRepository, + ], exports: [ CommonService, + TuyaService, HelperModule, AuthModule, EmailService, ErrorMessageService, + SceneDeviceRepository, ], imports: [ ConfigModule.forRoot({ diff --git a/libs/common/src/constants/automation.enum.ts b/libs/common/src/constants/automation.enum.ts index 7e431eb..8370cf9 100644 --- a/libs/common/src/constants/automation.enum.ts +++ b/libs/common/src/constants/automation.enum.ts @@ -1,4 +1,3 @@ -// automation.enum.ts export enum ActionExecutorEnum { DEVICE_ISSUE = 'device_issue', DELAY = 'delay', @@ -7,3 +6,15 @@ export enum ActionExecutorEnum { export enum EntityTypeEnum { DEVICE_REPORT = 'device_report', } +export const AUTOMATION_CONFIG = { + DEFAULT_START_TIME: '00:00', + DEFAULT_END_TIME: '23:59', + DEFAULT_LOOPS: '1111111', + DECISION_EXPR: 'and', + CONDITION_TYPE: 'device_report', + ACTION_EXECUTOR: 'rule_trigger', + COMPARATOR: '==', + SCENE_STATUS_VALUE: 'scene', +}; +export const AUTOMATION_TYPE = 'automation'; +export const AUTO_PREFIX = 'Auto_'; diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index a1e43ee..eeacd0f 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -8,4 +8,681 @@ export class ControllerRoute { 'Retrieve the list of all regions registered in Syncrow.'; }; }; + + static COMMUNITY = class { + public static readonly ROUTE = 'communities'; + static ACTIONS = class { + public static readonly GET_COMMUNITY_BY_ID_SUMMARY = + 'Get community by community community uuid'; + + public static readonly GET_COMMUNITY_BY_ID_DESCRIPTION = + 'Get community by community community uuid - ( [a-zA-Z0-9]{10} )'; + + public static readonly LIST_COMMUNITY_SUMMARY = 'Get list of community'; + + public static readonly LIST_COMMUNITY_DESCRIPTION = + 'Return a list of community'; + public static readonly UPDATE_COMMUNITY_SUMMARY = 'Update community'; + + public static readonly UPDATE_COMMUNITY_DESCRIPTION = + 'Update community in the database and return updated community'; + + public static readonly DELETE_COMMUNITY_SUMMARY = 'Delete community'; + + public static readonly DELETE_COMMUNITY_DESCRIPTION = + 'Delete community matching by community id'; + + public static readonly CREATE_COMMUNITY_SUMMARY = 'Create community'; + + public static readonly CREATE_COMMUNITY_DESCRIPTION = + 'Create community in the database and return in model'; + }; + }; + + static COMMUNITY_SPACE = class { + public static readonly ROUTE = 'communities/:communityUuid/space'; + static ACTIONS = class { + public static readonly GET_COMMUNITY_SPACES_HIERARCHY_SUMMARY = + 'Fetch hierarchical structure of spaces within a community.'; + + public static readonly GET_COMMUNITY_SPACES_HIERARCHY_DESCRIPTION = + 'retrieves all the spaces associated with a given community, organized into a hierarchical structure.'; + }; + }; + + static USER_COMMUNITY = class { + public static readonly ROUTE = '/user/:userUuid/communities'; + static ACTIONS = class { + public static readonly GET_USER_COMMUNITIES_SUMMARY = + 'Get communities associated with a user by user UUID'; + public static readonly GET_USER_COMMUNITIES_DESCRIPTION = + 'This endpoint returns the list of communities a specific user is associated with'; + + public static readonly ASSOCIATE_USER_COMMUNITY_SUMMARY = + 'Associate a user with a community'; + public static readonly ASSOCIATE_USER_COMMUNITY_DESCRIPTION = + 'This endpoint associates a user with a community.'; + + public static readonly DISASSOCIATE_USER_COMMUNITY_SUMMARY = + 'Disassociate a user from a community'; + public static readonly DISASSOCIATE_USER_COMMUNITY_DESCRIPTION = + 'This endpoint disassociates a user from a community. It removes the relationship between the specified user and the community. If the user is not associated with the community, an error will be returned.'; + }; + }; + + static USER_SPACE = class { + public static readonly ROUTE = '/user/:userUuid/spaces'; + static ACTIONS = class { + public static readonly GET_USER_SPACES_SUMMARY = + 'Retrieve list of spaces a user belongs to'; + public static readonly GET_USER_SPACES_DESCRIPTION = + 'This endpoint retrieves all the spaces that a user is associated with, based on the user ID. It fetches the user spaces by querying the UserSpaceEntity to find the spaces where the user has an association.'; + public static readonly VERIFY_CODE_AND_ADD_USER_SPACE_SUMMARY = + 'Verify code and add user space'; + public static readonly VERIFY_CODE_AND_ADD_USER_SPACE_DESCRIPTION = + 'This endpoint verifies a provided code and associates the user with a space. It checks the validity of the code and, if valid, links the user to the corresponding space in the system.'; + }; + }; + + static SCENE = class { + public static readonly ROUTE = 'scene'; + static ACTIONS = class { + public static readonly CREATE_TAP_TO_RUN_SCENE_SUMMARY = + 'Create a Tap-to-Run Scene'; + public static readonly CREATE_TAP_TO_RUN_SCENE_DESCRIPTION = + 'Creates a new Tap-to-Run scene in Tuya and stores the scene in the local database.'; + + public static readonly DELETE_TAP_TO_RUN_SCENE_SUMMARY = + 'Delete a Tap-to-Run Scene'; + public static readonly DELETE_TAP_TO_RUN_SCENE_DESCRIPTION = + 'Deletes a Tap-to-Run scene from Tuya and removes it from the local database.'; + + public static readonly TRIGGER_TAP_TO_RUN_SCENE_SUMMARY = + 'Trigger a Tap-to-Run Scene'; + public static readonly TRIGGER_TAP_TO_RUN_SCENE_DESCRIPTION = + 'Triggers an existing Tap-to-Run scene in Tuya by scene UUID, executing its actions immediately.'; + + public static readonly GET_TAP_TO_RUN_SCENE_SUMMARY = + 'Get Tap-to-Run Scene Details'; + public static readonly GET_TAP_TO_RUN_SCENE_DESCRIPTION = + 'Retrieves detailed information of a specific Tap-to-Run scene identified by the scene UUID.'; + + public static readonly UPDATE_TAP_TO_RUN_SCENE_SUMMARY = + 'Update a Tap-to-Run Scene'; + public static readonly UPDATE_TAP_TO_RUN_SCENE_DESCRIPTION = + 'Updates an existing Tap-to-Run scene in Tuya and updates the scene in the local database, reflecting any new configurations or actions.'; + }; + }; + + static SPACE = class { + public static readonly ROUTE = '/communities/:communityUuid/spaces'; + static ACTIONS = class { + public static readonly CREATE_SPACE_SUMMARY = 'Create a new space'; + public static readonly CREATE_SPACE_DESCRIPTION = + 'This endpoint allows you to create a space in a specified community. Optionally, you can specify a parent space to nest the new space under it.'; + + public static readonly LIST_SPACE_SUMMARY = 'List spaces in community'; + public static readonly LIST_SPACE_DESCRIPTION = + 'List spaces in specified community by community id'; + + public static readonly GET_SPACE_SUMMARY = 'Get a space in community'; + public static readonly GET_SPACE_DESCRIPTION = + 'Get Space in specified community by community id'; + + public static readonly DELETE_SPACE_SUMMARY = 'Delete a space'; + public static readonly DELETE_SPACE_DESCRIPTION = + 'Deletes a space by its UUID and community ID. If the space has children, they will also be deleted due to cascade delete.'; + + public static readonly UPDATE_SPACE_SUMMARY = 'Update a space'; + public static readonly UPDATE_SPACE_DESCRIPTION = + 'Updates a space by its UUID and community ID. You can update the name, parent space, and other properties. If a parent space is provided and not already a parent, its `isParent` flag will be set to true.'; + + public static readonly GET_HEIRARCHY_SUMMARY = 'Get space hierarchy'; + public static readonly GET_HEIRARCHY_DESCRIPTION = + 'This endpoint retrieves the hierarchical structure of spaces under a given space ID. It returns all the child spaces nested within the specified space, organized by their parent-child relationships. '; + + public static readonly CREATE_INVITATION_CODE_SPACE_SUMMARY = + 'Generate a new invitation code for a specific space'; + public static readonly CREATE_INVITATION_CODE_SPACE_DESCRIPTION = + 'This endpoint generates a new 6-character invitation code for a space identified by its UUID and stores it in the space entity'; + + public static readonly GET_COMMUNITY_SPACES_HIERARCHY_SUMMARY = + 'Fetch hierarchical structure of spaces within a community.'; + + public static readonly GET_COMMUNITY_SPACES_HIERARCHY_DESCRIPTION = + 'retrieves all the spaces associated with a given community, organized into a hierarchical structure.'; + }; + }; + + static SPACE_SCENE = class { + public static readonly ROUTE = + '/communities/:communityUuid/spaces/:spaceUuid/scenes'; + static ACTIONS = class { + public static readonly GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY = + 'Retrieve Tap-to-Run Scenes by Space'; + public static readonly GET_TAP_TO_RUN_SCENE_BY_SPACE_DESCRIPTION = + 'Fetches all Tap-to-Run scenes associated with a specified space UUID. An optional query parameter can filter the results to show only scenes marked for the homepage display.'; + }; + }; + + static SPACE_USER = class { + public static readonly ROUTE = + '/communities/:communityUuid/spaces/:spaceUuid/user'; + static ACTIONS = class { + public static readonly ASSOCIATE_SPACE_USER_SUMMARY = + 'Associate a user to a space'; + public static readonly ASSOCIATE_SPACE_USER_DESCRIPTION = + 'Associates a user with a given space by their respective UUIDs'; + + public static readonly DISSOCIATE_SPACE_USER_SUMMARY = + 'Disassociate a user from a space'; + public static readonly DISSOCIATE_SPACE_USER_DESCRIPTION = + 'Disassociates a user from a space by removing the existing association.'; + }; + }; + + static SPACE_DEVICES = class { + public static readonly ROUTE = + '/communities/:communityUuid/spaces/:spaceUuid/devices'; + static ACTIONS = class { + public static readonly LIST_SPACE_DEVICE_SUMMARY = + 'List devices in a space'; + public static readonly LIST_SPACE_DEVICE_DESCRIPTION = + 'Retrieves a list of all devices associated with a specified space.'; + }; + }; + + static SUBSPACE = class { + public static readonly ROUTE = + '/communities/:communityUuid/spaces/:spaceUuid/subspaces'; + static ACTIONS = class { + public static readonly CREATE_SUBSPACE_SUMMARY = 'Create Subspace'; + public static readonly CREATE_SUBSPACE_DESCRIPTION = + 'Creates a new subspace within a specific space and community.'; + + public static readonly LIST_SUBSPACES_SUMMARY = 'List Subspaces'; + public static readonly LIST_SUBSPACES_DESCRIPTION = + 'Retrieves a list of subspaces within a specified space and community.'; + + public static readonly GET_SUBSPACE_SUMMARY = 'Get Subspace'; + public static readonly GET_SUBSPACE_DESCRIPTION = + 'Fetches a specific subspace by UUID within a given space and community'; + + public static readonly UPDATE_SUBSPACE_SUMMARY = 'Update Subspace'; + public static readonly UPDATE_SUBSPACE_DESCRIPTION = + 'Updates a specific subspace within a given space and community.'; + + public static readonly DELETE_SUBSPACE_SUMMARY = 'Delete Subspace'; + public static readonly DELETE_SUBSPACE_DESCRIPTION = + 'Deletes a specific subspace within a given space and community.'; + }; + }; + + static SUBSPACE_DEVICE = class { + public static readonly ROUTE = + '/communities/:communityUuid/spaces/:spaceUuid/subspaces/:subSpaceUuid/devices'; + + static ACTIONS = class { + public static readonly LIST_SUBSPACE_DEVICE_SUMMARY = + 'List devices in a subspace'; + public static readonly LIST_SUBSPACE_DEVICE_DESCRIPTION = + 'Retrieves a list of all devices associated with a specified subspace.'; + + public static readonly ASSOCIATE_SUBSPACE_DEVICE_SUMMARY = + 'Associate a device to a subspace'; + public static readonly ASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION = + 'Associates a device with a specific subspace, enabling it to be managed within that subspace context.'; + + public static readonly DISASSOCIATE_SUBSPACE_DEVICE_SUMMARY = + 'Disassociate a device from a subspace'; + public static readonly DISASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION = + 'Removes the association of a device from a specific subspace, making it no longer managed within that subspace.'; + }; + }; + + static PRODUCT = class { + public static readonly ROUTE = 'products'; + static ACTIONS = class { + public static readonly LIST_PRODUCT_SUMMARY = 'Retrieve all products'; + public static readonly LIST_PRODUCT_DESCRIPTION = + 'Fetches a list of all products along with their associated device details'; + }; + }; + static USER = class { + public static readonly ROUTE = '/user'; + + static ACTIONS = class { + public static readonly GET_USER_DETAILS_SUMMARY = + 'Retrieve user details by user UUID'; + public static readonly GET_USER_DETAILS_DESCRIPTION = + 'This endpoint retrieves detailed information for a specific user based on their UUID.'; + + public static readonly UPDATE_PROFILE_PICTURE_SUMMARY = + 'Update profile picture by user UUID'; + public static readonly UPDATE_PROFILE_PICTURE_DESCRIPTION = + 'This endpoint updates the profile picture for a user identified by their UUID.'; + + public static readonly UPDATE_REGION_SUMMARY = + 'Update region by user UUID'; + public static readonly UPDATE_REGION_DESCRIPTION = + 'This endpoint updates the region information for a user identified by their UUID.'; + + public static readonly UPDATE_TIMEZONE_SUMMARY = + 'Update timezone by user UUID'; + public static readonly UPDATE_TIMEZONE_DESCRIPTION = + 'This endpoint updates the timezone information for a user identified by their UUID.'; + + public static readonly UPDATE_NAME_SUMMARY = 'Update name by user UUID'; + public static readonly UPDATE_NAME_DESCRIPTION = + 'This endpoint updates the name for a user identified by their UUID.'; + + public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID'; + public static readonly DELETE_USER_DESCRIPTION = + 'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.'; + }; + }; + static AUTHENTICATION = class { + public static readonly ROUTE = 'authentication'; + + static ACTIONS = class { + public static readonly SIGN_UP_SUMMARY = + 'Sign up a new user and return a JWT token'; + public static readonly SIGN_UP_DESCRIPTION = + 'This endpoint is used to register a new user by providing the user details. A JWT token will be generated and returned upon successful registration.'; + + public static readonly LOGIN_SUMMARY = + 'Login a user and return an access token'; + public static readonly LOGIN_DESCRIPTION = + 'This endpoint allows an existing user to log in using their credentials. Upon successful login, an access token will be returned.'; + + public static readonly SEND_OTP_SUMMARY = + 'Generate and send OTP to the user'; + public static readonly SEND_OTP_DESCRIPTION = + 'This endpoint generates and sends an OTP to the user for verification, such as for password reset or account verification.'; + + public static readonly VERIFY_OTP_SUMMARY = + 'Verify the OTP entered by the user'; + public static readonly VERIFY_OTP_DESCRIPTION = + 'This endpoint verifies the OTP entered by the user. If the OTP is valid, the process continues (e.g., password reset).'; + + public static readonly FORGET_PASSWORD_SUMMARY = + 'Reset the user password after OTP verification'; + public static readonly FORGET_PASSWORD_DESCRIPTION = + 'This endpoint allows users who have forgotten their password to reset it. After verifying the OTP, the user can set a new password.'; + + public static readonly USER_LIST_SUMMARY = 'Fetch the list of all users'; + public static readonly USER_LIST_DESCRIPTION = + 'This endpoint retrieves a list of all users in the system. Access is restricted to super admins only.'; + + public static readonly REFRESH_TOKEN_SUMMARY = + 'Refresh the user session token'; + public static readonly REFRESH_TOKEN_DESCRIPTION = + 'This endpoint allows a user to refresh their session token. The user must provide a valid refresh token to get a new session token.'; + }; + }; + static ROLE = class { + public static readonly ROUTE = 'role'; + + static ACTIONS = class { + public static readonly FETCH_ROLE_TYPES_SUMMARY = + 'Fetch available role types'; + public static readonly FETCH_ROLE_TYPES_DESCRIPTION = + 'This endpoint retrieves all available role types in the system.'; + + public static readonly ADD_USER_ROLE_SUMMARY = 'Add a new user role'; + public static readonly ADD_USER_ROLE_DESCRIPTION = + 'This endpoint adds a new user role to the system based on the provided role data.'; + }; + }; + static GROUP = class { + public static readonly ROUTE = 'group'; + + static ACTIONS = class { + public static readonly GET_GROUPS_BY_SPACE_UUID_SUMMARY = + 'Get groups by space UUID'; + public static readonly GET_GROUPS_BY_SPACE_UUID_DESCRIPTION = + 'This endpoint retrieves all groups for a specific space, identified by the space UUID.'; + + public static readonly GET_UNIT_DEVICES_BY_GROUP_NAME_SUMMARY = + 'Get devices by group name in a space'; + public static readonly GET_UNIT_DEVICES_BY_GROUP_NAME_DESCRIPTION = + 'This endpoint retrieves all devices in a specified group within a space, based on the group name and space UUID.'; + }; + }; + static DEVICE = class { + public static readonly ROUTE = 'device'; + + static ACTIONS = class { + public static readonly ADD_DEVICE_TO_USER_SUMMARY = 'Add device to user'; + public static readonly ADD_DEVICE_TO_USER_DESCRIPTION = + 'This endpoint adds a device to a user in the system.'; + + public static readonly GET_DEVICES_BY_USER_SUMMARY = + 'Get devices by user UUID'; + public static readonly GET_DEVICES_BY_USER_DESCRIPTION = + 'This endpoint retrieves all devices associated with a specific user.'; + + public static readonly GET_DEVICES_BY_SPACE_UUID_SUMMARY = + 'Get devices by space UUID'; + public static readonly GET_DEVICES_BY_SPACE_UUID_DESCRIPTION = + 'This endpoint retrieves all devices associated with a specific space UUID.'; + + public static readonly UPDATE_DEVICE_IN_ROOM_SUMMARY = + 'Update device in Space'; + public static readonly UPDATE_DEVICE_IN_ROOM_DESCRIPTION = + 'This endpoint updates the device in a specific space with new details.'; + + public static readonly GET_DEVICE_DETAILS_SUMMARY = 'Get device details'; + public static readonly GET_DEVICE_DETAILS_DESCRIPTION = + 'This endpoint retrieves details of a specific device by its UUID.'; + + public static readonly UPDATE_DEVICE_SUMMARY = 'Update device'; + public static readonly UPDATE_DEVICE_DESCRIPTION = + 'This endpoint updates the details of a device by its UUID.'; + + public static readonly GET_DEVICE_INSTRUCTION_SUMMARY = + 'Get device instruction'; + public static readonly GET_DEVICE_INSTRUCTION_DESCRIPTION = + 'This endpoint retrieves the instruction details for a specific device.'; + + public static readonly GET_DEVICE_STATUS_SUMMARY = 'Get device status'; + public static readonly GET_DEVICE_STATUS_DESCRIPTION = + 'This endpoint retrieves the current status of a specific device.'; + + public static readonly CONTROL_DEVICE_SUMMARY = 'Control device'; + public static readonly CONTROL_DEVICE_DESCRIPTION = + 'This endpoint allows control operations (like power on/off) on a specific device.'; + + public static readonly UPDATE_DEVICE_FIRMWARE_SUMMARY = + 'Update device firmware'; + public static readonly UPDATE_DEVICE_FIRMWARE_DESCRIPTION = + 'This endpoint updates the firmware of a specific device to the provided version.'; + + public static readonly GET_DEVICES_IN_GATEWAY_SUMMARY = + 'Get devices in gateway'; + public static readonly GET_DEVICES_IN_GATEWAY_DESCRIPTION = + 'This endpoint retrieves all devices associated with a specific gateway.'; + + public static readonly GET_ALL_DEVICES_SUMMARY = 'Get all devices'; + public static readonly GET_ALL_DEVICES_DESCRIPTION = + 'This endpoint retrieves all devices in the system.'; + + public static readonly GET_DEVICE_LOGS_SUMMARY = 'Get device logs'; + public static readonly GET_DEVICE_LOGS_DESCRIPTION = + 'This endpoint retrieves the logs for a specific device based on device UUID.'; + + public static readonly BATCH_CONTROL_DEVICES_SUMMARY = + 'Batch control devices'; + public static readonly BATCH_CONTROL_DEVICES_DESCRIPTION = + 'This endpoint controls a batch of devices with the specified actions.'; + + public static readonly BATCH_STATUS_DEVICES_SUMMARY = + 'Batch status devices'; + public static readonly BATCH_STATUS_DEVICES_DESCRIPTION = + 'This endpoint retrieves the status of a batch of devices.'; + + public static readonly BATCH_FACTORY_RESET_DEVICES_SUMMARY = + 'Batch factory reset devices'; + public static readonly BATCH_FACTORY_RESET_DEVICES_DESCRIPTION = + 'This endpoint performs a factory reset on a batch of devices.'; + + public static readonly GET_POWER_CLAMP_STATUS_SUMMARY = + 'Get power clamp status'; + public static readonly GET_POWER_CLAMP_STATUS_DESCRIPTION = + 'This endpoint retrieves the status of a specific power clamp device.'; + + public static readonly ADD_SCENE_TO_DEVICE_SUMMARY = + 'Add scene to device (4 Scene and 6 Scene devices only)'; + public static readonly ADD_SCENE_TO_DEVICE_DESCRIPTION = + 'This endpoint adds a scene to a specific switch device.'; + + public static readonly GET_SCENES_BY_DEVICE_SUMMARY = + 'Get scenes by device (4 Scene and 6 Scene devices only)'; + public static readonly GET_SCENES_BY_DEVICE_DESCRIPTION = + 'This endpoint retrieves all scenes associated with a specific switch device.'; + public static readonly DELETE_SCENES_BY_SWITCH_NAME_SUMMARY = + 'Delete scenes by device uuid and switch name (4 Scene and 6 Scene devices only)'; + public static readonly DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION = + 'This endpoint deletes all scenes associated with a specific switch device.'; + }; + }; + + static DEVICE_PERMISSION = class { + public static readonly ROUTE = 'device-permission'; + + static ACTIONS = class { + public static readonly ADD_PERMISSION_SUMMARY = + 'Add user permission for device'; + public static readonly ADD_PERMISSION_DESCRIPTION = + 'This endpoint adds a user permission for a specific device. Accessible only by users with the Super Admin role.'; + + public static readonly EDIT_PERMISSION_SUMMARY = + 'Edit user permission for device'; + public static readonly EDIT_PERMISSION_DESCRIPTION = + 'This endpoint updates a user permission for a specific device. Accessible only by users with the Super Admin role.'; + + public static readonly FETCH_PERMISSION_SUMMARY = + 'Fetch user permission for device'; + public static readonly FETCH_PERMISSION_DESCRIPTION = + 'This endpoint retrieves the user permission for a specific device. Accessible only by users with the Super Admin role.'; + + public static readonly DELETE_PERMISSION_SUMMARY = + 'Delete user permission for device'; + public static readonly DELETE_PERMISSION_DESCRIPTION = + 'This endpoint deletes the user permission for a specific device. Accessible only by users with the Super Admin role.'; + }; + }; + + static USER_NOTIFICATION = class { + public static readonly ROUTE = 'user-notification/subscription'; + + static ACTIONS = class { + public static readonly ADD_SUBSCRIPTION_SUMMARY = + 'Add user notification subscription'; + public static readonly ADD_SUBSCRIPTION_DESCRIPTION = + 'This endpoint adds a subscription for user notifications.'; + + public static readonly FETCH_SUBSCRIPTIONS_SUMMARY = + 'Fetch user notification subscriptions'; + public static readonly FETCH_SUBSCRIPTIONS_DESCRIPTION = + 'This endpoint retrieves the subscriptions of a specific user based on their UUID.'; + + public static readonly UPDATE_SUBSCRIPTION_SUMMARY = + 'Update user notification subscription'; + public static readonly UPDATE_SUBSCRIPTION_DESCRIPTION = + 'This endpoint updates the notification subscription details for a user.'; + }; + }; + + static AUTOMATION = class { + public static readonly ROUTE = 'automation'; + + static ACTIONS = class { + public static readonly ADD_AUTOMATION_SUMMARY = 'Add automation'; + public static readonly ADD_AUTOMATION_DESCRIPTION = + 'This endpoint creates a new automation based on the provided details.'; + + public static readonly GET_AUTOMATION_BY_SPACE_SUMMARY = + 'Get automation by space'; + public static readonly GET_AUTOMATION_BY_SPACE_DESCRIPTION = + 'This endpoint retrieves the automations associated with a particular space.'; + + public static readonly GET_AUTOMATION_DETAILS_SUMMARY = + 'Get automation details'; + public static readonly GET_AUTOMATION_DETAILS_DESCRIPTION = + 'This endpoint retrieves detailed information about a specific automation.'; + + public static readonly DELETE_AUTOMATION_SUMMARY = 'Delete automation'; + public static readonly DELETE_AUTOMATION_DESCRIPTION = + 'This endpoint deletes an automation identified by its UUID.'; + + public static readonly UPDATE_AUTOMATION_SUMMARY = 'Update automation'; + public static readonly UPDATE_AUTOMATION_DESCRIPTION = + 'This endpoint updates the details of an existing automation.'; + + public static readonly UPDATE_AUTOMATION_STATUS_SUMMARY = + 'Update automation status'; + public static readonly UPDATE_AUTOMATION_STATUS_DESCRIPTION = + 'This endpoint updates the status of an automation identified by its UUID (enabled/disabled).'; + }; + }; + + static DOOR_LOCK = class { + public static readonly ROUTE = 'door-lock'; + + static ACTIONS = class { + public static readonly ADD_ONLINE_TEMPORARY_PASSWORD_SUMMARY = + 'Add online temporary password'; + public static readonly ADD_ONLINE_TEMPORARY_PASSWORD_DESCRIPTION = + 'This endpoint allows you to add an online temporary password to a door lock.'; + + public static readonly ADD_OFFLINE_ONE_TIME_TEMPORARY_PASSWORD_SUMMARY = + 'Add offline one-time temporary password'; + public static readonly ADD_OFFLINE_ONE_TIME_TEMPORARY_PASSWORD_DESCRIPTION = + 'This endpoint allows you to add an offline one-time temporary password to a door lock.'; + + public static readonly ADD_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORD_SUMMARY = + 'Add offline multiple-time temporary password'; + public static readonly ADD_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORD_DESCRIPTION = + 'This endpoint allows you to add an offline multiple-time temporary password to a door lock.'; + + public static readonly GET_ONLINE_TEMPORARY_PASSWORDS_SUMMARY = + 'Get online temporary passwords'; + public static readonly GET_ONLINE_TEMPORARY_PASSWORDS_DESCRIPTION = + 'This endpoint retrieves the list of online temporary passwords for a door lock.'; + + public static readonly DELETE_ONLINE_TEMPORARY_PASSWORD_SUMMARY = + 'Delete online temporary password'; + public static readonly DELETE_ONLINE_TEMPORARY_PASSWORD_DESCRIPTION = + 'This endpoint deletes an online temporary password for a door lock.'; + + public static readonly GET_OFFLINE_ONE_TIME_TEMPORARY_PASSWORDS_SUMMARY = + 'Get offline one-time temporary passwords'; + public static readonly GET_OFFLINE_ONE_TIME_TEMPORARY_PASSWORDS_DESCRIPTION = + 'This endpoint retrieves the list of offline one-time temporary passwords for a door lock.'; + + public static readonly GET_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORDS_SUMMARY = + 'Get offline multiple-time temporary passwords'; + public static readonly GET_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORDS_DESCRIPTION = + 'This endpoint retrieves the list of offline multiple-time temporary passwords for a door lock.'; + + public static readonly UPDATE_OFFLINE_TEMPORARY_PASSWORD_SUMMARY = + 'Update offline temporary password'; + public static readonly UPDATE_OFFLINE_TEMPORARY_PASSWORD_DESCRIPTION = + 'This endpoint updates an offline temporary password for a door lock.'; + + public static readonly OPEN_DOOR_LOCK_SUMMARY = 'Open door lock'; + public static readonly OPEN_DOOR_LOCK_DESCRIPTION = + 'This endpoint allows you to open a door lock.'; + }; + }; + static TIMEZONE = class { + public static readonly ROUTE = 'timezone'; + + static ACTIONS = class { + public static readonly GET_ALL_TIME_ZONES_SUMMARY = 'Get all time zones'; + public static readonly GET_ALL_TIME_ZONES_DESCRIPTION = + 'This endpoint retrieves all available time zones.'; + }; + }; + static VISITOR_PASSWORD = class { + public static readonly ROUTE = 'visitor-password'; + + static ACTIONS = class { + public static readonly ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY = + 'Add online temporary passwords (multiple-time)'; + public static readonly ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION = + 'This endpoint adds multiple online temporary passwords for door locks.'; + + public static readonly ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY = + 'Add online temporary password (one-time)'; + public static readonly ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION = + 'This endpoint adds a one-time online temporary password for a door lock.'; + + public static readonly ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY = + 'Add offline temporary password (one-time)'; + public static readonly ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION = + 'This endpoint adds a one-time offline temporary password for a door lock.'; + + public static readonly ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY = + 'Add offline temporary passwords (multiple-time)'; + public static readonly ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION = + 'This endpoint adds multiple offline temporary passwords for door locks.'; + + public static readonly GET_VISITOR_PASSWORD_SUMMARY = + 'Get visitor passwords'; + public static readonly GET_VISITOR_PASSWORD_DESCRIPTION = + 'This endpoint retrieves all visitor passwords.'; + + public static readonly GET_VISITOR_DEVICES_SUMMARY = + 'Get visitor devices'; + public static readonly GET_VISITOR_DEVICES_DESCRIPTION = + 'This endpoint retrieves all devices associated with visitor passwords.'; + }; + }; + static SCHEDULE = class { + public static readonly ROUTE = 'schedule'; + + static ACTIONS = class { + public static readonly ADD_DEVICE_SCHEDULE_SUMMARY = + 'Add device schedule'; + public static readonly ADD_DEVICE_SCHEDULE_DESCRIPTION = + 'This endpoint allows you to add a schedule for a specific device.'; + + public static readonly GET_DEVICE_SCHEDULE_BY_CATEGORY_SUMMARY = + 'Get device schedule by category'; + public static readonly GET_DEVICE_SCHEDULE_BY_CATEGORY_DESCRIPTION = + 'This endpoint retrieves the schedule for a specific device based on the given category.'; + + public static readonly DELETE_DEVICE_SCHEDULE_SUMMARY = + 'Delete device schedule'; + public static readonly DELETE_DEVICE_SCHEDULE_DESCRIPTION = + 'This endpoint deletes a specific schedule for a device.'; + + public static readonly ENABLE_DEVICE_SCHEDULE_SUMMARY = + 'Enable device schedule'; + public static readonly ENABLE_DEVICE_SCHEDULE_DESCRIPTION = + 'This endpoint enables a device schedule for a specific device.'; + + public static readonly UPDATE_DEVICE_SCHEDULE_SUMMARY = + 'Update device schedule'; + public static readonly UPDATE_DEVICE_SCHEDULE_DESCRIPTION = + 'This endpoint updates the schedule for a specific device.'; + }; + }; + static DEVICE_STATUS_FIREBASE = class { + public static readonly ROUTE = 'device-status-firebase'; + + static ACTIONS = class { + public static readonly ADD_DEVICE_STATUS_SUMMARY = + 'Add device status to Firebase'; + public static readonly ADD_DEVICE_STATUS_DESCRIPTION = + 'This endpoint adds a device status in Firebase based on the provided device UUID.'; + + public static readonly GET_DEVICE_STATUS_SUMMARY = + 'Get device status from Firebase'; + public static readonly GET_DEVICE_STATUS_DESCRIPTION = + 'This endpoint retrieves a device status from Firebase using the device UUID.'; + }; + }; + static DEVICE_MESSAGES_SUBSCRIPTION = class { + public static readonly ROUTE = 'device-messages/subscription'; + + static ACTIONS = class { + public static readonly ADD_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY = + 'Add device messages subscription'; + public static readonly ADD_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION = + 'This endpoint adds a subscription for device messages.'; + + public static readonly GET_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY = + 'Get device messages subscription'; + public static readonly GET_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION = + 'This endpoint fetches a user’s subscription for a specific device.'; + + public static readonly DELETE_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY = + 'Delete device messages subscription'; + public static readonly DELETE_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION = + 'This endpoint deletes a user’s subscription for device messages.'; + }; + }; } diff --git a/libs/common/src/constants/direction.enum.ts b/libs/common/src/constants/direction.enum.ts new file mode 100644 index 0000000..5791b57 --- /dev/null +++ b/libs/common/src/constants/direction.enum.ts @@ -0,0 +1,5 @@ +export enum Direction { + LEFT = 'left', + RIGHT = 'right', + DOWN = 'down', +} diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index 2f518fb..d368865 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -16,4 +16,6 @@ export enum ProductType { GD = 'GD', CUR = 'CUR', PC = 'PC', + FOUR_S = '4S', + SIX_S = '6S', } diff --git a/libs/common/src/constants/scene-switch-type.enum.ts b/libs/common/src/constants/scene-switch-type.enum.ts new file mode 100644 index 0000000..62175d5 --- /dev/null +++ b/libs/common/src/constants/scene-switch-type.enum.ts @@ -0,0 +1,8 @@ +export enum SceneSwitchesTypeEnum { + SCENE_1 = 'scene_1', + SCENE_2 = 'scene_2', + SCENE_3 = 'scene_3', + SCENE_4 = 'scene_4', + SCENE_5 = 'scene_5', + SCENE_6 = 'scene_6', +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 5d9d7f1..8b4acf4 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -8,8 +8,11 @@ import { UserOtpEntity } from '../modules/user/entities'; import { ProductEntity } from '../modules/product/entities'; import { DeviceEntity } from '../modules/device/entities'; import { PermissionTypeEntity } from '../modules/permission/entities'; -import { SpaceEntity } from '../modules/space/entities'; -import { SpaceTypeEntity } from '../modules/space/entities'; +import { + SpaceEntity, + SpaceLinkEntity, + SubspaceEntity, +} from '../modules/space/entities'; import { UserSpaceEntity } from '../modules/user/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities'; import { UserRoleEntity } from '../modules/user/entities'; @@ -19,8 +22,11 @@ import { DeviceNotificationEntity } from '../modules/device/entities'; import { RegionEntity } from '../modules/region/entities'; import { TimeZoneEntity } from '../modules/timezone/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; +import { CommunityEntity } from '../modules/community/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; +import { SceneDeviceEntity } from '../modules/scene-device/entities'; +import { SpaceProductEntity } from '../modules/space/entities/space-product.entity'; @Module({ imports: [ @@ -43,8 +49,11 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; DeviceUserPermissionEntity, DeviceEntity, PermissionTypeEntity, + CommunityEntity, SpaceEntity, - SpaceTypeEntity, + SpaceLinkEntity, + SubspaceEntity, + SpaceProductEntity, UserSpaceEntity, DeviceUserPermissionEntity, UserRoleEntity, @@ -57,6 +66,7 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; DeviceStatusLogEntity, SceneEntity, SceneIconEntity, + SceneDeviceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/dto/base.response.dto.ts b/libs/common/src/dto/base.response.dto.ts new file mode 100644 index 0000000..c51d38c --- /dev/null +++ b/libs/common/src/dto/base.response.dto.ts @@ -0,0 +1,23 @@ +import { WithOptional } from '../type/optional.type'; + +export class BaseResponseDto { + statusCode?: number; + + message: string; + + error?: string; + + data?: any; + + success?: boolean; + + static wrap({ + data, + statusCode = 200, + message = 'Success', + success = true, + error = undefined, + }: WithOptional) { + return { data, statusCode, success, message, error }; + } +} diff --git a/libs/common/src/dto/pagination.request.dto.ts b/libs/common/src/dto/pagination.request.dto.ts new file mode 100644 index 0000000..a2d011f --- /dev/null +++ b/libs/common/src/dto/pagination.request.dto.ts @@ -0,0 +1,66 @@ +import { IsDate, IsOptional } from 'class-validator'; +import { IsPageRequestParam } from '../validators/is-page-request-param.validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsSizeRequestParam } from '../validators/is-size-request-param.validator'; +import { Transform } from 'class-transformer'; +import { parseToDate } from '../util/parseToDate'; + +export class PaginationRequestGetListDto { + @IsOptional() + @IsPageRequestParam({ + message: 'Page must be bigger than 0', + }) + @ApiProperty({ + name: 'page', + required: false, + description: 'Page request', + }) + page?: number; + + @IsOptional() + @IsSizeRequestParam({ + message: 'Size must not be negative', + }) + @ApiProperty({ + name: 'size', + required: false, + description: 'Size request', + }) + size?: number; + + @IsOptional() + @ApiProperty({ + name: 'name', + required: false, + description: 'Name to be filtered', + }) + name?: string; + + @ApiProperty({ + name: 'from', + required: false, + type: Number, + description: `Start time in UNIX timestamp format to filter`, + example: 1674172800000, + }) + @IsOptional() + @Transform(({ value }) => parseToDate(value)) + @IsDate({ + message: `From must be in UNIX timestamp format in order to parse to Date instance`, + }) + from?: Date; + + @ApiProperty({ + name: 'to', + required: false, + type: Number, + description: `End time in UNIX timestamp format to filter`, + example: 1674259200000, + }) + @IsOptional() + @Transform(({ value }) => parseToDate(value)) + @IsDate({ + message: `To must be in UNIX timestamp format in order to parse to Date instance`, + }) + to?: Date; +} diff --git a/libs/common/src/dto/pagination.response.dto.ts b/libs/common/src/dto/pagination.response.dto.ts new file mode 100644 index 0000000..5a0e4cf --- /dev/null +++ b/libs/common/src/dto/pagination.response.dto.ts @@ -0,0 +1,62 @@ +import { BaseResponseDto } from './base.response.dto'; + +export interface PageResponseDto { + // Original paging information from the request ( or default ) + page: number; + size: number; + + // Useful for display (N Records found) + totalItem: number; + + // Use for display N Pages ( 0... N ) + totalPage: number; + + // Has next is false when cursor is at last page + hasNext: boolean; + + // Has previous is false when cursor is at first page + hasPrevious: boolean; +} + +export class PageResponse implements BaseResponseDto, PageResponseDto { + code?: number; + + message: string; + + data: Array; + + page: number; + + size: number; + + totalItem: number; + + totalPage: number; + + hasNext: boolean; + + hasPrevious: boolean; + + constructor( + baseResponseDto: BaseResponseDto, + pageResponseDto: PageResponseDto, + ) { + if (baseResponseDto.statusCode) { + this.code = baseResponseDto.statusCode; + } else { + this.code = 200; + } + + if (baseResponseDto.data) { + this.data = baseResponseDto.data; + } + + this.message = baseResponseDto.message; + this.page = pageResponseDto.page; + this.size = pageResponseDto.size; + this.totalItem = pageResponseDto.totalItem; + this.totalPage = pageResponseDto.totalPage; + this.hasNext = pageResponseDto.hasNext; + this.hasPrevious = pageResponseDto.hasPrevious; + } +} diff --git a/libs/common/src/dto/success.response.dto.ts b/libs/common/src/dto/success.response.dto.ts new file mode 100644 index 0000000..64cd0c4 --- /dev/null +++ b/libs/common/src/dto/success.response.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BaseResponseDto } from './base.response.dto'; + +export class SuccessResponseDto implements BaseResponseDto { + @ApiProperty({ + example: 200, + }) + statusCode: number; + + data: Type; + + @ApiProperty({ + example: 'Success message', + }) + message: string; + + @ApiProperty({ + example: true, + description: 'Indicates that the operation was successful', + }) + success: boolean; + + constructor(input: BaseResponseDto) { + if (input.statusCode) this.statusCode = input.statusCode; + else this.statusCode = 200; + if (input.message) this.message = input.message; + if (input.data) this.data = input.data; + this.success = true; + } +} diff --git a/libs/common/src/firebase/devices-status/controllers/devices-status.controller.ts b/libs/common/src/firebase/devices-status/controllers/devices-status.controller.ts index 4a3458d..5a79b1c 100644 --- a/libs/common/src/firebase/devices-status/controllers/devices-status.controller.ts +++ b/libs/common/src/firebase/devices-status/controllers/devices-status.controller.ts @@ -1,13 +1,14 @@ import { Controller, Post, Param } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto'; import { DeviceStatusFirebaseService } from '../services/devices-status.service'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Device Status Firebase Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'device-status-firebase', + path: ControllerRoute.DEVICE_STATUS_FIREBASE.ROUTE, }) export class DeviceStatusFirebaseController { constructor( @@ -16,6 +17,13 @@ export class DeviceStatusFirebaseController { @ApiBearerAuth() @Post(':deviceTuyaUuid') + @ApiOperation({ + summary: + ControllerRoute.DEVICE_STATUS_FIREBASE.ACTIONS.ADD_DEVICE_STATUS_SUMMARY, + description: + ControllerRoute.DEVICE_STATUS_FIREBASE.ACTIONS + .ADD_DEVICE_STATUS_DESCRIPTION, + }) async addDeviceStatus( @Param('deviceTuyaUuid') deviceTuyaUuid: string, ): Promise { diff --git a/libs/common/src/helper/helper.module.ts b/libs/common/src/helper/helper.module.ts index 34a51c2..b4cf2fd 100644 --- a/libs/common/src/helper/helper.module.ts +++ b/libs/common/src/helper/helper.module.ts @@ -10,19 +10,27 @@ import { DeviceMessagesService } from './services/device.messages.service'; import { DeviceRepositoryModule } from '../modules/device/device.repository.module'; import { DeviceNotificationRepository } from '../modules/device/repositories'; import { DeviceStatusFirebaseModule } from '../firebase/devices-status/devices-status.module'; +import { CommunityPermissionService } from './services/community.permission.service'; +import { CommunityRepository } from '../modules/community/repositories'; @Global() @Module({ providers: [ HelperHashService, SpacePermissionService, + CommunityPermissionService, SpaceRepository, TuyaWebSocketService, OneSignalService, DeviceMessagesService, DeviceNotificationRepository, + CommunityRepository, + ], + exports: [ + HelperHashService, + SpacePermissionService, + CommunityPermissionService, ], - exports: [HelperHashService, SpacePermissionService], controllers: [], imports: [ SpaceRepositoryModule, diff --git a/libs/common/src/helper/services/community.permission.service.ts b/libs/common/src/helper/services/community.permission.service.ts new file mode 100644 index 0000000..2422e38 --- /dev/null +++ b/libs/common/src/helper/services/community.permission.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; + +@Injectable() +export class CommunityPermissionService { + constructor(private readonly communityRepository: CommunityRepository) {} + + async checkUserPermission(communityUuid: string): Promise { + try { + const communityData = await this.communityRepository.findOne({ + where: { + uuid: communityUuid, + }, + relations: ['users'], + }); + + if (!communityData) { + throw new BadRequestException( + 'You do not have permission to access this community', + ); + } + } catch (err) { + throw new BadRequestException(err.message || 'Invalid UUID'); + } + } +} diff --git a/libs/common/src/helper/services/space.permission.service.ts b/libs/common/src/helper/services/space.permission.service.ts index 0f11cbc..f5a739d 100644 --- a/libs/common/src/helper/services/space.permission.service.ts +++ b/libs/common/src/helper/services/space.permission.service.ts @@ -9,27 +9,23 @@ export class SpacePermissionService { async checkUserPermission( spaceUuid: string, userUuid: string, - type: string, ): Promise { try { const spaceData = await this.spaceRepository.findOne({ where: { uuid: spaceUuid, - spaceType: { - type: type, - }, userSpaces: { user: { uuid: userUuid, }, }, }, - relations: ['spaceType', 'userSpaces', 'userSpaces.user'], + relations: ['userSpaces', 'userSpaces.user'], }); if (!spaceData) { throw new BadRequestException( - `You do not have permission to access this ${type}`, + `You do not have permission to access this space`, ); } } catch (err) { diff --git a/libs/common/src/integrations/tuya/interfaces/automation.interface.ts b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts new file mode 100644 index 0000000..fb061c2 --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/automation.interface.ts @@ -0,0 +1,9 @@ +import { TuyaResponseInterface } from './tuya.response.interface'; + +export interface AddTuyaResponseInterface extends TuyaResponseInterface { + result: { + id: string; + }; + t?: number; + tid?: string; +} diff --git a/libs/common/src/integrations/tuya/interfaces/index.ts b/libs/common/src/integrations/tuya/interfaces/index.ts new file mode 100644 index 0000000..06eff87 --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './tuya.response.interface'; +export * from './tap-to-run-action.interface'; +export * from './automation.interface'; diff --git a/libs/common/src/integrations/tuya/interfaces/tap-to-run-action.interface.ts b/libs/common/src/integrations/tuya/interfaces/tap-to-run-action.interface.ts new file mode 100644 index 0000000..13b8031 --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/tap-to-run-action.interface.ts @@ -0,0 +1,11 @@ +export interface ConvertedExecutorProperty { + function_code?: string; + function_value?: any; + delay_seconds?: number; +} + +export interface ConvertedAction { + entity_id: string; + action_executor: string; + executor_property?: ConvertedExecutorProperty; +} diff --git a/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts new file mode 100644 index 0000000..141613d --- /dev/null +++ b/libs/common/src/integrations/tuya/interfaces/tuya.response.interface.ts @@ -0,0 +1,5 @@ +export interface TuyaResponseInterface { + success: boolean; + msg?: string; + result: boolean | { id: string }; +} diff --git a/libs/common/src/integrations/tuya/services/tuya.service.ts b/libs/common/src/integrations/tuya/services/tuya.service.ts new file mode 100644 index 0000000..76c6582 --- /dev/null +++ b/libs/common/src/integrations/tuya/services/tuya.service.ts @@ -0,0 +1,251 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { + AddTuyaResponseInterface, + ConvertedAction, + TuyaResponseInterface, +} from '../interfaces'; + +@Injectable() +export class TuyaService { + private tuya: TuyaContext; + + constructor(private readonly configService: ConfigService) { + const accessKey = this.configService.get('auth-config.ACCESS_KEY'); + const secretKey = this.configService.get('auth-config.SECRET_KEY'); + const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); + this.tuya = new TuyaContext({ + baseUrl: tuyaEuUrl, + accessKey, + secretKey, + }); + } + async createSpace({ name }: { name: string }) { + const path = '/v2.0/cloud/space/creation'; + const body = { name }; + + const response = await this.tuya.request({ + method: 'POST', + path, + body, + }); + + if (response.success) { + return response.result as string; + } else { + throw new HttpException( + 'Error creating space in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getDeviceDetails(deviceId: string) { + const path = `/v1.1/iot-03/devices/${deviceId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + if (!response.success) { + throw new HttpException( + `Error fetching device details: ${response.msg}`, + HttpStatus.BAD_REQUEST, + ); + } + + return response.result; + } + + async deleteSceneRule(sceneId: string, spaceId: string) { + const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${spaceId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (response.success) { + return response; + } else { + throw new HttpException( + `Error deleting scene rule: ${response.msg}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getSceneRule(sceneId: string) { + const path = `/v2.0/cloud/scene/rule/${sceneId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + + if (response.success) { + return response; + } else { + throw new HttpException( + `Error fetching scene rule: ${response.msg}`, + HttpStatus.BAD_REQUEST, + ); + } + } + + async addTapToRunScene( + spaceId: string, + sceneName: string, + actions: ConvertedAction[], + decisionExpr: string, + ) { + const path = `/v2.0/cloud/scene/rule`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: spaceId, + name: sceneName, + type: 'scene', + decision_expr: decisionExpr, + actions: actions, + }, + }); + + if (response.success) { + return response; + } else { + throw new HttpException( + `Error fetching scene rule: ${response.msg}`, + HttpStatus.BAD_REQUEST, + ); + } + } + async updateTapToRunScene( + sceneTuyaUuid: string, + spaceId: string, + sceneName: string, + actions: ConvertedAction[], + decisionExpr: string, + ) { + const path = `/v2.0/cloud/scene/rule/${sceneTuyaUuid}`; + const response = await this.tuya.request({ + method: 'PUT', + path, + body: { + space_id: spaceId, + name: sceneName, + type: 'scene', + decision_expr: decisionExpr, + conditions: [], + actions: actions, + }, + }); + + if (response.success) { + return response; + } else { + throw new HttpException( + `Error fetching scene rule: ${response.msg}`, + HttpStatus.BAD_REQUEST, + ); + } + } + + async triggerScene(sceneId: string): Promise { + const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`; + const response: TuyaResponseInterface = await this.tuya.request({ + method: 'POST', + path, + }); + + if (!response.success) { + throw new HttpException( + response.msg || 'Error triggering scene', + HttpStatus.BAD_REQUEST, + ); + } + + return response; + } + + async createAutomation( + spaceId: string, + automationName: string, + effectiveTime: any, + decisionExpr: string, + conditions: any[], + actions: any[], + ) { + const path = `/v2.0/cloud/scene/rule`; + + const response: AddTuyaResponseInterface = await this.tuya.request({ + method: 'POST', + path, + body: { + space_id: spaceId, + name: automationName, + effective_time: { + ...effectiveTime, + timezone_id: 'Asia/Dubai', + }, + type: 'automation', + decision_expr: decisionExpr, + conditions: conditions, + actions: actions, + }, + }); + + if (!response.success) { + throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); + } + + return response; + } + + async deleteAutomation(spaceId: string, automationUuid: string) { + const path = `/v2.0/cloud/scene/rule?ids=${automationUuid}&space_id=${spaceId}`; + const response = await this.tuya.request({ + method: 'DELETE', + path, + }); + + if (!response.success) { + throw new HttpException( + 'Failed to delete automation', + HttpStatus.NOT_FOUND, + ); + } + + return response; + } + + async updateAutomationState( + spaceId: string, + automationUuid: string, + isEnable: boolean, + ) { + const path = `/v2.0/cloud/scene/rule/state?space_id=${spaceId}`; + + try { + const response: TuyaResponseInterface = await this.tuya.request({ + method: 'PUT', + path, + body: { + ids: automationUuid, + is_enable: isEnable, + }, + }); + + if (!response.success) { + throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + } + + return response; + } catch (error) { + throw new HttpException( + error.message || 'Failed to update automation state', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/libs/common/src/models/typeOrmCustom.model.ts b/libs/common/src/models/typeOrmCustom.model.ts new file mode 100644 index 0000000..0f3abbc --- /dev/null +++ b/libs/common/src/models/typeOrmCustom.model.ts @@ -0,0 +1,145 @@ +import { FindManyOptions, Repository } from 'typeorm'; +import { InternalServerErrorException } from '@nestjs/common'; +import { BaseResponseDto } from '../dto/base.response.dto'; +import { PageResponseDto } from '../dto/pagination.response.dto'; +import { buildTypeORMSortQuery } from '../util/buildTypeORMSortQuery'; +import { buildTypeORMIncludeQuery } from '../util/buildTypeORMIncludeQuery'; +import { buildTypeORMWhereClause } from '../util/buildTypeORMWhereClause'; +import { getPaginationResponseDto } from '../util/getPaginationResponseDto'; + +export interface TypeORMCustomModelFindAllQuery { + page: number | undefined; + size: number | undefined; + sort?: string; + modelName?: string; + include?: string; + where?: { [key: string]: unknown }; + select?: string[]; + includeDisable?: boolean | string; +} +interface CustomFindAllQuery { + page?: number; + size?: number; + [key: string]: any; +} + +interface FindAllQueryWithDefaults extends CustomFindAllQuery { + page: number; + size: number; +} + +function getDefaultQueryOptions( + query: Partial, +): FindManyOptions & FindAllQueryWithDefaults { + const { page, size, includeDisable, modelName, ...rest } = query; + + // Set default if undefined or null + const returnPage = page ? Number(page) : 1; + const returnSize = size ? Number(size) : 10; + const returnIncludeDisable = + includeDisable === true || includeDisable === 'true'; + + // Return query with defaults and ensure modelName is passed through + return { + skip: (returnPage - 1) * returnSize, + take: returnSize, + where: { + ...rest, + }, + page: returnPage, + size: returnSize, + includeDisable: returnIncludeDisable, + modelName: modelName || query.modelName, // Ensure modelName is passed through + }; +} + +export interface TypeORMCustomModelFindAllQueryWithDefault + extends TypeORMCustomModelFindAllQuery { + page: number; + size: number; +} + +export type TypeORMCustomModelFindAllResponse = { + baseResponseDto: BaseResponseDto; + paginationResponseDto: PageResponseDto; +}; + +export function TypeORMCustomModel(repository: Repository) { + return Object.assign(repository, { + findAll: async function ( + query: Partial, + ): Promise { + // Extract values from the query + const { + page = 1, + size = 10, + sort, + modelName, + include, + where, + select, + } = getDefaultQueryOptions(query); + + // Ensure modelName is set before proceeding + if (!modelName) { + console.error( + 'modelName is missing after getDefaultQueryOptions:', + query, + ); + throw new InternalServerErrorException( + `[TypeORMCustomModel] Cannot findAll with unknown modelName`, + ); + } + + const skip = (page - 1) * size; + const order = buildTypeORMSortQuery(sort); + const relations = buildTypeORMIncludeQuery(modelName, include); + + // Use the where clause directly, without wrapping it under 'where' + const whereClause = buildTypeORMWhereClause({ where }); + console.log('Final where clause:', whereClause); + + // Ensure the whereClause is passed directly to findAndCount + const [data, count] = await repository.findAndCount({ + where: whereClause, + take: size, + skip: skip, + order: order, + select: select, + relations: relations, + }); + + const paginationResponseDto = getPaginationResponseDto(count, page, size); + const baseResponseDto: BaseResponseDto = { + data, + message: getResponseMessage(modelName, { where }), + }; + + return { baseResponseDto, paginationResponseDto }; + }, + }); +} + +function getResponseMessage( + modelName: string, + query?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where?: any; + }, +): string { + if (!query) { + return `Success get list ${modelName}`; + } + + const { where } = query; + if (modelName === 'user' && where && where?.community) { + const { + some: { communityId }, + } = where.community; + if (typeof communityId === 'string') { + return `Success get list ${modelName} belong to community`; + } + } + + return `Success get list ${modelName}`; +} diff --git a/libs/common/src/modules/community/dtos/community.dto.ts b/libs/common/src/modules/community/dtos/community.dto.ts new file mode 100644 index 0000000..ebafb7e --- /dev/null +++ b/libs/common/src/modules/community/dtos/community.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class CommunityDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsOptional() + public description?: string; + + @IsUUID() + @IsNotEmpty() + public regionId: string; +} diff --git a/libs/common/src/modules/community/dtos/index.ts b/libs/common/src/modules/community/dtos/index.ts new file mode 100644 index 0000000..e030f3a --- /dev/null +++ b/libs/common/src/modules/community/dtos/index.ts @@ -0,0 +1 @@ +export * from './community.dto'; diff --git a/libs/common/src/modules/community/entities/community.entity.ts b/libs/common/src/modules/community/entities/community.entity.ts new file mode 100644 index 0000000..3b6f122 --- /dev/null +++ b/libs/common/src/modules/community/entities/community.entity.ts @@ -0,0 +1,34 @@ +import { Column, Entity, OneToMany, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { CommunityDto } from '../dtos'; +import { SpaceEntity } from '../../space/entities'; + +@Entity({ name: 'community' }) +@Unique(['name']) +export class CommunityEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + length: 255, + nullable: false, + }) + name: string; + + @Column({ length: 255, nullable: true }) + description: string; + + @OneToMany(() => SpaceEntity, (space) => space.community) + spaces: SpaceEntity[]; + + @Column({ + type: 'varchar', + length: 255, + nullable: true, + }) + externalId: string; +} diff --git a/libs/common/src/modules/community/entities/index.ts b/libs/common/src/modules/community/entities/index.ts new file mode 100644 index 0000000..61f1d4c --- /dev/null +++ b/libs/common/src/modules/community/entities/index.ts @@ -0,0 +1 @@ +export * from './community.entity'; diff --git a/libs/common/src/modules/community/repositories/community.repository.ts b/libs/common/src/modules/community/repositories/community.repository.ts new file mode 100644 index 0000000..137ef2e --- /dev/null +++ b/libs/common/src/modules/community/repositories/community.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { CommunityEntity } from '../entities'; + +@Injectable() +export class CommunityRepository extends Repository { + constructor(private dataSource: DataSource) { + super(CommunityEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/community/repositories/index.ts b/libs/common/src/modules/community/repositories/index.ts new file mode 100644 index 0000000..fb4a11d --- /dev/null +++ b/libs/common/src/modules/community/repositories/index.ts @@ -0,0 +1 @@ +export * from './community.repository'; diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 5a96255..9a75950 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -1,11 +1,20 @@ -import { Column, Entity, ManyToOne, OneToMany, Unique, Index } from 'typeorm'; +import { + Column, + Entity, + ManyToOne, + OneToMany, + Unique, + Index, + JoinColumn, +} from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto'; -import { SpaceEntity } from '../../space/entities'; +import { SpaceEntity, SubspaceEntity } from '../../space/entities'; import { ProductEntity } from '../../product/entities'; import { UserEntity } from '../../user/entities'; import { DeviceNotificationDto } from '../dtos'; import { PermissionTypeEntity } from '../../permission/entities'; +import { SceneDeviceEntity } from '../../scene-device/entities'; @Entity({ name: 'device' }) @Unique(['deviceTuyaUuid']) @@ -42,7 +51,7 @@ export class DeviceEntity extends AbstractEntity { ) deviceUserNotification: DeviceNotificationEntity[]; - @ManyToOne(() => SpaceEntity, (space) => space.devicesSpaceEntity, { + @ManyToOne(() => SpaceEntity, (space) => space.devices, { nullable: true, }) spaceDevice: SpaceEntity; @@ -52,10 +61,19 @@ export class DeviceEntity extends AbstractEntity { }) productDevice: ProductEntity; + @ManyToOne(() => SubspaceEntity, (subspace) => subspace.devices, { + nullable: true, + }) + @JoinColumn({ name: 'subspace_id' }) + subspace: SubspaceEntity; + @Index() @Column({ nullable: false }) uuid: string; + @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {}) + sceneDevices: SceneDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 6553dc1..2c731a0 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; import { ProductDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; +import { SpaceProductEntity } from '../../space/entities/space-product.entity'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @@ -16,11 +17,19 @@ export class ProductEntity extends AbstractEntity { }) public prodId: string; + @Column({ + nullable: true, + }) + public name: string; + @Column({ nullable: false, }) public prodType: string; + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product) + spaceProducts: SpaceProductEntity[]; + @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/scene-device/dtos/index.ts b/libs/common/src/modules/scene-device/dtos/index.ts new file mode 100644 index 0000000..96f03a1 --- /dev/null +++ b/libs/common/src/modules/scene-device/dtos/index.ts @@ -0,0 +1 @@ +export * from './scene-device.dto'; diff --git a/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts b/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts new file mode 100644 index 0000000..fd7e26a --- /dev/null +++ b/libs/common/src/modules/scene-device/dtos/scene-device.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SceneDeviceDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public deviceUuid: string; + @IsString() + @IsNotEmpty() + public sceneUuid: string; + @IsString() + @IsNotEmpty() + public switchName: string; + @IsString() + @IsNotEmpty() + public automationTuyaUuid: string; +} diff --git a/libs/common/src/modules/scene-device/entities/index.ts b/libs/common/src/modules/scene-device/entities/index.ts new file mode 100644 index 0000000..337a094 --- /dev/null +++ b/libs/common/src/modules/scene-device/entities/index.ts @@ -0,0 +1 @@ +export * from './scene-device.entity'; diff --git a/libs/common/src/modules/scene-device/entities/scene-device.entity.ts b/libs/common/src/modules/scene-device/entities/scene-device.entity.ts new file mode 100644 index 0000000..9814fdf --- /dev/null +++ b/libs/common/src/modules/scene-device/entities/scene-device.entity.ts @@ -0,0 +1,51 @@ +import { Column, Entity, JoinColumn, ManyToOne, Unique } from 'typeorm'; +import { SceneDeviceDto } from '../dtos'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; +import { DeviceEntity } from '../../device/entities'; +import { SceneEntity } from '../../scene/entities'; + +@Entity({ name: 'scene-device' }) +@Unique(['device', 'switchName']) +export class SceneDeviceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @ManyToOne(() => DeviceEntity, (device) => device.sceneDevices, { + nullable: false, + }) + @JoinColumn({ name: 'device_uuid' }) + device: DeviceEntity; + + @Column({ + nullable: false, + }) + sceneUuid: string; + + @Column({ + nullable: false, + type: 'enum', + enum: SceneSwitchesTypeEnum, + }) + switchName: SceneSwitchesTypeEnum; + + @Column({ + nullable: false, + }) + automationTuyaUuid: string; + + @ManyToOne(() => SceneEntity, (scene) => scene.sceneDevices, { + nullable: false, + }) + @JoinColumn({ name: 'scene_uuid' }) + scene: SceneEntity; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/scene-device/repositories/index.ts b/libs/common/src/modules/scene-device/repositories/index.ts new file mode 100644 index 0000000..f10ccb5 --- /dev/null +++ b/libs/common/src/modules/scene-device/repositories/index.ts @@ -0,0 +1 @@ +export * from './scene-device.repository'; diff --git a/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts b/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts new file mode 100644 index 0000000..1d7915c --- /dev/null +++ b/libs/common/src/modules/scene-device/repositories/scene-device.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SceneDeviceEntity } from '../entities'; + +@Injectable() +export class SceneDeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SceneDeviceEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/scene-device/scene-device.repository.module.ts b/libs/common/src/modules/scene-device/scene-device.repository.module.ts new file mode 100644 index 0000000..96fa8a7 --- /dev/null +++ b/libs/common/src/modules/scene-device/scene-device.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SceneDeviceEntity } from './entities'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SceneDeviceEntity])], +}) +export class SceneDeviceRepositoryModule {} diff --git a/libs/common/src/modules/scene/entities/scene.entity.ts b/libs/common/src/modules/scene/entities/scene.entity.ts index 2ade9ad..5daa690 100644 --- a/libs/common/src/modules/scene/entities/scene.entity.ts +++ b/libs/common/src/modules/scene/entities/scene.entity.ts @@ -1,7 +1,9 @@ -import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { SceneDto, SceneIconDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; +import { SceneDeviceEntity } from '../../scene-device/entities'; +import { SpaceEntity } from '../../space/entities'; // Define SceneIconEntity before SceneEntity @Entity({ name: 'scene-icon' }) @@ -44,20 +46,27 @@ export class SceneEntity extends AbstractEntity { nullable: false, }) sceneTuyaUuid: string; - @Column({ - nullable: false, - }) - unitUuid: string; + @Column({ nullable: false, }) showInHomePage: boolean; + @ManyToOne(() => SpaceEntity, (space) => space.scenes, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_uuid' }) + space: SpaceEntity; + @ManyToOne(() => SceneIconEntity, (icon) => icon.scenesIconEntity, { nullable: false, }) sceneIcon: SceneIconEntity; + @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.scene) + sceneDevices: SceneDeviceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/space/dtos/index.ts b/libs/common/src/modules/space/dtos/index.ts index 9144208..fcc0fdd 100644 --- a/libs/common/src/modules/space/dtos/index.ts +++ b/libs/common/src/modules/space/dtos/index.ts @@ -1 +1,2 @@ export * from './space.dto'; +export * from './subspace.dto'; diff --git a/libs/common/src/modules/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts index 04072fd..98706d0 100644 --- a/libs/common/src/modules/space/dtos/space.dto.ts +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -21,13 +21,3 @@ export class SpaceDto { @IsNotEmpty() public invitationCode: string; } - -export class SpaceTypeDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public type: string; -} diff --git a/libs/common/src/modules/space/dtos/subspace.dto.ts b/libs/common/src/modules/space/dtos/subspace.dto.ts new file mode 100644 index 0000000..d852345 --- /dev/null +++ b/libs/common/src/modules/space/dtos/subspace.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { SpaceDto } from './space.dto'; +import { DeviceDto } from '../../device/dtos'; + +export class SubspaceDto { + @ApiProperty({ + example: 'd7a44e8a-32d5-4f39-ae2e-013f1245aead', + description: 'UUID of the subspace', + }) + uuid: string; + + @ApiProperty({ + example: 'Meeting Room 1', + description: 'Name of the subspace', + }) + subspaceName: string; + + @ApiProperty({ + type: () => SpaceDto, + description: 'The space to which this subspace belongs', + }) + space: SpaceDto; + + @ApiProperty({ + example: [], + description: 'List of devices assigned to this subspace, if any', + }) + devices: DeviceDto[]; +} diff --git a/libs/common/src/modules/space/entities/index.ts b/libs/common/src/modules/space/entities/index.ts index bce8032..f720cf4 100644 --- a/libs/common/src/modules/space/entities/index.ts +++ b/libs/common/src/modules/space/entities/index.ts @@ -1 +1,3 @@ export * from './space.entity'; +export * from './subspace.entity'; +export * from './space-link.entity'; diff --git a/libs/common/src/modules/space/entities/space-link.entity.ts b/libs/common/src/modules/space/entities/space-link.entity.ts new file mode 100644 index 0000000..a62ce4f --- /dev/null +++ b/libs/common/src/modules/space/entities/space-link.entity.ts @@ -0,0 +1,26 @@ +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from './space.entity'; +import { Direction } from '@app/common/constants/direction.enum'; + +@Entity({ name: 'space-link' }) +export class SpaceLinkEntity extends AbstractEntity { + @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'start_space_id' }) + public startSpace: SpaceEntity; + + @ManyToOne(() => SpaceEntity, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'end_space_id' }) + public endSpace: SpaceEntity; + + @Column({ + nullable: false, + enum: Object.values(Direction), + }) + direction: string; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/space-product.entity.ts b/libs/common/src/modules/space/entities/space-product.entity.ts new file mode 100644 index 0000000..92ad411 --- /dev/null +++ b/libs/common/src/modules/space/entities/space-product.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm'; +import { SpaceEntity } from './space.entity'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities'; + +@Entity({ name: 'space-product' }) +export class SpaceProductEntity extends AbstractEntity { + @ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_uuid' }) + space: SpaceEntity; + + @ManyToOne(() => ProductEntity, (product) => product.spaceProducts, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'product_uuid' }) + product: ProductEntity; + + @Column({ + nullable: false, + type: 'int', + }) + productCount: number; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 10e78a8..103f0a2 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,30 +1,20 @@ -import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; -import { SpaceDto, SpaceTypeDto } from '../dtos'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + Unique, +} from 'typeorm'; +import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { UserSpaceEntity } from '../../user/entities'; import { DeviceEntity } from '../../device/entities'; - -@Entity({ name: 'space-type' }) -export class SpaceTypeEntity extends AbstractEntity { - @Column({ - type: 'uuid', - default: () => 'gen_random_uuid()', - nullable: false, - }) - public uuid: string; - - @Column({ - nullable: false, - }) - type: string; - - @OneToMany(() => SpaceEntity, (space) => space.spaceType) - spaces: SpaceEntity[]; - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} +import { CommunityEntity } from '../../community/entities'; +import { SubspaceEntity } from './subspace.entity'; +import { SpaceLinkEntity } from './space-link.entity'; +import { SpaceProductEntity } from './space-product.entity'; +import { SceneEntity } from '../../scene/entities'; @Entity({ name: 'space' }) @Unique(['invitationCode']) @@ -45,6 +35,13 @@ export class SpaceEntity extends AbstractEntity { }) public spaceName: string; + @ManyToOne(() => CommunityEntity, (community) => community.spaces, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'community_id' }) + community: CommunityEntity; + @Column({ nullable: true, }) @@ -52,21 +49,54 @@ export class SpaceEntity extends AbstractEntity { @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) parent: SpaceEntity; - @OneToMany(() => SpaceEntity, (space) => space.parent) - children: SpaceEntity[]; - @ManyToOne(() => SpaceTypeEntity, (spaceType) => spaceType.spaces, { + @OneToMany(() => SpaceEntity, (space) => space.parent, { nullable: false, + onDelete: 'CASCADE', }) - spaceType: SpaceTypeEntity; + children: SpaceEntity[]; @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.space) userSpaces: UserSpaceEntity[]; + @OneToMany(() => SubspaceEntity, (subspace) => subspace.space, { + nullable: true, + }) + subspaces?: SubspaceEntity[]; + + // Position columns + @Column({ type: 'float', nullable: false, default: 0 }) + public x: number; // X coordinate for position + + @Column({ type: 'float', nullable: false, default: 0 }) + public y: number; // Y coordinate for position + @OneToMany( () => DeviceEntity, (devicesSpaceEntity) => devicesSpaceEntity.spaceDevice, ) - devicesSpaceEntity: DeviceEntity[]; + devices: DeviceEntity[]; + + @OneToMany(() => SpaceLinkEntity, (connection) => connection.startSpace, { + nullable: true, + }) + public outgoingConnections: SpaceLinkEntity[]; + + @OneToMany(() => SpaceLinkEntity, (connection) => connection.endSpace, { + nullable: true, + }) + public incomingConnections: SpaceLinkEntity[]; + + @Column({ + nullable: true, + type: 'text', + }) + public icon: string; + + @OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space) + spaceProducts: SpaceProductEntity[]; + + @OneToMany(() => SceneEntity, (scene) => scene.space) + scenes: SceneEntity[]; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/subspace.entity.ts b/libs/common/src/modules/space/entities/subspace.entity.ts new file mode 100644 index 0000000..ab38b00 --- /dev/null +++ b/libs/common/src/modules/space/entities/subspace.entity.ts @@ -0,0 +1,37 @@ +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { DeviceEntity } from '../../device/entities'; +import { SpaceEntity } from './space.entity'; +import { SubspaceDto } from '../dtos'; + +@Entity({ name: 'subspace' }) +export class SubspaceEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + nullable: false, + }) + public subspaceName: string; + + @ManyToOne(() => SpaceEntity, (space) => space.subspaces, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'space_id' }) + space: SpaceEntity; + + @OneToMany(() => DeviceEntity, (device) => device.subspace, { + nullable: true, + }) + devices: DeviceEntity[]; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index ea11a6e..43ce45e 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,6 +1,7 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; -import { SpaceEntity, SpaceTypeEntity } from '../entities'; +import { SpaceProductEntity } from '../entities/space-product.entity'; +import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities'; @Injectable() export class SpaceRepository extends Repository { @@ -8,10 +9,22 @@ export class SpaceRepository extends Repository { super(SpaceEntity, dataSource.createEntityManager()); } } - @Injectable() -export class SpaceTypeRepository extends Repository { +export class SubspaceRepository extends Repository { constructor(private dataSource: DataSource) { - super(SpaceTypeEntity, dataSource.createEntityManager()); + super(SubspaceEntity, dataSource.createEntityManager()); + } +} + +@Injectable() +export class SpaceLinkRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceLinkEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class SpaceProductRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceProductEntity, dataSource.createEntityManager()); } } diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts index 9906708..90916c2 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SpaceEntity, SpaceTypeEntity } from './entities'; +import { SpaceEntity, SubspaceEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [TypeOrmModule.forFeature([SpaceEntity, SpaceTypeEntity])], + imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity])], }) export class SpaceRepositoryModule {} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 141f5de..c9a11e6 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -115,6 +115,7 @@ export class UserEntity extends AbstractEntity { (visitorPassword) => visitorPassword.user, ) public visitorPasswords: VisitorPasswordEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/seed/seeder.module.ts b/libs/common/src/seed/seeder.module.ts index 0c89c5c..eeb245b 100644 --- a/libs/common/src/seed/seeder.module.ts +++ b/libs/common/src/seed/seeder.module.ts @@ -7,8 +7,6 @@ import { ConfigModule } from '@nestjs/config'; import { RoleTypeRepositoryModule } from '../modules/role-type/role.type.repository.module'; import { RoleTypeRepository } from '../modules/role-type/repositories'; import { RoleTypeSeeder } from './services/role.type.seeder'; -import { SpaceTypeRepository } from '../modules/space/repositories'; -import { SpaceTypeSeeder } from './services/space.type.seeder'; import { SpaceRepositoryModule } from '../modules/space/space.repository.module'; import { SuperAdminSeeder } from './services/supper.admin.seeder'; import { UserRepository } from '../modules/user/repositories'; @@ -25,11 +23,9 @@ import { SceneIconRepository } from '../modules/scene/repositories'; providers: [ PermissionTypeSeeder, RoleTypeSeeder, - SpaceTypeSeeder, SeederService, PermissionTypeRepository, RoleTypeRepository, - SpaceTypeRepository, SuperAdminSeeder, UserRepository, UserRoleRepository, diff --git a/libs/common/src/seed/services/scene.icon.seeder.ts b/libs/common/src/seed/services/scene.icon.seeder.ts index 3f363ce..3d698eb 100644 --- a/libs/common/src/seed/services/scene.icon.seeder.ts +++ b/libs/common/src/seed/services/scene.icon.seeder.ts @@ -14,8 +14,6 @@ export class SceneIconSeeder { }); if (defaultSceneIconData.length <= 0) { - console.log('Creating default scene icon...'); - await this.createDefaultSceneIcon(); } } catch (err) { diff --git a/libs/common/src/seed/services/seeder.service.ts b/libs/common/src/seed/services/seeder.service.ts index e627e44..09fb91d 100644 --- a/libs/common/src/seed/services/seeder.service.ts +++ b/libs/common/src/seed/services/seeder.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PermissionTypeSeeder } from './permission.type.seeder'; import { RoleTypeSeeder } from './role.type.seeder'; -import { SpaceTypeSeeder } from './space.type.seeder'; import { SuperAdminSeeder } from './supper.admin.seeder'; import { RegionSeeder } from './regions.seeder'; import { TimeZoneSeeder } from './timezone.seeder'; @@ -11,7 +10,6 @@ export class SeederService { constructor( private readonly permissionTypeSeeder: PermissionTypeSeeder, private readonly roleTypeSeeder: RoleTypeSeeder, - private readonly spaceTypeSeeder: SpaceTypeSeeder, private readonly regionSeeder: RegionSeeder, private readonly timeZoneSeeder: TimeZoneSeeder, private readonly superAdminSeeder: SuperAdminSeeder, @@ -21,7 +19,6 @@ export class SeederService { async seed() { await this.permissionTypeSeeder.addPermissionTypeDataIfNotFound(); await this.roleTypeSeeder.addRoleTypeDataIfNotFound(); - await this.spaceTypeSeeder.addSpaceTypeDataIfNotFound(); await this.regionSeeder.addRegionDataIfNotFound(); await this.timeZoneSeeder.addTimeZoneDataIfNotFound(); await this.superAdminSeeder.createSuperAdminIfNotFound(); diff --git a/libs/common/src/seed/services/space.type.seeder.ts b/libs/common/src/seed/services/space.type.seeder.ts deleted file mode 100644 index 56c2ee8..0000000 --- a/libs/common/src/seed/services/space.type.seeder.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SpaceType } from '../../constants/space-type.enum'; -import { SpaceTypeRepository } from '../../modules/space/repositories'; - -@Injectable() -export class SpaceTypeSeeder { - constructor(private readonly spaceTypeRepository: SpaceTypeRepository) {} - - async addSpaceTypeDataIfNotFound(): Promise { - try { - const existingSpaceTypes = await this.spaceTypeRepository.find(); - - const spaceTypeNames = existingSpaceTypes.map((pt) => pt.type); - - const missingSpaceTypes = []; - if (!spaceTypeNames.includes(SpaceType.COMMUNITY)) { - missingSpaceTypes.push(SpaceType.COMMUNITY); - } - if (!spaceTypeNames.includes(SpaceType.BUILDING)) { - missingSpaceTypes.push(SpaceType.BUILDING); - } - if (!spaceTypeNames.includes(SpaceType.FLOOR)) { - missingSpaceTypes.push(SpaceType.FLOOR); - } - if (!spaceTypeNames.includes(SpaceType.UNIT)) { - missingSpaceTypes.push(SpaceType.UNIT); - } - if (!spaceTypeNames.includes(SpaceType.ROOM)) { - missingSpaceTypes.push(SpaceType.ROOM); - } - if (missingSpaceTypes.length > 0) { - await this.addSpaceTypeData(missingSpaceTypes); - } - } catch (err) { - console.error('Error while checking space type data:', err); - throw err; - } - } - - private async addSpaceTypeData(spaceTypes: string[]): Promise { - try { - const spaceTypeEntities = spaceTypes.map((type) => ({ - type, - })); - - await this.spaceTypeRepository.save(spaceTypeEntities); - } catch (err) { - console.error('Error while adding space type data:', err); - throw err; - } - } -} diff --git a/libs/common/src/type/optional.type.ts b/libs/common/src/type/optional.type.ts new file mode 100644 index 0000000..cf62d1c --- /dev/null +++ b/libs/common/src/type/optional.type.ts @@ -0,0 +1,2 @@ +export type WithOptional = Omit & Partial; +export type WithRequired = Omit & Required; diff --git a/libs/common/src/util/buildTypeORMIncludeQuery.ts b/libs/common/src/util/buildTypeORMIncludeQuery.ts new file mode 100644 index 0000000..c2d401e --- /dev/null +++ b/libs/common/src/util/buildTypeORMIncludeQuery.ts @@ -0,0 +1,43 @@ +type TypeORMIncludeQuery = string[]; + +const mappingInclude: { [key: string]: any } = { + roles: { + role: true, + }, + users: { + user: true, + }, + community: { + community: true, + }, + space: { + space: true, + }, + subspace: { + subspace: true, + }, +}; + +export function buildTypeORMIncludeQuery( + modelName: string, + includeParam?: string, +): TypeORMIncludeQuery | undefined { + if (includeParam) { + const relations: TypeORMIncludeQuery = []; + const fieldsToInclude: string[] = includeParam.split(','); + + fieldsToInclude.forEach((field: string) => { + if (mappingInclude[field]) { + relations.push(field); // Push mapped field + } else { + console.warn( + `Field ${field} not found in mappingInclude for ${modelName}`, + ); + } + }); + + return relations; + } + + return undefined; // If no includes, return undefined +} diff --git a/libs/common/src/util/buildTypeORMSortQuery.ts b/libs/common/src/util/buildTypeORMSortQuery.ts new file mode 100644 index 0000000..6f8c8be --- /dev/null +++ b/libs/common/src/util/buildTypeORMSortQuery.ts @@ -0,0 +1,18 @@ +type TypeORMSortQuery = { [key: string]: 'ASC' | 'DESC' }; + +export function buildTypeORMSortQuery( + sortParam: string | undefined, +): TypeORMSortQuery { + // sortParam format: userId:asc,createdDate:desc + if (!sortParam) { + return {}; + } + + const conditions: string[] = sortParam.split(','); + + return conditions.reduce((acc: TypeORMSortQuery, condition) => { + const [field, direction] = condition.split(':').map((str) => str.trim()); + acc[field] = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + return acc; + }, {}); +} diff --git a/libs/common/src/util/buildTypeORMWhereClause.ts b/libs/common/src/util/buildTypeORMWhereClause.ts new file mode 100644 index 0000000..87ef32e --- /dev/null +++ b/libs/common/src/util/buildTypeORMWhereClause.ts @@ -0,0 +1,26 @@ +export function buildTypeORMWhereClause({ where }) { + if (!where) return {}; + + // Remove extra nesting if `where` is wrapped within an additional `where` property + const condition = where.where ? where.where : where; + + const convertToNestedObject = (condition: any): any => { + const result = {}; + for (const [key, value] of Object.entries(condition)) { + if (key.includes('.')) { + const [parentKey, childKey] = key.split('.'); + result[parentKey] = { + ...(result[parentKey] || {}), + [childKey]: value, + }; + } else { + result[key] = value; + } + } + return result; + }; + + return Array.isArray(condition) + ? condition.map((item) => convertToNestedObject(item)) + : convertToNestedObject(condition); +} diff --git a/libs/common/src/util/getPaginationResponseDto.ts b/libs/common/src/util/getPaginationResponseDto.ts new file mode 100644 index 0000000..f721aa7 --- /dev/null +++ b/libs/common/src/util/getPaginationResponseDto.ts @@ -0,0 +1,21 @@ +import { PageResponseDto } from '../dto/pagination.response.dto'; + +export function getPaginationResponseDto( + count: number, + page: number, + size: number, +): PageResponseDto { + const totalItem = count; + const totalPage = Math.ceil(totalItem / size); + const hasNext = page < totalPage ? true : false; + const hasPrevious = page === 1 || page > totalPage ? false : true; + + return { + hasNext, + hasPrevious, + page, + size, + totalItem, + totalPage, + }; +} diff --git a/libs/common/src/util/parseToDate.ts b/libs/common/src/util/parseToDate.ts new file mode 100644 index 0000000..aa642a1 --- /dev/null +++ b/libs/common/src/util/parseToDate.ts @@ -0,0 +1,4 @@ +export function parseToDate(value: unknown): Date { + const valueInNumber = Number(value); + return new Date(valueInNumber); +} diff --git a/libs/common/src/validators/is-page-request-param.validator.ts b/libs/common/src/validators/is-page-request-param.validator.ts new file mode 100644 index 0000000..a6459e4 --- /dev/null +++ b/libs/common/src/validators/is-page-request-param.validator.ts @@ -0,0 +1,21 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsPageRequestParam( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: 'IsPageRequestParam', + validator: { + validate(value) { + return IsPageParam(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsPageParam(value: any): boolean { + return !isNaN(Number(value)) && value > 0; +} diff --git a/libs/common/src/validators/is-size-request-param.validator.ts b/libs/common/src/validators/is-size-request-param.validator.ts new file mode 100644 index 0000000..4928a1b --- /dev/null +++ b/libs/common/src/validators/is-size-request-param.validator.ts @@ -0,0 +1,21 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsSizeRequestParam( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: 'IsSizeRequestParam', + validator: { + validate(value) { + return IsSizeParam(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsSizeParam(value: any): boolean { + return !isNaN(Number(value)) && value > -1; +} diff --git a/libs/common/src/validators/is-sort-param.validator.ts b/libs/common/src/validators/is-sort-param.validator.ts new file mode 100644 index 0000000..dceee2e --- /dev/null +++ b/libs/common/src/validators/is-sort-param.validator.ts @@ -0,0 +1,54 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsSortParam( + validationOptions?: ValidationOptions, + allowedFieldName?: string[], +): PropertyDecorator { + return ValidateBy( + { + name: 'IsSortParam', + validator: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + validate(value) { + return IsValidMultipleSortParam(value, allowedFieldName); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsValidMultipleSortParam( + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + value: any, + allowedFieldName?: string[], +): boolean { + if (typeof value !== 'string') { + return false; + } + + const conditions: string[] = value.split(','); + const isValid: boolean = conditions.every((condition) => { + const combination: string[] = condition.split(':'); + if (combination.length !== 2) { + return false; + } + const field = combination[0].trim(); + const direction = combination[1].trim(); + + if (!field) { + return false; + } + + if (allowedFieldName?.length && !allowedFieldName.includes(field)) { + return false; + } + + if (!['asc', 'desc'].includes(direction.toLowerCase())) { + return false; + } + + return true; + }); + return isValid; +} diff --git a/src/app.module.ts b/src/app.module.ts index 5c3a52c..609147c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,14 +3,10 @@ import { ConfigModule } from '@nestjs/config'; import config from './config'; import { AuthenticationModule } from './auth/auth.module'; import { UserModule } from './users/user.module'; -import { RoomModule } from './room/room.module'; import { GroupModule } from './group/group.module'; import { DeviceModule } from './device/device.module'; import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; import { CommunityModule } from './community/community.module'; -import { BuildingModule } from './building/building.module'; -import { FloorModule } from './floor/floor.module'; -import { UnitModule } from './unit/unit.module'; import { RoleModule } from './role/role.module'; import { SeederModule } from '@app/common/seed/seeder.module'; import { UserNotificationModule } from './user-notification/user-notification.module'; @@ -24,6 +20,8 @@ import { RegionModule } from './region/region.module'; import { TimeZoneModule } from './timezone/timezone.module'; import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; import { ScheduleModule } from './schedule/schedule.module'; +import { SpaceModule } from './space/space.module'; +import { ProductModule } from './product'; @Module({ imports: [ ConfigModule.forRoot({ @@ -33,11 +31,9 @@ import { ScheduleModule } from './schedule/schedule.module'; UserModule, RoleModule, CommunityModule, - BuildingModule, - FloorModule, - UnitModule, - RoomModule, - RoomModule, + + SpaceModule, + GroupModule, DeviceModule, DeviceMessagesSubscriptionModule, @@ -51,6 +47,7 @@ import { ScheduleModule } from './schedule/schedule.module'; TimeZoneModule, VisitorPasswordModule, ScheduleModule, + ProductModule, ], providers: [ { diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index e7074da..616f4ad 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -10,7 +10,7 @@ import { } from '@nestjs/common'; import { UserAuthService } from '../services/user-auth.service'; import { UserSignUpDto } from '../dtos/user-auth.dto'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ResponseMessage } from '../../../libs/common/src/response/response.decorator'; import { UserLoginDto } from '../dtos/user-login.dto'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; @@ -18,17 +18,22 @@ import { RefreshTokenGuard } from '@app/common/guards/jwt-refresh.auth.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { OtpType } from '@app/common/constants/otp-type.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'authentication', + path: ControllerRoute.AUTHENTICATION.ROUTE, }) -@ApiTags('Auth') +@ApiTags('Authentication Module') export class UserAuthController { constructor(private readonly userAuthService: UserAuthService) {} @ResponseMessage('User Registered Successfully') @Post('user/signup') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_SUMMARY, + description: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_DESCRIPTION, + }) async signUp(@Body() userSignUpDto: UserSignUpDto) { const signupUser = await this.userAuthService.signUp(userSignUpDto); return { @@ -41,8 +46,12 @@ export class UserAuthController { }; } - @ResponseMessage('user logged in successfully') + @ResponseMessage('User Logged in Successfully') @Post('user/login') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.LOGIN_SUMMARY, + description: ControllerRoute.AUTHENTICATION.ACTIONS.LOGIN_DESCRIPTION, + }) async userLogin(@Body() data: UserLoginDto) { const accessToken = await this.userAuthService.userLogin(data); return { @@ -53,6 +62,10 @@ export class UserAuthController { } @Post('user/send-otp') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.SEND_OTP_SUMMARY, + description: ControllerRoute.AUTHENTICATION.ACTIONS.SEND_OTP_DESCRIPTION, + }) async sendOtp(@Body() otpDto: UserOtpDto) { const otpCode = await this.userAuthService.generateOTP(otpDto); return { @@ -60,11 +73,15 @@ export class UserAuthController { data: { ...otpCode, }, - message: 'Otp Send Successfully', + message: 'Otp Sent Successfully', }; } @Post('user/verify-otp') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.VERIFY_OTP_SUMMARY, + description: ControllerRoute.AUTHENTICATION.ACTIONS.VERIFY_OTP_DESCRIPTION, + }) async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { await this.userAuthService.verifyOTP(verifyOtpDto); return { @@ -75,6 +92,11 @@ export class UserAuthController { } @Post('user/forget-password') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.FORGET_PASSWORD_SUMMARY, + description: + ControllerRoute.AUTHENTICATION.ACTIONS.FORGET_PASSWORD_DESCRIPTION, + }) async forgetPassword(@Body() forgetPasswordDto: ForgetPasswordDto) { const otpResult = await this.userAuthService.verifyOTP( { @@ -102,6 +124,10 @@ export class UserAuthController { @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard) @Get('user') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.USER_LIST_SUMMARY, + description: ControllerRoute.AUTHENTICATION.ACTIONS.USER_LIST_DESCRIPTION, + }) async userList() { const userList = await this.userAuthService.userList(); return { @@ -114,6 +140,11 @@ export class UserAuthController { @ApiBearerAuth() @UseGuards(RefreshTokenGuard) @Get('refresh-token') + @ApiOperation({ + summary: ControllerRoute.AUTHENTICATION.ACTIONS.REFRESH_TOKEN_SUMMARY, + description: + ControllerRoute.AUTHENTICATION.ACTIONS.REFRESH_TOKEN_DESCRIPTION, + }) async refreshToken(@Req() req) { const refreshToken = await this.userAuthService.refreshToken( req.user.uuid, diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index 49c3b02..0a05f0e 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -8,16 +8,28 @@ import { DeviceService } from 'src/device/services'; import { DeviceRepository } from '@app/common/modules/device/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], controllers: [AutomationController], providers: [ AutomationService, + TuyaService, SpaceRepository, DeviceService, DeviceRepository, ProductRepository, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [AutomationService], }) diff --git a/src/automation/controllers/automation.controller.ts b/src/automation/controllers/automation.controller.ts index af5ee30..8d9d1ab 100644 --- a/src/automation/controllers/automation.controller.ts +++ b/src/automation/controllers/automation.controller.ts @@ -10,7 +10,7 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddAutomationDto, UpdateAutomationDto, @@ -18,11 +18,13 @@ import { } from '../dtos/automation.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { AutomationParamDto, SpaceParamDto } from '../dtos'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Automation Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'automation', + path: ControllerRoute.AUTOMATION.ROUTE, }) export class AutomationController { constructor(private readonly automationService: AutomationService) {} @@ -30,6 +32,10 @@ export class AutomationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() + @ApiOperation({ + summary: ControllerRoute.AUTOMATION.ACTIONS.ADD_AUTOMATION_SUMMARY, + description: ControllerRoute.AUTOMATION.ACTIONS.ADD_AUTOMATION_DESCRIPTION, + }) async addAutomation(@Body() addAutomationDto: AddAutomationDto) { const automation = await this.automationService.addAutomation(addAutomationDto); @@ -40,45 +46,68 @@ export class AutomationController { data: automation, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':unitUuid') - async getAutomationByUnit(@Param('unitUuid') unitUuid: string) { - const automation = - await this.automationService.getAutomationByUnit(unitUuid); + @Get(':spaceUuid') + @ApiOperation({ + summary: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_BY_SPACE_SUMMARY, + description: + ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_BY_SPACE_DESCRIPTION, + }) + async getAutomationBySpace(@Param() param: SpaceParamDto) { + const automation = await this.automationService.getAutomationBySpace( + param.spaceUuid, + ); return automation; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('details/:automationId') - async getAutomationDetails(@Param('automationId') automationId: string) { - const automation = - await this.automationService.getAutomationDetails(automationId); + @Get('details/:automationUuid') + @ApiOperation({ + summary: ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_DETAILS_SUMMARY, + description: + ControllerRoute.AUTOMATION.ACTIONS.GET_AUTOMATION_DETAILS_DESCRIPTION, + }) + async getAutomationDetails(@Param() param: AutomationParamDto) { + const automation = await this.automationService.getAutomationDetails( + param.automationUuid, + ); return automation; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Delete(':unitUuid/:automationId') - async deleteAutomation( - @Param('unitUuid') unitUuid: string, - @Param('automationId') automationId: string, - ) { - await this.automationService.deleteAutomation(unitUuid, automationId); + @Delete(':automationUuid') + @ApiOperation({ + summary: ControllerRoute.AUTOMATION.ACTIONS.DELETE_AUTOMATION_SUMMARY, + description: + ControllerRoute.AUTOMATION.ACTIONS.DELETE_AUTOMATION_DESCRIPTION, + }) + async deleteAutomation(@Param() param: AutomationParamDto) { + await this.automationService.deleteAutomation(param); return { statusCode: HttpStatus.OK, message: 'Automation Deleted Successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put(':automationId') + @Put(':automationUuid') + @ApiOperation({ + summary: ControllerRoute.AUTOMATION.ACTIONS.UPDATE_AUTOMATION_SUMMARY, + description: + ControllerRoute.AUTOMATION.ACTIONS.UPDATE_AUTOMATION_DESCRIPTION, + }) async updateAutomation( @Body() updateAutomationDto: UpdateAutomationDto, - @Param('automationId') automationId: string, + @Param() param: AutomationParamDto, ) { const automation = await this.automationService.updateAutomation( updateAutomationDto, - automationId, + param.automationUuid, ); return { statusCode: HttpStatus.CREATED, @@ -87,16 +116,23 @@ export class AutomationController { data: automation, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put('status/:automationId') + @Put('status/:automationUuid') + @ApiOperation({ + summary: + ControllerRoute.AUTOMATION.ACTIONS.UPDATE_AUTOMATION_STATUS_SUMMARY, + description: + ControllerRoute.AUTOMATION.ACTIONS.UPDATE_AUTOMATION_STATUS_DESCRIPTION, + }) async updateAutomationStatus( @Body() updateAutomationStatusDto: UpdateAutomationStatusDto, - @Param('automationId') automationId: string, + @Param() param: AutomationParamDto, ) { await this.automationService.updateAutomationStatus( updateAutomationStatusDto, - automationId, + param.automationUuid, ); return { statusCode: HttpStatus.CREATED, diff --git a/src/automation/dtos/automation.dto.ts b/src/automation/dtos/automation.dto.ts index 05d2936..6241cce 100644 --- a/src/automation/dtos/automation.dto.ts +++ b/src/automation/dtos/automation.dto.ts @@ -10,7 +10,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; -class EffectiveTime { +export class EffectiveTime { @ApiProperty({ description: 'Start time', required: true }) @IsString() @IsNotEmpty() @@ -114,10 +114,10 @@ class Action { } export class AddAutomationDto { - @ApiProperty({ description: 'Unit ID', required: true }) + @ApiProperty({ description: 'Space ID', required: true }) @IsString() @IsNotEmpty() - public unitUuid: string; + public spaceUuid: string; @ApiProperty({ description: 'Automation name', required: true }) @IsString() @@ -197,10 +197,10 @@ export class UpdateAutomationDto { } } export class UpdateAutomationStatusDto { - @ApiProperty({ description: 'Unit uuid', required: true }) + @ApiProperty({ description: 'Space uuid', required: true }) @IsString() @IsNotEmpty() - public unitUuid: string; + public spaceUuid: string; @ApiProperty({ description: 'Is enable', required: true }) @IsBoolean() diff --git a/src/automation/dtos/automation.param.dto.ts b/src/automation/dtos/automation.param.dto.ts new file mode 100644 index 0000000..e0b3990 --- /dev/null +++ b/src/automation/dtos/automation.param.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AutomationParamDto { + @ApiProperty({ + description: 'TuyaId of the automation', + example: 'SfFi2Tbn09btes84', + }) + @IsString() + @IsNotEmpty() + automationUuid: string; +} diff --git a/src/automation/dtos/delete.automation.param.dto.ts b/src/automation/dtos/delete.automation.param.dto.ts new file mode 100644 index 0000000..6421a80 --- /dev/null +++ b/src/automation/dtos/delete.automation.param.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class DeleteAutomationParamDto { + @ApiProperty({ + description: 'UUID of the Space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceUuid: string; + + @ApiProperty({ + description: 'TuyaId of the automation', + example: 'SfFi2Tbn09btes84', + }) + @IsString() + @IsNotEmpty() + automationUuid: string; +} diff --git a/src/automation/dtos/index.ts b/src/automation/dtos/index.ts index 4cdad58..03a74a0 100644 --- a/src/automation/dtos/index.ts +++ b/src/automation/dtos/index.ts @@ -1 +1,4 @@ export * from './automation.dto'; +export * from './space.param.dto'; +export * from './automation.param.dto'; +export * from './delete.automation.param.dto'; diff --git a/src/automation/dtos/space.param.dto.ts b/src/automation/dtos/space.param.dto.ts new file mode 100644 index 0000000..0631ea1 --- /dev/null +++ b/src/automation/dtos/space.param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class SpaceParamDto { + @ApiProperty({ + description: 'UUID of the space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceUuid: string; +} diff --git a/src/automation/interface/automation.interface.ts b/src/automation/interface/automation.interface.ts index 298d58c..9d7f40f 100644 --- a/src/automation/interface/automation.interface.ts +++ b/src/automation/interface/automation.interface.ts @@ -1,3 +1,5 @@ +import { EffectiveTime } from '../dtos'; + export interface AddAutomationInterface { success: boolean; msg?: string; @@ -5,7 +7,7 @@ export interface AddAutomationInterface { id: string; }; } -export interface GetAutomationByUnitInterface { +export interface GetAutomationBySpaceInterface { success: boolean; msg?: string; result: { @@ -48,3 +50,12 @@ export interface AutomationDetailsResult { name: string; type: string; } + +export interface AddAutomationParams { + actions: Action[]; + conditions: Condition[]; + automationName: string; + effectiveTime: EffectiveTime; + decisionExpr: string; + spaceTuyaId: string; +} diff --git a/src/automation/services/automation.service.ts b/src/automation/services/automation.service.ts index 3f5a4d5..91cca1a 100644 --- a/src/automation/services/automation.service.ts +++ b/src/automation/services/automation.service.ts @@ -7,27 +7,32 @@ import { import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddAutomationDto, + AutomationParamDto, UpdateAutomationDto, UpdateAutomationStatusDto, } from '../dtos'; -import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; import { ConfigService } from '@nestjs/config'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { DeviceService } from 'src/device/services'; import { - AddAutomationInterface, + Action, + AddAutomationParams, AutomationDetailsResult, AutomationResponseData, - DeleteAutomationInterface, - GetAutomationByUnitInterface, + Condition, + GetAutomationBySpaceInterface, } from '../interface/automation.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { SpaceType } from '@app/common/constants/space-type.enum'; import { ActionExecutorEnum, + AUTO_PREFIX, + AUTOMATION_TYPE, EntityTypeEnum, } from '@app/common/constants/automation.enum'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Injectable() export class AutomationService { @@ -36,6 +41,8 @@ export class AutomationService { private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly deviceService: DeviceService, + private readonly tuyaService: TuyaService, + private readonly sceneDeviceRepository: SceneDeviceRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -47,77 +54,28 @@ export class AutomationService { }); } - async addAutomation(addAutomationDto: AddAutomationDto, spaceTuyaId = null) { + async addAutomation(addAutomationDto: AddAutomationDto) { try { - let unitSpaceTuyaId; - if (!spaceTuyaId) { - const unitDetails = await this.getUnitByUuid(addAutomationDto.unitUuid); - - unitSpaceTuyaId = unitDetails.spaceTuyaUuid; - if (!unitDetails) { - throw new BadRequestException('Invalid unit UUID'); - } - } else { - unitSpaceTuyaId = spaceTuyaId; - } - - const actions = addAutomationDto.actions.map((action) => - convertKeysToSnakeCase(action), - ); - const conditions = addAutomationDto.conditions.map((condition) => - convertKeysToSnakeCase(condition), - ); - - for (const action of actions) { - if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { - const device = await this.deviceService.getDeviceByDeviceUuid( - action.entity_id, - false, - ); - if (device) { - action.entity_id = device.deviceTuyaUuid; - } - } - } - - for (const condition of conditions) { - if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) { - const device = await this.deviceService.getDeviceByDeviceUuid( - condition.entity_id, - false, - ); - if (device) { - condition.entity_id = device.deviceTuyaUuid; - } - } - } - - const path = `/v2.0/cloud/scene/rule`; - const response: AddAutomationInterface = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: unitSpaceTuyaId, - name: addAutomationDto.automationName, - effective_time: { - ...addAutomationDto.effectiveTime, - timezone_id: 'Asia/Dubai', - }, - type: 'automation', - decision_expr: addAutomationDto.decisionExpr, - conditions: conditions, - actions: actions, - }, + const { + automationName, + effectiveTime, + decisionExpr, + actions, + conditions, + } = addAutomationDto; + const space = await this.getSpaceByUuid(addAutomationDto.spaceUuid); + const response = await this.add({ + automationName, + effectiveTime, + decisionExpr, + actions, + conditions, + spaceTuyaId: space.spaceTuyaUuid, }); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - return { - id: response.result.id, - }; + return response; } catch (err) { if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + throw err; } else { throw new HttpException( err.message || 'Automation not found', @@ -126,45 +84,70 @@ export class AutomationService { } } } - async getUnitByUuid(unitUuid: string): Promise { + + async add(params: AddAutomationParams) { try { - const unit = await this.spaceRepository.findOne({ + const formattedActions = await this.prepareActions(params.actions); + const formattedCondition = await this.prepareConditions( + params.conditions, + ); + + const response = await this.tuyaService.createAutomation( + params.spaceTuyaId, + params.automationName, + params.effectiveTime, + params.decisionExpr, + formattedCondition, + formattedActions, + ); + + return { + id: response?.result.id, + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to add automation', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getSpaceByUuid(spaceUuid: string) { + try { + const space = await this.spaceRepository.findOne({ where: { - uuid: unitUuid, - spaceType: { - type: SpaceType.UNIT, - }, + uuid: spaceUuid, }, - relations: ['spaceType'], + relations: ['community'], }); - if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) { - throw new BadRequestException('Invalid unit UUID'); + if (!space) { + throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); } return { - uuid: unit.uuid, - createdAt: unit.createdAt, - updatedAt: unit.updatedAt, - name: unit.spaceName, - type: unit.spaceType.type, - spaceTuyaUuid: unit.spaceTuyaUuid, + uuid: space.uuid, + createdAt: space.createdAt, + updatedAt: space.updatedAt, + name: space.spaceName, + spaceTuyaUuid: space.community.externalId, }; } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + throw new HttpException('Space not found', HttpStatus.NOT_FOUND); } } } - async getAutomationByUnit(unitUuid: string) { + + async getAutomationBySpace(spaceUuid: string) { try { - const unit = await this.getUnitByUuid(unitUuid); - if (!unit.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); + const space = await this.getSpaceByUuid(spaceUuid); + if (!space.spaceTuyaUuid) { + throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); } - const path = `/v2.0/cloud/scene/rule?space_id=${unit.spaceTuyaUuid}&type=automation`; - const response: GetAutomationByUnitInterface = await this.tuya.request({ + const path = `/v2.0/cloud/scene/rule?space_id=${space.spaceTuyaUuid}&type=automation`; + const response: GetAutomationBySpaceInterface = await this.tuya.request({ method: 'GET', path, }); @@ -173,14 +156,16 @@ export class AutomationService { throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); } - return response.result.list.map((item) => { - return { - id: item.id, - name: item.name, - status: item.status, - type: 'automation', - }; - }); + return response.result.list + .filter((item) => item.name && !item.name.startsWith(AUTO_PREFIX)) + .map((item) => { + return { + id: item.id, + name: item.name, + status: item.status, + type: AUTOMATION_TYPE, + }; + }); } catch (err) { if (err instanceof BadRequestException) { throw err; // Re-throw BadRequestException @@ -192,11 +177,12 @@ export class AutomationService { } } } + async getTapToRunSceneDetailsTuya( - sceneId: string, + sceneUuid: string, ): Promise { try { - const path = `/v2.0/cloud/scene/rule/${sceneId}`; + const path = `/v2.0/cloud/scene/rule/${sceneUuid}`; const response = await this.tuya.request({ method: 'GET', path, @@ -224,9 +210,9 @@ export class AutomationService { } } } - async getAutomationDetails(automationId: string, withSpaceId = false) { + async getAutomationDetails(automationUuid: string, withSpaceId = false) { try { - const path = `/v2.0/cloud/scene/rule/${automationId}`; + const path = `/v2.0/cloud/scene/rule/${automationUuid}`; const response = await this.tuya.request({ method: 'GET', path, @@ -252,6 +238,8 @@ export class AutomationService { if (device) { action.entityId = device.uuid; + action.productUuid = device.productDevice.uuid; + action.productType = device.productDevice.prodType; } } else if ( action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE && @@ -280,6 +268,8 @@ export class AutomationService { if (device) { condition.entityId = device.uuid; + condition.productUuid = device.productDevice.uuid; + condition.productType = device.productDevice.prodType; } } } @@ -312,37 +302,64 @@ export class AutomationService { } } - async deleteAutomation( - unitUuid: string, - automationId: string, - spaceTuyaId = null, - ) { + async deleteAutomation(param: AutomationParamDto) { try { - let unitSpaceTuyaId; - if (!spaceTuyaId) { - const unitDetails = await this.getUnitByUuid(unitUuid); - unitSpaceTuyaId = unitDetails.spaceTuyaUuid; - if (!unitSpaceTuyaId) { - throw new BadRequestException('Invalid unit UUID'); - } - } else { - unitSpaceTuyaId = spaceTuyaId; - } + const { automationUuid } = param; - const path = `/v2.0/cloud/scene/rule?ids=${automationId}&space_id=${unitSpaceTuyaId}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'DELETE', - path, + const automation = await this.getAutomationDetails(automationUuid, true); + + if (!automation && !automation.spaceId) { + throw new HttpException( + `Invalid automationid ${automationUuid}`, + HttpStatus.BAD_REQUEST, + ); + } + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { automationTuyaUuid: automationUuid }, }); - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); + if (existingSceneDevice) { + await this.sceneDeviceRepository.delete({ + automationTuyaUuid: automationUuid, + }); } + const response = this.tuyaService.deleteAutomation( + automation.spaceId, + automationUuid, + ); return response; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + err.message || 'Automation not found', + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + + async delete(tuyaSpaceId: string, automationUuid: string) { + try { + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { automationTuyaUuid: automationUuid }, + }); + + if (existingSceneDevice) { + await this.sceneDeviceRepository.delete({ + automationTuyaUuid: automationUuid, + }); + } + const response = await this.tuyaService.deleteAutomation( + tuyaSpaceId, + automationUuid, + ); + return response; + } catch (err) { + if (err instanceof HttpException) { + throw err; } else { throw new HttpException( err.message || 'Automation not found', @@ -354,26 +371,30 @@ export class AutomationService { async updateAutomation( updateAutomationDto: UpdateAutomationDto, - automationId: string, + automationUuid: string, ) { + const { actions, conditions, automationName, effectiveTime, decisionExpr } = + updateAutomationDto; try { - const spaceTuyaId = await this.getAutomationDetails(automationId, true); - if (!spaceTuyaId.spaceId) { + const automation = await this.getAutomationDetails(automationUuid, true); + if (!automation.spaceId) { throw new HttpException( "Automation doesn't exist", HttpStatus.NOT_FOUND, ); } - const addAutomation = { - ...updateAutomationDto, - unitUuid: null, - }; - const newAutomation = await this.addAutomation( - addAutomation, - spaceTuyaId.spaceId, - ); + + const newAutomation = await this.add({ + actions, + conditions, + automationName, + effectiveTime, + decisionExpr, + spaceTuyaId: automation.spaceId, + }); + if (newAutomation.id) { - await this.deleteAutomation(null, automationId, spaceTuyaId.spaceId); + await this.delete(automation.spaceId, automationUuid); return newAutomation; } } catch (err) { @@ -389,29 +410,23 @@ export class AutomationService { } async updateAutomationStatus( updateAutomationStatusDto: UpdateAutomationStatusDto, - automationId: string, + automationUuid: string, ) { + const { isEnable, spaceUuid } = updateAutomationStatusDto; try { - const unitDetails = await this.getUnitByUuid( - updateAutomationStatusDto.unitUuid, + const space = await this.getSpaceByUuid(spaceUuid); + if (!space.spaceTuyaUuid) { + throw new HttpException( + `Invalid space UUID ${spaceUuid}`, + HttpStatus.NOT_FOUND, + ); + } + + const response = await this.tuyaService.updateAutomationState( + space.spaceTuyaUuid, + automationUuid, + isEnable, ); - if (!unitDetails.spaceTuyaUuid) { - throw new BadRequestException('Invalid unit UUID'); - } - - const path = `/v2.0/cloud/scene/rule/state?space_id=${unitDetails.spaceTuyaUuid}`; - const response: DeleteAutomationInterface = await this.tuya.request({ - method: 'PUT', - path, - body: { - ids: automationId, - is_enable: updateAutomationStatusDto.isEnable, - }, - }); - - if (!response.success) { - throw new HttpException('Automation not found', HttpStatus.NOT_FOUND); - } return response; } catch (err) { @@ -425,4 +440,42 @@ export class AutomationService { } } } + + private async prepareActions(actions: Action[]): Promise { + const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[]; + + await Promise.all( + convertedData.map(async (action) => { + if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, + false, + ); + if (device) { + action.entity_id = device.deviceTuyaUuid; + } + } + }), + ); + + return convertedData; + } + + private async prepareConditions(conditions: Condition[]) { + const convertedData = convertKeysToSnakeCase(conditions); + await Promise.all( + convertedData.map(async (condition) => { + if (condition.entity_type === EntityTypeEnum.DEVICE_REPORT) { + const device = await this.deviceService.getDeviceByDeviceUuid( + condition.entity_id, + false, + ); + if (device) { + condition.entity_id = device.deviceTuyaUuid; + } + } + }), + ); + return convertedData; + } } diff --git a/src/building/building.module.ts b/src/building/building.module.ts deleted file mode 100644 index a78c049..0000000 --- a/src/building/building.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BuildingService } from './services/building.service'; -import { BuildingController } from './controllers/building.controller'; -import { ConfigModule } from '@nestjs/config'; -import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { SpaceTypeRepository } from '@app/common/modules/space/repositories'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; - -@Module({ - imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], - controllers: [BuildingController], - providers: [ - BuildingService, - SpaceRepository, - SpaceTypeRepository, - UserSpaceRepository, - UserRepository, - ], - exports: [BuildingService], -}) -export class BuildingModule {} diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts deleted file mode 100644 index 8c7d1b1..0000000 --- a/src/building/controllers/building.controller.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { BuildingService } from '../services/building.service'; -import { - Body, - Controller, - Get, - HttpStatus, - Param, - Post, - Put, - Query, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddBuildingDto, AddUserBuildingDto } from '../dtos/add.building.dto'; -import { GetBuildingChildDto } from '../dtos/get.building.dto'; -import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; -import { CheckCommunityTypeGuard } from 'src/guards/community.type.guard'; -import { CheckUserBuildingGuard } from 'src/guards/user.building.guard'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { BuildingPermissionGuard } from 'src/guards/building.permission.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@ApiTags('Building Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.BUILDING, -}) -export class BuildingController { - constructor(private readonly buildingService: BuildingService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) - @Post() - async addBuilding(@Body() addBuildingDto: AddBuildingDto) { - const building = await this.buildingService.addBuilding(addBuildingDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Building added successfully', - data: building, - }; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, BuildingPermissionGuard) - @Get(':buildingUuid') - async getBuildingByUuid(@Param('buildingUuid') buildingUuid: string) { - const building = await this.buildingService.getBuildingByUuid(buildingUuid); - return building; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, BuildingPermissionGuard) - @Get('child/:buildingUuid') - async getBuildingChildByUuid( - @Param('buildingUuid') buildingUuid: string, - @Query() query: GetBuildingChildDto, - ) { - const building = await this.buildingService.getBuildingChildByUuid( - buildingUuid, - query, - ); - return building; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, BuildingPermissionGuard) - @Get('parent/:buildingUuid') - async getBuildingParentByUuid(@Param('buildingUuid') buildingUuid: string) { - const building = - await this.buildingService.getBuildingParentByUuid(buildingUuid); - return building; - } - @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUserBuildingGuard) - @Post('user') - async addUserBuilding(@Body() addUserBuildingDto: AddUserBuildingDto) { - await this.buildingService.addUserBuilding(addUserBuildingDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user building added successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('user/:userUuid') - async getBuildingsByUserId(@Param('userUuid') userUuid: string) { - return await this.buildingService.getBuildingsByUserId(userUuid); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, BuildingPermissionGuard) - @Put(':buildingUuid') - async renameBuildingByUuid( - @Param('buildingUuid') buildingUuid: string, - @Body() updateBuildingDto: UpdateBuildingNameDto, - ) { - const building = await this.buildingService.renameBuildingByUuid( - buildingUuid, - updateBuildingDto, - ); - return building; - } -} diff --git a/src/building/controllers/index.ts b/src/building/controllers/index.ts deleted file mode 100644 index b5ec3c2..0000000 --- a/src/building/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './building.controller'; diff --git a/src/building/dtos/add.building.dto.ts b/src/building/dtos/add.building.dto.ts deleted file mode 100644 index 5d79231..0000000 --- a/src/building/dtos/add.building.dto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class AddBuildingDto { - @ApiProperty({ - description: 'buildingName', - required: true, - }) - @IsString() - @IsNotEmpty() - public buildingName: string; - - @ApiProperty({ - description: 'communityUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public communityUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} -export class AddUserBuildingDto { - @ApiProperty({ - description: 'buildingUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public buildingUuid: string; - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/building/dtos/get.building.dto.ts b/src/building/dtos/get.building.dto.ts deleted file mode 100644 index fae0c6b..0000000 --- a/src/building/dtos/get.building.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BooleanValues } from '@app/common/constants/boolean-values.enum'; -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsBoolean, - IsInt, - IsNotEmpty, - IsOptional, - IsString, - Min, -} from 'class-validator'; - -export class GetBuildingDto { - @ApiProperty({ - description: 'buildingUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public buildingUuid: string; -} - -export class GetBuildingChildDto { - @ApiProperty({ example: 1, description: 'Page number', required: true }) - @IsInt({ message: 'Page must be a number' }) - @Min(1, { message: 'Page must not be less than 1' }) - @IsNotEmpty() - public page: number; - - @ApiProperty({ - example: 10, - description: 'Number of items per page', - required: true, - }) - @IsInt({ message: 'Page size must be a number' }) - @Min(1, { message: 'Page size must not be less than 1' }) - @IsNotEmpty() - public pageSize: number; - - @ApiProperty({ - example: true, - description: 'Flag to determine whether to fetch full hierarchy', - required: false, - default: false, - }) - @IsOptional() - @IsBoolean() - @Transform((value) => { - return value.obj.includeSubSpaces === BooleanValues.TRUE; - }) - public includeSubSpaces: boolean = false; -} diff --git a/src/building/dtos/index.ts b/src/building/dtos/index.ts deleted file mode 100644 index 93e7c6f..0000000 --- a/src/building/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.building.dto'; diff --git a/src/building/dtos/update.building.dto.ts b/src/building/dtos/update.building.dto.ts deleted file mode 100644 index 0f07cbe..0000000 --- a/src/building/dtos/update.building.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class UpdateBuildingNameDto { - @ApiProperty({ - description: 'buildingName', - required: true, - }) - @IsString() - @IsNotEmpty() - public buildingName: string; - - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/building/interface/building.interface.ts b/src/building/interface/building.interface.ts deleted file mode 100644 index 1127456..0000000 --- a/src/building/interface/building.interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface GetBuildingByUuidInterface { - uuid: string; - createdAt: Date; - updatedAt: Date; - name: string; - type: string; -} - -export interface BuildingChildInterface { - uuid: string; - name: string; - type: string; - totalCount?: number; - children?: BuildingChildInterface[]; -} -export interface BuildingParentInterface { - uuid: string; - name: string; - type: string; - parent?: BuildingParentInterface; -} -export interface RenameBuildingByUuidInterface { - uuid: string; - name: string; - type: string; -} -export interface GetBuildingByUserUuidInterface { - uuid: string; - name: string; - type: string; -} diff --git a/src/building/services/building.service.ts b/src/building/services/building.service.ts deleted file mode 100644 index b3de5d4..0000000 --- a/src/building/services/building.service.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { GetBuildingChildDto } from '../dtos/get.building.dto'; -import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddBuildingDto, AddUserBuildingDto } from '../dtos'; -import { - BuildingChildInterface, - BuildingParentInterface, - GetBuildingByUserUuidInterface, - GetBuildingByUuidInterface, - RenameBuildingByUuidInterface, -} from '../interface/building.interface'; -import { SpaceEntity } from '@app/common/modules/space/entities'; -import { UpdateBuildingNameDto } from '../dtos/update.building.dto'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; - -@Injectable() -export class BuildingService { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, - private readonly userSpaceRepository: UserSpaceRepository, - ) {} - - async addBuilding(addBuildingDto: AddBuildingDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.BUILDING, - }, - }); - - if (!spaceType) { - throw new BadRequestException('Invalid building UUID'); - } - const building = await this.spaceRepository.save({ - spaceName: addBuildingDto.buildingName, - parent: { uuid: addBuildingDto.communityUuid }, - spaceType: { uuid: spaceType.uuid }, - }); - return building; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - } - } - - async getBuildingByUuid( - buildingUuid: string, - ): Promise { - try { - const building = await this.spaceRepository.findOne({ - where: { - uuid: buildingUuid, - spaceType: { - type: SpaceType.BUILDING, - }, - }, - relations: ['spaceType'], - }); - if ( - !building || - !building.spaceType || - building.spaceType.type !== SpaceType.BUILDING - ) { - throw new BadRequestException('Invalid building UUID'); - } - return { - uuid: building.uuid, - createdAt: building.createdAt, - updatedAt: building.updatedAt, - name: building.spaceName, - type: building.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - } - } - async getBuildingChildByUuid( - buildingUuid: string, - getBuildingChildDto: GetBuildingChildDto, - ): Promise { - try { - const { includeSubSpaces, page, pageSize } = getBuildingChildDto; - - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: buildingUuid }, - relations: ['children', 'spaceType'], - }); - if ( - !space || - !space.spaceType || - space.spaceType.type !== SpaceType.BUILDING - ) { - throw new BadRequestException('Invalid building UUID'); - } - - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - } - } - - private async buildHierarchy( - space: SpaceEntity, - includeSubSpaces: boolean, - page: number, - pageSize: number, - ): Promise { - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['spaceType'], - skip: (page - 1) * pageSize, - take: pageSize, - }); - - if (!children || children.length === 0 || !includeSubSpaces) { - return children - .filter( - (child) => - child.spaceType.type !== SpaceType.BUILDING && - child.spaceType.type !== SpaceType.COMMUNITY, - ) // Filter remaining building and community types - .map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); - } - - const childHierarchies = await Promise.all( - children - .filter( - (child) => - child.spaceType.type !== SpaceType.BUILDING && - child.spaceType.type !== SpaceType.COMMUNITY, - ) // Filter remaining building and community types - .map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), - ); - - return childHierarchies; - } - - async getBuildingParentByUuid( - buildingUuid: string, - ): Promise { - try { - const building = await this.spaceRepository.findOne({ - where: { - uuid: buildingUuid, - spaceType: { - type: SpaceType.BUILDING, - }, - }, - relations: ['spaceType', 'parent', 'parent.spaceType'], - }); - if ( - !building || - !building.spaceType || - building.spaceType.type !== SpaceType.BUILDING - ) { - throw new BadRequestException('Invalid building UUID'); - } - return { - uuid: building.uuid, - name: building.spaceName, - type: building.spaceType.type, - parent: { - uuid: building.parent.uuid, - name: building.parent.spaceName, - type: building.parent.spaceType.type, - }, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - } - } - - async getBuildingsByUserId( - userUuid: string, - ): Promise { - try { - const buildings = await this.userSpaceRepository.find({ - relations: ['space', 'space.spaceType'], - where: { - user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.BUILDING } }, - }, - }); - - if (buildings.length === 0) { - throw new HttpException( - 'this user has no buildings', - HttpStatus.NOT_FOUND, - ); - } - const spaces = buildings.map((building) => ({ - uuid: building.space.uuid, - name: building.space.spaceName, - type: building.space.spaceType.type, - })); - - return spaces; - } catch (err) { - if (err instanceof HttpException) { - throw err; - } else { - throw new HttpException('user not found', HttpStatus.NOT_FOUND); - } - } - } - async addUserBuilding(addUserBuildingDto: AddUserBuildingDto) { - try { - await this.userSpaceRepository.save({ - user: { uuid: addUserBuildingDto.userUuid }, - space: { uuid: addUserBuildingDto.buildingUuid }, - }); - } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { - throw new HttpException( - 'User already belongs to this building', - HttpStatus.BAD_REQUEST, - ); - } - throw new HttpException( - err.message || 'Internal Server Error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async renameBuildingByUuid( - buildingUuid: string, - updateBuildingNameDto: UpdateBuildingNameDto, - ): Promise { - try { - const building = await this.spaceRepository.findOneOrFail({ - where: { uuid: buildingUuid }, - relations: ['spaceType'], - }); - - if ( - !building || - !building.spaceType || - building.spaceType.type !== SpaceType.BUILDING - ) { - throw new BadRequestException('Invalid building UUID'); - } - - await this.spaceRepository.update( - { uuid: buildingUuid }, - { spaceName: updateBuildingNameDto.buildingName }, - ); - - // Fetch the updated building - const updatedBuilding = await this.spaceRepository.findOneOrFail({ - where: { uuid: buildingUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedBuilding.uuid, - name: updatedBuilding.spaceName, - type: updatedBuilding.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Building not found', HttpStatus.NOT_FOUND); - } - } - } -} diff --git a/src/building/services/index.ts b/src/building/services/index.ts deleted file mode 100644 index 7b260d2..0000000 --- a/src/building/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './building.service'; diff --git a/src/community/community.module.ts b/src/community/community.module.ts index d5f7d93..106d9d1 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -4,11 +4,11 @@ import { CommunityController } from './controllers/community.controller'; import { ConfigModule } from '@nestjs/config'; import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { SpaceTypeRepository } from '@app/common/modules/space/repositories'; import { UserSpaceRepository } from '@app/common/modules/user/repositories'; import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { UserRepository } from '@app/common/modules/user/repositories'; import { SpacePermissionService } from '@app/common/helper/services'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], @@ -16,9 +16,9 @@ import { SpacePermissionService } from '@app/common/helper/services'; providers: [ CommunityService, SpaceRepository, - SpaceTypeRepository, UserSpaceRepository, - UserRepository, + TuyaService, + CommunityRepository, SpacePermissionService, ], exports: [CommunityService, SpacePermissionService], diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 356c90c..86bad74 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -2,31 +2,28 @@ import { CommunityService } from '../services/community.service'; import { Body, Controller, + Delete, Get, - HttpStatus, Param, Post, Put, Query, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { - AddCommunityDto, - AddUserCommunityDto, -} from '../dtos/add.community.dto'; -import { GetCommunityChildDto } from '../dtos/get.community.dto'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { AddCommunityDto } from '../dtos/add.community.dto'; +import { GetCommunityParams } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; +// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -// import { CommunityPermissionGuard } from 'src/guards/community.permission.guard'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; @ApiTags('Community Module') @Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.COMMUNITY, + version: '1', + path: ControllerRoute.COMMUNITY.ROUTE, }) export class CommunityController { constructor(private readonly communityService: CommunityService) {} @@ -34,73 +31,70 @@ export class CommunityController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() - async addCommunity(@Body() addCommunityDto: AddCommunityDto) { - const community = await this.communityService.addCommunity(addCommunityDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Community added successfully', - data: community, - }; + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_DESCRIPTION, + }) + async createCommunity( + @Body() addCommunityDto: AddCommunityDto, + ): Promise { + return await this.communityService.createCommunity(addCommunityDto); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':communityUuid') - async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { - const community = - await this.communityService.getCommunityByUuid(communityUuid); - return community; + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_SUMMARY, + description: + ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_DESCRIPTION, + }) + @Get('/:communityUuid') + async getCommunityByUuid( + @Param() params: GetCommunityParams, + ): Promise { + return await this.communityService.getCommunityById(params.communityUuid); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_DESCRIPTION, + }) @Get() - async getCommunities() { - const communities = await this.communityService.getCommunities(); - return communities; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('child/:communityUuid') - async getCommunityChildByUuid( - @Param('communityUuid') communityUuid: string, - @Query() query: GetCommunityChildDto, - ) { - const community = await this.communityService.getCommunityChildByUuid( - communityUuid, - query, - ); - return community; + async getCommunities( + @Query() query: PaginationRequestGetListDto, + ): Promise { + return this.communityService.getCommunities(query); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('user/:userUuid') - async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { - return await this.communityService.getCommunitiesByUserId(userUuid); - } - @ApiBearerAuth() - @UseGuards(AdminRoleGuard) - @Post('user') - async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { - await this.communityService.addUserCommunity(addUserCommunityDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user community added successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put(':communityUuid') - async renameCommunityByUuid( - @Param('communityUuid') communityUuid: string, + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_DESCRIPTION, + }) + @Put('/:communityUuid') + async updateCommunity( + @Param() param: GetCommunityParams, @Body() updateCommunityDto: UpdateCommunityNameDto, ) { - const community = await this.communityService.renameCommunityByUuid( - communityUuid, + return this.communityService.updateCommunity( + param.communityUuid, updateCommunityDto, ); - return community; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('/:communityUuid') + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_DESCRIPTION, + }) + async deleteCommunity( + @Param() param: GetCommunityParams, + ): Promise { + return this.communityService.deleteCommunity(param.communityUuid); } } diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index 06aec7c..1ae91da 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -1,19 +1,30 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; export class AddCommunityDto { @ApiProperty({ - description: 'communityName', + description: 'The name of the community', + example: 'Community A', required: true, }) @IsString() @IsNotEmpty() - public communityName: string; + public name: string; + + @ApiProperty({ + description: 'A description of the community', + example: 'This is a community for developers.', + required: false, + }) + @IsString() + @IsOptional() + public description?: string; constructor(dto: Partial) { Object.assign(this, dto); } } + export class AddUserCommunityDto { @ApiProperty({ description: 'communityUuid', diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts index cae892b..fe2cf46 100644 --- a/src/community/dtos/get.community.dto.ts +++ b/src/community/dtos/get.community.dto.ts @@ -7,6 +7,7 @@ import { IsNotEmpty, IsOptional, IsString, + IsUUID, Min, } from 'class-validator'; @@ -20,6 +21,18 @@ export class GetCommunityDto { public communityUuid: string; } +export class GetCommunityParams { + @ApiProperty({ + description: 'Community id of the specific community', + required: true, + name: 'communityUuid', + }) + @IsUUID() + @IsString() + @IsNotEmpty() + public communityUuid: string; +} + export class GetCommunityChildDto { @ApiProperty({ example: 1, description: 'Page number', required: true }) @IsInt({ message: 'Page must be a number' }) diff --git a/src/community/dtos/update.community.dto.ts b/src/community/dtos/update.community.dto.ts index 6f15d43..aba1fc2 100644 --- a/src/community/dtos/update.community.dto.ts +++ b/src/community/dtos/update.community.dto.ts @@ -3,12 +3,12 @@ import { IsNotEmpty, IsString } from 'class-validator'; export class UpdateCommunityNameDto { @ApiProperty({ - description: 'communityName', + description: 'community name', required: true, }) @IsString() @IsNotEmpty() - public communityName: string; + public name: string; constructor(dto: Partial) { Object.assign(this, dto); diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 145dc13..e833416 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,278 +1,172 @@ -import { GetCommunityChildDto } from './../dtos/get.community.dto'; -import { SpaceTypeRepository } from './../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddCommunityDto, AddUserCommunityDto } from '../dtos'; -import { - CommunityChildInterface, - GetCommunitiesInterface, - GetCommunityByUserUuidInterface, - GetCommunityByUuidInterface, - RenameCommunityByUuidInterface, -} from '../interface/community.interface'; -import { SpaceEntity } from '@app/common/modules/space/entities'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { AddCommunityDto } from '../dtos'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { CommunityDto } from '@app/common/modules/community/dtos'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; @Injectable() export class CommunityService { constructor( - private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, - private readonly userSpaceRepository: UserSpaceRepository, + private readonly communityRepository: CommunityRepository, + private readonly tuyaService: TuyaService, ) {} - async addCommunity(addCommunityDto: AddCommunityDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.COMMUNITY, - }, - }); + async createCommunity(dto: AddCommunityDto): Promise { + const { name, description } = dto; - const community = await this.spaceRepository.save({ - spaceName: addCommunityDto.communityName, - spaceType: { uuid: spaceType.uuid }, - }); - return community; - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - async getCommunityByUuid( - communityUuid: string, - ): Promise { - try { - const community = await this.spaceRepository.findOne({ - where: { - uuid: communityUuid, - spaceType: { - type: SpaceType.COMMUNITY, - }, - }, - relations: ['spaceType'], - }); - if ( - !community || - !community.spaceType || - community.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - return { - uuid: community.uuid, - createdAt: community.createdAt, - updatedAt: community.updatedAt, - name: community.spaceName, - type: community.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } - } - } - async getCommunities(): Promise { - try { - const community = await this.spaceRepository.find({ - where: { spaceType: { type: SpaceType.COMMUNITY } }, - relations: ['spaceType'], - }); - return community.map((community) => ({ - uuid: community.uuid, - createdAt: community.createdAt, - updatedAt: community.updatedAt, - name: community.spaceName, - type: community.spaceType.type, - })); - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - async getCommunityChildByUuid( - communityUuid: string, - getCommunityChildDto: GetCommunityChildDto, - ): Promise { - try { - const { includeSubSpaces, page, pageSize } = getCommunityChildDto; - - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['children', 'spaceType'], - }); - - if ( - !space || - !space.spaceType || - space.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, + const existingCommunity = await this.communityRepository.findOneBy({ + name, + }); + if (existingCommunity) { + throw new HttpException( + `A community with the name '${name}' already exists.`, + HttpStatus.BAD_REQUEST, ); - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } } - } - private async buildHierarchy( - space: SpaceEntity, - includeSubSpaces: boolean, - page: number, - pageSize: number, - ): Promise { - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['spaceType'], - skip: (page - 1) * pageSize, - take: pageSize, + // Create the new community entity + const community = this.communityRepository.create({ + name: name, + description: description, }); - if (!children || children.length === 0 || !includeSubSpaces) { - return children - .filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type - .map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); + // Save the community to the database + try { + const externalId = await this.createTuyaSpace(name); + community.externalId = externalId; + await this.communityRepository.save(community); + return new SuccessResponseDto({ + statusCode: HttpStatus.CREATED, + success: true, + data: community, + message: 'Community created successfully', + }); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); } - - const childHierarchies = await Promise.all( - children - .filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type - .map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), - ); - - return childHierarchies; } - async getCommunitiesByUserId( - userUuid: string, - ): Promise { + async getCommunityById(communityUuid: string): Promise { + const community = await this.communityRepository.findOneBy({ + uuid: communityUuid, + }); + + // If the community is not found, throw a 404 NotFoundException + if (!community) { + throw new HttpException( + `Community with ID ${communityUuid} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + // Return a success response + return new SuccessResponseDto({ + data: community, + message: 'Community fetched successfully', + }); + } + + async getCommunities( + pageable: Partial, + ): Promise { + pageable.modelName = 'community'; + + const customModel = TypeORMCustomModel(this.communityRepository); + + const { baseResponseDto, paginationResponseDto } = + await customModel.findAll(pageable); + + return new PageResponse( + baseResponseDto, + paginationResponseDto, + ); + } + + async updateCommunity( + communityUuid: string, + updateCommunityDto: UpdateCommunityNameDto, + ): Promise { + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + }); + + // If the community doesn't exist, throw a 404 error + if (!community) { + throw new HttpException( + `Community with ID ${communityUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + try { - const communities = await this.userSpaceRepository.find({ - relations: ['space', 'space.spaceType'], - where: { - user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.COMMUNITY } }, - }, + const { name } = updateCommunityDto; + + community.name = name; + + const updatedCommunity = await this.communityRepository.save(community); + + return new SuccessResponseDto({ + message: 'Success update Community', + data: updatedCommunity, }); - - if (communities.length === 0) { - throw new HttpException( - 'this user has no communities', - HttpStatus.NOT_FOUND, - ); + } catch (err) { + // Catch and handle any errors + if (err instanceof HttpException) { + throw err; // If it's an HttpException, rethrow it + } else { + // Throw a generic 404 error if anything else goes wrong + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); } - const spaces = communities.map((community) => ({ - uuid: community.space.uuid, - name: community.space.spaceName, - type: community.space.spaceType.type, - })); + } + } - return spaces; + async deleteCommunity(communityUuid: string): Promise { + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + }); + + // If the community is not found, throw an error + if (!community) { + throw new HttpException( + `Community with ID ${communityUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + try { + await this.communityRepository.remove(community); + + return new SuccessResponseDto({ + message: `Community with ID ${communityUuid} has been successfully deleted`, + }); } catch (err) { if (err instanceof HttpException) { throw err; } else { - throw new HttpException('user not found', HttpStatus.NOT_FOUND); + throw new HttpException( + 'An error occurred while deleting the community', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } - async addUserCommunity(addUserCommunityDto: AddUserCommunityDto) { + private async createTuyaSpace(name: string): Promise { try { - await this.userSpaceRepository.save({ - user: { uuid: addUserCommunityDto.userUuid }, - space: { uuid: addUserCommunityDto.communityUuid }, - }); - } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { - throw new HttpException( - 'User already belongs to this community', - HttpStatus.BAD_REQUEST, - ); - } + const response = await this.tuyaService.createSpace({ name }); + return response; + } catch (error) { throw new HttpException( - err.message || 'Internal Server Error', + 'Failed to create a Tuya space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async renameCommunityByUuid( - communityUuid: string, - updateCommunityDto: UpdateCommunityNameDto, - ): Promise { - try { - const community = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - if ( - !community || - !community.spaceType || - community.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - - await this.spaceRepository.update( - { uuid: communityUuid }, - { spaceName: updateCommunityDto.communityName }, - ); - - // Fetch the updated community - const updatedCommunity = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedCommunity.uuid, - name: updatedCommunity.spaceName, - type: updatedCommunity.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } - } - } } diff --git a/src/device-messages/controllers/device-messages.controller.ts b/src/device-messages/controllers/device-messages.controller.ts index 6f942b4..d41d477 100644 --- a/src/device-messages/controllers/device-messages.controller.ts +++ b/src/device-messages/controllers/device-messages.controller.ts @@ -8,17 +8,18 @@ import { Post, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { DeviceMessagesSubscriptionService } from '../services/device-messages.service'; import { DeviceMessagesAddDto } from '../dtos/device-messages.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Device Messages Status Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'device-messages/subscription', + path: ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ROUTE, }) export class DeviceMessagesSubscriptionController { constructor( @@ -28,6 +29,14 @@ export class DeviceMessagesSubscriptionController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() + @ApiOperation({ + summary: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .ADD_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY, + description: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .ADD_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION, + }) async addDeviceMessagesSubscription( @Body() deviceMessagesAddDto: DeviceMessagesAddDto, ) { @@ -45,6 +54,14 @@ export class DeviceMessagesSubscriptionController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceUuid/user/:userUuid') + @ApiOperation({ + summary: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .GET_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY, + description: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .GET_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION, + }) async getDeviceMessagesSubscription( @Param('deviceUuid') deviceUuid: string, @Param('userUuid') userUuid: string, @@ -60,9 +77,18 @@ export class DeviceMessagesSubscriptionController { data: deviceDetails, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Delete() + @ApiOperation({ + summary: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .DELETE_DEVICE_MESSAGES_SUBSCRIPTION_SUMMARY, + description: + ControllerRoute.DEVICE_MESSAGES_SUBSCRIPTION.ACTIONS + .DELETE_DEVICE_MESSAGES_SUBSCRIPTION_DESCRIPTION, + }) async deleteDeviceMessagesSubscription( @Body() deviceMessagesAddDto: DeviceMessagesAddDto, ) { diff --git a/src/device/controllers/device.controller.ts b/src/device/controllers/device.controller.ts index 7a2543e..a49b1f7 100644 --- a/src/device/controllers/device.controller.ts +++ b/src/device/controllers/device.controller.ts @@ -10,38 +10,48 @@ import { UseGuards, Req, Put, + Delete, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { - GetDeviceByRoomUuidDto, - GetDeviceLogsDto, -} from '../dtos/get.device.dto'; + AddDeviceDto, + AddSceneToFourSceneDeviceDto, + UpdateDeviceDto, + UpdateDeviceInSpaceDto, +} from '../dtos/add.device.dto'; +import { GetDeviceLogsDto } from '../dtos/get.device.dto'; import { ControlDeviceDto, BatchControlDevicesDto, BatchStatusDevicesDto, BatchFactoryResetDevicesDto, + GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; import { CheckRoomGuard } from 'src/guards/room.guard'; -import { CheckUserHavePermission } from 'src/guards/user.device.permission.guard'; -import { CheckUserHaveControllablePermission } from 'src/guards/user.device.controllable.permission.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { CheckDeviceGuard } from 'src/guards/device.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; +import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { DeviceSceneParamDto } from '../dtos/device.param.dto'; +import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto'; @ApiTags('Device Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'device', + path: ControllerRoute.DEVICE.ROUTE, }) export class DeviceController { constructor(private readonly deviceService: DeviceService) {} @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard, CheckDeviceGuard) @Post() + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.ADD_DEVICE_TO_USER_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.ADD_DEVICE_TO_USER_DESCRIPTION, + }) async addDeviceUser(@Body() addDeviceDto: AddDeviceDto) { const device = await this.deviceService.addDeviceUser(addDeviceDto); @@ -55,36 +65,38 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_USER_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_USER_DESCRIPTION, + }) async getDevicesByUser(@Param('userUuid') userUuid: string) { return await this.deviceService.getDevicesByUser(userUuid); } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckRoomGuard) - @Get(SpaceType.ROOM) - async getDevicesByRoomId( - @Query() getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, - @Req() req: any, - ) { - const userUuid = req.user.uuid; - return await this.deviceService.getDevicesByRoomId( - getDeviceByRoomUuidDto, - userUuid, - ); - } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('unit/:unitUuid') - async getDevicesByUnitId(@Param('unitUuid') unitUuid: string) { - return await this.deviceService.getDevicesByUnitId(unitUuid); + @Get('space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_SPACE_UUID_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_SPACE_UUID_DESCRIPTION, + }) + async getDevicesByUnitId(@Param('spaceUuid') spaceUuid: string) { + return await this.deviceService.getDevicesBySpaceUuid(spaceUuid); } @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckRoomGuard) - @Put('room') + @Put('space') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_IN_ROOM_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_IN_ROOM_DESCRIPTION, + }) async updateDeviceInRoom( - @Body() updateDeviceInRoomDto: UpdateDeviceInRoomDto, + @Body() updateDeviceInSpaceDto: UpdateDeviceInSpaceDto, ) { - const device = await this.deviceService.updateDeviceInRoom( - updateDeviceInRoomDto, + const device = await this.deviceService.updateDeviceInSpace( + updateDeviceInSpaceDto, ); return { @@ -96,8 +108,12 @@ export class DeviceController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard) @Get(':deviceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_DETAILS_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_DETAILS_DESCRIPTION, + }) async getDeviceDetailsByDeviceId( @Param('deviceUuid') deviceUuid: string, @Req() req: any, @@ -109,23 +125,60 @@ export class DeviceController { ); } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard) + @Put(':deviceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_DESCRIPTION, + }) + async updateDevice( + @Param('deviceUuid') deviceUuid: string, + @Body() updateDeviceDto: UpdateDeviceDto, + ) { + const device = await this.deviceService.updateDevice( + deviceUuid, + updateDeviceDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'device updated successfully', + data: device, + }; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) @Get(':deviceUuid/functions') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_INSTRUCTION_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_INSTRUCTION_DESCRIPTION, + }) async getDeviceInstructionByDeviceId( @Param('deviceUuid') deviceUuid: string, ) { return await this.deviceService.getDeviceInstructionByDeviceId(deviceUuid); } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHavePermission) + @UseGuards(JwtAuthGuard) @Get(':deviceUuid/functions/status') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_STATUS_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_STATUS_DESCRIPTION, + }) async getDevicesInstructionStatus(@Param('deviceUuid') deviceUuid: string) { return await this.deviceService.getDevicesInstructionStatus(deviceUuid); } @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserHaveControllablePermission) + @UseGuards(JwtAuthGuard) @Post(':deviceUuid/control') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.CONTROL_DEVICE_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.CONTROL_DEVICE_DESCRIPTION, + }) async controlDevice( @Body() controlDeviceDto: ControlDeviceDto, @Param('deviceUuid') deviceUuid: string, @@ -135,6 +188,11 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post(':deviceUuid/firmware/:firmwareVersion') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_FIRMWARE_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_FIRMWARE_DESCRIPTION, + }) async updateDeviceFirmware( @Param('deviceUuid') deviceUuid: string, @Param('firmwareVersion') firmwareVersion: number, @@ -147,18 +205,32 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('gateway/:gatewayUuid/devices') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_IN_GATEWAY_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_IN_GATEWAY_DESCRIPTION, + }) async getDevicesInGateway(@Param('gatewayUuid') gatewayUuid: string) { return await this.deviceService.getDevicesInGateway(gatewayUuid); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get() + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_DESCRIPTION, + }) async getAllDevices() { return await this.deviceService.getAllDevices(); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('report-logs/:deviceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_LOGS_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_LOGS_DESCRIPTION, + }) async getBuildingChildByUuid( @Param('deviceUuid') deviceUuid: string, @Query() query: GetDeviceLogsDto, @@ -168,6 +240,11 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('control/batch') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.BATCH_CONTROL_DEVICES_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.BATCH_CONTROL_DEVICES_DESCRIPTION, + }) async batchControlDevices( @Body() batchControlDevicesDto: BatchControlDevicesDto, ) { @@ -176,6 +253,11 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('status/batch') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.BATCH_STATUS_DEVICES_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.BATCH_STATUS_DEVICES_DESCRIPTION, + }) async batchStatusDevices( @Query() batchStatusDevicesDto: BatchStatusDevicesDto, ) { @@ -184,6 +266,11 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('factory/reset/:deviceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.BATCH_FACTORY_RESET_DEVICES_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.BATCH_FACTORY_RESET_DEVICES_DESCRIPTION, + }) async batchFactoryResetDevices( @Body() batchFactoryResetDevicesDto: BatchFactoryResetDevicesDto, ) { @@ -194,6 +281,11 @@ export class DeviceController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':powerClampUuid/power-clamp/status') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_POWER_CLAMP_STATUS_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.GET_POWER_CLAMP_STATUS_DESCRIPTION, + }) async getPowerClampInstructionStatus( @Param('powerClampUuid') powerClampUuid: string, ) { @@ -201,4 +293,59 @@ export class DeviceController { powerClampUuid, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.ADD_SCENE_TO_DEVICE_SUMMARY, + description: ControllerRoute.DEVICE.ACTIONS.ADD_SCENE_TO_DEVICE_DESCRIPTION, + }) + @Post(':deviceUuid/scenes') + async addSceneToSceneDevice( + @Param('deviceUuid') deviceUuid: string, + @Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, + ) { + const device = await this.deviceService.addSceneToSceneDevice( + deviceUuid, + addSceneToFourSceneDeviceDto, + ); + + return { + statusCode: HttpStatus.CREATED, + success: true, + message: `scene added successfully to device ${deviceUuid}`, + data: device, + }; + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard) + @Get(':deviceUuid/scenes') + @ApiOperation({ + summary: ControllerRoute.DEVICE.ACTIONS.GET_SCENES_BY_DEVICE_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.GET_SCENES_BY_DEVICE_DESCRIPTION, + }) + async getScenesBySceneDevice( + @Param('deviceUuid') deviceUuid: string, + @Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, + ) { + return await this.deviceService.getScenesBySceneDevice( + deviceUuid, + getSceneFourSceneDeviceDto, + ); + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':deviceUuid/scenes') + @ApiOperation({ + summary: + ControllerRoute.DEVICE.ACTIONS.DELETE_SCENES_BY_SWITCH_NAME_SUMMARY, + description: + ControllerRoute.DEVICE.ACTIONS.DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION, + }) + async deleteSceneFromSceneDevice( + @Param() param: DeviceSceneParamDto, + @Query() query: DeleteSceneFromSceneDeviceDto, + ): Promise { + return await this.deviceService.deleteSceneFromSceneDevice(param, query); + } } diff --git a/src/device/device.module.ts b/src/device/device.module.ts index 5d17318..39b1be6 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -11,9 +11,18 @@ import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; import { UserRepository } from '@app/common/modules/user/repositories'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; +import { SpaceModule } from 'src/space/space.module'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ ConfigModule, + SpaceModule, ProductRepositoryModule, DeviceRepositoryModule, DeviceStatusFirebaseModule, @@ -27,6 +36,11 @@ import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/ SpaceRepository, DeviceRepository, UserRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/add.device.dto.ts b/src/device/dtos/add.device.dto.ts index f732317..69f42c4 100644 --- a/src/device/dtos/add.device.dto.ts +++ b/src/device/dtos/add.device.dto.ts @@ -1,5 +1,6 @@ +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; export class AddDeviceDto { @ApiProperty({ @@ -18,7 +19,7 @@ export class AddDeviceDto { @IsNotEmpty() public userUuid: string; } -export class UpdateDeviceInRoomDto { +export class UpdateDeviceInSpaceDto { @ApiProperty({ description: 'deviceUuid', required: true, @@ -28,10 +29,43 @@ export class UpdateDeviceInRoomDto { public deviceUuid: string; @ApiProperty({ - description: 'roomUuid', + description: 'spaceUuid', required: true, }) @IsString() @IsNotEmpty() - public roomUuid: string; + public spaceUuid: string; +} +export class AddSceneToFourSceneDeviceDto { + @ApiProperty({ + description: 'switchName', + required: true, + }) + @IsEnum(SceneSwitchesTypeEnum) + @IsNotEmpty() + switchName: SceneSwitchesTypeEnum; + + @ApiProperty({ + description: 'sceneUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public sceneUuid: string; + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; +} +export class UpdateDeviceDto { + @ApiProperty({ + description: 'deviceName', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceName: string; } diff --git a/src/device/dtos/control.device.dto.ts b/src/device/dtos/control.device.dto.ts index d917a96..1303640 100644 --- a/src/device/dtos/control.device.dto.ts +++ b/src/device/dtos/control.device.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class ControlDeviceDto { @ApiProperty({ @@ -54,3 +54,12 @@ export class BatchFactoryResetDevicesDto { @IsNotEmpty() public devicesUuid: [string]; } +export class GetSceneFourSceneDeviceDto { + @ApiProperty({ + description: 'switchName', + required: false, + }) + @IsString() + @IsOptional() + public switchName?: string; +} diff --git a/src/room/dtos/update.room.dto.ts b/src/device/dtos/delete.device.dto.ts similarity index 50% rename from src/room/dtos/update.room.dto.ts rename to src/device/dtos/delete.device.dto.ts index 8f54092..2e60435 100644 --- a/src/room/dtos/update.room.dto.ts +++ b/src/device/dtos/delete.device.dto.ts @@ -1,16 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; -export class UpdateRoomNameDto { +export class DeleteSceneFromSceneDeviceDto { @ApiProperty({ - description: 'roomName', + description: 'switchName', required: true, }) @IsString() @IsNotEmpty() - public roomName: string; - - constructor(dto: Partial) { - Object.assign(this, dto); - } + public switchName: string; } diff --git a/src/device/dtos/device.param.dto.ts b/src/device/dtos/device.param.dto.ts new file mode 100644 index 0000000..0a0eb6c --- /dev/null +++ b/src/device/dtos/device.param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class DeviceSceneParamDto { + @ApiProperty({ + description: 'UUID of the device', + example: 'b3a37332-9c03-4ce2-ac94-bea75382b365', + }) + @IsUUID() + deviceUuid: string; +} diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 13204de..f3ce5cc 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,14 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; -export class GetDeviceByRoomUuidDto { +export class GetDeviceBySpaceUuidDto { @ApiProperty({ - description: 'roomUuid', + description: 'spaceUuid', required: true, }) @IsString() @IsNotEmpty() - public roomUuid: string; + public spaceUuid: string; } export class GetDeviceLogsDto { @ApiProperty({ diff --git a/src/device/interfaces/get.device.interface.ts b/src/device/interfaces/get.device.interface.ts index 93438b9..3215364 100644 --- a/src/device/interfaces/get.device.interface.ts +++ b/src/device/interfaces/get.device.interface.ts @@ -1,6 +1,6 @@ export interface GetDeviceDetailsInterface { activeTime: number; - assetId: string; + assetId?: string; category: string; categoryName: string; createTime: number; @@ -13,6 +13,7 @@ export interface GetDeviceDetailsInterface { lon: string; model: string; name: string; + battery?: number; nodeId: string; online: boolean; productId?: string; @@ -23,6 +24,18 @@ export interface GetDeviceDetailsInterface { uuid: string; productType: string; productUuid: string; + spaces?: SpaceInterface[]; + community?: CommunityInterface; +} + +export interface SpaceInterface { + uuid: string; + spaceName: string; +} + +export interface CommunityInterface { + uuid: string; + name: string; } export interface addDeviceInRoomInterface { @@ -77,3 +90,9 @@ export interface GetPowerClampFunctionsStatusInterface { success: boolean; msg: string; } +export interface GetMacAddressInterface { + uuid: string; + mac: string; + sn: string; + id: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index ebe73e2..add62c9 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -5,22 +5,30 @@ import { HttpStatus, NotFoundException, BadRequestException, + forwardRef, + Inject, } from '@nestjs/common'; import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; -import { AddDeviceDto, UpdateDeviceInRoomDto } from '../dtos/add.device.dto'; +import { + AddDeviceDto, + AddSceneToFourSceneDeviceDto, + UpdateDeviceDto, + UpdateDeviceInSpaceDto, +} from '../dtos/add.device.dto'; import { DeviceInstructionResponse, GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsInterface, + GetMacAddressInterface, GetPowerClampFunctionsStatusInterface, controlDeviceInterface, getDeviceLogsInterface, updateDeviceFirmwareInterface, } from '../interfaces/get.device.interface'; import { - GetDeviceByRoomUuidDto, + GetDeviceBySpaceUuidDto, GetDeviceLogsDto, } from '../dtos/get.device.dto'; import { @@ -28,6 +36,7 @@ import { BatchFactoryResetDevicesDto, BatchStatusDevicesDto, ControlDeviceDto, + GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { DeviceRepository } from '@app/common/modules/device/repositories'; @@ -39,6 +48,17 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status import { DeviceStatuses } from '@app/common/constants/device-status.enum'; import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum'; +import { SpaceEntity } from '@app/common/modules/space/entities'; +import { SceneService } from 'src/scene/services'; +import { AddAutomationDto } from 'src/automation/dtos'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; +import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; +import { DeviceSceneParamDto } from '../dtos/device.param.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto'; @Injectable() export class DeviceService { @@ -46,9 +66,13 @@ export class DeviceService { constructor( private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, + private readonly sceneDeviceRepository: SceneDeviceRepository, private readonly productRepository: ProductRepository, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly spaceRepository: SpaceRepository, + @Inject(forwardRef(() => SceneService)) + private readonly sceneService: SceneService, + private readonly tuyaService: TuyaService, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -63,13 +87,18 @@ export class DeviceService { deviceUuid: string, withProductDevice: boolean = true, ) { - return await this.deviceRepository.findOne({ - where: { - uuid: deviceUuid, - }, - ...(withProductDevice && { relations: ['productDevice'] }), + const relations = ['subspace']; + + if (withProductDevice) { + relations.push('productDevice'); + } + + return this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations, }); } + async getDeviceByDeviceTuyaUuid(deviceTuyaUuid: string) { return await this.deviceRepository.findOne({ where: { @@ -78,6 +107,7 @@ export class DeviceService { relations: ['productDevice'], }); } + async addDeviceUser(addDeviceDto: AddDeviceDto) { try { const device = await this.getDeviceDetailsByDeviceIdTuya( @@ -117,12 +147,13 @@ export class DeviceService { ); } else { throw new HttpException( - error.message || 'Failed to add device in room', + error.message || 'Failed to add device in space', error.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } } + async getDevicesByUser( userUuid: string, ): Promise { @@ -145,38 +176,48 @@ export class DeviceService { 'permission.permissionType', ], }); - const devicesData = await Promise.all( - devices.map(async (device) => { + const safeFetchDeviceDetails = async (device: any) => { + try { + const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + ); + return { - haveRoom: device.spaceDevice ? true : false, + haveRoom: !!device.spaceDevice, productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, permissionType: device.permission[0].permissionType.type, - ...(await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - )), + ...tuyaDetails, uuid: device.uuid, } as GetDeviceDetailsInterface; - }), + } catch (error) { + console.warn( + `Skipping device with deviceTuyaUuid: ${device.deviceTuyaUuid} due to error.`, + ); + return null; + } + }; + const devicesData = await Promise.all( + devices.map(safeFetchDeviceDetails), ); - return devicesData; + return devicesData.filter(Boolean); // Remove null or undefined entries } catch (error) { - // Handle the error here + console.error('Error fetching devices by user:', error); throw new HttpException( 'User does not have any devices', HttpStatus.NOT_FOUND, ); } } - async getDevicesByRoomId( - getDeviceByRoomUuidDto: GetDeviceByRoomUuidDto, + async getDevicesBySpaceId( + getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto, userUuid: string, ): Promise { try { const devices = await this.deviceRepository.find({ where: { - spaceDevice: { uuid: getDeviceByRoomUuidDto.roomUuid }, + spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid }, isActive: true, permission: { userUuid, @@ -211,22 +252,22 @@ export class DeviceService { } catch (error) { // Handle the error here throw new HttpException( - 'Error fetching devices by room', + 'Error fetching devices by space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } - async updateDeviceInRoom(updateDeviceInRoomDto: UpdateDeviceInRoomDto) { + async updateDeviceInSpace(updateDeviceInSpaceDto: UpdateDeviceInSpaceDto) { try { await this.deviceRepository.update( - { uuid: updateDeviceInRoomDto.deviceUuid }, + { uuid: updateDeviceInSpaceDto.deviceUuid }, { - spaceDevice: { uuid: updateDeviceInRoomDto.roomUuid }, + spaceDevice: { uuid: updateDeviceInSpaceDto.spaceUuid }, }, ); const device = await this.deviceRepository.findOne({ where: { - uuid: updateDeviceInRoomDto.deviceUuid, + uuid: updateDeviceInSpaceDto.deviceUuid, }, relations: ['spaceDevice', 'spaceDevice.parent'], }); @@ -239,15 +280,16 @@ export class DeviceService { return { uuid: device.uuid, - roomUuid: device.spaceDevice.uuid, + spaceUuid: device.spaceDevice.uuid, }; } catch (error) { throw new HttpException( - 'Failed to add device in room', + 'Failed to add device in space', HttpStatus.INTERNAL_SERVER_ERROR, ); } } + async transferDeviceInSpacesTuya( deviceId: string, spaceId: string, @@ -268,6 +310,26 @@ export class DeviceService { ); } } + async updateDeviceNameTuya( + deviceId: string, + deviceName: string, + ): Promise { + try { + const path = `/v2.0/cloud/thing/${deviceId}/attribute`; + const response = await this.tuya.request({ + method: 'POST', + path, + body: { type: 1, data: deviceName }, + }); + + return response as controlDeviceInterface; + } catch (error) { + throw new HttpException( + 'Error updating device name from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); @@ -295,6 +357,7 @@ export class DeviceService { ); } } + async factoryResetDeviceTuya( deviceUuid: string, ): Promise { @@ -337,6 +400,7 @@ export class DeviceService { ); } } + async batchControlDevices(batchControlDevicesDto: BatchControlDevicesDto) { const { devicesUuid } = batchControlDevicesDto; @@ -511,6 +575,9 @@ export class DeviceService { const response = await this.getDeviceDetailsByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); + const macAddress = await this.getMacAddressByDeviceIdTuya( + deviceDetails.deviceTuyaUuid, + ); return { ...response, @@ -518,6 +585,8 @@ export class DeviceService { productUuid: deviceDetails.productDevice.uuid, productType: deviceDetails.productDevice.prodType, permissionType: userDevicePermission, + macAddress: macAddress.mac, + subspace: deviceDetails.subspace ? deviceDetails.subspace : {}, }; } catch (error) { throw new HttpException( @@ -526,6 +595,27 @@ export class DeviceService { ); } } + async updateDevice(deviceUuid: string, updateDeviceDto: UpdateDeviceDto) { + try { + const device = await this.getDeviceByDeviceUuid(deviceUuid); + if (device.deviceTuyaUuid) { + await this.updateDeviceNameTuya( + device.deviceTuyaUuid, + updateDeviceDto.deviceName, + ); + } + + return { + uuid: device.uuid, + deviceName: updateDeviceDto.deviceName, + }; + } catch (error) { + throw new HttpException( + 'Error updating device', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceDetailsByDeviceIdTuya( deviceId: string, ): Promise { @@ -558,17 +648,33 @@ export class DeviceService { ); } } + async getMacAddressByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const path = `/v1.0/devices/factory-infos?device_ids=${deviceId}`; + const response = await this.tuya.request({ + method: 'GET', + path, + }); + return response.result[0]; + } catch (error) { + throw new HttpException( + 'Error fetching mac address device from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceInstructionByDeviceId( deviceUuid: string, ): Promise { + const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); + + if (!deviceDetails) { + throw new NotFoundException('Device Not Found'); + } try { - const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); - - if (!deviceDetails) { - throw new NotFoundException('Device Not Found'); - } - const response = await this.getDeviceInstructionByDeviceIdTuya( deviceDetails.deviceTuyaUuid, ); @@ -778,22 +884,22 @@ export class DeviceService { ); } } - async getDevicesByUnitId(unitUuid: string) { + async getDevicesBySpaceUuid(SpaceUuid: string) { try { const spaces = await this.spaceRepository.find({ where: { parent: { - uuid: unitUuid, + uuid: SpaceUuid, }, - devicesSpaceEntity: { + devices: { isActive: true, }, }, - relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + relations: ['devices', 'devices.productDevice'], }); const devices = spaces.flatMap((space) => { - return space.devicesSpaceEntity.map((device) => device); + return space.devices.map((device) => device); }); const devicesData = await Promise.all( @@ -814,7 +920,7 @@ export class DeviceService { return devicesData; } catch (error) { throw new HttpException( - 'This unit does not have any devices', + 'This space does not have any devices', HttpStatus.NOT_FOUND, ); } @@ -825,11 +931,13 @@ export class DeviceService { where: { isActive: true }, relations: [ 'spaceDevice.parent', + 'spaceDevice.community', 'productDevice', 'permission', 'permission.permissionType', ], }); + const devicesData = await Promise.allSettled( devices.map(async (device) => { let battery = null; @@ -874,20 +982,24 @@ export class DeviceService { battery = batteryStatus.value; } } - const spaceDevice = device?.spaceDevice; - const parentDevice = spaceDevice?.parent; + + const spaceHierarchy = await this.getFullSpaceHierarchy( + device?.spaceDevice, + ); + const orderedHierarchy = spaceHierarchy.reverse(); + return { - room: { - uuid: spaceDevice?.uuid, - name: spaceDevice?.spaceName, - }, - unit: { - uuid: parentDevice?.uuid, - name: parentDevice?.spaceName, - }, + spaces: orderedHierarchy.map((space) => ({ + uuid: space.uuid, + spaceName: space.spaceName, + })), productUuid: device.productDevice.uuid, productType: device.productDevice.prodType, - permissionType: device.permission[0].permissionType.type, + community: { + uuid: device.spaceDevice.community.uuid, + name: device.spaceDevice.community.name, + }, + // permissionType: device.permission[0].permissionType.type, ...(await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, )), @@ -913,6 +1025,7 @@ export class DeviceService { ); } } + async getDeviceLogs(deviceUuid: string, query: GetDeviceLogsDto) { try { const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid); @@ -966,6 +1079,40 @@ export class DeviceService { ); } } + + async getFullSpaceHierarchy( + space: SpaceEntity, + ): Promise<{ uuid: string; spaceName: string }[]> { + try { + // Fetch only the relevant spaces, starting with the target space + const targetSpace = await this.spaceRepository.findOne({ + where: { uuid: space.uuid }, + relations: ['parent', 'children'], + }); + + // Fetch only the ancestors of the target space + const ancestors = await this.fetchAncestors(targetSpace); + + // Optionally, fetch descendants if required + const descendants = await this.fetchDescendants(targetSpace); + + const fullHierarchy = [...ancestors, targetSpace, ...descendants].map( + (space) => ({ + uuid: space.uuid, + spaceName: space.spaceName, + }), + ); + + return fullHierarchy; + } catch (error) { + console.error('Error fetching space hierarchy:', error.message); + throw new HttpException( + 'Error fetching space hierarchy', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getPowerClampInstructionStatus(powerClampUuid: string) { try { const deviceDetails = await this.getDeviceByDeviceUuid(powerClampUuid); @@ -1043,4 +1190,276 @@ export class DeviceService { ); } } + + private async fetchAncestors(space: SpaceEntity): Promise { + const ancestors: SpaceEntity[] = []; + + let currentSpace = space; + while (currentSpace && currentSpace.parent) { + // Fetch the parent space + const parent = await this.spaceRepository.findOne({ + where: { uuid: currentSpace.parent.uuid }, + relations: ['parent'], // To continue fetching upwards + }); + + if (parent) { + ancestors.push(parent); + currentSpace = parent; + } else { + currentSpace = null; + } + } + + // Return the ancestors in reverse order to have the root at the start + return ancestors.reverse(); + } + + private async fetchDescendants(space: SpaceEntity): Promise { + const descendants: SpaceEntity[] = []; + + // Fetch the immediate children of the current space + const children = await this.spaceRepository.find({ + where: { parent: { uuid: space.uuid } }, + relations: ['children'], // To continue fetching downwards + }); + + for (const child of children) { + // Add the child to the descendants list + descendants.push(child); + + // Recursively fetch the child's descendants + const childDescendants = await this.fetchDescendants(child); + descendants.push(...childDescendants); + } + + return descendants; + } + + async addSceneToSceneDevice( + deviceUuid: string, + addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, + ) { + try { + const { spaceUuid, sceneUuid, switchName } = addSceneToFourSceneDeviceDto; + + if (!spaceUuid || !sceneUuid || !switchName) { + throw new BadRequestException('Missing required fields in DTO'); + } + + const [sceneData, spaceData, deviceData] = await Promise.all([ + this.sceneService.findScene(sceneUuid), + this.sceneService.getSpaceByUuid(spaceUuid), + this.getDeviceByDeviceUuid(deviceUuid), + ]); + + const shortUuid = deviceUuid.slice(0, 6); // First 6 characters of the UUID + const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds + const automationName = `Auto_${shortUuid}_${timestamp}`; + + const addAutomationData: AddAutomationDto = { + spaceUuid: spaceData.spaceTuyaUuid, + automationName, + decisionExpr: AUTOMATION_CONFIG.DECISION_EXPR, + effectiveTime: { + start: AUTOMATION_CONFIG.DEFAULT_START_TIME, + end: AUTOMATION_CONFIG.DEFAULT_END_TIME, + loops: AUTOMATION_CONFIG.DEFAULT_LOOPS, + }, + conditions: [ + { + code: 1, + entityId: deviceData.deviceTuyaUuid, + entityType: AUTOMATION_CONFIG.CONDITION_TYPE, + expr: { + comparator: AUTOMATION_CONFIG.COMPARATOR, + statusCode: switchName, + statusValue: AUTOMATION_CONFIG.SCENE_STATUS_VALUE, + }, + }, + ], + actions: [ + { + actionExecutor: AUTOMATION_CONFIG.ACTION_EXECUTOR, + entityId: sceneData.sceneTuyaUuid, + }, + ], + }; + + const automation = await this.tuyaService.createAutomation( + addAutomationData.spaceUuid, + addAutomationData.automationName, + addAutomationData.effectiveTime, + addAutomationData.decisionExpr, + addAutomationData.conditions, + addAutomationData.actions, + ); + + if (automation.success) { + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + switchName: switchName, + }, + relations: ['scene', 'device'], + }); + + if (existingSceneDevice) { + await this.tuyaService.deleteAutomation( + spaceData.spaceTuyaUuid, + existingSceneDevice.automationTuyaUuid, + ); + + existingSceneDevice.automationTuyaUuid = automation.result.id; + existingSceneDevice.scene = sceneData; + existingSceneDevice.device = deviceData; + existingSceneDevice.switchName = switchName; + + return await this.sceneDeviceRepository.save(existingSceneDevice); + } else { + const sceneDevice = await this.sceneDeviceRepository.save({ + scene: sceneData, + device: deviceData, + automationTuyaUuid: automation.result.id, + switchName: switchName, + }); + return sceneDevice; + } + } + } catch (err) { + const errorMessage = err.message || 'Error creating automation'; + const errorStatus = err.status || HttpStatus.INTERNAL_SERVER_ERROR; + throw new HttpException(errorMessage, errorStatus); + } + } + + async getScenesBySceneDevice( + deviceUuid: string, + getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto, + ): Promise { + try { + if (getSceneFourSceneDeviceDto.switchName) { + // Query for a single record directly when switchName is provided + const sceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + switchName: + getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum, + }, + relations: ['device', 'scene'], + }); + + if (!sceneDevice) { + return {}; + } + + const sceneDetails = await this.sceneService.getSceneByUuid( + sceneDevice.scene.uuid, + ); + + return { + switchSceneUuid: sceneDevice.uuid, + switchName: sceneDevice.switchName, + createdAt: sceneDevice.createdAt, + updatedAt: sceneDevice.updatedAt, + deviceUuid: sceneDevice.device.uuid, + scene: sceneDetails.data, + }; + } + + // Query for multiple records if switchName is not provided + const sceneDevices = await this.sceneDeviceRepository.find({ + where: { device: { uuid: deviceUuid } }, + relations: ['device', 'scene'], + }); + + if (!sceneDevices.length) { + return []; + } + + const results = await Promise.all( + sceneDevices.map(async (sceneDevice) => { + const sceneDetails = await this.sceneService.getSceneByUuid( + sceneDevice.scene.uuid, + ); + + return { + switchSceneUuid: sceneDevice.uuid, + switchName: sceneDevice.switchName, + createdAt: sceneDevice.createdAt, + updatedAt: sceneDevice.updatedAt, + deviceUuid: sceneDevice.device.uuid, + scene: sceneDetails.data, + }; + }), + ); + + return results; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch scenes for device', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async deleteSceneFromSceneDevice( + params: DeviceSceneParamDto, + query: DeleteSceneFromSceneDeviceDto, + ): Promise { + const { deviceUuid } = params; + const { switchName } = query; + + try { + const existingSceneDevice = await this.sceneDeviceRepository.findOne({ + where: { + device: { uuid: deviceUuid }, + switchName: switchName as SceneSwitchesTypeEnum, + }, + relations: ['scene.space.community'], + }); + + if (!existingSceneDevice) { + throw new HttpException( + `No scene found for device with UUID ${deviceUuid} and switch name ${switchName}`, + HttpStatus.NOT_FOUND, + ); + } + + const deleteResult = await this.sceneDeviceRepository.delete({ + device: { uuid: deviceUuid }, + switchName: switchName as SceneSwitchesTypeEnum, + }); + + if (deleteResult.affected === 0) { + throw new HttpException( + `Failed to delete Switch Scene with device ID ${deviceUuid} and switch name ${switchName}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const tuyaAutomationResult = await this.tuyaService.deleteAutomation( + existingSceneDevice.scene.space.community.externalId, + existingSceneDevice.automationTuyaUuid, + ); + + if (!tuyaAutomationResult.success) { + throw new HttpException( + `Failed to delete Tuya automation for Switch Scene with ID ${existingSceneDevice.automationTuyaUuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return new SuccessResponseDto({ + message: `Switch Scene with device ID ${deviceUuid} and switch name ${switchName} deleted successfully`, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + + throw new HttpException( + error.message || `An unexpected error occurred`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/door-lock/controllers/door.lock.controller.ts b/src/door-lock/controllers/door.lock.controller.ts index 3e6f459..a965ecb 100644 --- a/src/door-lock/controllers/door.lock.controller.ts +++ b/src/door-lock/controllers/door.lock.controller.ts @@ -10,23 +10,31 @@ import { UseGuards, Put, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddDoorLockOnlineDto } from '../dtos/add.online-temp.dto'; import { AddDoorLockOfflineTempMultipleTimeDto } from '../dtos/add.offline-temp.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { UpdateDoorLockOfflineTempDto } from '../dtos/update.offline-temp.dto'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Door Lock Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'door-lock', + path: ControllerRoute.DOOR_LOCK.ROUTE, }) export class DoorLockController { constructor(private readonly doorLockService: DoorLockService) {} @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/online/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS.ADD_ONLINE_TEMPORARY_PASSWORD_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .ADD_ONLINE_TEMPORARY_PASSWORD_DESCRIPTION, + }) async addOnlineTemporaryPassword( @Body() addDoorLockDto: AddDoorLockOnlineDto, @Param('doorLockUuid') doorLockUuid: string, @@ -40,15 +48,24 @@ export class DoorLockController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'online temporary password added successfully', + message: 'Online temporary password added successfully', data: { id: temporaryPassword.id, }, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/one-time/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .ADD_OFFLINE_ONE_TIME_TEMPORARY_PASSWORD_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .ADD_OFFLINE_ONE_TIME_TEMPORARY_PASSWORD_DESCRIPTION, + }) async addOfflineOneTimeTemporaryPassword( @Param('doorLockUuid') doorLockUuid: string, ) { @@ -60,13 +77,22 @@ export class DoorLockController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'offline temporary password added successfully', + message: 'Offline one-time temporary password added successfully', data: temporaryPassword, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/multiple-time/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .ADD_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORD_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .ADD_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORD_DESCRIPTION, + }) async addOfflineMultipleTimeTemporaryPassword( @Body() addDoorLockOfflineTempMultipleTimeDto: AddDoorLockOfflineTempMultipleTimeDto, @@ -81,13 +107,21 @@ export class DoorLockController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'offline temporary password added successfully', + message: 'Offline multiple-time temporary password added successfully', data: temporaryPassword, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('temporary-password/online/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS.GET_ONLINE_TEMPORARY_PASSWORDS_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .GET_ONLINE_TEMPORARY_PASSWORDS_DESCRIPTION, + }) async getOnlineTemporaryPasswords( @Param('doorLockUuid') doorLockUuid: string, ) { @@ -95,9 +129,18 @@ export class DoorLockController { doorLockUuid, ); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Delete('temporary-password/online/:doorLockUuid/:passwordId') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .DELETE_ONLINE_TEMPORARY_PASSWORD_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .DELETE_ONLINE_TEMPORARY_PASSWORD_DESCRIPTION, + }) async deleteDoorLockPassword( @Param('doorLockUuid') doorLockUuid: string, @Param('passwordId') passwordId: string, @@ -105,12 +148,21 @@ export class DoorLockController { await this.doorLockService.deleteDoorLockPassword(doorLockUuid, passwordId); return { statusCode: HttpStatus.OK, - message: 'Temporary Password deleted Successfully', + message: 'Temporary password deleted successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('temporary-password/offline/one-time/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .GET_OFFLINE_ONE_TIME_TEMPORARY_PASSWORDS_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .GET_OFFLINE_ONE_TIME_TEMPORARY_PASSWORDS_DESCRIPTION, + }) async getOfflineOneTimeTemporaryPasswords( @Param('doorLockUuid') doorLockUuid: string, ) { @@ -118,9 +170,18 @@ export class DoorLockController { doorLockUuid, ); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('temporary-password/offline/multiple-time/:doorLockUuid') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .GET_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORDS_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .GET_OFFLINE_MULTIPLE_TIME_TEMPORARY_PASSWORDS_DESCRIPTION, + }) async getOfflineMultipleTimeTemporaryPasswords( @Param('doorLockUuid') doorLockUuid: string, ) { @@ -132,9 +193,16 @@ export class DoorLockController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('temporary-password/:doorLockUuid/offline/:passwordId') + @ApiOperation({ + summary: + ControllerRoute.DOOR_LOCK.ACTIONS + .UPDATE_OFFLINE_TEMPORARY_PASSWORD_SUMMARY, + description: + ControllerRoute.DOOR_LOCK.ACTIONS + .UPDATE_OFFLINE_TEMPORARY_PASSWORD_DESCRIPTION, + }) async updateOfflineTemporaryPassword( - @Body() - updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto, + @Body() updateDoorLockOfflineTempDto: UpdateDoorLockOfflineTempDto, @Param('doorLockUuid') doorLockUuid: string, @Param('passwordId') passwordId: string, ) { @@ -148,20 +216,25 @@ export class DoorLockController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'offline temporary password updated successfully', + message: 'Offline temporary password updated successfully', data: temporaryPassword, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('open/:doorLockUuid') + @ApiOperation({ + summary: ControllerRoute.DOOR_LOCK.ACTIONS.OPEN_DOOR_LOCK_SUMMARY, + description: ControllerRoute.DOOR_LOCK.ACTIONS.OPEN_DOOR_LOCK_DESCRIPTION, + }) async openDoorLock(@Param('doorLockUuid') doorLockUuid: string) { await this.doorLockService.openDoorLock(doorLockUuid); return { statusCode: HttpStatus.CREATED, success: true, - message: 'door lock opened successfully', + message: 'Door lock opened successfully', }; } } diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index d5f070e..9bbc0d5 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -11,6 +11,13 @@ import { ProductRepository } from '@app/common/modules/product/repositories'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -24,6 +31,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log DeviceStatusFirebaseService, SpaceRepository, DeviceStatusLogRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [DoorLockService], }) diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts deleted file mode 100644 index 9142db6..0000000 --- a/src/floor/controllers/floor.controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { FloorService } from '../services/floor.service'; -import { - Body, - Controller, - Get, - HttpStatus, - Param, - Post, - Put, - Query, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddFloorDto, AddUserFloorDto } from '../dtos/add.floor.dto'; -import { GetFloorChildDto } from '../dtos/get.floor.dto'; -import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; -import { CheckBuildingTypeGuard } from 'src/guards/building.type.guard'; -import { CheckUserFloorGuard } from 'src/guards/user.floor.guard'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { FloorPermissionGuard } from 'src/guards/floor.permission.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@ApiTags('Floor Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.FLOOR, -}) -export class FloorController { - constructor(private readonly floorService: FloorService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) - @Post() - async addFloor(@Body() addFloorDto: AddFloorDto) { - const floor = await this.floorService.addFloor(addFloorDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Floor added successfully', - data: floor, - }; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, FloorPermissionGuard) - @Get(':floorUuid') - async getFloorByUuid(@Param('floorUuid') floorUuid: string) { - const floor = await this.floorService.getFloorByUuid(floorUuid); - return floor; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, FloorPermissionGuard) - @Get('child/:floorUuid') - async getFloorChildByUuid( - @Param('floorUuid') floorUuid: string, - @Query() query: GetFloorChildDto, - ) { - const floor = await this.floorService.getFloorChildByUuid(floorUuid, query); - return floor; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, FloorPermissionGuard) - @Get('parent/:floorUuid') - async getFloorParentByUuid(@Param('floorUuid') floorUuid: string) { - const floor = await this.floorService.getFloorParentByUuid(floorUuid); - return floor; - } - - @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUserFloorGuard) - @Post('user') - async addUserFloor(@Body() addUserFloorDto: AddUserFloorDto) { - await this.floorService.addUserFloor(addUserFloorDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user floor added successfully', - }; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('user/:userUuid') - async getFloorsByUserId(@Param('userUuid') userUuid: string) { - return await this.floorService.getFloorsByUserId(userUuid); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, FloorPermissionGuard) - @Put(':floorUuid') - async renameFloorByUuid( - @Param('floorUuid') floorUuid: string, - @Body() updateFloorNameDto: UpdateFloorNameDto, - ) { - const floor = await this.floorService.renameFloorByUuid( - floorUuid, - updateFloorNameDto, - ); - return floor; - } -} diff --git a/src/floor/controllers/index.ts b/src/floor/controllers/index.ts deleted file mode 100644 index 99eb600..0000000 --- a/src/floor/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './floor.controller'; diff --git a/src/floor/dtos/add.floor.dto.ts b/src/floor/dtos/add.floor.dto.ts deleted file mode 100644 index 3d1655a..0000000 --- a/src/floor/dtos/add.floor.dto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class AddFloorDto { - @ApiProperty({ - description: 'floorName', - required: true, - }) - @IsString() - @IsNotEmpty() - public floorName: string; - - @ApiProperty({ - description: 'buildingUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public buildingUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} -export class AddUserFloorDto { - @ApiProperty({ - description: 'floorUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public floorUuid: string; - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/floor/dtos/get.floor.dto.ts b/src/floor/dtos/get.floor.dto.ts deleted file mode 100644 index 957f81b..0000000 --- a/src/floor/dtos/get.floor.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BooleanValues } from '@app/common/constants/boolean-values.enum'; -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsBoolean, - IsInt, - IsNotEmpty, - IsOptional, - IsString, - Min, -} from 'class-validator'; - -export class GetFloorDto { - @ApiProperty({ - description: 'floorUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public floorUuid: string; -} - -export class GetFloorChildDto { - @ApiProperty({ example: 1, description: 'Page number', required: true }) - @IsInt({ message: 'Page must be a number' }) - @Min(1, { message: 'Page must not be less than 1' }) - @IsNotEmpty() - public page: number; - - @ApiProperty({ - example: 10, - description: 'Number of items per page', - required: true, - }) - @IsInt({ message: 'Page size must be a number' }) - @Min(1, { message: 'Page size must not be less than 1' }) - @IsNotEmpty() - public pageSize: number; - - @ApiProperty({ - example: true, - description: 'Flag to determine whether to fetch full hierarchy', - required: false, - default: false, - }) - @IsOptional() - @IsBoolean() - @Transform((value) => { - return value.obj.includeSubSpaces === BooleanValues.TRUE; - }) - public includeSubSpaces: boolean = false; -} diff --git a/src/floor/dtos/index.ts b/src/floor/dtos/index.ts deleted file mode 100644 index 9c08a9f..0000000 --- a/src/floor/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.floor.dto'; diff --git a/src/floor/dtos/update.floor.dto.ts b/src/floor/dtos/update.floor.dto.ts deleted file mode 100644 index 11c97b0..0000000 --- a/src/floor/dtos/update.floor.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class UpdateFloorNameDto { - @ApiProperty({ - description: 'floorName', - required: true, - }) - @IsString() - @IsNotEmpty() - public floorName: string; - - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/floor/floor.module.ts b/src/floor/floor.module.ts deleted file mode 100644 index 9fdc1c7..0000000 --- a/src/floor/floor.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { FloorService } from './services/floor.service'; -import { FloorController } from './controllers/floor.controller'; -import { ConfigModule } from '@nestjs/config'; -import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { SpaceTypeRepository } from '@app/common/modules/space/repositories'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { UserRepository } from '@app/common/modules/user/repositories'; - -@Module({ - imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], - controllers: [FloorController], - providers: [ - FloorService, - SpaceRepository, - SpaceTypeRepository, - UserSpaceRepository, - UserRepository, - ], - exports: [FloorService], -}) -export class FloorModule {} diff --git a/src/floor/interface/floor.interface.ts b/src/floor/interface/floor.interface.ts deleted file mode 100644 index 37f35c4..0000000 --- a/src/floor/interface/floor.interface.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface GetFloorByUuidInterface { - uuid: string; - createdAt: Date; - updatedAt: Date; - name: string; - type: string; -} - -export interface FloorChildInterface { - uuid: string; - name: string; - type: string; - totalCount?: number; - children?: FloorChildInterface[]; -} -export interface FloorParentInterface { - uuid: string; - name: string; - type: string; - parent?: FloorParentInterface; -} -export interface RenameFloorByUuidInterface { - uuid: string; - name: string; - type: string; -} - -export interface GetFloorByUserUuidInterface { - uuid: string; - name: string; - type: string; -} diff --git a/src/floor/services/floor.service.ts b/src/floor/services/floor.service.ts deleted file mode 100644 index 1f7604f..0000000 --- a/src/floor/services/floor.service.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { GetFloorChildDto } from '../dtos/get.floor.dto'; -import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddFloorDto, AddUserFloorDto } from '../dtos'; -import { - FloorChildInterface, - FloorParentInterface, - GetFloorByUserUuidInterface, - GetFloorByUuidInterface, - RenameFloorByUuidInterface, -} from '../interface/floor.interface'; -import { SpaceEntity } from '@app/common/modules/space/entities'; -import { UpdateFloorNameDto } from '../dtos/update.floor.dto'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; - -@Injectable() -export class FloorService { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, - private readonly userSpaceRepository: UserSpaceRepository, - ) {} - - async addFloor(addFloorDto: AddFloorDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.FLOOR, - }, - }); - - const floor = await this.spaceRepository.save({ - spaceName: addFloorDto.floorName, - parent: { uuid: addFloorDto.buildingUuid }, - spaceType: { uuid: spaceType.uuid }, - }); - return floor; - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - async getFloorByUuid(floorUuid: string): Promise { - try { - const floor = await this.spaceRepository.findOne({ - where: { - uuid: floorUuid, - spaceType: { - type: SpaceType.FLOOR, - }, - }, - relations: ['spaceType'], - }); - if ( - !floor || - !floor.spaceType || - floor.spaceType.type !== SpaceType.FLOOR - ) { - throw new BadRequestException('Invalid floor UUID'); - } - - return { - uuid: floor.uuid, - createdAt: floor.createdAt, - updatedAt: floor.updatedAt, - name: floor.spaceName, - type: floor.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - } - } - async getFloorChildByUuid( - floorUuid: string, - getFloorChildDto: GetFloorChildDto, - ): Promise { - try { - const { includeSubSpaces, page, pageSize } = getFloorChildDto; - - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: floorUuid }, - relations: ['children', 'spaceType'], - }); - - if ( - !space || - !space.spaceType || - space.spaceType.type !== SpaceType.FLOOR - ) { - throw new BadRequestException('Invalid floor UUID'); - } - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, - ); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - } - } - - private async buildHierarchy( - space: SpaceEntity, - includeSubSpaces: boolean, - page: number, - pageSize: number, - ): Promise { - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['spaceType'], - skip: (page - 1) * pageSize, - take: pageSize, - }); - - if (!children || children.length === 0 || !includeSubSpaces) { - return children - .filter( - (child) => - child.spaceType.type !== SpaceType.FLOOR && - child.spaceType.type !== SpaceType.BUILDING && - child.spaceType.type !== SpaceType.COMMUNITY, - ) // Filter remaining floor and building and community types - .map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); - } - - const childHierarchies = await Promise.all( - children - .filter( - (child) => - child.spaceType.type !== SpaceType.FLOOR && - child.spaceType.type !== SpaceType.BUILDING && - child.spaceType.type !== SpaceType.COMMUNITY, - ) // Filter remaining floor and building and community types - .map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), - ); - - return childHierarchies; - } - - async getFloorParentByUuid(floorUuid: string): Promise { - try { - const floor = await this.spaceRepository.findOne({ - where: { - uuid: floorUuid, - spaceType: { - type: SpaceType.FLOOR, - }, - }, - relations: ['spaceType', 'parent', 'parent.spaceType'], - }); - if ( - !floor || - !floor.spaceType || - floor.spaceType.type !== SpaceType.FLOOR - ) { - throw new BadRequestException('Invalid floor UUID'); - } - - return { - uuid: floor.uuid, - name: floor.spaceName, - type: floor.spaceType.type, - parent: { - uuid: floor.parent.uuid, - name: floor.parent.spaceName, - type: floor.parent.spaceType.type, - }, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - } - } - - async getFloorsByUserId( - userUuid: string, - ): Promise { - try { - const floors = await this.userSpaceRepository.find({ - relations: ['space', 'space.spaceType'], - where: { - user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.FLOOR } }, - }, - }); - - if (floors.length === 0) { - throw new HttpException( - 'this user has no floors', - HttpStatus.NOT_FOUND, - ); - } - const spaces = floors.map((floor) => ({ - uuid: floor.space.uuid, - name: floor.space.spaceName, - type: floor.space.spaceType.type, - })); - - return spaces; - } catch (err) { - if (err instanceof HttpException) { - throw err; - } else { - throw new HttpException('user not found', HttpStatus.NOT_FOUND); - } - } - } - async addUserFloor(addUserFloorDto: AddUserFloorDto) { - try { - await this.userSpaceRepository.save({ - user: { uuid: addUserFloorDto.userUuid }, - space: { uuid: addUserFloorDto.floorUuid }, - }); - } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { - throw new HttpException( - 'User already belongs to this floor', - HttpStatus.BAD_REQUEST, - ); - } - throw new HttpException( - err.message || 'Internal Server Error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async renameFloorByUuid( - floorUuid: string, - updateFloorDto: UpdateFloorNameDto, - ): Promise { - try { - const floor = await this.spaceRepository.findOneOrFail({ - where: { uuid: floorUuid }, - relations: ['spaceType'], - }); - - if ( - !floor || - !floor.spaceType || - floor.spaceType.type !== SpaceType.FLOOR - ) { - throw new BadRequestException('Invalid floor UUID'); - } - - await this.spaceRepository.update( - { uuid: floorUuid }, - { spaceName: updateFloorDto.floorName }, - ); - - // Fetch the updated floor - const updatedFloor = await this.spaceRepository.findOneOrFail({ - where: { uuid: floorUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedFloor.uuid, - name: updatedFloor.spaceName, - type: updatedFloor.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Floor not found', HttpStatus.NOT_FOUND); - } - } - } -} diff --git a/src/floor/services/index.ts b/src/floor/services/index.ts deleted file mode 100644 index e6f7946..0000000 --- a/src/floor/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './floor.service'; diff --git a/src/group/controllers/group.controller.ts b/src/group/controllers/group.controller.ts index 85a4a69..a5d7cd7 100644 --- a/src/group/controllers/group.controller.ts +++ b/src/group/controllers/group.controller.ts @@ -1,36 +1,48 @@ import { GroupService } from '../services/group.service'; import { Controller, Get, UseGuards, Param, Req } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; // Assuming this is where the routes are defined @ApiTags('Group Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'group', + path: ControllerRoute.GROUP.ROUTE, // use the static route constant }) export class GroupController { constructor(private readonly groupService: GroupService) {} @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get(':unitUuid') - async getGroupsBySpaceUuid(@Param('unitUuid') unitUuid: string) { - return await this.groupService.getGroupsByUnitUuid(unitUuid); + @UseGuards(JwtAuthGuard) + @Get(':spaceUuid') + @ApiOperation({ + summary: ControllerRoute.GROUP.ACTIONS.GET_GROUPS_BY_SPACE_UUID_SUMMARY, + description: + ControllerRoute.GROUP.ACTIONS.GET_GROUPS_BY_SPACE_UUID_DESCRIPTION, + }) + async getGroupsBySpaceUuid(@Param('spaceUuid') spaceUuid: string) { + return await this.groupService.getGroupsBySpaceUuid(spaceUuid); } + @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get(':unitUuid/devices/:groupName') + @UseGuards(JwtAuthGuard) + @Get(':spaceUuid/devices/:groupName') + @ApiOperation({ + summary: + ControllerRoute.GROUP.ACTIONS.GET_UNIT_DEVICES_BY_GROUP_NAME_SUMMARY, + description: + ControllerRoute.GROUP.ACTIONS.GET_UNIT_DEVICES_BY_GROUP_NAME_DESCRIPTION, + }) async getUnitDevicesByGroupName( - @Param('unitUuid') unitUuid: string, + @Param('spaceUuid') spaceUuid: string, @Param('groupName') groupName: string, @Req() req: any, ) { const userUuid = req.user.uuid; - return await this.groupService.getUnitDevicesByGroupName( - unitUuid, + return await this.groupService.getSpaceDevicesByGroupName( + spaceUuid, groupName, userUuid, ); diff --git a/src/group/services/group.service.ts b/src/group/services/group.service.ts index f549afc..049e82d 100644 --- a/src/group/services/group.service.ts +++ b/src/group/services/group.service.ts @@ -27,21 +27,17 @@ export class GroupService { }); } - async getGroupsByUnitUuid(unitUuid: string) { + async getGroupsBySpaceUuid(spaceUuid: string) { try { const spaces = await this.spaceRepository.find({ where: { - parent: { - uuid: unitUuid, - }, + uuid: spaceUuid, }, - relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], + relations: ['devices', 'devices.productDevice'], }); const groupNames = spaces.flatMap((space) => { - return space.devicesSpaceEntity.map( - (device) => device.productDevice.prodType, - ); + return space.devices.map((device) => device.productDevice.prodType); }); const uniqueGroupNames = [...new Set(groupNames)]; @@ -65,24 +61,22 @@ export class GroupService { ); } catch (error) { throw new HttpException( - 'This unit does not have any groups', + 'This space does not have any groups', HttpStatus.NOT_FOUND, ); } } - async getUnitDevicesByGroupName( - unitUuid: string, + async getSpaceDevicesByGroupName( + spaceUuid: string, groupName: string, userUuid: string, ) { try { const spaces = await this.spaceRepository.find({ where: { - parent: { - uuid: unitUuid, - }, - devicesSpaceEntity: { + uuid: spaceUuid, + devices: { productDevice: { prodType: groupName, }, @@ -95,18 +89,18 @@ export class GroupService { }, }, relations: [ - 'devicesSpaceEntity', - 'devicesSpaceEntity.productDevice', - 'devicesSpaceEntity.spaceDevice', - 'devicesSpaceEntity.permission', - 'devicesSpaceEntity.permission.permissionType', + 'devices', + 'devices.productDevice', + 'devices.spaceDevice', + 'devices.permission', + 'devices.permission.permissionType', ], }); const devices = await Promise.all( spaces.flatMap(async (space) => { return await Promise.all( - space.devicesSpaceEntity.map(async (device) => { + space.devices.map(async (device) => { const deviceDetails = await this.getDeviceDetailsByDeviceIdTuya( device.deviceTuyaUuid, ); @@ -127,7 +121,7 @@ export class GroupService { return devices.flat(); // Flatten the array since flatMap was used } catch (error) { throw new HttpException( - 'This unit does not have any devices for the specified group name', + 'This space does not have any devices for the specified group name', HttpStatus.NOT_FOUND, ); } diff --git a/src/guards/building.permission.guard.ts b/src/guards/building.permission.guard.ts deleted file mode 100644 index 5d496a9..0000000 --- a/src/guards/building.permission.guard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; -import { - BadRequestException, - CanActivate, - ExecutionContext, - Injectable, -} from '@nestjs/common'; - -@Injectable() -export class BuildingPermissionGuard implements CanActivate { - constructor(private readonly permissionService: SpacePermissionService) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { buildingUuid } = req.params; - const { user } = req; - - if (!buildingUuid) { - throw new BadRequestException('buildingUuid is required'); - } - - await this.permissionService.checkUserPermission( - buildingUuid, - user.uuid, - SpaceType.BUILDING, - ); - - return true; - } catch (error) { - throw error; - } - } -} diff --git a/src/guards/building.type.guard.ts b/src/guards/building.type.guard.ts deleted file mode 100644 index dd2870e..0000000 --- a/src/guards/building.type.guard.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { - Injectable, - CanActivate, - HttpStatus, - BadRequestException, - ExecutionContext, -} from '@nestjs/common'; - -@Injectable() -export class CheckBuildingTypeGuard implements CanActivate { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { floorName, buildingUuid } = req.body; - - if (!floorName) { - throw new BadRequestException('floorName is required'); - } - - if (!buildingUuid) { - throw new BadRequestException('buildingUuid is required'); - } - - await this.checkBuildingIsBuildingType(buildingUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - async checkBuildingIsBuildingType(buildingUuid: string) { - const buildingData = await this.spaceRepository.findOne({ - where: { uuid: buildingUuid }, - relations: ['spaceType'], - }); - if ( - !buildingData || - !buildingData.spaceType || - buildingData.spaceType.type !== SpaceType.BUILDING - ) { - throw new BadRequestException('Invalid building UUID'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - console.error(error); - - if (error instanceof BadRequestException) { - response - .status(HttpStatus.BAD_REQUEST) - .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); - } else { - response.status(HttpStatus.NOT_FOUND).json({ - statusCode: HttpStatus.NOT_FOUND, - message: 'Building not found', - }); - } - } -} diff --git a/src/guards/community.permission.guard.ts b/src/guards/community.permission.guard.ts index a078b70..10d9c5e 100644 --- a/src/guards/community.permission.guard.ts +++ b/src/guards/community.permission.guard.ts @@ -1,5 +1,5 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; +import { RoleType } from '@app/common/constants/role.type.enum'; +import { CommunityPermissionService } from '@app/common/helper/services/community.permission.service'; import { BadRequestException, CanActivate, @@ -9,7 +9,7 @@ import { @Injectable() export class CommunityPermissionGuard implements CanActivate { - constructor(private readonly permissionService: SpacePermissionService) {} + constructor(private readonly permissionService: CommunityPermissionService) {} async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); @@ -18,15 +18,22 @@ export class CommunityPermissionGuard implements CanActivate { const { communityUuid } = req.params; const { user } = req; + if ( + user && + user.roles && + user.roles.some( + (role) => + role.type === RoleType.ADMIN || role.type === RoleType.SUPER_ADMIN, + ) + ) { + return true; + } + if (!communityUuid) { throw new BadRequestException('communityUuid is required'); } - await this.permissionService.checkUserPermission( - communityUuid, - user.uuid, - SpaceType.COMMUNITY, - ); + await this.permissionService.checkUserPermission(communityUuid); return true; } catch (error) { diff --git a/src/guards/community.type.guard.ts b/src/guards/community.type.guard.ts deleted file mode 100644 index 9dc5d01..0000000 --- a/src/guards/community.type.guard.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { BadRequestException } from '@nestjs/common'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@Injectable() -export class CheckCommunityTypeGuard implements CanActivate { - constructor(private readonly spaceRepository: SpaceRepository) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { buildingName, communityUuid } = req.body; - - if (!buildingName) { - throw new BadRequestException('buildingName is required'); - } - - if (!communityUuid) { - throw new BadRequestException('communityUuid is required'); - } - - await this.checkCommunityIsCommunityType(communityUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkCommunityIsCommunityType(communityUuid: string) { - const communityData = await this.spaceRepository.findOne({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - if ( - !communityData || - !communityData.spaceType || - communityData.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - console.error(error); - - if (error instanceof BadRequestException) { - response - .status(HttpStatus.BAD_REQUEST) - .json({ statusCode: HttpStatus.BAD_REQUEST, message: error.message }); - } else { - response.status(HttpStatus.NOT_FOUND).json({ - statusCode: HttpStatus.NOT_FOUND, - message: 'Community not found', - }); - } - } -} diff --git a/src/guards/floor.permission.guard.ts b/src/guards/floor.permission.guard.ts deleted file mode 100644 index 7092264..0000000 --- a/src/guards/floor.permission.guard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; -import { - BadRequestException, - CanActivate, - ExecutionContext, - Injectable, -} from '@nestjs/common'; - -@Injectable() -export class FloorPermissionGuard implements CanActivate { - constructor(private readonly permissionService: SpacePermissionService) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { floorUuid } = req.params; - const { user } = req; - - if (!floorUuid) { - throw new BadRequestException('floorUuid is required'); - } - - await this.permissionService.checkUserPermission( - floorUuid, - user.uuid, - SpaceType.FLOOR, - ); - - return true; - } catch (error) { - throw error; - } - } -} diff --git a/src/guards/floor.type.guard.ts b/src/guards/floor.type.guard.ts index 3e6b875..b7b7215 100644 --- a/src/guards/floor.type.guard.ts +++ b/src/guards/floor.type.guard.ts @@ -1,4 +1,3 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { Injectable, @@ -37,14 +36,9 @@ export class CheckFloorTypeGuard implements CanActivate { async checkFloorIsFloorType(floorUuid: string) { const floorData = await this.spaceRepository.findOne({ where: { uuid: floorUuid }, - relations: ['spaceType'], }); - if ( - !floorData || - !floorData.spaceType || - floorData.spaceType.type !== SpaceType.FLOOR - ) { + if (!floorData) { throw new BadRequestException('Invalid floor UUID'); } } diff --git a/src/guards/room.guard.ts b/src/guards/room.guard.ts index bd63520..e8d0550 100644 --- a/src/guards/room.guard.ts +++ b/src/guards/room.guard.ts @@ -8,7 +8,6 @@ import { import { SpaceRepository } from '@app/common/modules/space/repositories'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; @Injectable() export class CheckRoomGuard implements CanActivate { @@ -43,9 +42,6 @@ export class CheckRoomGuard implements CanActivate { const room = await this.spaceRepository.findOne({ where: { uuid: roomUuid, - spaceType: { - type: SpaceType.ROOM, - }, }, }); if (!room) { diff --git a/src/guards/room.permission.guard.ts b/src/guards/room.permission.guard.ts deleted file mode 100644 index d1e7042..0000000 --- a/src/guards/room.permission.guard.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; -import { - BadRequestException, - CanActivate, - ExecutionContext, - Injectable, -} from '@nestjs/common'; - -@Injectable() -export class RoomPermissionGuard implements CanActivate { - constructor(private readonly permissionService: SpacePermissionService) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { roomUuid } = req.params; - const { user } = req; - - if (!roomUuid) { - throw new BadRequestException('roomUuid is required'); - } - - await this.permissionService.checkUserPermission( - roomUuid, - user.uuid, - SpaceType.ROOM, - ); - - return true; - } catch (error) { - throw error; - } - } -} diff --git a/src/guards/scene.device.type.guard.ts b/src/guards/scene.device.type.guard.ts new file mode 100644 index 0000000..777e60e --- /dev/null +++ b/src/guards/scene.device.type.guard.ts @@ -0,0 +1,42 @@ +import { ProductType } from '@app/common/constants/product-type.enum'; +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, + HttpException, +} from '@nestjs/common'; +import { DeviceService } from 'src/device/services'; + +@Injectable() +export class CheckFourAndSixSceneDeviceTypeGuard implements CanActivate { + constructor(private readonly deviceService: DeviceService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const deviceUuid = request.params.deviceUuid; + + if (!deviceUuid) { + throw new BadRequestException('Device UUID is required'); + } + + try { + const deviceDetails = + await this.deviceService.getDeviceByDeviceUuid(deviceUuid); + + if ( + deviceDetails.productDevice.prodType !== ProductType.FOUR_S && + deviceDetails.productDevice.prodType !== ProductType.SIX_S + ) { + throw new BadRequestException('The device type is not supported'); + } + + return true; + } catch (error) { + throw new HttpException( + error.message || 'An error occurred', + error.status || 500, + ); + } + } +} diff --git a/src/guards/unit.permission.guard.ts b/src/guards/unit.permission.guard.ts index fe2b969..9e91494 100644 --- a/src/guards/unit.permission.guard.ts +++ b/src/guards/unit.permission.guard.ts @@ -1,4 +1,3 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; import { SpacePermissionService } from '@app/common/helper/services/space.permission.service'; import { BadRequestException, @@ -22,11 +21,7 @@ export class UnitPermissionGuard implements CanActivate { throw new BadRequestException('unitUuid is required'); } - await this.permissionService.checkUserPermission( - unitUuid, - user.uuid, - SpaceType.UNIT, - ); + await this.permissionService.checkUserPermission(unitUuid, user.uuid); return true; } catch (error) { diff --git a/src/guards/unit.type.guard.ts b/src/guards/unit.type.guard.ts index a753756..08f0d09 100644 --- a/src/guards/unit.type.guard.ts +++ b/src/guards/unit.type.guard.ts @@ -1,4 +1,3 @@ -import { SpaceType } from '@app/common/constants/space-type.enum'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { Injectable, @@ -34,18 +33,13 @@ export class CheckUnitTypeGuard implements CanActivate { } } - async checkFloorIsFloorType(unitUuid: string) { - const unitData = await this.spaceRepository.findOne({ - where: { uuid: unitUuid }, - relations: ['spaceType'], + async checkFloorIsFloorType(spaceUuid: string) { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, }); - if ( - !unitData || - !unitData.spaceType || - unitData.spaceType.type !== SpaceType.UNIT - ) { - throw new BadRequestException('Invalid unit UUID'); + if (!space) { + throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); } } diff --git a/src/guards/user.building.guard.ts b/src/guards/user.building.guard.ts deleted file mode 100644 index 3f74500..0000000 --- a/src/guards/user.building.guard.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@Injectable() -export class CheckUserBuildingGuard implements CanActivate { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly userRepository: UserRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { userUuid, buildingUuid } = req.body; - - await this.checkUserIsFound(userUuid); - - await this.checkBuildingIsFound(buildingUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkUserIsFound(userUuid: string) { - const userData = await this.userRepository.findOne({ - where: { uuid: userUuid }, - }); - if (!userData) { - throw new NotFoundException('User not found'); - } - } - - private async checkBuildingIsFound(spaceUuid: string) { - const spaceData = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, spaceType: { type: SpaceType.BUILDING } }, - relations: ['spaceType'], - }); - if (!spaceData) { - throw new NotFoundException('Building not found'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - if ( - error instanceof BadRequestException || - error instanceof NotFoundException - ) { - response - .status(HttpStatus.NOT_FOUND) - .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); - } else { - response.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'invalid userUuid or buildingUuid', - }); - } - } -} diff --git a/src/guards/user.community.guard.ts b/src/guards/user.community.guard.ts index e8dea71..d3c1e5a 100644 --- a/src/guards/user.community.guard.ts +++ b/src/guards/user.community.guard.ts @@ -7,7 +7,6 @@ import { import { SpaceRepository } from '@app/common/modules/space/repositories'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { UserRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; @Injectable() export class CheckUserCommunityGuard implements CanActivate { @@ -44,8 +43,7 @@ export class CheckUserCommunityGuard implements CanActivate { private async checkCommunityIsFound(spaceUuid: string) { const spaceData = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, spaceType: { type: SpaceType.COMMUNITY } }, - relations: ['spaceType'], + where: { uuid: spaceUuid }, }); if (!spaceData) { throw new NotFoundException('Community not found'); diff --git a/src/guards/user.floor.guard.ts b/src/guards/user.floor.guard.ts deleted file mode 100644 index 6faa520..0000000 --- a/src/guards/user.floor.guard.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@Injectable() -export class CheckUserFloorGuard implements CanActivate { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly userRepository: UserRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { userUuid, floorUuid } = req.body; - - await this.checkUserIsFound(userUuid); - - await this.checkFloorIsFound(floorUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkUserIsFound(userUuid: string) { - const userData = await this.userRepository.findOne({ - where: { uuid: userUuid }, - }); - if (!userData) { - throw new NotFoundException('User not found'); - } - } - - private async checkFloorIsFound(spaceUuid: string) { - const spaceData = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, spaceType: { type: SpaceType.FLOOR } }, - relations: ['spaceType'], - }); - if (!spaceData) { - throw new NotFoundException('Floor not found'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - if ( - error instanceof BadRequestException || - error instanceof NotFoundException - ) { - response - .status(HttpStatus.NOT_FOUND) - .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); - } else { - response.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'invalid userUuid or floorUuid', - }); - } - } -} diff --git a/src/guards/user.room.guard.ts b/src/guards/user.room.guard.ts deleted file mode 100644 index 49c77b8..0000000 --- a/src/guards/user.room.guard.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@Injectable() -export class CheckUserRoomGuard implements CanActivate { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly userRepository: UserRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { userUuid, roomUuid } = req.body; - - await this.checkUserIsFound(userUuid); - - await this.checkRoomIsFound(roomUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkUserIsFound(userUuid: string) { - const userData = await this.userRepository.findOne({ - where: { uuid: userUuid }, - }); - if (!userData) { - throw new NotFoundException('User not found'); - } - } - - private async checkRoomIsFound(spaceUuid: string) { - const spaceData = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, spaceType: { type: SpaceType.ROOM } }, - relations: ['spaceType'], - }); - if (!spaceData) { - throw new NotFoundException('Room not found'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - if ( - error instanceof BadRequestException || - error instanceof NotFoundException - ) { - response - .status(HttpStatus.NOT_FOUND) - .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); - } else { - response.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'invalid userUuid or roomUuid', - }); - } - } -} diff --git a/src/guards/user.unit.guard.ts b/src/guards/user.unit.guard.ts deleted file mode 100644 index eb60a27..0000000 --- a/src/guards/user.unit.guard.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - HttpStatus, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@Injectable() -export class CheckUserUnitGuard implements CanActivate { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly userRepository: UserRepository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const req = context.switchToHttp().getRequest(); - - try { - const { userUuid, unitUuid } = req.body; - - await this.checkUserIsFound(userUuid); - - await this.checkUnitIsFound(unitUuid); - - return true; - } catch (error) { - this.handleGuardError(error, context); - return false; - } - } - - private async checkUserIsFound(userUuid: string) { - const userData = await this.userRepository.findOne({ - where: { uuid: userUuid }, - }); - if (!userData) { - throw new NotFoundException('User not found'); - } - } - - private async checkUnitIsFound(spaceUuid: string) { - const spaceData = await this.spaceRepository.findOne({ - where: { uuid: spaceUuid, spaceType: { type: SpaceType.UNIT } }, - relations: ['spaceType'], - }); - if (!spaceData) { - throw new NotFoundException('Unit not found'); - } - } - - private handleGuardError(error: Error, context: ExecutionContext) { - const response = context.switchToHttp().getResponse(); - if ( - error instanceof BadRequestException || - error instanceof NotFoundException - ) { - response - .status(HttpStatus.NOT_FOUND) - .json({ statusCode: HttpStatus.NOT_FOUND, message: error.message }); - } else { - response.status(HttpStatus.BAD_REQUEST).json({ - statusCode: HttpStatus.BAD_REQUEST, - message: 'invalid userUuid or unitUuid', - }); - } - } -} diff --git a/src/product/controllers/index.ts b/src/product/controllers/index.ts new file mode 100644 index 0000000..eead553 --- /dev/null +++ b/src/product/controllers/index.ts @@ -0,0 +1 @@ +export * from './product.controller'; diff --git a/src/product/controllers/product.controller.ts b/src/product/controllers/product.controller.ts new file mode 100644 index 0000000..f5fdf53 --- /dev/null +++ b/src/product/controllers/product.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { ProductService } from '../services'; + +@ApiTags('Product Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.PRODUCT.ROUTE, +}) +export class ProductController { + constructor(private readonly productService: ProductService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + @ApiOperation({ + summary: ControllerRoute.PRODUCT.ACTIONS.LIST_PRODUCT_SUMMARY, + description: ControllerRoute.PRODUCT.ACTIONS.LIST_PRODUCT_DESCRIPTION, + }) + async getProducts(): Promise { + return await this.productService.list(); + } +} diff --git a/src/product/index.ts b/src/product/index.ts new file mode 100644 index 0000000..8b3162c --- /dev/null +++ b/src/product/index.ts @@ -0,0 +1 @@ +export * from './product.module'; diff --git a/src/product/product.module.ts b/src/product/product.module.ts new file mode 100644 index 0000000..e5ffd25 --- /dev/null +++ b/src/product/product.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProductService } from './services'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProductController } from './controllers'; + +@Module({ + controllers: [ProductController], + providers: [ProductService, ProductRepository], +}) +export class ProductModule {} diff --git a/src/product/services/index.ts b/src/product/services/index.ts new file mode 100644 index 0000000..59832ef --- /dev/null +++ b/src/product/services/index.ts @@ -0,0 +1 @@ +export * from './product.service'; diff --git a/src/product/services/product.service.ts b/src/product/services/product.service.ts new file mode 100644 index 0000000..cebf9ba --- /dev/null +++ b/src/product/services/product.service.ts @@ -0,0 +1,24 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + +@Injectable() +export class ProductService { + constructor(private readonly productRepository: ProductRepository) {} + + async list(): Promise { + const products = await this.productRepository.find(); + + if (!products) { + throw new HttpException( + `No products found in the system`, + HttpStatus.NOT_FOUND, + ); + } + return new SuccessResponseDto({ + data: products, + message: 'List of products retrieved successfully', + }); + } +} diff --git a/src/role/controllers/role.controller.ts b/src/role/controllers/role.controller.ts index f08adb4..a53cd1c 100644 --- a/src/role/controllers/role.controller.ts +++ b/src/role/controllers/role.controller.ts @@ -6,22 +6,28 @@ import { Post, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { RoleService } from '../services/role.service'; import { AddUserRoleDto } from '../dtos'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; // Assuming this is where the routes are defined @ApiTags('Role Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'role', + path: ControllerRoute.ROLE.ROUTE, // use the static route constant }) export class RoleController { constructor(private readonly roleService: RoleService) {} + @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard) @Get('types') + @ApiOperation({ + summary: ControllerRoute.ROLE.ACTIONS.FETCH_ROLE_TYPES_SUMMARY, + description: ControllerRoute.ROLE.ACTIONS.FETCH_ROLE_TYPES_DESCRIPTION, + }) async fetchRoleTypes() { const roleTypes = await this.roleService.fetchRoleTypes(); return { @@ -30,9 +36,14 @@ export class RoleController { data: roleTypes, }; } + @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard) @Post() + @ApiOperation({ + summary: ControllerRoute.ROLE.ACTIONS.ADD_USER_ROLE_SUMMARY, + description: ControllerRoute.ROLE.ACTIONS.ADD_USER_ROLE_DESCRIPTION, + }) async addUserRoleType(@Body() addUserRoleDto: AddUserRoleDto) { await this.roleService.addUserRoleType(addUserRoleDto); return { diff --git a/src/room/controllers/index.ts b/src/room/controllers/index.ts deleted file mode 100644 index 4225d61..0000000 --- a/src/room/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './room.controller'; diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts deleted file mode 100644 index e3eb687..0000000 --- a/src/room/controllers/room.controller.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { RoomService } from '../services/room.service'; -import { - Body, - Controller, - Get, - HttpStatus, - Param, - Post, - Put, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddRoomDto, AddUserRoomDto } from '../dtos/add.room.dto'; -import { UpdateRoomNameDto } from '../dtos/update.room.dto'; -import { CheckUnitTypeGuard } from 'src/guards/unit.type.guard'; -import { CheckUserRoomGuard } from 'src/guards/user.room.guard'; -import { AdminRoleGuard } from 'src/guards/admin.role.guard'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { RoomPermissionGuard } from 'src/guards/room.permission.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@ApiTags('Room Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.ROOM, -}) -export class RoomController { - constructor(private readonly roomService: RoomService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) - @Post() - async addRoom(@Body() addRoomDto: AddRoomDto) { - const room = await this.roomService.addRoom(addRoomDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Room added successfully', - data: room, - }; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, RoomPermissionGuard) - @Get(':roomUuid') - async getRoomByUuid(@Param('roomUuid') roomUuid: string) { - const room = await this.roomService.getRoomByUuid(roomUuid); - return room; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, RoomPermissionGuard) - @Get('parent/:roomUuid') - async getRoomParentByUuid(@Param('roomUuid') roomUuid: string) { - const room = await this.roomService.getRoomParentByUuid(roomUuid); - return room; - } - @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUserRoomGuard) - @Post('user') - async addUserRoom(@Body() addUserRoomDto: AddUserRoomDto) { - await this.roomService.addUserRoom(addUserRoomDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user room added successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('user/:userUuid') - async getRoomsByUserId(@Param('userUuid') userUuid: string) { - return await this.roomService.getRoomsByUserId(userUuid); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, RoomPermissionGuard) - @Put(':roomUuid') - async renameRoomByUuid( - @Param('roomUuid') roomUuid: string, - @Body() updateRoomNameDto: UpdateRoomNameDto, - ) { - const room = await this.roomService.renameRoomByUuid( - roomUuid, - updateRoomNameDto, - ); - return room; - } -} diff --git a/src/room/dtos/add.room.dto.ts b/src/room/dtos/add.room.dto.ts deleted file mode 100644 index 2718a29..0000000 --- a/src/room/dtos/add.room.dto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class AddRoomDto { - @ApiProperty({ - description: 'roomName', - required: true, - }) - @IsString() - @IsNotEmpty() - public roomName: string; - - @ApiProperty({ - description: 'unitUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} -export class AddUserRoomDto { - @ApiProperty({ - description: 'roomUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public roomUuid: string; - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/room/dtos/index.ts b/src/room/dtos/index.ts deleted file mode 100644 index a510b75..0000000 --- a/src/room/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.room.dto'; diff --git a/src/room/interface/room.interface.ts b/src/room/interface/room.interface.ts deleted file mode 100644 index 49473a3..0000000 --- a/src/room/interface/room.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface GetRoomByUuidInterface { - uuid: string; - createdAt: Date; - updatedAt: Date; - name: string; - type: string; -} - -export interface RoomParentInterface { - uuid: string; - name: string; - type: string; - parent?: RoomParentInterface; -} -export interface RenameRoomByUuidInterface { - uuid: string; - name: string; - type: string; -} -export interface GetRoomByUserUuidInterface { - uuid: string; - name: string; - type: string; -} diff --git a/src/room/room.module.ts b/src/room/room.module.ts deleted file mode 100644 index 4a07d1a..0000000 --- a/src/room/room.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RoomService } from './services/room.service'; -import { RoomController } from './controllers/room.controller'; -import { ConfigModule } from '@nestjs/config'; -import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { SpaceTypeRepository } from '@app/common/modules/space/repositories'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { UserRepository } from '@app/common/modules/user/repositories'; - -@Module({ - imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], - controllers: [RoomController], - providers: [ - RoomService, - SpaceRepository, - SpaceTypeRepository, - UserSpaceRepository, - UserRepository, - ], - exports: [RoomService], -}) -export class RoomModule {} diff --git a/src/room/services/index.ts b/src/room/services/index.ts deleted file mode 100644 index 4f45e9a..0000000 --- a/src/room/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './room.service'; diff --git a/src/room/services/room.service.ts b/src/room/services/room.service.ts deleted file mode 100644 index 340df0f..0000000 --- a/src/room/services/room.service.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddRoomDto, AddUserRoomDto } from '../dtos'; -import { - RoomParentInterface, - GetRoomByUuidInterface, - RenameRoomByUuidInterface, - GetRoomByUserUuidInterface, -} from '../interface/room.interface'; -import { UpdateRoomNameDto } from '../dtos/update.room.dto'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; - -@Injectable() -export class RoomService { - constructor( - private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, - private readonly userSpaceRepository: UserSpaceRepository, - ) {} - - async addRoom(addRoomDto: AddRoomDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.ROOM, - }, - }); - - const room = await this.spaceRepository.save({ - spaceName: addRoomDto.roomName, - parent: { uuid: addRoomDto.unitUuid }, - spaceType: { uuid: spaceType.uuid }, - }); - return room; - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - async getRoomByUuid(roomUuid: string): Promise { - try { - const room = await this.spaceRepository.findOne({ - where: { - uuid: roomUuid, - spaceType: { - type: SpaceType.ROOM, - }, - }, - relations: ['spaceType'], - }); - if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) { - throw new BadRequestException('Invalid room UUID'); - } - - return { - uuid: room.uuid, - createdAt: room.createdAt, - updatedAt: room.updatedAt, - name: room.spaceName, - type: room.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } - } - } - - async getRoomParentByUuid(roomUuid: string): Promise { - try { - const room = await this.spaceRepository.findOne({ - where: { - uuid: roomUuid, - spaceType: { - type: SpaceType.ROOM, - }, - }, - relations: ['spaceType', 'parent', 'parent.spaceType'], - }); - if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) { - throw new BadRequestException('Invalid room UUID'); - } - - return { - uuid: room.uuid, - name: room.spaceName, - type: room.spaceType.type, - parent: { - uuid: room.parent.uuid, - name: room.parent.spaceName, - type: room.parent.spaceType.type, - }, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } - } - } - - async getRoomsByUserId( - userUuid: string, - ): Promise { - try { - const rooms = await this.userSpaceRepository.find({ - relations: ['space', 'space.spaceType'], - where: { - user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.ROOM } }, - }, - }); - - if (rooms.length === 0) { - throw new HttpException('this user has no rooms', HttpStatus.NOT_FOUND); - } - const spaces = rooms.map((room) => ({ - uuid: room.space.uuid, - name: room.space.spaceName, - type: room.space.spaceType.type, - })); - - return spaces; - } catch (err) { - if (err instanceof HttpException) { - throw err; - } else { - throw new HttpException('user not found', HttpStatus.NOT_FOUND); - } - } - } - async addUserRoom(addUserRoomDto: AddUserRoomDto) { - try { - await this.userSpaceRepository.save({ - user: { uuid: addUserRoomDto.userUuid }, - space: { uuid: addUserRoomDto.roomUuid }, - }); - } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { - throw new HttpException( - 'User already belongs to this room', - HttpStatus.BAD_REQUEST, - ); - } - throw new HttpException( - err.message || 'Internal Server Error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async renameRoomByUuid( - roomUuid: string, - updateRoomNameDto: UpdateRoomNameDto, - ): Promise { - try { - const room = await this.spaceRepository.findOneOrFail({ - where: { uuid: roomUuid }, - relations: ['spaceType'], - }); - - if (!room || !room.spaceType || room.spaceType.type !== SpaceType.ROOM) { - throw new BadRequestException('Invalid room UUID'); - } - - await this.spaceRepository.update( - { uuid: roomUuid }, - { spaceName: updateRoomNameDto.roomName }, - ); - - // Fetch the updated room - const updateRoom = await this.spaceRepository.findOneOrFail({ - where: { uuid: roomUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updateRoom.uuid, - name: updateRoom.spaceName, - type: updateRoom.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Room not found', HttpStatus.NOT_FOUND); - } - } - } -} diff --git a/src/scene/controllers/scene.controller.ts b/src/scene/controllers/scene.controller.ts index 4e986e2..eaf67ec 100644 --- a/src/scene/controllers/scene.controller.ts +++ b/src/scene/controllers/scene.controller.ts @@ -8,23 +8,24 @@ import { Param, Post, Put, - Query, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddSceneIconDto, AddSceneTapToRunDto, - GetSceneDto, UpdateSceneTapToRunDto, } from '../dtos/scene.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { SceneParamDto } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Scene Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'scene', + path: ControllerRoute.SCENE.ROUTE, }) export class SceneController { constructor(private readonly sceneService: SceneService) {} @@ -32,80 +33,74 @@ export class SceneController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('tap-to-run') - async addTapToRunScene(@Body() addSceneTapToRunDto: AddSceneTapToRunDto) { - const tapToRunScene = - await this.sceneService.addTapToRunScene(addSceneTapToRunDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Scene added successfully', - data: tapToRunScene, - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('tap-to-run/:unitUuid') - async getTapToRunSceneByUnit( - @Param('unitUuid') unitUuid: string, - @Query() inHomePage: GetSceneDto, - ) { - const tapToRunScenes = await this.sceneService.getTapToRunSceneByUnit( - unitUuid, - inHomePage, - ); - return tapToRunScenes; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Delete('tap-to-run/:unitUuid/:sceneId') - async deleteTapToRunScene( - @Param('unitUuid') unitUuid: string, - @Param('sceneId') sceneId: string, - ) { - await this.sceneService.deleteTapToRunScene(unitUuid, sceneId); - return { - statusCode: HttpStatus.OK, - message: 'Scene Deleted Successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('tap-to-run/trigger/:sceneId') - async triggerTapToRunScene(@Param('sceneId') sceneId: string) { - await this.sceneService.triggerTapToRunScene(sceneId); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Scene trigger successfully', - }; + @ApiOperation({ + summary: ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_SUMMARY, + description: + ControllerRoute.SCENE.ACTIONS.CREATE_TAP_TO_RUN_SCENE_DESCRIPTION, + }) + async addTapToRunScene( + @Body() addSceneTapToRunDto: AddSceneTapToRunDto, + ): Promise { + return await this.sceneService.createScene(addSceneTapToRunDto); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('tap-to-run/details/:sceneId') - async getTapToRunSceneDetails(@Param('sceneId') sceneId: string) { - const tapToRunScenes = - await this.sceneService.getTapToRunSceneDetails(sceneId); - return tapToRunScenes; + @Delete('tap-to-run/:sceneUuid') + @ApiOperation({ + summary: ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_SUMMARY, + description: + ControllerRoute.SCENE.ACTIONS.DELETE_TAP_TO_RUN_SCENE_DESCRIPTION, + }) + async deleteTapToRunScene( + @Param() param: SceneParamDto, + ): Promise { + return await this.sceneService.deleteScene(param); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Put('tap-to-run/:sceneId') + @Post('tap-to-run/:sceneUuid/trigger') + @ApiOperation({ + summary: ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_SUMMARY, + description: + ControllerRoute.SCENE.ACTIONS.TRIGGER_TAP_TO_RUN_SCENE_DESCRIPTION, + }) + async triggerTapToRunScene(@Param() param: SceneParamDto) { + return await this.sceneService.triggerTapToRunScene(param.sceneUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('tap-to-run/:sceneUuid') + @ApiOperation({ + summary: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_SUMMARY, + description: ControllerRoute.SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_DESCRIPTION, + }) + async getTapToRunSceneDetails( + @Param() param: SceneParamDto, + ): Promise { + return await this.sceneService.getSceneByUuid(param.sceneUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('tap-to-run/:sceneUuid') + @ApiOperation({ + summary: ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_SUMMARY, + description: + ControllerRoute.SCENE.ACTIONS.UPDATE_TAP_TO_RUN_SCENE_DESCRIPTION, + }) async updateTapToRunScene( @Body() updateSceneTapToRunDto: UpdateSceneTapToRunDto, - @Param('sceneId') sceneId: string, + @Param() param: SceneParamDto, ) { - const tapToRunScene = await this.sceneService.updateTapToRunScene( + return await this.sceneService.updateTapToRunScene( updateSceneTapToRunDto, - sceneId, + param.sceneUuid, ); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Scene updated successfully', - data: tapToRunScene, - }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('icon') diff --git a/src/scene/dtos/delete.scene.param.dto.ts b/src/scene/dtos/delete.scene.param.dto.ts new file mode 100644 index 0000000..5b71956 --- /dev/null +++ b/src/scene/dtos/delete.scene.param.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class DeleteSceneParamDto { + @ApiProperty({ + description: 'UUID of the Space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceUuid: string; + + @ApiProperty({ + description: 'UUID of the Scene', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + sceneUuid: string; +} diff --git a/src/scene/dtos/index.ts b/src/scene/dtos/index.ts index b1ca9c8..784bd90 100644 --- a/src/scene/dtos/index.ts +++ b/src/scene/dtos/index.ts @@ -1 +1,4 @@ export * from './scene.dto'; +export * from './space.param.dto'; +export * from './scene.param.dto'; +export * from './delete.scene.param.dto'; diff --git a/src/scene/dtos/scene.dto.ts b/src/scene/dtos/scene.dto.ts index 24334e9..1062d90 100644 --- a/src/scene/dtos/scene.dto.ts +++ b/src/scene/dtos/scene.dto.ts @@ -36,7 +36,7 @@ class ExecutorProperty { public delaySeconds?: number; } -class Action { +export class Action { @ApiProperty({ description: 'Entity ID', required: true, @@ -66,12 +66,13 @@ class Action { export class AddSceneTapToRunDto { @ApiProperty({ - description: 'Unit UUID', + description: 'Space UUID', required: true, }) @IsString() @IsNotEmpty() - public unitUuid: string; + public spaceUuid: string; + @ApiProperty({ description: 'Icon UUID', required: false, diff --git a/src/scene/dtos/scene.param.dto.ts b/src/scene/dtos/scene.param.dto.ts new file mode 100644 index 0000000..1163755 --- /dev/null +++ b/src/scene/dtos/scene.param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class SceneParamDto { + @ApiProperty({ + description: 'UUID of the Scene', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + sceneUuid: string; +} diff --git a/src/scene/dtos/space.param.dto.ts b/src/scene/dtos/space.param.dto.ts new file mode 100644 index 0000000..0631ea1 --- /dev/null +++ b/src/scene/dtos/space.param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class SpaceParamDto { + @ApiProperty({ + description: 'UUID of the space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceUuid: string; +} diff --git a/src/scene/interface/scene.interface.ts b/src/scene/interface/scene.interface.ts index e6508d9..c08c204 100644 --- a/src/scene/interface/scene.interface.ts +++ b/src/scene/interface/scene.interface.ts @@ -1,3 +1,5 @@ +import { Action } from '../dtos'; + export interface AddTapToRunSceneInterface { success: boolean; msg?: string; @@ -25,4 +27,19 @@ export interface SceneDetailsResult { id: string; name: string; type: string; + actions?: any; + status?: string; +} + +export interface SceneDetails { + uuid: string; + sceneTuyaId: string; + name: string; + status: string; + icon?: string; + iconUuid?: string; + showInHome: boolean; + type: string; + actions: Action[]; + spaceId: string; } diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index cdbf2d4..9da0156 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -12,6 +12,8 @@ import { SceneIconRepository, SceneRepository, } from '@app/common/modules/scene/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -20,10 +22,12 @@ import { SceneService, SpaceRepository, DeviceService, + TuyaService, DeviceRepository, ProductRepository, SceneIconRepository, SceneRepository, + SceneDeviceRepository, ], exports: [SceneService], }) diff --git a/src/scene/services/scene.service.ts b/src/scene/services/scene.service.ts index 8a2107e..15f1b23 100644 --- a/src/scene/services/scene.service.ts +++ b/src/scene/services/scene.service.ts @@ -3,193 +3,266 @@ import { HttpException, HttpStatus, BadRequestException, + forwardRef, + Inject, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { + Action, AddSceneIconDto, AddSceneTapToRunDto, GetSceneDto, + SceneParamDto, UpdateSceneTapToRunDto, } from '../dtos'; -import { GetUnitByUuidInterface } from 'src/unit/interface/unit.interface'; -import { ConfigService } from '@nestjs/config'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; -import { DeviceService } from 'src/device/services'; import { AddTapToRunSceneInterface, DeleteTapToRunSceneInterface, + SceneDetails, SceneDetailsResult, } from '../interface/scene.interface'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { SpaceType } from '@app/common/constants/space-type.enum'; import { ActionExecutorEnum } from '@app/common/constants/automation.enum'; import { SceneIconRepository, SceneRepository, } from '@app/common/modules/scene/repositories'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; +import { + SceneEntity, + SceneIconEntity, +} from '@app/common/modules/scene/entities'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { HttpStatusCode } from 'axios'; +import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; +import { DeviceService } from 'src/device/services'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Injectable() export class SceneService { - private tuya: TuyaContext; constructor( - private readonly configService: ConfigService, private readonly spaceRepository: SpaceRepository, private readonly sceneIconRepository: SceneIconRepository, private readonly sceneRepository: SceneRepository, + private readonly sceneDeviceRepository: SceneDeviceRepository, + private readonly tuyaService: TuyaService, + @Inject(forwardRef(() => DeviceService)) private readonly deviceService: DeviceService, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); - this.tuya = new TuyaContext({ - baseUrl: tuyaEuUrl, - accessKey, - secretKey, - }); + ) {} + + async createScene( + addSceneTapToRunDto: AddSceneTapToRunDto, + ): Promise { + try { + const { spaceUuid } = addSceneTapToRunDto; + + const space = await this.getSpaceByUuid(spaceUuid); + + const scene = await this.create(space.spaceTuyaUuid, addSceneTapToRunDto); + + return new SuccessResponseDto({ + message: `Successfully created new scene with uuid ${scene.uuid}`, + data: scene, + statusCode: HttpStatus.CREATED, + }); + } catch (err) { + console.error( + `Error in createScene for space UUID ${addSceneTapToRunDto.spaceUuid}:`, + err.message, + ); + throw err instanceof HttpException + ? err + : new HttpException( + 'Failed to create scene', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } - async addTapToRunScene( + async create( + spaceTuyaUuid: string, addSceneTapToRunDto: AddSceneTapToRunDto, - spaceTuyaId = null, - unitUuid?: string, - ) { + ): Promise { + const { iconUuid, showInHomePage, spaceUuid } = addSceneTapToRunDto; + try { - let unitSpaceTuyaId; - if (!spaceTuyaId) { - const unitDetails = await this.getUnitByUuid( - addSceneTapToRunDto.unitUuid, + const [defaultSceneIcon] = await Promise.all([ + this.getDefaultSceneIcon(), + ]); + if (!defaultSceneIcon) { + throw new HttpException( + 'Default scene icon not found', + HttpStatus.INTERNAL_SERVER_ERROR, ); - unitSpaceTuyaId = unitDetails.spaceTuyaUuid; - if (!unitDetails) { - throw new BadRequestException('Invalid unit UUID'); - } - } else { - unitSpaceTuyaId = spaceTuyaId; } - const actions = addSceneTapToRunDto.actions.map((action) => { - return { - ...action, - }; + const response = await this.createSceneExternalService( + spaceTuyaUuid, + addSceneTapToRunDto, + ); + + const scene = await this.sceneRepository.save({ + sceneTuyaUuid: response.result.id, + sceneIcon: { uuid: iconUuid || defaultSceneIcon.uuid }, + showInHomePage, + space: { uuid: spaceUuid }, }); - const convertedData = convertKeysToSnakeCase(actions); - for (const action of convertedData) { - if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { - const device = await this.deviceService.getDeviceByDeviceUuid( - action.entity_id, - false, - ); - if (device) { - action.entity_id = device.deviceTuyaUuid; - } - } - } - const path = `/v2.0/cloud/scene/rule`; - const response: AddTapToRunSceneInterface = await this.tuya.request({ - method: 'POST', - path, - body: { - space_id: unitSpaceTuyaId, - name: addSceneTapToRunDto.sceneName, - type: 'scene', - decision_expr: addSceneTapToRunDto.decisionExpr, - actions: convertedData, - }, - }); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } else { - const defaultSceneIcon = await this.sceneIconRepository.findOne({ - where: { iconType: SceneIconType.Default }, - }); - - await this.sceneRepository.save({ - sceneTuyaUuid: response.result.id, - sceneIcon: { - uuid: addSceneTapToRunDto.iconUuid - ? addSceneTapToRunDto.iconUuid - : defaultSceneIcon.uuid, - }, - showInHomePage: addSceneTapToRunDto.showInHomePage, - unitUuid: unitUuid ? unitUuid : addSceneTapToRunDto.unitUuid, - }); - } - return { - id: response.result.id, - }; + return scene; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + if (err instanceof HttpException) { + throw err; + } else if (err.message?.includes('tuya')) { + throw new HttpException( + 'API error: Failed to create scene', + HttpStatus.BAD_GATEWAY, + ); } else { throw new HttpException( - err.message || 'Scene not found', - err.status || HttpStatus.NOT_FOUND, + 'Database error: Failed to save scene', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } } - async getUnitByUuid(unitUuid: string): Promise { + async updateSceneExternalService( + spaceTuyaUuid: string, + sceneTuyaUuid: string, + updateSceneTapToRunDto: UpdateSceneTapToRunDto, + ) { + const { sceneName, decisionExpr, actions } = updateSceneTapToRunDto; try { - const unit = await this.spaceRepository.findOne({ - where: { - uuid: unitUuid, - spaceType: { - type: SpaceType.UNIT, - }, - }, - relations: ['spaceType'], - }); - if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) { - throw new BadRequestException('Invalid unit UUID'); + const formattedActions = await this.prepareActions(actions); + + const response = (await this.tuyaService.updateTapToRunScene( + sceneTuyaUuid, + spaceTuyaUuid, + sceneName, + formattedActions, + decisionExpr, + )) as AddTapToRunSceneInterface; + + if (!response.success) { + throw new HttpException( + 'Failed to update scene in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); } - return { - uuid: unit.uuid, - createdAt: unit.createdAt, - updatedAt: unit.updatedAt, - name: unit.spaceName, - type: unit.spaceType.type, - spaceTuyaUuid: unit.spaceTuyaUuid, - }; + + return response; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + if (err instanceof HttpException) { + throw err; + } else if (err.message?.includes('tuya')) { + throw new HttpException( + 'API error: Failed to update scene', + HttpStatus.BAD_GATEWAY, + ); } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + throw new HttpException( + `An Internal error has been occured ${err}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } - async getTapToRunSceneByUnit(unitUuid: string, inHomePage: GetSceneDto) { + + async createSceneExternalService( + spaceTuyaUuid: string, + addSceneTapToRunDto: AddSceneTapToRunDto, + ) { + const { sceneName, decisionExpr, actions } = addSceneTapToRunDto; try { - const showInHomePage = inHomePage?.showInHomePage; + const formattedActions = await this.prepareActions(actions); + + const response = (await this.tuyaService.addTapToRunScene( + spaceTuyaUuid, + sceneName, + formattedActions, + decisionExpr, + )) as AddTapToRunSceneInterface; + + if (!response.result?.id) { + throw new HttpException( + 'Failed to create scene in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return response; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else if (err.message?.includes('tuya')) { + throw new HttpException( + 'API error: Failed to create scene', + HttpStatus.BAD_GATEWAY, + ); + } else { + throw new HttpException( + `An Internal error has been occured ${err}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async findScenesBySpace(spaceUuid: string, filter: GetSceneDto) { + try { + await this.getSpaceByUuid(spaceUuid); + const showInHomePage = filter?.showInHomePage; const scenesData = await this.sceneRepository.find({ where: { - unitUuid, + space: { uuid: spaceUuid }, ...(showInHomePage ? { showInHomePage } : {}), }, + relations: ['sceneIcon', 'space'], }); const scenes = await Promise.all( scenesData.map(async (scene) => { - const sceneData = await this.getTapToRunSceneDetails( - scene.sceneTuyaUuid, - false, - ); // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { actions, ...rest } = sceneData; - return { - ...rest, - }; + const { actions, ...sceneDetails } = await this.getScene( + scene, + spaceUuid, + ); + + return sceneDetails; }), ); return scenes; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + console.error( + `Error fetching Tap-to-Run scenes for space UUID ${spaceUuid}:`, + err.message, + ); + + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + 'An error occurred while retrieving scenes', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async triggerTapToRunScene(sceneUuid: string) { + try { + const scene = await this.findScene(sceneUuid); + await this.tuyaService.triggerScene(scene.sceneTuyaUuid); + return new SuccessResponseDto({ + message: `Scene with ID ${sceneUuid} triggered successfully`, + }); + } catch (err) { + if (err instanceof HttpException) { + throw err; } else { throw new HttpException( err.message || 'Scene not found', @@ -198,215 +271,100 @@ export class SceneService { } } } - async deleteTapToRunScene( - unitUuid: string, - sceneId: string, - spaceTuyaId = null, - ) { - try { - let unitSpaceTuyaId; - if (!spaceTuyaId) { - const unitDetails = await this.getUnitByUuid(unitUuid); - unitSpaceTuyaId = unitDetails.spaceTuyaUuid; - if (!unitSpaceTuyaId) { - throw new BadRequestException('Invalid unit UUID'); - } - } else { - unitSpaceTuyaId = spaceTuyaId; - } - const path = `/v2.0/cloud/scene/rule?ids=${sceneId}&space_id=${unitSpaceTuyaId}`; - const response: DeleteTapToRunSceneInterface = await this.tuya.request({ - method: 'DELETE', - path, - }); - - if (!response.success) { - throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); - } else { - await this.sceneRepository.delete({ sceneTuyaUuid: sceneId }); - } - - return response; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException( - err.message || 'Scene not found', - err.status || HttpStatus.NOT_FOUND, - ); - } - } - } - async triggerTapToRunScene(sceneId: string) { - try { - const path = `/v2.0/cloud/scene/rule/${sceneId}/actions/trigger`; - const response: DeleteTapToRunSceneInterface = await this.tuya.request({ - method: 'POST', - path, - }); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - return response; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException( - err.message || 'Scene not found', - err.status || HttpStatus.NOT_FOUND, - ); - } - } - } - async getTapToRunSceneDetails(sceneId: string, withSpaceId = false) { - try { - const path = `/v2.0/cloud/scene/rule/${sceneId}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } - const responseData = convertKeysToCamelCase(response.result); - const actions = responseData.actions.map((action) => { - return { - ...action, - }; - }); - - for (const action of actions) { - if (action.actionExecutor === ActionExecutorEnum.DEVICE_ISSUE) { - const device = await this.deviceService.getDeviceByDeviceTuyaUuid( - action.entityId, - ); - - if (device) { - action.entityId = device.uuid; - } - } else if ( - action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE && - action.actionExecutor !== ActionExecutorEnum.DELAY - ) { - const sceneDetails = await this.getTapToRunSceneDetailsTuya( - action.entityId, - ); - - if (sceneDetails.id) { - action.name = sceneDetails.name; - action.type = sceneDetails.type; - } - } - } - const scene = await this.sceneRepository.findOne({ - where: { sceneTuyaUuid: sceneId }, - relations: ['sceneIcon'], - }); - return { - id: responseData.id, - name: responseData.name, - status: responseData.status, - icon: scene.sceneIcon?.icon, - iconUuid: scene.sceneIcon?.uuid, - showInHome: scene.showInHomePage, - type: 'tap_to_run', - actions: actions, - ...(withSpaceId && { spaceId: responseData.spaceId }), - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Scene not found', HttpStatus.NOT_FOUND); - } - } - } - async getTapToRunSceneDetailsTuya( + async fetchSceneDetailsFromTuya( sceneId: string, ): Promise { try { - const path = `/v2.0/cloud/scene/rule/${sceneId}`; - const response = await this.tuya.request({ - method: 'GET', - path, - }); - - if (!response.success) { - throw new HttpException(response.msg, HttpStatus.BAD_REQUEST); - } + const response = await this.tuyaService.getSceneRule(sceneId); const camelCaseResponse = convertKeysToCamelCase(response); - const { id, name, type } = camelCaseResponse.result; + const { + id, + name, + type, + status, + actions: tuyaActions = [], + } = camelCaseResponse.result; + + const actions = tuyaActions.map((action) => ({ ...action })); return { id, name, type, + status, + actions, } as SceneDetailsResult; } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException + console.error( + `Error fetching scene details for scene ID ${sceneId}:`, + err, + ); + if (err instanceof HttpException) { + throw err; } else { throw new HttpException( - 'Scene not found for Tuya', - HttpStatus.NOT_FOUND, + 'An error occurred while fetching scene details from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, ); } } } + async updateTapToRunScene( updateSceneTapToRunDto: UpdateSceneTapToRunDto, - sceneId: string, + sceneUuid: string, ) { try { - const spaceTuyaId = await this.getTapToRunSceneDetails(sceneId, true); - if (!spaceTuyaId.spaceId) { - throw new HttpException("Scene doesn't exist", HttpStatus.NOT_FOUND); - } + const scene = await this.findScene(sceneUuid); + const space = await this.getSpaceByUuid(scene.space.uuid); + const addSceneTapToRunDto: AddSceneTapToRunDto = { ...updateSceneTapToRunDto, - unitUuid: null, + spaceUuid: scene.space.uuid, iconUuid: updateSceneTapToRunDto.iconUuid, showInHomePage: updateSceneTapToRunDto.showInHomePage, }; - const scene = await this.sceneRepository.findOne({ - where: { sceneTuyaUuid: sceneId }, - }); - const newTapToRunScene = await this.addTapToRunScene( + const updateTuyaSceneResponse = await this.updateSceneExternalService( + space.spaceTuyaUuid, + scene.sceneTuyaUuid, addSceneTapToRunDto, - spaceTuyaId.spaceId, - scene.unitUuid, ); - if (newTapToRunScene.id) { - await this.deleteTapToRunScene(null, sceneId, spaceTuyaId.spaceId); - await this.sceneRepository.update( - { sceneTuyaUuid: sceneId }, - { - sceneTuyaUuid: newTapToRunScene.id, - showInHomePage: addSceneTapToRunDto.showInHomePage, - sceneIcon: { - uuid: addSceneTapToRunDto.iconUuid, - }, - unitUuid: scene.unitUuid, - }, + if (!updateTuyaSceneResponse.success) { + throw new HttpException( + `Failed to update a external scene`, + HttpStatus.BAD_GATEWAY, ); - return newTapToRunScene; } + + const updatedScene = await this.sceneRepository.update( + { uuid: sceneUuid }, + { + showInHomePage: addSceneTapToRunDto.showInHomePage, + sceneIcon: { + uuid: addSceneTapToRunDto.iconUuid, + }, + space: { uuid: scene.space.uuid }, + }, + ); + return new SuccessResponseDto({ + data: updatedScene, + message: `Scene with ID ${sceneUuid} updated successfully`, + }); } catch (err) { - if (err instanceof BadRequestException) { + if (err instanceof HttpException) { throw err; // Re-throw BadRequestException } else { throw new HttpException( - err.message || 'Scene not found', + err.message || `Scene not found for id ${sceneUuid}`, err.status || HttpStatus.NOT_FOUND, ); } } } + async addSceneIcon(addSceneIconDto: AddSceneIconDto) { try { const icon = await this.sceneIconRepository.save({ @@ -424,6 +382,7 @@ export class SceneService { ); } } + async getAllIcons() { try { const icons = await this.sceneIconRepository.find({ @@ -449,4 +408,215 @@ export class SceneService { ); } } + + async getSceneByUuid(sceneUuid: string): Promise { + try { + const scene = await this.findScene(sceneUuid); + const sceneDetails = await this.getScene(scene, sceneUuid); + + return new SuccessResponseDto({ + data: sceneDetails, + message: `Scene details for ${sceneUuid} retrieved successfully`, + }); + } catch (error) { + console.error( + `Error fetching scene details for sceneUuid ${sceneUuid}:`, + error, + ); + + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'An error occurred while retrieving scene details', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async getScene(scene: SceneEntity, sceneUuid: string): Promise { + try { + const { actions, name, status } = await this.fetchSceneDetailsFromTuya( + scene.sceneTuyaUuid, + ); + + for (const action of actions) { + if (action.actionExecutor === ActionExecutorEnum.DEVICE_ISSUE) { + const device = await this.deviceService.getDeviceByDeviceTuyaUuid( + action.entityId, + ); + + if (device) { + action.entityId = device.uuid; + action.productUuid = device.productDevice.uuid; + action.productType = device.productDevice.prodType; + } + } else if ( + action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE && + action.actionExecutor !== ActionExecutorEnum.DELAY + ) { + const sceneDetails = await this.fetchSceneDetailsFromTuya( + action.entityId, + ); + + if (sceneDetails.id) { + action.name = sceneDetails.name; + action.type = sceneDetails.type; + } + } + } + return { + uuid: scene.uuid, + sceneTuyaId: scene.sceneTuyaUuid, + name, + status, + icon: scene.sceneIcon?.icon, + iconUuid: scene.sceneIcon?.uuid, + showInHome: scene.showInHomePage, + type: 'tap_to_run', + actions, + spaceId: sceneUuid, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException( + `An error occurred while retrieving scene details for ${scene.uuid}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async deleteScene(params: SceneParamDto): Promise { + const { sceneUuid } = params; + try { + const scene = await this.findScene(sceneUuid); + const space = await this.getSpaceByUuid(scene.space.uuid); + + await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); + await this.sceneDeviceRepository.delete({ + scene: { uuid: sceneUuid }, + }); + await this.sceneRepository.delete({ + uuid: sceneUuid, + }); + return new SuccessResponseDto({ + message: `Scene with ID ${sceneUuid} deleted successfully`, + }); + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + err.message || `Scene not found for id ${params.sceneUuid}`, + err.status || HttpStatus.NOT_FOUND, + ); + } + } + } + + async findScene(sceneUuid: string): Promise { + const scene = await this.sceneRepository.findOne({ + where: { uuid: sceneUuid }, + relations: ['sceneIcon', 'space'], + }); + + if (!scene) { + throw new HttpException( + `Invalid scene with id ${sceneUuid}`, + HttpStatus.NOT_FOUND, + ); + } + return scene; + } + + async delete(tuyaSceneId: string, tuyaSpaceId: string) { + try { + const response = (await this.tuyaService.deleteSceneRule( + tuyaSceneId, + tuyaSpaceId, + )) as DeleteTapToRunSceneInterface; + return response; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'Failed to delete scene rule in Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + private async prepareActions(actions: Action[]): Promise { + const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[]; + + await Promise.all( + convertedData.map(async (action) => { + if (action.action_executor === ActionExecutorEnum.DEVICE_ISSUE) { + const device = await this.deviceService.getDeviceByDeviceUuid( + action.entity_id, + false, + ); + if (device) { + action.entity_id = device.deviceTuyaUuid; + } + } + }), + ); + + return convertedData; + } + + private async getDefaultSceneIcon(): Promise { + const defaultIcon = await this.sceneIconRepository.findOne({ + where: { iconType: SceneIconType.Default }, + }); + return defaultIcon; + } + + async getSpaceByUuid(spaceUuid: string) { + try { + const space = await this.spaceRepository.findOne({ + where: { + uuid: spaceUuid, + }, + relations: ['community'], + }); + + if (!space) { + throw new HttpException( + `Invalid space UUID ${spaceUuid}`, + HttpStatusCode.BadRequest, + ); + } + + if (!space.community.externalId) { + throw new HttpException( + `Space doesn't have any association with tuya${spaceUuid}`, + HttpStatusCode.BadRequest, + ); + } + return { + uuid: space.uuid, + createdAt: space.createdAt, + updatedAt: space.updatedAt, + name: space.spaceName, + spaceTuyaUuid: space.community.externalId, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException( + `Space with id ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + } + } } diff --git a/src/schedule/controllers/schedule.controller.ts b/src/schedule/controllers/schedule.controller.ts index 82676bf..d4c7a91 100644 --- a/src/schedule/controllers/schedule.controller.ts +++ b/src/schedule/controllers/schedule.controller.ts @@ -11,7 +11,7 @@ import { Delete, Query, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddScheduleDto, EnableScheduleDto, @@ -21,17 +21,24 @@ import { import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Schedule Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'schedule', + path: ControllerRoute.SCHEDULE.ROUTE, }) export class ScheduleController { constructor(private readonly scheduleService: ScheduleService) {} + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post(':deviceUuid') + @ApiOperation({ + summary: ControllerRoute.SCHEDULE.ACTIONS.ADD_DEVICE_SCHEDULE_SUMMARY, + description: + ControllerRoute.SCHEDULE.ACTIONS.ADD_DEVICE_SCHEDULE_DESCRIPTION, + }) async addDeviceSchedule( @Param('deviceUuid') deviceUuid: string, @Body() addScheduleDto: AddScheduleDto, @@ -44,13 +51,21 @@ export class ScheduleController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'schedule added successfully', + message: 'Schedule added successfully', data: schedule, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':deviceUuid') + @ApiOperation({ + summary: + ControllerRoute.SCHEDULE.ACTIONS.GET_DEVICE_SCHEDULE_BY_CATEGORY_SUMMARY, + description: + ControllerRoute.SCHEDULE.ACTIONS + .GET_DEVICE_SCHEDULE_BY_CATEGORY_DESCRIPTION, + }) async getDeviceScheduleByCategory( @Param('deviceUuid') deviceUuid: string, @Query() query: GetScheduleDeviceDto, @@ -60,9 +75,15 @@ export class ScheduleController { query.category, ); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Delete(':deviceUuid/:scheduleId') + @ApiOperation({ + summary: ControllerRoute.SCHEDULE.ACTIONS.DELETE_DEVICE_SCHEDULE_SUMMARY, + description: + ControllerRoute.SCHEDULE.ACTIONS.DELETE_DEVICE_SCHEDULE_DESCRIPTION, + }) async deleteDeviceSchedule( @Param('deviceUuid') deviceUuid: string, @Param('scheduleId') scheduleId: string, @@ -71,12 +92,18 @@ export class ScheduleController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'schedule deleted successfully', + message: 'Schedule deleted successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('enable/:deviceUuid') + @ApiOperation({ + summary: ControllerRoute.SCHEDULE.ACTIONS.ENABLE_DEVICE_SCHEDULE_SUMMARY, + description: + ControllerRoute.SCHEDULE.ACTIONS.ENABLE_DEVICE_SCHEDULE_DESCRIPTION, + }) async enableDeviceSchedule( @Param('deviceUuid') deviceUuid: string, @Body() enableScheduleDto: EnableScheduleDto, @@ -88,12 +115,18 @@ export class ScheduleController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'schedule updated successfully', + message: 'Schedule updated successfully', }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put(':deviceUuid') + @ApiOperation({ + summary: ControllerRoute.SCHEDULE.ACTIONS.UPDATE_DEVICE_SCHEDULE_SUMMARY, + description: + ControllerRoute.SCHEDULE.ACTIONS.UPDATE_DEVICE_SCHEDULE_DESCRIPTION, + }) async updateDeviceSchedule( @Param('deviceUuid') deviceUuid: string, @Body() updateScheduleDto: UpdateScheduleDto, @@ -106,7 +139,7 @@ export class ScheduleController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'schedule updated successfully', + message: 'Schedule updated successfully', data: schedule, }; } diff --git a/src/space/controllers/index.ts b/src/space/controllers/index.ts new file mode 100644 index 0000000..f9587fb --- /dev/null +++ b/src/space/controllers/index.ts @@ -0,0 +1,5 @@ +export * from './space.controller'; +export * from './space-user.controller'; +export * from './space-device.controller'; +export * from './space-scene.controller'; +export * from './subspace'; diff --git a/src/space/controllers/space-device.controller.ts b/src/space/controllers/space-device.controller.ts new file mode 100644 index 0000000..160a130 --- /dev/null +++ b/src/space/controllers/space-device.controller.ts @@ -0,0 +1,30 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { GetSpaceParam } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SpaceDeviceService } from '../services'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SPACE_DEVICES.ROUTE, +}) +export class SpaceDeviceController { + constructor(private readonly spaceDeviceService: SpaceDeviceService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE_DEVICES.ACTIONS.LIST_SPACE_DEVICE_SUMMARY, + description: + ControllerRoute.SPACE_DEVICES.ACTIONS.LIST_SPACE_DEVICE_DESCRIPTION, + }) + @Get() + async listDevicesInSpace( + @Param() params: GetSpaceParam, + ): Promise { + return await this.spaceDeviceService.listDevicesInSpace(params); + } +} diff --git a/src/space/controllers/space-scene.controller.ts b/src/space/controllers/space-scene.controller.ts new file mode 100644 index 0000000..5517362 --- /dev/null +++ b/src/space/controllers/space-scene.controller.ts @@ -0,0 +1,34 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { SpaceSceneService } from '../services'; +import { GetSceneDto } from '../../scene/dtos'; +import { GetSpaceParam } from '../dtos'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SPACE_SCENE.ROUTE, +}) +export class SpaceSceneController { + constructor(private readonly sceneService: SpaceSceneService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SPACE_SCENE.ACTIONS.GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY, + description: + ControllerRoute.SPACE_SCENE.ACTIONS + .GET_TAP_TO_RUN_SCENE_BY_SPACE_DESCRIPTION, + }) + @Get() + async getTapToRunSceneByUnit( + @Param() params: GetSpaceParam, + @Query() inHomePage: GetSceneDto, + ): Promise { + return await this.sceneService.getScenes(params, inHomePage); + } +} diff --git a/src/space/controllers/space-user.controller.ts b/src/space/controllers/space-user.controller.ts new file mode 100644 index 0000000..89fa3a0 --- /dev/null +++ b/src/space/controllers/space-user.controller.ts @@ -0,0 +1,51 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { Controller, Delete, Param, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { SpaceUserService } from '../services'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { UserSpaceParam } from '../dtos'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SPACE_USER.ROUTE, +}) +export class SpaceUserController { + constructor(private readonly spaceUserService: SpaceUserService) {} + + @ApiBearerAuth() + @Post('/:userUuid') + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION, + description: + ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION, + }) + async associateUserToSpace( + @Param() params: UserSpaceParam, + ): Promise { + return this.spaceUserService.associateUserToSpace( + params.userUuid, + params.spaceUuid, + ); + } + + @ApiBearerAuth() + @Delete('/:userUuid') + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_SUMMARY, + description: + ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_DESCRIPTION, + }) + async disassociateUserFromSpace( + @Param() params: UserSpaceParam, + ): Promise { + return this.spaceUserService.disassociateUserFromSpace( + params.userUuid, + params.spaceUuid, + ); + } +} diff --git a/src/space/controllers/space.controller.ts b/src/space/controllers/space.controller.ts new file mode 100644 index 0000000..e0615eb --- /dev/null +++ b/src/space/controllers/space.controller.ts @@ -0,0 +1,128 @@ +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { SpaceService } from '../services'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + UseGuards, +} from '@nestjs/common'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { AddSpaceDto, CommunitySpaceParam } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { GetSpaceParam } from '../dtos/get.space.param'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SPACE.ROUTE, +}) +export class SpaceController { + constructor(private readonly spaceService: SpaceService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.CREATE_SPACE_SUMMARY, + description: ControllerRoute.SPACE.ACTIONS.CREATE_SPACE_DESCRIPTION, + }) + @Post() + async createSpace( + @Body() addSpaceDto: AddSpaceDto, + @Param() communitySpaceParam: CommunitySpaceParam, + ): Promise { + return await this.spaceService.createSpace( + addSpaceDto, + communitySpaceParam.communityUuid, + ); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SPACE.ACTIONS.GET_COMMUNITY_SPACES_HIERARCHY_SUMMARY, + description: + ControllerRoute.SPACE.ACTIONS.GET_COMMUNITY_SPACES_HIERARCHY_DESCRIPTION, + }) + @Get() + async getHierarchy( + @Param() param: CommunitySpaceParam, + ): Promise { + return this.spaceService.getSpacesHierarchyForCommunity( + param.communityUuid, + ); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_SUMMARY, + description: ControllerRoute.SPACE.ACTIONS.DELETE_SPACE_DESCRIPTION, + }) + @Delete('/:spaceUuid') + async deleteSpace(@Param() params: GetSpaceParam): Promise { + return this.spaceService.delete(params.spaceUuid, params.communityUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put('/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.UPDATE_SPACE_SUMMARY, + description: ControllerRoute.SPACE.ACTIONS.UPDATE_SPACE_SUMMARY, + }) + async updateSpace( + @Param() params: GetSpaceParam, + @Body() updateSpaceDto: AddSpaceDto, + ): Promise { + return this.spaceService.updateSpace( + params.spaceUuid, + params.communityUuid, + updateSpaceDto, + ); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.GET_SPACE_SUMMARY, + description: ControllerRoute.SPACE.ACTIONS.GET_SPACE_DESCRIPTION, + }) + @Get('/:spaceUuid') + async get(@Param() params: GetSpaceParam): Promise { + return this.spaceService.findOne(params.spaceUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_SUMMARY, + description: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_DESCRIPTION, + }) + @Get('/:spaceUuid/hierarchy') + async getHierarchyUnderSpace( + @Param() params: GetSpaceParam, + ): Promise { + return this.spaceService.getSpacesHierarchyForSpace(params.spaceUuid); + } + + //should it be post? + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_SUMMARY, + description: + ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_DESCRIPTION, + }) + @Get(':spaceUuid/invitation-code') + async generateSpaceInvitationCode( + @Param() params: GetSpaceParam, + ): Promise { + return this.spaceService.getSpaceInvitationCode(params.spaceUuid); + } +} diff --git a/src/space/controllers/subspace/index.ts b/src/space/controllers/subspace/index.ts new file mode 100644 index 0000000..de80b58 --- /dev/null +++ b/src/space/controllers/subspace/index.ts @@ -0,0 +1,2 @@ +export * from './subspace.controller'; +export * from './subspace-device.controller'; diff --git a/src/space/controllers/subspace/subspace-device.controller.ts b/src/space/controllers/subspace/subspace-device.controller.ts new file mode 100644 index 0000000..189d490 --- /dev/null +++ b/src/space/controllers/subspace/subspace-device.controller.ts @@ -0,0 +1,73 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { + Controller, + Delete, + Get, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; +import { SubspaceDeviceService } from 'src/space/services'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SUBSPACE_DEVICE.ROUTE, +}) +export class SubSpaceDeviceController { + constructor(private readonly subspaceDeviceService: SubspaceDeviceService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS.LIST_SUBSPACE_DEVICE_SUMMARY, + description: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS.LIST_SUBSPACE_DEVICE_DESCRIPTION, + }) + @Get() + async listDevicesInSubspace( + @Param() params: GetSubSpaceParam, + ): Promise { + return await this.subspaceDeviceService.listDevicesInSubspace(params); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS.ASSOCIATE_SUBSPACE_DEVICE_SUMMARY, + description: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS + .ASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION, + }) + @Post('/:deviceUuid') + async associateDeviceToSubspace( + @Param() params: DeviceSubSpaceParam, + ): Promise { + return await this.subspaceDeviceService.associateDeviceToSubspace(params); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS + .DISASSOCIATE_SUBSPACE_DEVICE_SUMMARY, + description: + ControllerRoute.SUBSPACE_DEVICE.ACTIONS + .DISASSOCIATE_SUBSPACE_DEVICE_DESCRIPTION, + }) + @Delete('/:deviceUuid') + async disassociateDeviceFromSubspace( + @Param() params: DeviceSubSpaceParam, + ): Promise { + return await this.subspaceDeviceService.disassociateDeviceFromSubspace( + params, + ); + } +} diff --git a/src/space/controllers/subspace/subspace.controller.ts b/src/space/controllers/subspace/subspace.controller.ts new file mode 100644 index 0000000..9f766c4 --- /dev/null +++ b/src/space/controllers/subspace/subspace.controller.ts @@ -0,0 +1,91 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { SubSpaceService } from '../../services'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; + +@ApiTags('Space Module') +@Controller({ + version: '1', + path: ControllerRoute.SUBSPACE.ROUTE, +}) +export class SubSpaceController { + constructor(private readonly subSpaceService: SubSpaceService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + @ApiOperation({ + summary: ControllerRoute.SUBSPACE.ACTIONS.CREATE_SUBSPACE_SUMMARY, + description: ControllerRoute.SUBSPACE.ACTIONS.CREATE_SUBSPACE_DESCRIPTION, + }) + async createSubspace( + @Param() params: GetSpaceParam, + @Body() addSubspaceDto: AddSubspaceDto, + ): Promise { + return this.subSpaceService.createSubspace(addSubspaceDto, params); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SUBSPACE.ACTIONS.LIST_SUBSPACES_SUMMARY, + description: ControllerRoute.SUBSPACE.ACTIONS.LIST_SUBSPACES_DESCRIPTION, + }) + @Get() + async list( + @Param() params: GetSpaceParam, + @Query() query: PaginationRequestGetListDto, + ): Promise { + return this.subSpaceService.list(params, query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SUBSPACE.ACTIONS.GET_SUBSPACE_SUMMARY, + description: ControllerRoute.SUBSPACE.ACTIONS.GET_SUBSPACE_DESCRIPTION, + }) + @Get(':subSpaceUuid') + async findOne(@Param() params: GetSubSpaceParam): Promise { + return this.subSpaceService.findOne(params); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SUBSPACE.ACTIONS.UPDATE_SUBSPACE_SUMMARY, + description: ControllerRoute.SUBSPACE.ACTIONS.UPDATE_SUBSPACE_DESCRIPTION, + }) + @Put(':subSpaceUuid') + async updateSubspace( + @Param() params: GetSubSpaceParam, + @Body() updateSubSpaceDto: AddSubspaceDto, + ): Promise { + return this.subSpaceService.updateSubSpace(params, updateSubSpaceDto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.SUBSPACE.ACTIONS.DELETE_SUBSPACE_SUMMARY, + description: ControllerRoute.SUBSPACE.ACTIONS.DELETE_SUBSPACE_DESCRIPTION, + }) + @Delete(':subSpaceUuid') + async delete(@Param() params: GetSubSpaceParam): Promise { + return this.subSpaceService.delete(params); + } +} diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts new file mode 100644 index 0000000..1f369b1 --- /dev/null +++ b/src/space/dtos/add.space.dto.ts @@ -0,0 +1,110 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; + +export class AddSpaceDto { + @ApiProperty({ + description: 'Name of the space (e.g., Floor 1, Unit 101)', + example: 'Unit 101', + }) + @IsString() + @IsNotEmpty() + spaceName: string; + + @ApiProperty({ + description: 'UUID of the parent space (if any, for hierarchical spaces)', + example: 'f5d7e9c3-44bc-4b12-88f1-1b3cda84752e', + required: false, + }) + @IsUUID() + @IsOptional() + parentUuid?: string; + + @IsString() + @IsNotEmpty() + public icon: string; + + @ApiProperty({ + description: 'Indicates whether the space is private or public', + example: false, + default: false, + }) + @IsBoolean() + isPrivate: boolean; + + @ApiProperty({ description: 'X position on canvas', example: 120 }) + @IsNumber() + x: number; + + @ApiProperty({ description: 'Y position on canvas', example: 200 }) + @IsNumber() + y: number; + + @ApiProperty({ description: 'Y position on canvas', example: 200 }) + @IsString() + @IsOptional() + direction: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ProductAssignmentDto) + products: ProductAssignmentDto[]; +} + +export class AddUserSpaceDto { + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +export class AddUserSpaceUsingCodeDto { + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +class ProductAssignmentDto { + @IsNotEmpty() + productId: string; + + @IsNotEmpty() + count: number; +} diff --git a/src/space/dtos/community-space.param.ts b/src/space/dtos/community-space.param.ts new file mode 100644 index 0000000..ab35e4e --- /dev/null +++ b/src/space/dtos/community-space.param.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class CommunitySpaceParam { + @ApiProperty({ + description: 'UUID of the community this space belongs to', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + communityUuid: string; +} diff --git a/src/space/dtos/get.space.param.ts b/src/space/dtos/get.space.param.ts new file mode 100644 index 0000000..b347acd --- /dev/null +++ b/src/space/dtos/get.space.param.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { CommunitySpaceParam } from './community-space.param'; + +export class GetSpaceParam extends CommunitySpaceParam { + @ApiProperty({ + description: 'UUID of the Space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + spaceUuid: string; +} diff --git a/src/space/dtos/index.ts b/src/space/dtos/index.ts new file mode 100644 index 0000000..2cebe7b --- /dev/null +++ b/src/space/dtos/index.ts @@ -0,0 +1,5 @@ +export * from './add.space.dto'; +export * from './community-space.param'; +export * from './get.space.param'; +export * from './user-space.param'; +export * from './subspace'; diff --git a/src/space/dtos/subspace/add.subspace-device.param.ts b/src/space/dtos/subspace/add.subspace-device.param.ts new file mode 100644 index 0000000..885e09d --- /dev/null +++ b/src/space/dtos/subspace/add.subspace-device.param.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { GetSubSpaceParam } from './get.subspace.param'; + +export class DeviceSubSpaceParam extends GetSubSpaceParam { + @ApiProperty({ + description: 'UUID of the device', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + deviceUuid: string; +} diff --git a/src/space/dtos/subspace/add.subspace.dto.ts b/src/space/dtos/subspace/add.subspace.dto.ts new file mode 100644 index 0000000..a2b12e2 --- /dev/null +++ b/src/space/dtos/subspace/add.subspace.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddSubspaceDto { + @ApiProperty({ + example: 'Meeting Room 1', + description: 'Name of the subspace', + }) + @IsNotEmpty() + @IsString() + subspaceName: string; +} diff --git a/src/space/dtos/subspace/get.subspace.param.ts b/src/space/dtos/subspace/get.subspace.param.ts new file mode 100644 index 0000000..e1c63be --- /dev/null +++ b/src/space/dtos/subspace/get.subspace.param.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { GetSpaceParam } from '../get.space.param'; + +export class GetSubSpaceParam extends GetSpaceParam { + @ApiProperty({ + description: 'UUID of the sub space', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + subSpaceUuid: string; +} diff --git a/src/space/dtos/subspace/index.ts b/src/space/dtos/subspace/index.ts new file mode 100644 index 0000000..0463a85 --- /dev/null +++ b/src/space/dtos/subspace/index.ts @@ -0,0 +1,3 @@ +export * from './add.subspace.dto'; +export * from './get.subspace.param'; +export * from './add.subspace-device.param'; diff --git a/src/space/dtos/user-space.param.ts b/src/space/dtos/user-space.param.ts new file mode 100644 index 0000000..ba62bab --- /dev/null +++ b/src/space/dtos/user-space.param.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { GetSpaceParam } from './get.space.param'; + +export class UserSpaceParam extends GetSpaceParam { + @ApiProperty({ + description: 'Uuid of the user to be associated/ dissociated', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + }) + @IsUUID() + userUuid: string; +} diff --git a/src/space/services/index.ts b/src/space/services/index.ts new file mode 100644 index 0000000..79eb32f --- /dev/null +++ b/src/space/services/index.ts @@ -0,0 +1,7 @@ +export * from './space.service'; +export * from './space-user.service'; +export * from './space-device.service'; +export * from './subspace'; +export * from './space-link'; +export * from './space-scene.service'; +export * from './space-products'; diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts new file mode 100644 index 0000000..4dc24ba --- /dev/null +++ b/src/space/services/space-device.service.ts @@ -0,0 +1,115 @@ +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { GetDeviceDetailsInterface } from 'src/device/interfaces/get.device.interface'; +import { GetSpaceParam } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { ProductRepository } from '@app/common/modules/product/repositories'; + +@Injectable() +export class SpaceDeviceService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly tuyaService: TuyaService, + private readonly productRepository: ProductRepository, + private readonly communityRepository: CommunityRepository, + ) {} + + async listDevicesInSpace(params: GetSpaceParam): Promise { + const { spaceUuid, communityUuid } = params; + try { + const space = await this.validateCommunityAndSpace( + communityUuid, + spaceUuid, + ); + + const safeFetch = async (device: any) => { + try { + const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + ); + + return { + uuid: device.uuid, + deviceTuyaUuid: device.deviceTuyaUuid, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + isActive: device.isActive, + updatedAt: device.updatedAt, + ...tuyaDetails, + }; + } catch (error) { + console.warn( + `Skipping device with deviceTuyaUuid: ${device.deviceTuyaUuid} due to error.`, + ); + return null; + } + }; + + const detailedDevices = await Promise.all(space.devices.map(safeFetch)); + + return new SuccessResponseDto({ + data: detailedDevices.filter(Boolean), // Remove null or undefined values + message: 'Successfully retrieved list of devices', + }); + } catch (error) { + console.error('Error listing devices in space:', error); + throw new HttpException( + error.message || 'Failed to retrieve devices in space', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) { + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + }); + if (!community) { + this.throwNotFound('Community', communityUuid); + } + + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, community: { uuid: communityUuid } }, + relations: ['devices', 'devices.productDevice'], + }); + if (!space) { + this.throwNotFound('Space', spaceUuid); + } + return space; + } + + private throwNotFound(entity: string, uuid: string) { + throw new HttpException( + `${entity} with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + private async getDeviceDetailsByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const tuyaDeviceDetails = + await this.tuyaService.getDeviceDetails(deviceId); + + // Convert keys to camel case + const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails); + + // Exclude specific keys and add `productUuid` + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { uuid, ...rest } = camelCaseResponse; + return { + ...rest, + } as GetDeviceDetailsInterface; + } catch (error) { + throw new HttpException( + `Error fetching device details from Tuya for device id ${deviceId}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space-link/index.ts b/src/space/services/space-link/index.ts new file mode 100644 index 0000000..ad41b9a --- /dev/null +++ b/src/space/services/space-link/index.ts @@ -0,0 +1 @@ +export * from './space-link.service'; diff --git a/src/space/services/space-link/space-link.service.ts b/src/space/services/space-link/space-link.service.ts new file mode 100644 index 0000000..0acece0 --- /dev/null +++ b/src/space/services/space-link/space-link.service.ts @@ -0,0 +1,88 @@ +import { + SpaceLinkRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + +@Injectable() +export class SpaceLinkService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly spaceLinkRepository: SpaceLinkRepository, + ) {} + + async saveSpaceLink( + startSpaceId: string, + endSpaceId: string, + direction: string, + ): Promise { + try { + // Check if a link between the startSpace and endSpace already exists + const existingLink = await this.spaceLinkRepository.findOne({ + where: { + startSpace: { uuid: startSpaceId }, + endSpace: { uuid: endSpaceId }, + }, + }); + + if (existingLink) { + // Update the direction if the link exists + existingLink.direction = direction; + await this.spaceLinkRepository.save(existingLink); + return; + } + + const existingEndSpaceLink = await this.spaceLinkRepository.findOne({ + where: { endSpace: { uuid: endSpaceId } }, + }); + + if ( + existingEndSpaceLink && + existingEndSpaceLink.startSpace.uuid !== startSpaceId + ) { + throw new Error( + `Space with ID ${endSpaceId} is already an endSpace in another link and cannot be reused.`, + ); + } + + // Find start space + const startSpace = await this.spaceRepository.findOne({ + where: { uuid: startSpaceId }, + }); + + if (!startSpace) { + throw new HttpException( + `Start space with ID ${startSpaceId} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + // Find end space + const endSpace = await this.spaceRepository.findOne({ + where: { uuid: endSpaceId }, + }); + + if (!endSpace) { + throw new HttpException( + `End space with ID ${endSpaceId} not found.`, + HttpStatus.NOT_FOUND, + ); + } + + // Create and save the space link + const spaceLink = this.spaceLinkRepository.create({ + startSpace, + endSpace, + direction, + }); + + await this.spaceLinkRepository.save(spaceLink); + } catch (error) { + throw new HttpException( + error.message || + `Failed to save space link. Internal Server Error: ${error.message}`, + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space-products/index.ts b/src/space/services/space-products/index.ts new file mode 100644 index 0000000..d0b92d2 --- /dev/null +++ b/src/space/services/space-products/index.ts @@ -0,0 +1 @@ +export * from './space-products.service'; diff --git a/src/space/services/space-products/space-products.service.ts b/src/space/services/space-products/space-products.service.ts new file mode 100644 index 0000000..5b218e4 --- /dev/null +++ b/src/space/services/space-products/space-products.service.ts @@ -0,0 +1,131 @@ +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SpaceEntity } from '@app/common/modules/space/entities'; +import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity'; +import { SpaceProductRepository } from '@app/common/modules/space/repositories'; +import { In } from 'typeorm'; + +@Injectable() +export class SpaceProductService { + constructor( + private readonly productRepository: ProductRepository, + private readonly spaceProductRepository: SpaceProductRepository, + ) {} + + async assignProductsToSpace( + space: SpaceEntity, + products: { productId: string; count: number }[], + ): Promise { + try { + const uniqueProducts = this.validateUniqueProducts(products); + const productEntities = await this.getProductEntities(uniqueProducts); + + // Fetch existing space products + const existingSpaceProducts = await this.spaceProductRepository.find({ + where: { + space: { + uuid: space.uuid, + }, + }, + relations: ['product'], + }); + + const updatedProducts = []; + const newProducts = []; + + for (const { productId, count } of uniqueProducts) { + const product = productEntities.get(productId); + if (!product) { + throw new HttpException( + `Product with ID ${productId} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Check if product already exists in the space + const existingProduct = existingSpaceProducts.find( + (spaceProduct) => spaceProduct.product.uuid === productId, + ); + + if (existingProduct) { + // If count is different, update the existing record + if (existingProduct.productCount !== count) { + existingProduct.productCount = count; + updatedProducts.push(existingProduct); + } + } else { + // Add new product if it doesn't exist + newProducts.push( + this.spaceProductRepository.create({ + space, + product, + productCount: count, + }), + ); + } + } + + // Save updates and new records + if (updatedProducts.length > 0) { + await this.spaceProductRepository.save(updatedProducts); + } + + if (newProducts.length > 0) { + await this.spaceProductRepository.save(newProducts); + } + + return [...updatedProducts, ...newProducts]; + } catch (error) { + console.error('Error assigning products to space:', error); + + if (!(error instanceof HttpException)) { + throw new HttpException( + 'An error occurred while assigning products to the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + throw error; + } + } + + private validateUniqueProducts( + products: { productId: string; count: number }[], + ): { productId: string; count: number }[] { + const productIds = new Set(); + const uniqueProducts = []; + + for (const product of products) { + if (productIds.has(product.productId)) { + throw new HttpException( + `Duplicate product ID found: ${product.productId}`, + HttpStatus.BAD_REQUEST, + ); + } + productIds.add(product.productId); + uniqueProducts.push(product); + } + + return uniqueProducts; + } + + private async getProductEntities( + products: { productId: string; count: number }[], + ): Promise> { + try { + const productIds = products.map((p) => p.productId); + + const productEntities = await this.productRepository.find({ + where: { uuid: In(productIds) }, + }); + + return new Map(productEntities.map((p) => [p.uuid, p])); + } catch (error) { + console.error('Error fetching product entities:', error); + throw new HttpException( + 'Failed to fetch product entities', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/space-scene.service.ts b/src/space/services/space-scene.service.ts new file mode 100644 index 0000000..ac26889 --- /dev/null +++ b/src/space/services/space-scene.service.ts @@ -0,0 +1,50 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { GetSpaceParam } from '../dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SpaceService } from './space.service'; +import { SceneService } from '../../scene/services'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { GetSceneDto } from '../../scene/dtos'; + +@Injectable() +export class SpaceSceneService { + constructor( + private readonly spaceSevice: SpaceService, + private readonly sceneSevice: SceneService, + ) {} + + async getScenes( + params: GetSpaceParam, + getSceneDto: GetSceneDto, + ): Promise { + try { + const { spaceUuid, communityUuid } = params; + + await this.spaceSevice.validateCommunityAndSpace( + communityUuid, + spaceUuid, + ); + + const scenes = await this.sceneSevice.findScenesBySpace( + spaceUuid, + getSceneDto, + ); + + return new SuccessResponseDto({ + message: `Scenes retrieved successfully for space ${spaceUuid}`, + data: scenes, + }); + } catch (error) { + console.error('Error retrieving scenes:', error); + + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'An error occurred while retrieving scenes', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } +} diff --git a/src/space/services/space-user.service.ts b/src/space/services/space-user.service.ts new file mode 100644 index 0000000..d4f2e9d --- /dev/null +++ b/src/space/services/space-user.service.ts @@ -0,0 +1,108 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + +@Injectable() +export class SpaceUserService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly userRepository: UserRepository, + private readonly userSpaceRepository: UserSpaceRepository, + ) {} + async associateUserToSpace( + userUuid: string, + spaceUuid: string, + ): Promise { + // Find the user by ID + const user = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!user) { + throw new HttpException( + `User with ID ${userUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Find the space by ID + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + }); + if (!space) { + throw new HttpException( + `Space with ID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Check if the association already exists + const existingAssociation = await this.userSpaceRepository.findOne({ + where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } }, + }); + if (existingAssociation) { + throw new HttpException( + `User is already associated with the space`, + HttpStatus.CONFLICT, + ); + } + + const userSpace = this.userSpaceRepository.create({ user, space }); + + await this.userSpaceRepository.save(userSpace); + return new SuccessResponseDto({ + data: userSpace, + message: `Space ${spaceUuid} has been successfully associated t user ${userUuid}`, + }); + } + + async disassociateUserFromSpace( + userUuid: string, + spaceUuid: string, + ): Promise { + // Find the user by ID + const user = await this.userRepository.findOne({ + where: { uuid: userUuid }, + }); + if (!user) { + throw new HttpException( + `User with ID ${userUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Find the space by ID + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + }); + if (!space) { + throw new HttpException( + `Space with ID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Find the existing association + const existingAssociation = await this.userSpaceRepository.findOne({ + where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } }, + }); + + if (!existingAssociation) { + throw new HttpException( + `No association found between user ${userUuid} and space ${spaceUuid}`, + HttpStatus.NOT_FOUND, + ); + } + + // Remove the association + await this.userSpaceRepository.remove(existingAssociation); + + return new SuccessResponseDto({ + message: `User ${userUuid} has been successfully disassociated from space ${spaceUuid}`, + }); + } +} diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts new file mode 100644 index 0000000..6e3e600 --- /dev/null +++ b/src/space/services/space.service.ts @@ -0,0 +1,310 @@ +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { + BadRequestException, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { AddSpaceDto } from '../dtos'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SpaceEntity } from '@app/common/modules/space/entities'; +import { generateRandomString } from '@app/common/helper/randomString'; +import { SpaceLinkService } from './space-link'; +import { SpaceProductService } from './space-products'; + +@Injectable() +export class SpaceService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly communityRepository: CommunityRepository, + private readonly spaceLinkService: SpaceLinkService, + private readonly spaceProductService: SpaceProductService, + ) {} + + async createSpace( + addSpaceDto: AddSpaceDto, + communityId: string, + ): Promise { + const { parentUuid, direction, products } = addSpaceDto; + + const community = await this.validateCommunity(communityId); + + const parent = parentUuid ? await this.validateSpace(parentUuid) : null; + try { + const newSpace = this.spaceRepository.create({ + ...addSpaceDto, + community, + parent: parentUuid ? parent : null, + }); + + await this.spaceRepository.save(newSpace); + + if (direction && parent) { + await this.spaceLinkService.saveSpaceLink( + parent.uuid, + newSpace.uuid, + direction, + ); + } + + if (products && products.length > 0) { + await this.spaceProductService.assignProductsToSpace( + newSpace, + products, + ); + } + + return new SuccessResponseDto({ + statusCode: HttpStatus.CREATED, + data: newSpace, + message: 'Space created successfully', + }); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getSpacesHierarchyForCommunity( + communityUuid: string, + ): Promise { + await this.validateCommunity(communityUuid); + try { + // Get all spaces related to the community, including the parent-child relations + const spaces = await this.spaceRepository.find({ + where: { community: { uuid: communityUuid } }, + relations: [ + 'parent', + 'children', + 'incomingConnections', + 'spaceProducts', + 'spaceProducts.product', + ], // Include parent and children relations + }); + + // Organize spaces into a hierarchical structure + const spaceHierarchy = this.buildSpaceHierarchy(spaces); + + return new SuccessResponseDto({ + message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`, + data: spaceHierarchy, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException( + 'An error occurred while fetching the spaces', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async findOne(spaceUuid: string): Promise { + try { + const space = await this.validateSpace(spaceUuid); + + return new SuccessResponseDto({ + message: `Space with ID ${spaceUuid} successfully fetched`, + data: space, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; // If it's an HttpException, rethrow it + } else { + throw new HttpException( + 'An error occurred while deleting the community', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async delete( + spaceUuid: string, + communityUuid: string, + ): Promise { + try { + // First, check if the community exists + const space = await this.validateCommunityAndSpace( + communityUuid, + spaceUuid, + ); + + // Delete the space + await this.spaceRepository.remove(space); + + return new SuccessResponseDto({ + message: `Space with ID ${spaceUuid} successfully deleted`, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + 'An error occurred while deleting the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async updateSpace( + spaceUuid: string, + communityId: string, + updateSpaceDto: AddSpaceDto, + ): Promise { + try { + const space = await this.validateCommunityAndSpace( + communityId, + spaceUuid, + ); + + // If a parentId is provided, check if the parent exists + const { parentUuid, products } = updateSpaceDto; + const parent = parentUuid ? await this.validateSpace(parentUuid) : null; + + // Update other space properties from updateSpaceDto + Object.assign(space, updateSpaceDto, { parent }); + + // Save the updated space + const updatedSpace = await this.spaceRepository.save(space); + + if (products && products.length > 0) { + await this.spaceProductService.assignProductsToSpace( + updatedSpace, + products, + ); + } + + return new SuccessResponseDto({ + message: `Space with ID ${spaceUuid} successfully updated`, + data: space, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + 'An error occurred while updating the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getSpacesHierarchyForSpace( + spaceUuid: string, + ): Promise { + await this.validateSpace(spaceUuid); + + try { + // Get all spaces that are children of the provided space, including the parent-child relations + const spaces = await this.spaceRepository.find({ + where: { parent: { uuid: spaceUuid } }, + relations: ['parent', 'children'], // Include parent and children relations + }); + + // Organize spaces into a hierarchical structure + const spaceHierarchy = this.buildSpaceHierarchy(spaces); + + return new SuccessResponseDto({ + message: `Spaces under space ${spaceUuid} successfully fetched in hierarchy`, + data: spaceHierarchy, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException( + 'An error occurred while fetching the spaces under the space', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getSpaceInvitationCode(spaceUuid: string): Promise { + try { + const invitationCode = generateRandomString(6); + + const space = await this.validateSpace(spaceUuid); + + space.invitationCode = invitationCode; + await this.spaceRepository.save(space); + + return new SuccessResponseDto({ + message: `Invitation code has been successfuly added to the space`, + data: space, + }); + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException('Space not found', HttpStatus.NOT_FOUND); + } + } + } + + private buildSpaceHierarchy(spaces: SpaceEntity[]): SpaceEntity[] { + const map = new Map(); + + // Step 1: Create a map of spaces by UUID + spaces.forEach((space) => { + map.set( + space.uuid, + this.spaceRepository.create({ + ...space, + children: [], // Add children if needed + }), + ); + }); + + // Step 2: Organize the hierarchy + const rootSpaces: SpaceEntity[] = []; + spaces.forEach((space) => { + if (space.parent && space.parent.uuid) { + const parent = map.get(space.parent.uuid); + parent?.children?.push(map.get(space.uuid)); + } else { + rootSpaces.push(map.get(space.uuid)); + } + }); + + return rootSpaces; + } + + private async validateCommunity(communityId: string) { + const community = await this.communityRepository.findOne({ + where: { uuid: communityId }, + }); + if (!community) { + throw new HttpException( + `Community with ID ${communityId} not found`, + HttpStatus.NOT_FOUND, + ); + } + return community; + } + + async validateCommunityAndSpace(communityUuid: string, spaceUuid: string) { + const community = await this.validateCommunity(communityUuid); + if (!community) { + this.throwNotFound('Community', communityUuid); + } + + const space = await this.validateSpace(spaceUuid); + return space; + } + + private async validateSpace(spaceUuid: string) { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + }); + if (!space) this.throwNotFound('Space', spaceUuid); + return space; + } + + private throwNotFound(entity: string, uuid: string) { + throw new HttpException( + `${entity} with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } +} diff --git a/src/space/services/subspace/index.ts b/src/space/services/subspace/index.ts new file mode 100644 index 0000000..973d199 --- /dev/null +++ b/src/space/services/subspace/index.ts @@ -0,0 +1,2 @@ +export * from './subspace.service'; +export * from './subspace-device.service'; diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts new file mode 100644 index 0000000..74f6610 --- /dev/null +++ b/src/space/services/subspace/subspace-device.service.ts @@ -0,0 +1,221 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { + SpaceRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { GetDeviceDetailsInterface } from '../../../device/interfaces/get.device.interface'; + +@Injectable() +export class SubspaceDeviceService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly communityRepository: CommunityRepository, + private readonly subspaceRepository: SubspaceRepository, + private readonly deviceRepository: DeviceRepository, + private readonly tuyaService: TuyaService, + private readonly productRepository: ProductRepository, + ) {} + + async listDevicesInSubspace( + params: GetSubSpaceParam, + ): Promise { + const { subSpaceUuid, spaceUuid, communityUuid } = params; + + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + const subspace = await this.findSubspaceWithDevices(subSpaceUuid); + + const safeFetch = async (device: any) => { + try { + const tuyaDetails = await this.getDeviceDetailsByDeviceIdTuya( + device.deviceTuyaUuid, + ); + + return { + uuid: device.uuid, + deviceTuyaUuid: device.deviceTuyaUuid, + productUuid: device.productDevice.uuid, + productType: device.productDevice.prodType, + isActive: device.isActive, + createdAt: device.createdAt, + updatedAt: device.updatedAt, + ...tuyaDetails, + }; + } catch (error) { + console.warn( + `Skipping device with deviceTuyaUuid: ${device.deviceTuyaUuid} due to error.`, + ); + return null; + } + }; + const detailedDevices = await Promise.all(subspace.devices.map(safeFetch)); + + return new SuccessResponseDto({ + data: detailedDevices.filter(Boolean), // Remove nulls + message: 'Successfully retrieved list of devices', + }); + } + + async associateDeviceToSubspace( + params: DeviceSubSpaceParam, + ): Promise { + const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params; + try { + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + const subspace = await this.findSubspace(subSpaceUuid); + const device = await this.findDevice(deviceUuid); + + device.subspace = subspace; + + const newDevice = await this.deviceRepository.save(device); + + return new SuccessResponseDto({ + data: newDevice, + message: 'Successfully associated device to subspace', + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'Failed to associate device to subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async disassociateDeviceFromSubspace( + params: DeviceSubSpaceParam, + ): Promise { + const { subSpaceUuid, deviceUuid, spaceUuid, communityUuid } = params; + try { + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + const subspace = await this.findSubspace(subSpaceUuid); + const device = await this.findDevice(deviceUuid); + + if (!device.subspace || device.subspace.uuid !== subspace.uuid) { + throw new HttpException( + `Device ${deviceUuid} is not associated with the specified subspace ${subSpaceUuid} `, + HttpStatus.BAD_REQUEST, + ); + } + + device.subspace = null; + const updatedDevice = await this.deviceRepository.save(device); + + return new SuccessResponseDto({ + data: updatedDevice, + message: 'Successfully dissociated device from subspace', + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException( + 'Failed to dissociate device from subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + // Helper method to validate community and space + private async validateCommunityAndSpace( + communityUuid: string, + spaceUuid: string, + ) { + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + }); + if (!community) { + this.throwNotFound('Community', communityUuid); + } + + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, community: { uuid: communityUuid } }, + }); + if (!space) { + this.throwNotFound('Space', spaceUuid); + } + return space; + } + + // Helper method to find subspace with devices relation + private async findSubspaceWithDevices(subSpaceUuid: string) { + const subspace = await this.subspaceRepository.findOne({ + where: { uuid: subSpaceUuid }, + relations: ['devices', 'devices.productDevice'], + }); + if (!subspace) { + this.throwNotFound('Subspace', subSpaceUuid); + } + return subspace; + } + + private async findSubspace(subSpaceUuid: string) { + const subspace = await this.subspaceRepository.findOne({ + where: { uuid: subSpaceUuid }, + }); + if (!subspace) { + this.throwNotFound('Subspace', subSpaceUuid); + } + return subspace; + } + + private async findDevice(deviceUuid: string) { + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations: ['subspace'], + }); + if (!device) { + this.throwNotFound('Device', deviceUuid); + } + return device; + } + + private throwNotFound(entity: string, uuid: string) { + throw new HttpException( + `${entity} with ID ${uuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + private async getDeviceDetailsByDeviceIdTuya( + deviceId: string, + ): Promise { + try { + const tuyaDeviceDetails = + await this.tuyaService.getDeviceDetails(deviceId); + + const camelCaseResponse = convertKeysToCamelCase(tuyaDeviceDetails); + + const product = await this.productRepository.findOne({ + where: { + prodId: camelCaseResponse.productId, + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { uuid, ...rest } = camelCaseResponse; + return { + ...rest, + productUuid: product?.uuid, + } as GetDeviceDetailsInterface; + } catch (error) { + throw new HttpException( + `Error fetching device details from Tuya for device uuid ${deviceId}.`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts new file mode 100644 index 0000000..22c60ee --- /dev/null +++ b/src/space/services/subspace/subspace.service.ts @@ -0,0 +1,203 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { + SpaceRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam } from '../../dtos'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { SubspaceDto } from '@app/common/modules/space/dtos'; + +@Injectable() +export class SubSpaceService { + constructor( + private readonly spaceRepository: SpaceRepository, + private readonly communityRepository: CommunityRepository, + private readonly subspaceRepository: SubspaceRepository, + ) {} + + async createSubspace( + addSubspaceDto: AddSubspaceDto, + params: GetSpaceParam, + ): Promise { + const { communityUuid, spaceUuid } = params; + const space = await this.validateCommunityAndSpace( + communityUuid, + spaceUuid, + ); + + try { + const newSubspace = this.subspaceRepository.create({ + ...addSubspaceDto, + space, + }); + + await this.subspaceRepository.save(newSubspace); + + return new SuccessResponseDto({ + statusCode: HttpStatus.CREATED, + data: newSubspace, + message: 'Subspace created successfully', + }); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async list( + params: GetSpaceParam, + pageable: Partial, + ): Promise { + const { communityUuid, spaceUuid } = params; + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + try { + pageable.modelName = 'subspace'; + pageable.where = { space: { uuid: spaceUuid } }; + const customModel = TypeORMCustomModel(this.subspaceRepository); + + const { baseResponseDto, paginationResponseDto } = + await customModel.findAll(pageable); + return new PageResponse( + baseResponseDto, + paginationResponseDto, + ); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async findOne(params: GetSubSpaceParam): Promise { + const { communityUuid, subSpaceUuid, spaceUuid } = params; + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + try { + const subSpace = await this.subspaceRepository.findOne({ + where: { + uuid: subSpaceUuid, + }, + }); + + // If space is not found, throw a NotFoundException + if (!subSpace) { + throw new HttpException( + `Sub Space with UUID ${subSpaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + return new SuccessResponseDto({ + message: `Subspace with ID ${subSpaceUuid} successfully fetched`, + data: subSpace, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; // If it's an HttpException, rethrow it + } else { + throw new HttpException( + 'An error occurred while deleting the subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + async updateSubSpace( + params: GetSubSpaceParam, + updateSubSpaceDto: AddSubspaceDto, + ): Promise { + const { spaceUuid, communityUuid, subSpaceUuid } = params; + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + const subSpace = await this.subspaceRepository.findOne({ + where: { uuid: subSpaceUuid }, + }); + + if (!subSpace) { + throw new HttpException( + `SubSpace with ID ${subSpaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + try { + Object.assign(subSpace, updateSubSpaceDto); + + await this.subspaceRepository.save(subSpace); + + return new SuccessResponseDto({ + message: `Subspace with ID ${subSpaceUuid} successfully updated`, + data: subSpace, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + 'An error occurred while updating the subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async delete(params: GetSubSpaceParam): Promise { + const { spaceUuid, communityUuid, subSpaceUuid } = params; + await this.validateCommunityAndSpace(communityUuid, spaceUuid); + + const subspace = await this.subspaceRepository.findOne({ + where: { uuid: subSpaceUuid }, + }); + + if (!subspace) { + throw new HttpException( + `Subspace with ID ${subSpaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + try { + await this.subspaceRepository.remove(subspace); + + return new SuccessResponseDto({ + message: `Subspace with ID ${subSpaceUuid} successfully deleted`, + statusCode: HttpStatus.OK, + }); + } catch (error) { + throw new HttpException( + 'An error occurred while deleting the subspace', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async validateCommunityAndSpace( + communityUuid: string, + spaceUuid: string, + ) { + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + }); + if (!community) { + throw new HttpException( + `Community with ID ${communityUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid, community: { uuid: communityUuid } }, + }); + if (!space) { + throw new HttpException( + `Space with ID ${spaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + return space; + } +} diff --git a/src/space/space.module.ts b/src/space/space.module.ts new file mode 100644 index 0000000..9c80fa1 --- /dev/null +++ b/src/space/space.module.ts @@ -0,0 +1,85 @@ +import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { + SpaceController, + SpaceDeviceController, + SpaceUserController, + SubSpaceController, + SubSpaceDeviceController, + SpaceSceneController, +} from './controllers'; +import { + SpaceDeviceService, + SpaceLinkService, + SpaceProductService, + SpaceSceneService, + SpaceService, + SpaceUserService, + SubspaceDeviceService, + SubSpaceService, +} from './services'; +import { + SpaceProductRepository, + SpaceRepository, + SubspaceRepository, + SpaceLinkRepository, +} from '@app/common/modules/space/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { SceneService } from '../scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { DeviceService } from 'src/device/services'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; + +@Module({ + imports: [ConfigModule, SpaceRepositoryModule], + controllers: [ + SpaceController, + SpaceUserController, + SpaceDeviceController, + SubSpaceController, + SubSpaceDeviceController, + SpaceSceneController, + ], + providers: [ + SpaceService, + TuyaService, + ProductRepository, + SubSpaceService, + SpaceDeviceService, + SpaceLinkService, + SubspaceDeviceService, + SpaceRepository, + DeviceRepository, + CommunityRepository, + SubspaceRepository, + SpaceLinkRepository, + UserSpaceRepository, + UserRepository, + SpaceUserService, + SpaceSceneService, + SceneService, + SceneIconRepository, + SceneRepository, + DeviceService, + DeviceStatusFirebaseService, + DeviceStatusLogRepository, + SceneDeviceRepository, + SpaceProductService, + SpaceProductRepository, + ], + exports: [SpaceService], +}) +export class SpaceModule {} diff --git a/src/timezone/controllers/timezone.controller.ts b/src/timezone/controllers/timezone.controller.ts index bc46381..756bdbb 100644 --- a/src/timezone/controllers/timezone.controller.ts +++ b/src/timezone/controllers/timezone.controller.ts @@ -1,13 +1,14 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { TimeZoneService } from '../services/timezone.service'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('TimeZone Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'timezone', + path: ControllerRoute.TIMEZONE.ROUTE, }) export class TimeZoneController { constructor(private readonly timeZoneService: TimeZoneService) {} @@ -15,6 +16,11 @@ export class TimeZoneController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get() + @ApiOperation({ + summary: ControllerRoute.TIMEZONE.ACTIONS.GET_ALL_TIME_ZONES_SUMMARY, + description: + ControllerRoute.TIMEZONE.ACTIONS.GET_ALL_TIME_ZONES_DESCRIPTION, + }) async getAllTimeZones() { return await this.timeZoneService.getAllTimeZones(); } diff --git a/src/unit/controllers/index.ts b/src/unit/controllers/index.ts deleted file mode 100644 index c8d7271..0000000 --- a/src/unit/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './unit.controller'; diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts deleted file mode 100644 index 8dbede7..0000000 --- a/src/unit/controllers/unit.controller.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { UnitService } from '../services/unit.service'; -import { - Body, - Controller, - Get, - HttpStatus, - Param, - Post, - Put, - Query, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { - AddUnitDto, - AddUserUnitDto, - AddUserUnitUsingCodeDto, -} from '../dtos/add.unit.dto'; -import { GetUnitChildDto } from '../dtos/get.unit.dto'; -import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; -import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; -import { CheckUserUnitGuard } from 'src/guards/user.unit.guard'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { UnitPermissionGuard } from 'src/guards/unit.permission.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; - -@ApiTags('Unit Module') -@Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.UNIT, -}) -export class UnitController { - constructor(private readonly unitService: UnitService) {} - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) - @Post() - async addUnit(@Body() addUnitDto: AddUnitDto) { - const unit = await this.unitService.addUnit(addUnitDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Unit added successfully', - data: unit, - }; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get(':unitUuid') - async getUnitByUuid(@Param('unitUuid') unitUuid: string) { - const unit = await this.unitService.getUnitByUuid(unitUuid); - return unit; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get('child/:unitUuid') - async getUnitChildByUuid( - @Param('unitUuid') unitUuid: string, - @Query() query: GetUnitChildDto, - ) { - const unit = await this.unitService.getUnitChildByUuid(unitUuid, query); - return unit; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get('parent/:unitUuid') - async getUnitParentByUuid(@Param('unitUuid') unitUuid: string) { - const unit = await this.unitService.getUnitParentByUuid(unitUuid); - return unit; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, CheckUserUnitGuard) - @Post('user') - async addUserUnit(@Body() addUserUnitDto: AddUserUnitDto) { - await this.unitService.addUserUnit(addUserUnitDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user unit added successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Get('user/:userUuid') - async getUnitsByUserId(@Param('userUuid') userUuid: string) { - return await this.unitService.getUnitsByUserId(userUuid); - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Put(':unitUuid') - async renameUnitByUuid( - @Param('unitUuid') unitUuid: string, - @Body() updateUnitNameDto: UpdateUnitNameDto, - ) { - const unit = await this.unitService.renameUnitByUuid( - unitUuid, - updateUnitNameDto, - ); - return unit; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard, UnitPermissionGuard) - @Get(':unitUuid/invitation-code') - async getUnitInvitationCode(@Param('unitUuid') unitUuid: string) { - const unit = await this.unitService.getUnitInvitationCode(unitUuid); - return unit; - } - - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Post('user/verify-code') - async verifyCodeAndAddUserUnit( - @Body() addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, - ) { - await this.unitService.verifyCodeAndAddUserUnit(addUserUnitUsingCodeDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user unit added successfully', - }; - } -} diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts deleted file mode 100644 index 9896c37..0000000 --- a/src/unit/dtos/add.unit.dto.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class AddUnitDto { - @ApiProperty({ - description: 'unitName', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitName: string; - - @ApiProperty({ - description: 'floorUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public floorUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} -export class AddUserUnitDto { - @ApiProperty({ - description: 'unitUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitUuid: string; - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} -export class AddUserUnitUsingCodeDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; - @ApiProperty({ - description: 'inviteCode', - required: true, - }) - @IsString() - @IsNotEmpty() - public inviteCode: string; - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/unit/dtos/get.unit.dto.ts b/src/unit/dtos/get.unit.dto.ts deleted file mode 100644 index 2fae52a..0000000 --- a/src/unit/dtos/get.unit.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; - -export class GetUnitDto { - @ApiProperty({ - description: 'unitUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitUuid: string; -} - -export class GetUnitChildDto { - @ApiProperty({ example: 1, description: 'Page number', required: true }) - @IsInt({ message: 'Page must be a number' }) - @Min(1, { message: 'Page must not be less than 1' }) - @IsNotEmpty() - public page: number; - - @ApiProperty({ - example: 10, - description: 'Number of items per page', - required: true, - }) - @IsInt({ message: 'Page size must be a number' }) - @Min(1, { message: 'Page size must not be less than 1' }) - @IsNotEmpty() - public pageSize: number; -} -export class GetUnitByUserIdDto { - @ApiProperty({ - description: 'userUuid', - required: true, - }) - @IsString() - @IsNotEmpty() - public userUuid: string; -} diff --git a/src/unit/dtos/index.ts b/src/unit/dtos/index.ts deleted file mode 100644 index 970d13d..0000000 --- a/src/unit/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './add.unit.dto'; diff --git a/src/unit/dtos/update.unit.dto.ts b/src/unit/dtos/update.unit.dto.ts deleted file mode 100644 index 2d69902..0000000 --- a/src/unit/dtos/update.unit.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class UpdateUnitNameDto { - @ApiProperty({ - description: 'unitName', - required: true, - }) - @IsString() - @IsNotEmpty() - public unitName: string; - - constructor(dto: Partial) { - Object.assign(this, dto); - } -} diff --git a/src/unit/interface/unit.interface.ts b/src/unit/interface/unit.interface.ts deleted file mode 100644 index 635f38e..0000000 --- a/src/unit/interface/unit.interface.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface GetUnitByUuidInterface { - uuid: string; - createdAt: Date; - updatedAt: Date; - name: string; - type: string; - spaceTuyaUuid: string; -} - -export interface UnitChildInterface { - uuid: string; - name: string; - type: string; - totalCount?: number; - children?: UnitChildInterface[]; -} -export interface UnitParentInterface { - uuid: string; - name: string; - type: string; - parent?: UnitParentInterface; -} -export interface RenameUnitByUuidInterface { - uuid: string; - name: string; - type: string; -} -export interface GetUnitByUserUuidInterface { - uuid: string; - name: string; - type: string; -} -export interface addTuyaSpaceInterface { - success: boolean; - result: string; - msg: string; -} diff --git a/src/unit/services/index.ts b/src/unit/services/index.ts deleted file mode 100644 index 0540c40..0000000 --- a/src/unit/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './unit.service'; diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts deleted file mode 100644 index 826c044..0000000 --- a/src/unit/services/unit.service.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { GetUnitChildDto } from '../dtos/get.unit.dto'; -import { SpaceTypeRepository } from '../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; -import { - UnitChildInterface, - UnitParentInterface, - GetUnitByUuidInterface, - RenameUnitByUuidInterface, - GetUnitByUserUuidInterface, - addTuyaSpaceInterface, -} from '../interface/unit.interface'; -import { SpaceEntity } from '@app/common/modules/space/entities'; -import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { generateRandomString } from '@app/common/helper/randomString'; -import { UserDevicePermissionService } from 'src/user-device-permission/services'; -import { PermissionType } from '@app/common/constants/permission-type.enum'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; -import { ConfigService } from '@nestjs/config'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; - -@Injectable() -export class UnitService { - private tuya: TuyaContext; - constructor( - private readonly configService: ConfigService, - private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, - private readonly userSpaceRepository: UserSpaceRepository, - private readonly userDevicePermissionService: UserDevicePermissionService, - ) { - const accessKey = this.configService.get('auth-config.ACCESS_KEY'); - const secretKey = this.configService.get('auth-config.SECRET_KEY'); - const tuyaEuUrl = this.configService.get('tuya-config.TUYA_EU_URL'); - - this.tuya = new TuyaContext({ - baseUrl: tuyaEuUrl, - accessKey, - secretKey, - }); - } - - async addUnit(addUnitDto: AddUnitDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.UNIT, - }, - }); - const tuyaUnit = await this.addUnitTuya(addUnitDto.unitName); - if (!tuyaUnit.result) { - throw new HttpException('Error creating unit', HttpStatus.BAD_REQUEST); - } - - const unit = await this.spaceRepository.save({ - spaceName: addUnitDto.unitName, - parent: { uuid: addUnitDto.floorUuid }, - spaceType: { uuid: spaceType.uuid }, - spaceTuyaUuid: tuyaUnit.result, - }); - return unit; - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - async addUnitTuya(unitName: string): Promise { - try { - const path = `/v2.0/cloud/space/creation`; - const response = await this.tuya.request({ - method: 'POST', - path, - body: { - name: unitName, - }, - }); - - return response as addTuyaSpaceInterface; - } catch (error) { - throw new HttpException( - 'Error creating unit from Tuya', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async getUnitByUuid(unitUuid: string): Promise { - try { - const unit = await this.spaceRepository.findOne({ - where: { - uuid: unitUuid, - spaceType: { - type: SpaceType.UNIT, - }, - }, - relations: ['spaceType'], - }); - if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) { - throw new BadRequestException('Invalid unit UUID'); - } - return { - uuid: unit.uuid, - createdAt: unit.createdAt, - updatedAt: unit.updatedAt, - name: unit.spaceName, - type: unit.spaceType.type, - spaceTuyaUuid: unit.spaceTuyaUuid, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } - } - } - async getUnitChildByUuid( - unitUuid: string, - getUnitChildDto: GetUnitChildDto, - ): Promise { - try { - const { page, pageSize } = getUnitChildDto; - - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: unitUuid }, - relations: ['children', 'spaceType'], - }); - - if ( - !space || - !space.spaceType || - space.spaceType.type !== SpaceType.UNIT - ) { - throw new BadRequestException('Invalid unit UUID'); - } - - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - - const children = await this.buildHierarchy(space, false, page, pageSize); - - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } - } - } - - private async buildHierarchy( - space: SpaceEntity, - includeSubSpaces: boolean, - page: number, - pageSize: number, - ): Promise { - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['spaceType'], - skip: (page - 1) * pageSize, - take: pageSize, - }); - - if (!children || children.length === 0 || !includeSubSpaces) { - return children - .filter( - (child) => - child.spaceType.type !== SpaceType.UNIT && - child.spaceType.type !== SpaceType.FLOOR && - child.spaceType.type !== SpaceType.COMMUNITY && - child.spaceType.type !== SpaceType.UNIT, - ) // Filter remaining unit and floor and community and unit types - .map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); - } - - const childHierarchies = await Promise.all( - children - .filter( - (child) => - child.spaceType.type !== SpaceType.UNIT && - child.spaceType.type !== SpaceType.FLOOR && - child.spaceType.type !== SpaceType.COMMUNITY && - child.spaceType.type !== SpaceType.UNIT, - ) // Filter remaining unit and floor and community and unit types - .map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), - ); - - return childHierarchies; - } - - async getUnitParentByUuid(unitUuid: string): Promise { - try { - const unit = await this.spaceRepository.findOne({ - where: { - uuid: unitUuid, - spaceType: { - type: SpaceType.UNIT, - }, - }, - relations: ['spaceType', 'parent', 'parent.spaceType'], - }); - if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) { - throw new BadRequestException('Invalid unit UUID'); - } - return { - uuid: unit.uuid, - name: unit.spaceName, - type: unit.spaceType.type, - parent: { - uuid: unit.parent.uuid, - name: unit.parent.spaceName, - type: unit.parent.spaceType.type, - }, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } - } - } - async getUnitsByUserId( - userUuid: string, - ): Promise { - try { - const units = await this.userSpaceRepository.find({ - relations: ['space', 'space.spaceType'], - where: { - user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.UNIT } }, - }, - }); - - if (units.length === 0) { - throw new HttpException('this user has no units', HttpStatus.NOT_FOUND); - } - const spaces = units.map((unit) => ({ - uuid: unit.space.uuid, - name: unit.space.spaceName, - type: unit.space.spaceType.type, - })); - - return spaces; - } catch (err) { - if (err instanceof HttpException) { - throw err; - } else { - throw new HttpException('user not found', HttpStatus.NOT_FOUND); - } - } - } - - async addUserUnit(addUserUnitDto: AddUserUnitDto) { - try { - return await this.userSpaceRepository.save({ - user: { uuid: addUserUnitDto.userUuid }, - space: { uuid: addUserUnitDto.unitUuid }, - }); - } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { - throw new HttpException( - 'User already belongs to this unit', - HttpStatus.BAD_REQUEST, - ); - } - throw new HttpException( - err.message || 'Internal Server Error', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async renameUnitByUuid( - unitUuid: string, - updateUnitNameDto: UpdateUnitNameDto, - ): Promise { - try { - const unit = await this.spaceRepository.findOneOrFail({ - where: { uuid: unitUuid }, - relations: ['spaceType'], - }); - - if (!unit || !unit.spaceType || unit.spaceType.type !== SpaceType.UNIT) { - throw new BadRequestException('Invalid unit UUID'); - } - - await this.spaceRepository.update( - { uuid: unitUuid }, - { spaceName: updateUnitNameDto.unitName }, - ); - - // Fetch the updated unit - const updatedUnit = await this.spaceRepository.findOneOrFail({ - where: { uuid: unitUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedUnit.uuid, - name: updatedUnit.spaceName, - type: updatedUnit.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } - } - } - async getUnitInvitationCode(unitUuid: string): Promise { - try { - // Generate a 6-character random invitation code - const invitationCode = generateRandomString(6); - - // Update the unit with the new invitation code - await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); - - // Fetch the updated unit - const updatedUnit = await this.spaceRepository.findOneOrFail({ - where: { uuid: unitUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedUnit.uuid, - invitationCode: updatedUnit.invitationCode, - type: updatedUnit.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; - } else { - throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); - } - } - } - async verifyCodeAndAddUserUnit( - addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, - ) { - try { - const unit = await this.findUnitByInviteCode( - addUserUnitUsingCodeDto.inviteCode, - ); - - await this.addUserToUnit(addUserUnitUsingCodeDto.userUuid, unit.uuid); - - await this.clearUnitInvitationCode(unit.uuid); - - const deviceUUIDs = await this.getDeviceUUIDsForUnit(unit.uuid); - - await this.addUserPermissionsToDevices( - addUserUnitUsingCodeDto.userUuid, - deviceUUIDs, - ); - } catch (err) { - throw new HttpException( - 'Invalid invitation code', - HttpStatus.BAD_REQUEST, - ); - } - } - - private async findUnitByInviteCode(inviteCode: string): Promise { - const unit = await this.spaceRepository.findOneOrFail({ - where: { - invitationCode: inviteCode, - spaceType: { type: SpaceType.UNIT }, - }, - relations: ['spaceType'], - }); - - return unit; - } - - private async addUserToUnit(userUuid: string, unitUuid: string) { - const user = await this.addUserUnit({ userUuid, unitUuid }); - - if (user.uuid) { - return user; - } else { - throw new HttpException( - 'Failed to add user to unit', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - private async clearUnitInvitationCode(unitUuid: string) { - await this.spaceRepository.update( - { uuid: unitUuid }, - { invitationCode: null }, - ); - } - - private async getDeviceUUIDsForUnit( - unitUuid: string, - ): Promise<{ uuid: string }[]> { - const devices = await this.spaceRepository.find({ - where: { parent: { uuid: unitUuid } }, - relations: ['devicesSpaceEntity', 'devicesSpaceEntity.productDevice'], - }); - - const allDevices = devices.flatMap((space) => space.devicesSpaceEntity); - - return allDevices.map((device) => ({ uuid: device.uuid })); - } - - private async addUserPermissionsToDevices( - userUuid: string, - deviceUUIDs: { uuid: string }[], - ): Promise { - const permissionPromises = deviceUUIDs.map(async (device) => { - try { - await this.userDevicePermissionService.addUserPermission({ - userUuid, - deviceUuid: device.uuid, - permissionType: PermissionType.CONTROLLABLE, - }); - } catch (error) { - console.error( - `Failed to add permission for device ${device.uuid}: ${error.message}`, - ); - } - }); - - await Promise.all(permissionPromises); - } -} diff --git a/src/unit/unit.module.ts b/src/unit/unit.module.ts deleted file mode 100644 index 7ecd965..0000000 --- a/src/unit/unit.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UnitService } from './services/unit.service'; -import { UnitController } from './controllers/unit.controller'; -import { ConfigModule } from '@nestjs/config'; -import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { SpaceTypeRepository } from '@app/common/modules/space/repositories'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { UserDevicePermissionService } from 'src/user-device-permission/services'; -import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; -import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; - -@Module({ - imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], - controllers: [UnitController], - providers: [ - UnitService, - SpaceRepository, - SpaceTypeRepository, - UserSpaceRepository, - UserRepository, - UserDevicePermissionService, - DeviceUserPermissionRepository, - PermissionTypeRepository, - ], - exports: [UnitService], -}) -export class UnitModule {} diff --git a/src/user-device-permission/controllers/user-device-permission.controller.ts b/src/user-device-permission/controllers/user-device-permission.controller.ts index 5ff5284..7d33b7f 100644 --- a/src/user-device-permission/controllers/user-device-permission.controller.ts +++ b/src/user-device-permission/controllers/user-device-permission.controller.ts @@ -9,17 +9,18 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiTags, ApiOperation } from '@nestjs/swagger'; import { UserDevicePermissionService } from '../services/user-device-permission.service'; import { UserDevicePermissionAddDto } from '../dtos/user-device-permission.add.dto'; import { UserDevicePermissionEditDto } from '../dtos/user-device-permission.edit.dto'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Device Permission Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'device-permission', + path: ControllerRoute.DEVICE_PERMISSION.ROUTE, }) export class UserDevicePermissionController { constructor( @@ -29,6 +30,11 @@ export class UserDevicePermissionController { @ApiBearerAuth() @UseGuards(AdminRoleGuard) @Post() + @ApiOperation({ + summary: ControllerRoute.DEVICE_PERMISSION.ACTIONS.ADD_PERMISSION_SUMMARY, + description: + ControllerRoute.DEVICE_PERMISSION.ACTIONS.ADD_PERMISSION_DESCRIPTION, + }) async addDevicePermission( @Body() userDevicePermissionDto: UserDevicePermissionAddDto, ) { @@ -45,6 +51,11 @@ export class UserDevicePermissionController { @ApiBearerAuth() @UseGuards(AdminRoleGuard) @Put(':devicePermissionUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE_PERMISSION.ACTIONS.EDIT_PERMISSION_SUMMARY, + description: + ControllerRoute.DEVICE_PERMISSION.ACTIONS.EDIT_PERMISSION_DESCRIPTION, + }) async editDevicePermission( @Param('devicePermissionUuid') devicePermissionUuid: string, @Body() userDevicePermissionEditDto: UserDevicePermissionEditDto, @@ -62,6 +73,11 @@ export class UserDevicePermissionController { @ApiBearerAuth() @UseGuards(AdminRoleGuard) @Get(':deviceUuid') + @ApiOperation({ + summary: ControllerRoute.DEVICE_PERMISSION.ACTIONS.FETCH_PERMISSION_SUMMARY, + description: + ControllerRoute.DEVICE_PERMISSION.ACTIONS.FETCH_PERMISSION_DESCRIPTION, + }) async fetchDevicePermission(@Param('deviceUuid') deviceUuid: string) { const deviceDetails = await this.userDevicePermissionService.fetchUserPermission(deviceUuid); @@ -71,9 +87,16 @@ export class UserDevicePermissionController { data: deviceDetails, }; } + @ApiBearerAuth() @UseGuards(AdminRoleGuard) @Delete(':devicePermissionUuid') + @ApiOperation({ + summary: + ControllerRoute.DEVICE_PERMISSION.ACTIONS.DELETE_PERMISSION_SUMMARY, + description: + ControllerRoute.DEVICE_PERMISSION.ACTIONS.DELETE_PERMISSION_DESCRIPTION, + }) async deleteDevicePermission( @Param('devicePermissionUuid') devicePermissionUuid: string, ) { diff --git a/src/user-notification/controllers/user-notification.controller.ts b/src/user-notification/controllers/user-notification.controller.ts index 8fe8586..8e57122 100644 --- a/src/user-notification/controllers/user-notification.controller.ts +++ b/src/user-notification/controllers/user-notification.controller.ts @@ -8,7 +8,7 @@ import { Put, UseGuards, } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiTags, ApiOperation } from '@nestjs/swagger'; import { UserNotificationService } from '../services/user-notification.service'; import { UserNotificationAddDto, @@ -17,11 +17,12 @@ import { import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('User Notification Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'user-notification/subscription', + path: ControllerRoute.USER_NOTIFICATION.ROUTE, }) export class UserNotificationController { constructor( @@ -31,6 +32,11 @@ export class UserNotificationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() + @ApiOperation({ + summary: ControllerRoute.USER_NOTIFICATION.ACTIONS.ADD_SUBSCRIPTION_SUMMARY, + description: + ControllerRoute.USER_NOTIFICATION.ACTIONS.ADD_SUBSCRIPTION_DESCRIPTION, + }) async addUserSubscription( @Body() userNotificationAddDto: UserNotificationAddDto, ) { @@ -47,6 +53,12 @@ export class UserNotificationController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':userUuid') + @ApiOperation({ + summary: + ControllerRoute.USER_NOTIFICATION.ACTIONS.FETCH_SUBSCRIPTIONS_SUMMARY, + description: + ControllerRoute.USER_NOTIFICATION.ACTIONS.FETCH_SUBSCRIPTIONS_DESCRIPTION, + }) async fetchUserSubscriptions(@Param('userUuid') userUuid: string) { const userDetails = await this.userNotificationService.fetchUserSubscriptions(userUuid); @@ -56,9 +68,16 @@ export class UserNotificationController { data: { ...userDetails }, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put() + @ApiOperation({ + summary: + ControllerRoute.USER_NOTIFICATION.ACTIONS.UPDATE_SUBSCRIPTION_SUMMARY, + description: + ControllerRoute.USER_NOTIFICATION.ACTIONS.UPDATE_SUBSCRIPTION_DESCRIPTION, + }) async updateUserSubscription( @Body() userNotificationUpdateDto: UserNotificationUpdateDto, ) { diff --git a/src/users/controllers/index.ts b/src/users/controllers/index.ts index edd3705..f79349e 100644 --- a/src/users/controllers/index.ts +++ b/src/users/controllers/index.ts @@ -1 +1,2 @@ export * from './user.controller'; +export * from './user-space.controller'; diff --git a/src/users/controllers/user-space.controller.ts b/src/users/controllers/user-space.controller.ts new file mode 100644 index 0000000..aed945a --- /dev/null +++ b/src/users/controllers/user-space.controller.ts @@ -0,0 +1,71 @@ +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { UserSpaceService } from '../services'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { AddUserSpaceUsingCodeDto, UserParamDto } from '../dtos'; + +@ApiTags('User Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.USER_SPACE.ROUTE, +}) +export class UserSpaceController { + constructor(private readonly userSpaceService: UserSpaceService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + @ApiOperation({ + summary: ControllerRoute.USER_SPACE.ACTIONS.GET_USER_SPACES_SUMMARY, + description: ControllerRoute.USER_SPACE.ACTIONS.GET_USER_SPACES_DESCRIPTION, + }) + async getSpacesForUser( + @Param() params: UserParamDto, + ): Promise { + return this.userSpaceService.getSpacesForUser(params.userUuid); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('/verify-code') + @ApiOperation({ + summary: + ControllerRoute.USER_SPACE.ACTIONS.VERIFY_CODE_AND_ADD_USER_SPACE_SUMMARY, + description: + ControllerRoute.USER_SPACE.ACTIONS + .VERIFY_CODE_AND_ADD_USER_SPACE_DESCRIPTION, + }) + async verifyCodeAndAddUserSpace( + @Body() dto: AddUserSpaceUsingCodeDto, + @Param() params: UserParamDto, + ) { + try { + await this.userSpaceService.verifyCodeAndAddUserSpace( + dto, + params.userUuid, + ); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user space added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/src/users/controllers/user.controller.ts b/src/users/controllers/user.controller.ts index 503f47c..6f0eded 100644 --- a/src/users/controllers/user.controller.ts +++ b/src/users/controllers/user.controller.ts @@ -9,7 +9,7 @@ import { UseGuards, } from '@nestjs/common'; import { UserService } from '../services/user.service'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { UpdateNameDto, @@ -20,11 +20,12 @@ import { import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('User Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'user', + path: ControllerRoute.USER.ROUTE, }) export class UserController { constructor(private readonly userService: UserService) {} @@ -32,12 +33,22 @@ export class UserController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':userUuid') + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.GET_USER_DETAILS_SUMMARY, + description: ControllerRoute.USER.ACTIONS.GET_USER_DETAILS_DESCRIPTION, + }) async getUserDetailsByUserUuid(@Param('userUuid') userUuid: string) { return await this.userService.getUserDetailsByUserUuid(userUuid); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard, CheckProfilePictureGuard) @Put('/profile-picture/:userUuid') + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.UPDATE_PROFILE_PICTURE_SUMMARY, + description: + ControllerRoute.USER.ACTIONS.UPDATE_PROFILE_PICTURE_DESCRIPTION, + }) async updateProfilePictureByUserUuid( @Param('userUuid') userUuid: string, @Body() updateProfilePictureDataDto: UpdateProfilePictureDataDto, @@ -49,13 +60,18 @@ export class UserController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'profile picture updated successfully', + message: 'Profile picture updated successfully', data: userData, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('/region/:userUuid') + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.UPDATE_REGION_SUMMARY, + description: ControllerRoute.USER.ACTIONS.UPDATE_REGION_DESCRIPTION, + }) async updateRegionByUserUuid( @Param('userUuid') userUuid: string, @Body() updateRegionDataDto: UpdateRegionDataDto, @@ -67,14 +83,19 @@ export class UserController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'region updated successfully', + message: 'Region updated successfully', data: userData, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('/timezone/:userUuid') - async updateNameByUserUuid( + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.UPDATE_TIMEZONE_SUMMARY, + description: ControllerRoute.USER.ACTIONS.UPDATE_TIMEZONE_DESCRIPTION, + }) + async updateTimezoneByUserUuid( @Param('userUuid') userUuid: string, @Body() updateTimezoneDataDto: UpdateTimezoneDataDto, ) { @@ -85,14 +106,19 @@ export class UserController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'timezone updated successfully', + message: 'Timezone updated successfully', data: userData, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('/name/:userUuid') - async updateTimezoneByUserUuid( + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.UPDATE_NAME_SUMMARY, + description: ControllerRoute.USER.ACTIONS.UPDATE_NAME_DESCRIPTION, + }) + async updateNameByUserUuid( @Param('userUuid') userUuid: string, @Body() updateNameDto: UpdateNameDto, ) { @@ -103,13 +129,18 @@ export class UserController { return { statusCode: HttpStatus.CREATED, success: true, - message: 'name updated successfully', + message: 'Name updated successfully', data: userData, }; } + @ApiBearerAuth() @UseGuards(SuperAdminRoleGuard) @Delete('/:userUuid') + @ApiOperation({ + summary: ControllerRoute.USER.ACTIONS.DELETE_USER_SUMMARY, + description: ControllerRoute.USER.ACTIONS.DELETE_USER_DESCRIPTION, + }) async userDelete(@Param('userUuid') userUuid: string) { await this.userService.deleteUser(userUuid); return { @@ -117,7 +148,7 @@ export class UserController { data: { userUuid, }, - message: 'User Deleted Successfully', + message: 'User deleted successfully', }; } } diff --git a/src/users/dtos/add.space.dto.ts b/src/users/dtos/add.space.dto.ts new file mode 100644 index 0000000..635cec9 --- /dev/null +++ b/src/users/dtos/add.space.dto.ts @@ -0,0 +1,68 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +export class AddSpaceDto { + @ApiProperty({ + description: 'Name of the space (e.g., Floor 1, Unit 101)', + example: 'Unit 101', + }) + @IsString() + @IsNotEmpty() + spaceName: string; + + @ApiProperty({ + description: 'UUID of the parent space (if any, for hierarchical spaces)', + example: 'f5d7e9c3-44bc-4b12-88f1-1b3cda84752e', + required: false, + }) + @IsUUID() + @IsOptional() + parentUuid?: string; + + @ApiProperty({ + description: 'Indicates whether the space is private or public', + example: false, + default: false, + }) + @IsBoolean() + isPrivate: boolean; +} + +export class AddUserSpaceDto { + @ApiProperty({ + description: 'spaceUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public spaceUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} + +export class AddUserSpaceUsingCodeDto { + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/users/dtos/index.ts b/src/users/dtos/index.ts index 37a7007..83bb739 100644 --- a/src/users/dtos/index.ts +++ b/src/users/dtos/index.ts @@ -1 +1,4 @@ export * from './update.user.dto'; +export * from './user-community-param.dto'; +export * from './user-param.dto'; +export * from './add.space.dto'; diff --git a/src/users/dtos/user-community-param.dto.ts b/src/users/dtos/user-community-param.dto.ts new file mode 100644 index 0000000..806d64f --- /dev/null +++ b/src/users/dtos/user-community-param.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; +import { UserParamDto } from './user-param.dto'; + +export class UserCommunityParamDto extends UserParamDto { + @ApiProperty({ + description: 'UUID of the community', + example: 'a9b2c7f5-4d6e-423d-a24d-6f9c758b1b92', + }) + @IsUUID() + communityUuid: string; +} diff --git a/src/users/dtos/user-param.dto.ts b/src/users/dtos/user-param.dto.ts new file mode 100644 index 0000000..323dd93 --- /dev/null +++ b/src/users/dtos/user-param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class UserParamDto { + @ApiProperty({ + description: 'UUID of the user', + example: 'e7e8ddf5-3f7d-4e3d-bf3e-8c745b9b7d2c', + }) + @IsUUID() + userUuid: string; +} diff --git a/src/users/services/index.ts b/src/users/services/index.ts index e17ee5c..c22eedd 100644 --- a/src/users/services/index.ts +++ b/src/users/services/index.ts @@ -1 +1,2 @@ export * from './user.service'; +export * from './user-space.service'; diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts new file mode 100644 index 0000000..1c7ac6e --- /dev/null +++ b/src/users/services/user-space.service.ts @@ -0,0 +1,153 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { UserSpaceRepository } from '@app/common/modules/user/repositories'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { AddUserSpaceDto, AddUserSpaceUsingCodeDto } from '../dtos'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { SpaceEntity } from '@app/common/modules/space/entities'; + +@Injectable() +export class UserSpaceService { + constructor( + private readonly userSpaceRepository: UserSpaceRepository, + private readonly spaceRepository: SpaceRepository, + private readonly userDevicePermissionService: UserDevicePermissionService, + ) {} + + async getSpacesForUser(userUuid: string): Promise { + let userSpaces = await this.userSpaceRepository.find({ + where: { user: { uuid: userUuid } }, + relations: ['space', 'space.community'], + }); + + if (!userSpaces || userSpaces.length === 0) { + userSpaces = []; + } + + return new SuccessResponseDto({ + data: userSpaces, + message: `Spaces for user ${userUuid} retrieved successfully`, + }); + } + + async verifyCodeAndAddUserSpace( + params: AddUserSpaceUsingCodeDto, + userUuid: string, + ) { + try { + const space = await this.findSpaceByInviteCode(params.inviteCode); + + await this.addUserToSpace(userUuid, space.uuid); + + await this.clearUnitInvitationCode(space.uuid); + + const deviceUUIDs = await this.getDeviceUUIDsForSpace(space.uuid); + + await this.addUserPermissionsToDevices(userUuid, deviceUUIDs); + } catch (err) { + if (err instanceof HttpException) { + throw err; + } else { + throw new HttpException( + `An unexpected error occurred: ${err.message}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } + + private async findSpaceByInviteCode( + inviteCode: string, + ): Promise { + try { + const space = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: inviteCode, + }, + }); + return space; + } catch (error) { + throw new HttpException( + 'Space with the provided invite code not found', + HttpStatus.NOT_FOUND, + ); + } + } + + private async addUserToSpace(userUuid: string, spaceUuid: string) { + try { + const user = await this.addUserSpace({ userUuid, spaceUuid }); + + return user; + } catch (error) { + throw new HttpException( + `An error occurred while adding user to space ${spaceUuid} : ${error.message}}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async addUserSpace(addUserSpaceDto: AddUserSpaceDto) { + try { + return await this.userSpaceRepository.save({ + user: { uuid: addUserSpaceDto.userUuid }, + space: { uuid: addUserSpaceDto.spaceUuid }, + }); + } catch (err) { + if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { + throw new HttpException( + 'User already belongs to this unit', + HttpStatus.BAD_REQUEST, + ); + } + throw new HttpException( + err.message || 'Internal Server Error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async clearUnitInvitationCode(spaceUuid: string) { + await this.spaceRepository.update( + { uuid: spaceUuid }, + { invitationCode: null }, + ); + } + + private async getDeviceUUIDsForSpace( + unitUuid: string, + ): Promise<{ uuid: string }[]> { + const devices = await this.spaceRepository.find({ + where: { uuid: unitUuid }, + relations: ['devices', 'devices.productDevice'], + }); + + const allDevices = devices.flatMap((space) => space.devices); + + return allDevices.map((device) => ({ uuid: device.uuid })); + } + + private async addUserPermissionsToDevices( + userUuid: string, + deviceUUIDs: { uuid: string }[], + ): Promise { + const permissionPromises = deviceUUIDs.map(async (device) => { + try { + await this.userDevicePermissionService.addUserPermission({ + userUuid, + deviceUuid: device.uuid, + permissionType: PermissionType.CONTROLLABLE, + }); + } catch (error) { + console.error( + `Failed to add permission for device ${device.uuid}: ${error.message}`, + ); + } + }); + + await Promise.all(permissionPromises); + } +} diff --git a/src/users/user.module.ts b/src/users/user.module.ts index 6cd6d80..20bd06f 100644 --- a/src/users/user.module.ts +++ b/src/users/user.module.ts @@ -2,18 +2,36 @@ import { Module } from '@nestjs/common'; import { UserService } from './services/user.service'; import { UserController } from './controllers/user.controller'; import { ConfigModule } from '@nestjs/config'; -import { UserRepository } from '@app/common/modules/user/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; import { RegionRepository } from '@app/common/modules/region/repositories'; import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; +import { UserSpaceController } from './controllers'; +import { CommunityModule } from 'src/community/community.module'; +import { UserSpaceService } from './services'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ - imports: [ConfigModule], - controllers: [UserController], + imports: [ConfigModule, CommunityModule], + controllers: [UserController, UserSpaceController], providers: [ UserService, UserRepository, RegionRepository, + SpaceRepository, TimeZoneRepository, + UserSpaceRepository, + CommunityRepository, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, + UserSpaceService, ], exports: [UserService], }) diff --git a/src/vistor-password/controllers/visitor-password.controller.ts b/src/vistor-password/controllers/visitor-password.controller.ts index 894b759..55276be 100644 --- a/src/vistor-password/controllers/visitor-password.controller.ts +++ b/src/vistor-password/controllers/visitor-password.controller.ts @@ -8,7 +8,7 @@ import { Get, Req, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddDoorLockOfflineMultipleDto, AddDoorLockOfflineOneTimeDto, @@ -17,19 +17,29 @@ import { } from '../dtos/temp-pass.dto'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; @ApiTags('Visitor Password Module') @Controller({ version: EnableDisableStatusEnum.ENABLED, - path: 'visitor-password', + path: ControllerRoute.VISITOR_PASSWORD.ROUTE, }) export class VisitorPasswordController { constructor( private readonly visitorPasswordService: VisitorPasswordService, ) {} + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/online/multiple-time') + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_ONLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION, + }) async addOnlineTemporaryPasswordMultipleTime( @Body() addDoorLockOnlineMultipleDto: AddDoorLockOnlineMultipleDto, @Req() req: any, @@ -46,9 +56,18 @@ export class VisitorPasswordController { data: temporaryPasswords, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/online/one-time') + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_ONLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION, + }) async addOnlineTemporaryPassword( @Body() addDoorLockOnlineOneTimeDto: AddDoorLockOnlineOneTimeDto, @Req() req: any, @@ -65,9 +84,18 @@ export class VisitorPasswordController { data: temporaryPasswords, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/one-time') + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_OFFLINE_TEMP_PASSWORD_ONE_TIME_DESCRIPTION, + }) async addOfflineOneTimeTemporaryPassword( @Body() addDoorLockOfflineOneTimeDto: AddDoorLockOfflineOneTimeDto, @Req() req: any, @@ -84,9 +112,18 @@ export class VisitorPasswordController { data: temporaryPassword, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('temporary-password/offline/multiple-time') + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS + .ADD_OFFLINE_TEMP_PASSWORD_MULTIPLE_TIME_DESCRIPTION, + }) async addOfflineMultipleTimeTemporaryPassword( @Body() addDoorLockOfflineMultipleDto: AddDoorLockOfflineMultipleDto, @@ -104,15 +141,29 @@ export class VisitorPasswordController { data: temporaryPassword, }; } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get() + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_PASSWORD_DESCRIPTION, + }) async GetVisitorPassword() { return await this.visitorPasswordService.getPasswords(); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('/devices') + @ApiOperation({ + summary: + ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_SUMMARY, + description: + ControllerRoute.VISITOR_PASSWORD.ACTIONS.GET_VISITOR_DEVICES_DESCRIPTION, + }) async GetVisitorDevices() { return await this.visitorPasswordService.getAllPassDevices(); } diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 990e1e7..e768ad5 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -13,6 +13,13 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status import { SpaceRepository } from '@app/common/modules/space/repositories'; import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { SceneService } from 'src/scene/services'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -27,6 +34,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log DeviceRepository, VisitorPasswordRepository, DeviceStatusLogRepository, + TuyaService, + SceneService, + SceneIconRepository, + SceneRepository, + SceneDeviceRepository, ], exports: [VisitorPasswordService], })