mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 07:07:21 +00:00
Merge branch 'dev'
This commit is contained in:
@ -5,4 +5,5 @@ export class AuthInterface {
|
||||
sessionId: string;
|
||||
id: number;
|
||||
role?: object;
|
||||
project?: object;
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export class AuthService {
|
||||
email,
|
||||
region: regionUuid ? { uuid: regionUuid } : undefined,
|
||||
},
|
||||
relations: ['roleType'],
|
||||
relations: ['roleType', 'project'],
|
||||
});
|
||||
if (
|
||||
platform === PlatformType.WEB &&
|
||||
@ -77,21 +77,28 @@ export class AuthService {
|
||||
return await this.sessionRepository.save(data);
|
||||
}
|
||||
|
||||
async getTokens(payload) {
|
||||
async getTokens(
|
||||
payload,
|
||||
isRefreshToken = true,
|
||||
accessTokenExpiry = '24h',
|
||||
refreshTokenExpiry = '30d',
|
||||
) {
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: '24h',
|
||||
}),
|
||||
this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: '7d',
|
||||
expiresIn: accessTokenExpiry,
|
||||
}),
|
||||
isRefreshToken
|
||||
? this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get<string>('JWT_SECRET'),
|
||||
expiresIn: refreshTokenExpiry,
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
...(isRefreshToken ? { refreshToken } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,6 +112,7 @@ export class AuthService {
|
||||
googleCode: user.googleCode,
|
||||
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
|
||||
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
|
||||
project: user?.project,
|
||||
};
|
||||
if (payload.googleCode) {
|
||||
const profile = await this.getProfile(payload.googleCode);
|
||||
|
@ -32,6 +32,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
uuid: payload.uuid,
|
||||
sessionId: payload.sessionId,
|
||||
role: payload?.role,
|
||||
project: payload?.project,
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestException('Unauthorized');
|
||||
|
@ -35,6 +35,7 @@ export class RefreshTokenStrategy extends PassportStrategy(
|
||||
uuid: payload.uuid,
|
||||
sessionId: payload.sessionId,
|
||||
role: payload?.role,
|
||||
project: payload?.project,
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestException('Unauthorized');
|
||||
|
@ -19,5 +19,7 @@ export default registerAs(
|
||||
process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID,
|
||||
MAILTRAP_EDIT_USER_TEMPLATE_UUID:
|
||||
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
|
||||
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
|
||||
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
|
||||
}),
|
||||
);
|
||||
|
4
libs/common/src/constants/batch-device.enum.ts
Normal file
4
libs/common/src/constants/batch-device.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum BatchDeviceTypeEnum {
|
||||
RESET = 'RESET',
|
||||
COMMAND = 'COMMAND',
|
||||
}
|
@ -1,4 +1,17 @@
|
||||
export class ControllerRoute {
|
||||
static CLIENT = class {
|
||||
public static readonly ROUTE = 'client';
|
||||
|
||||
static ACTIONS = class {
|
||||
public static readonly REGISTER_NEW_CLIENT_SUMMARY =
|
||||
'Register a new client';
|
||||
public static readonly REGISTER_NEW_CLIENT_DESCRIPTION =
|
||||
'This endpoint registers a new client in the system.';
|
||||
public static readonly LOGIN_CLIENT_SUMMARY = 'Login a client';
|
||||
public static readonly LOGIN_CLIENT_DESCRIPTION =
|
||||
'This endpoint allows a client to log in to the system.';
|
||||
};
|
||||
};
|
||||
static PROJECT = class {
|
||||
public static readonly ROUTE = 'projects';
|
||||
static ACTIONS = class {
|
||||
@ -30,6 +43,11 @@ export class ControllerRoute {
|
||||
'Get user by uuid in project';
|
||||
public static readonly GET_USER_BY_UUID_IN_PROJECT_DESCRIPTION =
|
||||
'This endpoint retrieves a user by their unique identifier (UUID) associated with a specific project.';
|
||||
|
||||
public static readonly EXPORT_STRUCTURE_CSV_SUMMARY =
|
||||
'Export project with their full structure to a CSV file';
|
||||
public static readonly EXPORT_STRUCTURE_CSV_DESCRIPTION =
|
||||
'This endpoint exports project along with their associated communities, spaces, and nested space hierarchy into a downloadable CSV file. Useful for backups, reports, or audits';
|
||||
};
|
||||
};
|
||||
static PROJECT_USER = class {
|
||||
@ -327,6 +345,9 @@ export class ControllerRoute {
|
||||
static PRODUCT = class {
|
||||
public static readonly ROUTE = 'products';
|
||||
static ACTIONS = class {
|
||||
public static readonly CREATE_PRODUCT_SUMMARY = 'Create a new product';
|
||||
public static readonly CREATE_PRODUCT_DESCRIPTION =
|
||||
'This endpoint allows you to create a new product in the system.';
|
||||
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';
|
||||
@ -469,18 +490,23 @@ export class ControllerRoute {
|
||||
'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 PowerClamp = class {
|
||||
public static readonly ROUTE = 'power-clamp';
|
||||
|
||||
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_ENERGY_SUMMARY =
|
||||
'Get power clamp historical data';
|
||||
public static readonly GET_ENERGY_DESCRIPTION =
|
||||
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
|
||||
};
|
||||
};
|
||||
static DEVICE = class {
|
||||
public static readonly ROUTE = 'devices';
|
||||
|
||||
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.';
|
||||
static ACTIONS = class {
|
||||
public static readonly ADD_DEVICE_SUMMARY = 'Add new device';
|
||||
public static readonly ADD_DEVICE_DESCRIPTION =
|
||||
'This endpoint adds a new device in the system.';
|
||||
|
||||
public static readonly GET_DEVICES_BY_SPACE_UUID_SUMMARY =
|
||||
'Get devices by space UUID';
|
||||
@ -552,21 +578,29 @@ export class ControllerRoute {
|
||||
'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)';
|
||||
'Add scene to device';
|
||||
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)';
|
||||
'Get scenes by device';
|
||||
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)';
|
||||
'Delete scenes by device uuid and switch name';
|
||||
public static readonly DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION =
|
||||
'This endpoint deletes all scenes associated with a specific switch device.';
|
||||
};
|
||||
};
|
||||
static DEVICE_COMMISSION = class {
|
||||
public static readonly ROUTE = '/projects/:projectUuid/devices/commission';
|
||||
|
||||
static ACTIONS = class {
|
||||
public static readonly ADD_ALL_DEVICES_SUMMARY = 'Add all devices';
|
||||
public static readonly ADD_ALL_DEVICES_DESCRIPTION =
|
||||
'This endpoint add all devices in the system from tuya.';
|
||||
};
|
||||
};
|
||||
static DEVICE_PROJECT = class {
|
||||
public static readonly ROUTE = '/projects/:projectUuid/devices';
|
||||
static ACTIONS = class {
|
||||
@ -624,18 +658,13 @@ export class ControllerRoute {
|
||||
};
|
||||
|
||||
static AUTOMATION = class {
|
||||
public static readonly ROUTE = 'automation';
|
||||
public static readonly ROUTE = '/projects/:projectUuid/automations';
|
||||
|
||||
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 =
|
||||
@ -656,6 +685,17 @@ export class ControllerRoute {
|
||||
};
|
||||
};
|
||||
|
||||
static AUTOMATION_SPACE = class {
|
||||
public static readonly ROUTE =
|
||||
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/automations';
|
||||
static ACTIONS = class {
|
||||
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.';
|
||||
};
|
||||
};
|
||||
|
||||
static DOOR_LOCK = class {
|
||||
public static readonly ROUTE = 'door-lock';
|
||||
|
||||
@ -715,31 +755,15 @@ export class ControllerRoute {
|
||||
};
|
||||
};
|
||||
static VISITOR_PASSWORD = class {
|
||||
public static readonly ROUTE = 'visitor-password';
|
||||
public static readonly ROUTE = 'visitor-passwords';
|
||||
public static readonly PROJECT_ROUTE =
|
||||
'/projects/:projectUuid/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 ADD_VISITOR_PASSWORD_SUMMARY =
|
||||
'Add visitor password';
|
||||
public static readonly ADD_VISITOR_PASSWORD_DESCRIPTION =
|
||||
'This endpoint allows you to add a visitor password based on the operation type.';
|
||||
public static readonly GET_VISITOR_PASSWORD_SUMMARY =
|
||||
'Get visitor passwords';
|
||||
public static readonly GET_VISITOR_PASSWORD_DESCRIPTION =
|
||||
|
3
libs/common/src/constants/device-type.enum.ts
Normal file
3
libs/common/src/constants/device-type.enum.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum DeviceTypeEnum {
|
||||
DOOR_LOCK = 'DOOR_LOCK',
|
||||
}
|
6
libs/common/src/constants/power.clamp.enargy.enum.ts
Normal file
6
libs/common/src/constants/power.clamp.enargy.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum PowerClampEnergyEnum {
|
||||
ENERGY_CONSUMED = 'EnergyConsumed',
|
||||
ENERGY_CONSUMED_A = 'EnergyConsumedA',
|
||||
ENERGY_CONSUMED_B = 'EnergyConsumedB',
|
||||
ENERGY_CONSUMED_C = 'EnergyConsumedC',
|
||||
}
|
@ -3,12 +3,14 @@ import { RoleType } from './role.type.enum';
|
||||
export const RolePermissions = {
|
||||
[RoleType.SUPER_ADMIN]: [
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
'DEVICE_UPDATE',
|
||||
'DEVICE_BATCH_CONTROL',
|
||||
'DEVICE_LOCATION_VIEW',
|
||||
'DEVICE_LOCATION_UPDATE',
|
||||
'DEVICE_ADD',
|
||||
'COMMUNITY_VIEW',
|
||||
'COMMUNITY_ADD',
|
||||
'COMMUNITY_UPDATE',
|
||||
@ -53,12 +55,16 @@ export const RolePermissions = {
|
||||
'VISITOR_PASSWORD_DELETE',
|
||||
'USER_ADD',
|
||||
'SPACE_MEMBER_ADD',
|
||||
'COMMISSION_DEVICE',
|
||||
'PRODUCT_ADD',
|
||||
],
|
||||
[RoleType.ADMIN]: [
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
'DEVICE_UPDATE',
|
||||
'DEVICE_ADD',
|
||||
'DEVICE_BATCH_CONTROL',
|
||||
'DEVICE_LOCATION_VIEW',
|
||||
'DEVICE_LOCATION_UPDATE',
|
||||
@ -106,6 +112,8 @@ export const RolePermissions = {
|
||||
'VISITOR_PASSWORD_DELETE',
|
||||
'USER_ADD',
|
||||
'SPACE_MEMBER_ADD',
|
||||
'COMMISSION_DEVICE',
|
||||
'PRODUCT_ADD',
|
||||
],
|
||||
[RoleType.SPACE_MEMBER]: [
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
@ -121,6 +129,7 @@ export const RolePermissions = {
|
||||
'SCENES_CONTROL',
|
||||
],
|
||||
[RoleType.SPACE_OWNER]: [
|
||||
'COMMISSION_DEVICE',
|
||||
'DEVICE_SINGLE_CONTROL',
|
||||
'DEVICE_VIEW',
|
||||
'DEVICE_DELETE',
|
||||
@ -163,5 +172,6 @@ export const RolePermissions = {
|
||||
'VISITOR_PASSWORD_DELETE',
|
||||
'USER_ADD',
|
||||
'SPACE_MEMBER_ADD',
|
||||
'COMMISSION_DEVICE',
|
||||
],
|
||||
};
|
||||
|
2
libs/common/src/constants/sql-query-path.ts
Normal file
2
libs/common/src/constants/sql-query-path.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const SQL_QUERIES_PATH = 'libs/common/src/sql/queries';
|
||||
export const SQL_PROCEDURES_PATH = 'libs/common/src/sql/procedures';
|
6
libs/common/src/constants/visitor-password.enum.ts
Normal file
6
libs/common/src/constants/visitor-password.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum VisitorPasswordEnum {
|
||||
ONLINE_ONE_TIME = 'ONLINE_ONE_TIME',
|
||||
ONLINE_MULTIPLE_TIME = 'ONLINE_MULTIPLE_TIME',
|
||||
OFFLINE_ONE_TIME = 'OFFLINE_ONE_TIME',
|
||||
OFFLINE_MULTIPLE_TIME = 'OFFLINE_MULTIPLE_TIME',
|
||||
}
|
8
libs/common/src/context/request-context.ts
Normal file
8
libs/common/src/context/request-context.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
export interface RequestContextStore {
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export const requestContext = new AsyncLocalStorage<RequestContextStore>();
|
@ -42,72 +42,92 @@ import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity';
|
||||
import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
||||
import { TagEntity } from '../modules/space/entities/tag.entity';
|
||||
import { ClientEntity } from '../modules/client/entities';
|
||||
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
|
||||
import { createLogger } from 'winston';
|
||||
import { winstonLoggerOptions } from '../logger/services/winston.logger';
|
||||
import {
|
||||
PowerClampDailyEntity,
|
||||
PowerClampHourlyEntity,
|
||||
PowerClampMonthlyEntity,
|
||||
} from '../modules/power-clamp/entities/power-clamp.entity';
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
name: 'default',
|
||||
type: 'postgres',
|
||||
host: configService.get('DB_HOST'),
|
||||
port: configService.get('DB_PORT'),
|
||||
username: configService.get('DB_USER'),
|
||||
password: configService.get('DB_PASSWORD'),
|
||||
database: configService.get('DB_NAME'),
|
||||
entities: [
|
||||
NewTagEntity,
|
||||
ProjectEntity,
|
||||
UserEntity,
|
||||
UserSessionEntity,
|
||||
UserOtpEntity,
|
||||
ProductEntity,
|
||||
DeviceUserPermissionEntity,
|
||||
DeviceEntity,
|
||||
PermissionTypeEntity,
|
||||
CommunityEntity,
|
||||
SpaceEntity,
|
||||
SpaceLinkEntity,
|
||||
SubspaceEntity,
|
||||
TagEntity,
|
||||
UserSpaceEntity,
|
||||
DeviceUserPermissionEntity,
|
||||
RoleTypeEntity,
|
||||
UserNotificationEntity,
|
||||
DeviceNotificationEntity,
|
||||
RegionEntity,
|
||||
TimeZoneEntity,
|
||||
VisitorPasswordEntity,
|
||||
DeviceStatusLogEntity,
|
||||
SceneEntity,
|
||||
SceneIconEntity,
|
||||
SceneDeviceEntity,
|
||||
SpaceModelEntity,
|
||||
SubspaceModelEntity,
|
||||
TagModel,
|
||||
InviteUserEntity,
|
||||
InviteUserSpaceEntity,
|
||||
InviteSpaceEntity,
|
||||
AutomationEntity,
|
||||
SpaceModelProductAllocationEntity,
|
||||
SubspaceModelProductAllocationEntity,
|
||||
SpaceProductAllocationEntity,
|
||||
SubspaceProductAllocationEntity,
|
||||
],
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||
logging: false,
|
||||
extra: {
|
||||
charset: 'utf8mb4',
|
||||
max: 20, // set pool max size
|
||||
idleTimeoutMillis: 5000, // close idle clients after 5 second
|
||||
connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established
|
||||
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
|
||||
},
|
||||
continuationLocalStorage: true,
|
||||
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))),
|
||||
}),
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const winstonLogger = createLogger(winstonLoggerOptions);
|
||||
const typeOrmLogger = new TypeOrmWinstonLogger(winstonLogger);
|
||||
return {
|
||||
name: 'default',
|
||||
type: 'postgres',
|
||||
host: configService.get('DB_HOST'),
|
||||
port: configService.get('DB_PORT'),
|
||||
username: configService.get('DB_USER'),
|
||||
password: configService.get('DB_PASSWORD'),
|
||||
database: configService.get('DB_NAME'),
|
||||
entities: [
|
||||
NewTagEntity,
|
||||
ProjectEntity,
|
||||
UserEntity,
|
||||
UserSessionEntity,
|
||||
UserOtpEntity,
|
||||
ProductEntity,
|
||||
DeviceUserPermissionEntity,
|
||||
DeviceEntity,
|
||||
PermissionTypeEntity,
|
||||
CommunityEntity,
|
||||
SpaceEntity,
|
||||
SpaceLinkEntity,
|
||||
SubspaceEntity,
|
||||
TagEntity,
|
||||
UserSpaceEntity,
|
||||
DeviceUserPermissionEntity,
|
||||
RoleTypeEntity,
|
||||
UserNotificationEntity,
|
||||
DeviceNotificationEntity,
|
||||
RegionEntity,
|
||||
TimeZoneEntity,
|
||||
VisitorPasswordEntity,
|
||||
DeviceStatusLogEntity,
|
||||
SceneEntity,
|
||||
SceneIconEntity,
|
||||
SceneDeviceEntity,
|
||||
SpaceModelEntity,
|
||||
SubspaceModelEntity,
|
||||
TagModel,
|
||||
InviteUserEntity,
|
||||
InviteUserSpaceEntity,
|
||||
InviteSpaceEntity,
|
||||
AutomationEntity,
|
||||
SpaceModelProductAllocationEntity,
|
||||
SubspaceModelProductAllocationEntity,
|
||||
SpaceProductAllocationEntity,
|
||||
SubspaceProductAllocationEntity,
|
||||
ClientEntity,
|
||||
PowerClampHourlyEntity,
|
||||
PowerClampDailyEntity,
|
||||
PowerClampMonthlyEntity,
|
||||
],
|
||||
namingStrategy: new SnakeNamingStrategy(),
|
||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||
logging: ['query', 'error', 'warn', 'schema', 'migration'],
|
||||
|
||||
logger: typeOrmLogger,
|
||||
extra: {
|
||||
charset: 'utf8mb4',
|
||||
max: 20, // set pool max size
|
||||
idleTimeoutMillis: 5000, // close idle clients after 5 second
|
||||
connectionTimeoutMillis: 12_000, // return an error after 11 second if connection could not be established
|
||||
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
|
||||
},
|
||||
continuationLocalStorage: true,
|
||||
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))),
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [TypeOrmWinstonLogger],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
|
12
libs/common/src/dto/community-space.param.ts
Normal file
12
libs/common/src/dto/community-space.param.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ProjectParam } from './project-param.dto';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class CommunityParam extends ProjectParam {
|
||||
@ApiProperty({
|
||||
description: 'UUID of the community this space belongs to',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
})
|
||||
@IsUUID()
|
||||
communityUuid: string;
|
||||
}
|
12
libs/common/src/dto/get.space.param.ts
Normal file
12
libs/common/src/dto/get.space.param.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { CommunityParam } from './community-space.param';
|
||||
|
||||
export class GetSpaceParam extends CommunityParam {
|
||||
@ApiProperty({
|
||||
description: 'UUID of the Space',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
})
|
||||
@IsUUID()
|
||||
spaceUuid: string;
|
||||
}
|
13
libs/common/src/dto/pagination-with-search.request.dto.ts
Normal file
13
libs/common/src/dto/pagination-with-search.request.dto.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { PaginationRequestGetListDto } from './pagination.request.dto';
|
||||
|
||||
export class PaginationRequestWithSearchGetListDto extends PaginationRequestGetListDto {
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
name: 'search',
|
||||
required: false,
|
||||
description: 'Search for community or space name',
|
||||
})
|
||||
search?: string;
|
||||
}
|
@ -1,11 +1,24 @@
|
||||
import { IsDate, IsOptional } from 'class-validator';
|
||||
import { IsBoolean, 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';
|
||||
import { BooleanValues } from '../constants/boolean-values.enum';
|
||||
|
||||
export class PaginationRequestGetListDto {
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: 'include spaces',
|
||||
required: false,
|
||||
default: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Transform((value) => {
|
||||
return value.obj.includeSpaces === BooleanValues.TRUE;
|
||||
})
|
||||
public includeSpaces?: boolean = false;
|
||||
@IsOptional()
|
||||
@IsPageRequestParam({
|
||||
message: 'Page must be bigger than 0',
|
||||
|
@ -3,12 +3,24 @@ import { DeviceStatusFirebaseController } from './controllers/devices-status.con
|
||||
import { DeviceStatusFirebaseService } from './services/devices-status.service';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories/device-status.repository';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
import {
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
} from '@app/common/modules/power-clamp/repositories';
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
DeviceStatusFirebaseService,
|
||||
DeviceRepository,
|
||||
DeviceStatusLogRepository,
|
||||
PowerClampService,
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
SqlLoaderService,
|
||||
],
|
||||
controllers: [DeviceStatusFirebaseController],
|
||||
exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository],
|
||||
|
@ -18,6 +18,9 @@ import {
|
||||
runTransaction,
|
||||
} from 'firebase/database';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import { ProductType } from '@app/common/constants/product-type.enum';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum';
|
||||
@Injectable()
|
||||
export class DeviceStatusFirebaseService {
|
||||
private tuya: TuyaContext;
|
||||
@ -25,6 +28,7 @@ export class DeviceStatusFirebaseService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly deviceRepository: DeviceRepository,
|
||||
private readonly powerClampService: PowerClampService,
|
||||
private deviceStatusLogRepository: DeviceStatusLogRepository,
|
||||
) {
|
||||
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
||||
@ -79,6 +83,7 @@ export class DeviceStatusFirebaseService {
|
||||
return await this.createDeviceStatusFirebase({
|
||||
deviceUuid: device.uuid,
|
||||
...addDeviceStatusDto,
|
||||
productType: device.productDevice.prodType,
|
||||
});
|
||||
}
|
||||
// Return null if device not found or no UUID
|
||||
@ -216,6 +221,25 @@ export class DeviceStatusFirebaseService {
|
||||
});
|
||||
await this.deviceStatusLogRepository.save(newLogs);
|
||||
|
||||
if (addDeviceStatusDto.productType === ProductType.PC) {
|
||||
const energyCodes = new Set([
|
||||
PowerClampEnergyEnum.ENERGY_CONSUMED,
|
||||
PowerClampEnergyEnum.ENERGY_CONSUMED_A,
|
||||
PowerClampEnergyEnum.ENERGY_CONSUMED_B,
|
||||
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
|
||||
]);
|
||||
|
||||
const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) =>
|
||||
energyCodes.has(status.code),
|
||||
);
|
||||
|
||||
if (energyStatus) {
|
||||
await this.powerClampService.updateEnergyConsumedHistoricalData(
|
||||
addDeviceStatusDto.deviceUuid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the updated data
|
||||
const snapshot: DataSnapshot = await get(dataRef);
|
||||
return snapshot.val();
|
||||
|
@ -12,6 +12,7 @@ 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';
|
||||
import { SosHandlerService } from './services/sos.handler.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@ -25,6 +26,7 @@ import { CommunityRepository } from '../modules/community/repositories';
|
||||
DeviceMessagesService,
|
||||
DeviceNotificationRepository,
|
||||
CommunityRepository,
|
||||
SosHandlerService,
|
||||
],
|
||||
exports: [
|
||||
HelperHashService,
|
||||
|
61
libs/common/src/helper/services/power.clamp.service.ts
Normal file
61
libs/common/src/helper/services/power.clamp.service.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SqlLoaderService } from './sql-loader.service';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
|
||||
|
||||
@Injectable()
|
||||
export class PowerClampService {
|
||||
constructor(
|
||||
private readonly sqlLoader: SqlLoaderService,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async updateEnergyConsumedHistoricalData(deviceUuid: string): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD
|
||||
const hour = now.getHours();
|
||||
const monthYear = now
|
||||
.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
.replace('/', '-'); // MM-YYYY
|
||||
|
||||
await this.executeProcedure('fact_hourly_energy_consumed_procedure', [
|
||||
deviceUuid,
|
||||
dateStr,
|
||||
hour,
|
||||
]);
|
||||
|
||||
await this.executeProcedure('fact_daily_energy_consumed_procedure', [
|
||||
deviceUuid,
|
||||
dateStr,
|
||||
]);
|
||||
|
||||
await this.executeProcedure('fact_monthly_energy_consumed_procedure', [
|
||||
deviceUuid,
|
||||
monthYear,
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Failed to insert or update energy data:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeProcedure(
|
||||
procedureFileName: string,
|
||||
params: (string | number | null)[],
|
||||
): Promise<void> {
|
||||
const query = this.loadQuery(procedureFileName);
|
||||
await this.dataSource.query(query, params);
|
||||
}
|
||||
|
||||
private loadQuery(fileName: string): string {
|
||||
return this.sqlLoader.loadQuery(
|
||||
'fact_energy_consumed',
|
||||
fileName,
|
||||
SQL_PROCEDURES_PATH,
|
||||
);
|
||||
}
|
||||
}
|
42
libs/common/src/helper/services/sos.handler.service.ts
Normal file
42
libs/common/src/helper/services/sos.handler.service.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||
|
||||
@Injectable()
|
||||
export class SosHandlerService {
|
||||
private readonly logger = new Logger(SosHandlerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
|
||||
) {}
|
||||
|
||||
isSosTriggered(status: any): boolean {
|
||||
return (
|
||||
Array.isArray(status) &&
|
||||
status.some((item) => item.code === 'sos' && item.value === 'sos')
|
||||
);
|
||||
}
|
||||
|
||||
async handleSosEvent(devId: string, logData: any): Promise<void> {
|
||||
try {
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: true }],
|
||||
log: logData,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
|
||||
deviceTuyaUuid: devId,
|
||||
status: [{ code: 'sos', value: false }],
|
||||
log: logData,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to send SOS false value', err);
|
||||
}
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to send SOS true value', err);
|
||||
}
|
||||
}
|
||||
}
|
22
libs/common/src/helper/services/sql-loader.service.ts
Normal file
22
libs/common/src/helper/services/sql-loader.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class SqlLoaderService {
|
||||
private readonly logger = new Logger(SqlLoaderService.name);
|
||||
private readonly sqlRootPath = join(__dirname, '../sql/queries');
|
||||
|
||||
loadQuery(module: string, queryName: string, path: string): string {
|
||||
const filePath = join(process.cwd(), path, module, `${queryName}.sql`);
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to load SQL query: ${module}/${queryName}`,
|
||||
error.stack,
|
||||
);
|
||||
throw new Error(`SQL query not found: ${module}/${queryName}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import TuyaWebsocket from '../../config/tuya-web-socket-config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||
import { SosHandlerService } from './sos.handler.service';
|
||||
|
||||
@Injectable()
|
||||
export class TuyaWebSocketService {
|
||||
@ -11,6 +12,7 @@ export class TuyaWebSocketService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
|
||||
private readonly sosHandlerService: SosHandlerService,
|
||||
) {
|
||||
this.isDevEnv =
|
||||
this.configService.get<string>('NODE_ENV') === 'development';
|
||||
@ -42,11 +44,15 @@ export class TuyaWebSocketService {
|
||||
try {
|
||||
const { devId, status, logData } = this.extractMessageData(message);
|
||||
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
|
||||
deviceTuyaUuid: devId,
|
||||
status: status,
|
||||
log: logData,
|
||||
});
|
||||
if (this.sosHandlerService.isSosTriggered(status)) {
|
||||
await this.sosHandlerService.handleSosEvent(devId, logData);
|
||||
} else {
|
||||
await this.deviceStatusFirebaseService.addDeviceStatusToFirebase({
|
||||
deviceTuyaUuid: devId,
|
||||
status: status,
|
||||
log: logData,
|
||||
});
|
||||
}
|
||||
|
||||
this.client.ackMessage(message.messageId);
|
||||
} catch (error) {
|
||||
|
12
libs/common/src/logger/logger.module.ts
Normal file
12
libs/common/src/logger/logger.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// src/common/logger/logger.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import { winstonLoggerOptions } from './services/winston.logger';
|
||||
import { TypeOrmWinstonLogger } from './services/typeorm.logger';
|
||||
|
||||
@Module({
|
||||
imports: [WinstonModule.forRoot(winstonLoggerOptions)],
|
||||
providers: [TypeOrmWinstonLogger],
|
||||
exports: [TypeOrmWinstonLogger],
|
||||
})
|
||||
export class LoggerModule {}
|
72
libs/common/src/logger/services/typeorm.logger.ts
Normal file
72
libs/common/src/logger/services/typeorm.logger.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Logger as WinstonLogger } from 'winston';
|
||||
import { Logger as TypeOrmLogger } from 'typeorm';
|
||||
import { requestContext } from '@app/common/context/request-context';
|
||||
|
||||
const ERROR_THRESHOLD = 2000;
|
||||
|
||||
export class TypeOrmWinstonLogger implements TypeOrmLogger {
|
||||
constructor(private readonly logger: WinstonLogger) {}
|
||||
|
||||
private getContext() {
|
||||
const context = requestContext.getStore();
|
||||
return {
|
||||
requestId: context?.requestId ?? 'N/A',
|
||||
userId: context?.userId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
private extractTable(query: string): string {
|
||||
const match =
|
||||
query.match(/from\s+["`]?(\w+)["`]?/i) ||
|
||||
query.match(/into\s+["`]?(\w+)["`]?/i);
|
||||
return match?.[1] ?? 'unknown';
|
||||
}
|
||||
|
||||
logQuery(query: string, parameters?: any[]) {
|
||||
const context = this.getContext();
|
||||
this.logger.debug(`[DB][QUERY] ${query}`, {
|
||||
...context,
|
||||
table: this.extractTable(query),
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
logQueryError(error: string | Error, query: string, parameters?: any[]) {
|
||||
const context = this.getContext();
|
||||
this.logger.error(`[DB][ERROR] ${query}`, {
|
||||
...context,
|
||||
table: this.extractTable(query),
|
||||
parameters,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
logQuerySlow(time: number, query: string, parameters?: any[]) {
|
||||
const context = this.getContext();
|
||||
const severity = time > ERROR_THRESHOLD ? 'error' : 'warn';
|
||||
const label = severity === 'error' ? 'VERY SLOW' : 'SLOW';
|
||||
|
||||
this.logger[severity](`[DB][${label} > ${time}ms] ${query}`, {
|
||||
...context,
|
||||
table: this.extractTable(query),
|
||||
parameters,
|
||||
duration: `${time}ms`,
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
logSchemaBuild(message: string) {
|
||||
this.logger.info(`[DB][SCHEMA] ${message}`);
|
||||
}
|
||||
|
||||
logMigration(message: string) {
|
||||
this.logger.info(`[DB][MIGRATION] ${message}`);
|
||||
}
|
||||
|
||||
log(level: 'log' | 'info' | 'warn', message: any) {
|
||||
this.logger.log({
|
||||
level,
|
||||
message: `[DB] ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
26
libs/common/src/logger/services/winston.logger.ts
Normal file
26
libs/common/src/logger/services/winston.logger.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
|
||||
export const winstonLoggerOptions: winston.LoggerOptions = {
|
||||
level:
|
||||
process.env.AZURE_POSTGRESQL_DATABASE === 'development' ? 'debug' : 'error',
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
nestWinstonModuleUtilities.format.nestLike('MyApp', {
|
||||
prettyPrint: true,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
format: winston.format.json(),
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log',
|
||||
format: winston.format.json(),
|
||||
}),
|
||||
],
|
||||
};
|
14
libs/common/src/middleware/request-context.middleware.ts
Normal file
14
libs/common/src/middleware/request-context.middleware.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { requestContext } from '../context/request-context';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class RequestContextMiddleware implements NestMiddleware {
|
||||
use(req: any, res: any, next: () => void) {
|
||||
const context = {
|
||||
requestId: req.headers['x-request-id'] || uuidv4(),
|
||||
};
|
||||
|
||||
requestContext.run(context, () => next());
|
||||
}
|
||||
}
|
@ -16,7 +16,14 @@ export interface TypeORMCustomModelFindAllQuery {
|
||||
where?: { [key: string]: unknown };
|
||||
select?: string[];
|
||||
includeDisable?: boolean | string;
|
||||
includeSpaces?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtendedTypeORMCustomModelFindAllQuery
|
||||
extends TypeORMCustomModelFindAllQuery {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
interface CustomFindAllQuery {
|
||||
page?: number;
|
||||
size?: number;
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ClientEntity } from './entities';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ClientEntity])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class ClientRepositoryModule {}
|
20
libs/common/src/modules/client/dtos/client.dto.ts
Normal file
20
libs/common/src/modules/client/dtos/client.dto.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ClientDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public clientId: string;
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public clientSecret: string;
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public redirectUri: string;
|
||||
@IsArray()
|
||||
@IsNotEmpty()
|
||||
public scopes: string[];
|
||||
}
|
1
libs/common/src/modules/client/dtos/index.ts
Normal file
1
libs/common/src/modules/client/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './client.dto';
|
46
libs/common/src/modules/client/entities/client.entity.ts
Normal file
46
libs/common/src/modules/client/entities/client.entity.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Entity, Column, Unique, OneToMany } from 'typeorm';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { ClientDto } from '../dtos';
|
||||
import { UserEntity } from '../../user/entities';
|
||||
|
||||
@Entity({ name: 'clients' })
|
||||
@Unique(['clientId'])
|
||||
export class ClientEntity extends AbstractEntity<ClientDto> {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
default: () => 'gen_random_uuid()',
|
||||
nullable: false,
|
||||
})
|
||||
public uuid: string;
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
nullable: false,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
clientId: string;
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
nullable: false,
|
||||
})
|
||||
clientSecret: string;
|
||||
|
||||
@Column({
|
||||
length: 255,
|
||||
nullable: false,
|
||||
})
|
||||
redirectUri: string;
|
||||
|
||||
@Column('simple-array')
|
||||
scopes: string[];
|
||||
|
||||
@OneToMany(() => UserEntity, (user) => user.client)
|
||||
users: UserEntity[];
|
||||
}
|
1
libs/common/src/modules/client/entities/index.ts
Normal file
1
libs/common/src/modules/client/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './client.entity';
|
@ -0,0 +1,10 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClientEntity } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
export class ClientRepository extends Repository<ClientEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(ClientEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
1
libs/common/src/modules/client/repositories/index.ts
Normal file
1
libs/common/src/modules/client/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './client.repository';
|
@ -17,6 +17,7 @@ import { SceneDeviceEntity } from '../../scene-device/entities';
|
||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
|
||||
import { NewTagEntity } from '../../tag';
|
||||
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
|
||||
|
||||
@Entity({ name: 'device' })
|
||||
@Unique(['deviceTuyaUuid'])
|
||||
@ -32,10 +33,10 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
|
||||
type: 'boolean',
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false })
|
||||
user: UserEntity;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
name: string;
|
||||
@OneToMany(
|
||||
() => DeviceUserPermissionEntity,
|
||||
(permission) => permission.device,
|
||||
@ -78,8 +79,9 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
|
||||
|
||||
@OneToMany(() => NewTagEntity, (tag) => tag.devices)
|
||||
// @JoinTable({ name: 'device_tags' })
|
||||
public tag: NewTagEntity[];
|
||||
|
||||
public tag: NewTagEntity;
|
||||
@OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device)
|
||||
powerClampHourly: PowerClampHourlyEntity[];
|
||||
constructor(partial: Partial<DeviceEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
1
libs/common/src/modules/power-clamp/dtos/index.ts
Normal file
1
libs/common/src/modules/power-clamp/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './power-clamp.dto';
|
43
libs/common/src/modules/power-clamp/dtos/power-clamp.dto.ts
Normal file
43
libs/common/src/modules/power-clamp/dtos/power-clamp.dto.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class PowerClampDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public deviceUuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public hour?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public day?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public month?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public energyConsumedKw: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public energyConsumedA: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public energyConsumedB: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public energyConsumedC: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public prodType: string;
|
||||
}
|
1
libs/common/src/modules/power-clamp/entities/index.ts
Normal file
1
libs/common/src/modules/power-clamp/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './power-clamp.entity';
|
@ -0,0 +1,95 @@
|
||||
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { PowerClampDto } from '../dtos';
|
||||
import { DeviceEntity } from '../../device/entities/device.entity';
|
||||
|
||||
@Entity({ name: 'power-clamp-energy-consumed-hourly' })
|
||||
@Unique(['deviceUuid', 'date', 'hour'])
|
||||
export class PowerClampHourlyEntity extends AbstractEntity<PowerClampDto> {
|
||||
@Column({ nullable: false })
|
||||
public deviceUuid: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public hour: string;
|
||||
|
||||
@Column({ nullable: false, type: 'date' })
|
||||
public date: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public energyConsumedKw: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedA: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedB: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedC: string;
|
||||
|
||||
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
|
||||
device: DeviceEntity;
|
||||
|
||||
constructor(partial: Partial<PowerClampHourlyEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
@Entity({ name: 'power-clamp-energy-consumed-daily' })
|
||||
@Unique(['deviceUuid', 'date'])
|
||||
export class PowerClampDailyEntity extends AbstractEntity<PowerClampDto> {
|
||||
@Column({ nullable: false })
|
||||
public deviceUuid: string;
|
||||
|
||||
@Column({ nullable: false, type: 'date' })
|
||||
public date: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public energyConsumedKw: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedA: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedB: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedC: string;
|
||||
|
||||
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
|
||||
device: DeviceEntity;
|
||||
|
||||
constructor(partial: Partial<PowerClampHourlyEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
@Entity({ name: 'power-clamp-energy-consumed-monthly' })
|
||||
@Unique(['deviceUuid', 'month'])
|
||||
export class PowerClampMonthlyEntity extends AbstractEntity<PowerClampDto> {
|
||||
@Column({ nullable: false })
|
||||
public deviceUuid: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public month: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public energyConsumedKw: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedA: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedB: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
public energyConsumedC: string;
|
||||
|
||||
@ManyToOne(() => DeviceEntity, (device) => device.powerClampHourly)
|
||||
device: DeviceEntity;
|
||||
|
||||
constructor(partial: Partial<PowerClampHourlyEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PowerClampHourlyEntity } from './entities/power-clamp.entity';
|
||||
|
||||
@Module({
|
||||
providers: [],
|
||||
exports: [],
|
||||
controllers: [],
|
||||
imports: [TypeOrmModule.forFeature([PowerClampHourlyEntity])],
|
||||
})
|
||||
export class PowerClampRepositoryModule {}
|
@ -0,0 +1 @@
|
||||
export * from './power-clamp.repository';
|
@ -0,0 +1,28 @@
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
PowerClampDailyEntity,
|
||||
PowerClampHourlyEntity,
|
||||
PowerClampMonthlyEntity,
|
||||
} from '../entities/power-clamp.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PowerClampHourlyRepository extends Repository<PowerClampHourlyEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(PowerClampHourlyEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PowerClampDailyRepository extends Repository<PowerClampDailyEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(PowerClampDailyEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PowerClampMonthlyRepository extends Repository<PowerClampMonthlyEntity> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(PowerClampMonthlyEntity, dataSource.createEntityManager());
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import { SpaceLinkEntity } from './space-link.entity';
|
||||
import { SceneEntity } from '../../scene/entities';
|
||||
import { SpaceModelEntity } from '../../space-model';
|
||||
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
|
||||
import { TagEntity } from './tag.entity';
|
||||
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
|
||||
@ -103,9 +102,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
|
||||
)
|
||||
invitedUsers: InviteUserSpaceEntity[];
|
||||
|
||||
@OneToMany(() => TagEntity, (tag) => tag.space)
|
||||
tags: TagEntity[];
|
||||
|
||||
@OneToMany(
|
||||
() => SpaceProductAllocationEntity,
|
||||
(allocation) => allocation.space,
|
||||
|
@ -3,7 +3,6 @@ import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import { ProductEntity } from '../../product/entities';
|
||||
import { TagDto } from '../dtos';
|
||||
import { TagModel } from '../../space-model/entities/tag-model.entity';
|
||||
import { SpaceEntity } from './space.entity';
|
||||
import { DeviceEntity } from '../../device/entities';
|
||||
import { SubspaceEntity } from './subspace/subspace.entity';
|
||||
|
||||
@ -22,9 +21,6 @@ export class TagEntity extends AbstractEntity<TagDto> {
|
||||
})
|
||||
product: ProductEntity;
|
||||
|
||||
@ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true })
|
||||
space: SpaceEntity;
|
||||
|
||||
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
|
||||
nullable: true,
|
||||
})
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
} from '../dtos';
|
||||
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
|
||||
import {
|
||||
DeviceEntity,
|
||||
DeviceNotificationEntity,
|
||||
DeviceUserPermissionEntity,
|
||||
} from '../../device/entities';
|
||||
@ -29,6 +28,7 @@ import { VisitorPasswordEntity } from '../../visitor-password/entities';
|
||||
import { InviteUserEntity } from '../../Invite-user/entities';
|
||||
import { ProjectEntity } from '../../project/entities';
|
||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||
import { ClientEntity } from '../../client/entities';
|
||||
|
||||
@Entity({ name: 'user' })
|
||||
export class UserEntity extends AbstractEntity<UserDto> {
|
||||
@ -97,9 +97,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
||||
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user)
|
||||
userSpaces: UserSpaceEntity[];
|
||||
|
||||
@OneToMany(() => DeviceEntity, (userDevice) => userDevice.user)
|
||||
userDevice: DeviceEntity[];
|
||||
|
||||
@OneToMany(
|
||||
() => UserNotificationEntity,
|
||||
(userNotification) => userNotification.user,
|
||||
@ -143,6 +140,13 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
||||
})
|
||||
@JoinColumn({ name: 'project_uuid' })
|
||||
public project: ProjectEntity;
|
||||
|
||||
@ManyToOne(() => ClientEntity, (client) => client.users, {
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'client_uuid' })
|
||||
public client: ClientEntity;
|
||||
|
||||
constructor(partial: Partial<UserEntity>) {
|
||||
super();
|
||||
Object.assign(this, partial);
|
||||
|
@ -0,0 +1,111 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$1::uuid AS device_id,
|
||||
$2::date AS target_date
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumed'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_A AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedA'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_B AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedB'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_C AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedC'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
final_data AS (
|
||||
SELECT
|
||||
t.device_id,
|
||||
t.date,
|
||||
t.event_year::text,
|
||||
t.event_month,
|
||||
t.hour,
|
||||
(t.max_value - t.min_value) AS energy_consumed_kW,
|
||||
(a.max_value - a.min_value) AS energy_consumed_A,
|
||||
(b.max_value - b.min_value) AS energy_consumed_B,
|
||||
(c.max_value - c.min_value) AS energy_consumed_C
|
||||
FROM total_energy t
|
||||
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
|
||||
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
|
||||
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
|
||||
)
|
||||
|
||||
-- Final upsert into daily table
|
||||
INSERT INTO public."power-clamp-energy-consumed-daily" (
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
date
|
||||
)
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
|
||||
date
|
||||
FROM final_data
|
||||
GROUP BY device_id, date
|
||||
ON CONFLICT (device_uuid, date) DO UPDATE
|
||||
SET
|
||||
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
|
||||
energy_consumed_a = EXCLUDED.energy_consumed_a,
|
||||
energy_consumed_b = EXCLUDED.energy_consumed_b,
|
||||
energy_consumed_c = EXCLUDED.energy_consumed_c;
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,113 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$1::uuid AS device_id,
|
||||
$2::date AS target_date,
|
||||
$3::text AS target_hour
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time)::text AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumed'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_A AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time)::text AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedA'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_B AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time)::text AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedB'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_C AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time)::text AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedC'
|
||||
AND log.device_id = params.device_id
|
||||
AND log.event_time::date = params.target_date
|
||||
AND EXTRACT(HOUR FROM log.event_time)::text = params.target_hour
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
final_data AS (
|
||||
SELECT
|
||||
t.device_id,
|
||||
t.date,
|
||||
t.event_year::text,
|
||||
t.event_month,
|
||||
t.hour,
|
||||
(t.max_value - t.min_value) AS energy_consumed_kW,
|
||||
(a.max_value - a.min_value) AS energy_consumed_A,
|
||||
(b.max_value - b.min_value) AS energy_consumed_B,
|
||||
(c.max_value - c.min_value) AS energy_consumed_C
|
||||
FROM total_energy t
|
||||
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
|
||||
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
|
||||
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
|
||||
)
|
||||
|
||||
-- UPSERT into hourly table
|
||||
INSERT INTO public."power-clamp-energy-consumed-hourly" (
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
date,
|
||||
hour
|
||||
)
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
|
||||
date,
|
||||
hour
|
||||
FROM final_data
|
||||
GROUP BY 1,6,7
|
||||
ON CONFLICT (device_uuid, date, hour) DO UPDATE
|
||||
SET
|
||||
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
|
||||
energy_consumed_a = EXCLUDED.energy_consumed_a,
|
||||
energy_consumed_b = EXCLUDED.energy_consumed_b,
|
||||
energy_consumed_c = EXCLUDED.energy_consumed_c;
|
@ -0,0 +1,107 @@
|
||||
WITH params AS (
|
||||
SELECT
|
||||
$1::uuid AS device_id,
|
||||
$2::text AS target_month -- Format should match 'MM-YYYY'
|
||||
),
|
||||
total_energy AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumed'
|
||||
AND log.device_id = params.device_id
|
||||
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_A AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedA'
|
||||
AND log.device_id = params.device_id
|
||||
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_B AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedB'
|
||||
AND log.device_id = params.device_id
|
||||
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
energy_phase_C AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log, params
|
||||
WHERE log.code = 'EnergyConsumedC'
|
||||
AND log.device_id = params.device_id
|
||||
AND TO_CHAR(log.event_time, 'MM-YYYY') = params.target_month
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
final_data AS (
|
||||
SELECT
|
||||
t.device_id,
|
||||
t.date,
|
||||
t.event_year::text,
|
||||
t.event_month,
|
||||
t.hour,
|
||||
(t.max_value - t.min_value) AS energy_consumed_kW,
|
||||
(a.max_value - a.min_value) AS energy_consumed_A,
|
||||
(b.max_value - b.min_value) AS energy_consumed_B,
|
||||
(c.max_value - c.min_value) AS energy_consumed_C
|
||||
FROM total_energy t
|
||||
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
|
||||
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
|
||||
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
|
||||
)
|
||||
|
||||
-- Final monthly UPSERT
|
||||
INSERT INTO public."power-clamp-energy-consumed-monthly" (
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
month
|
||||
)
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kW AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_A AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_B AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_C AS NUMERIC))::VARCHAR,
|
||||
event_month
|
||||
FROM final_data
|
||||
GROUP BY device_id, event_month
|
||||
ON CONFLICT (device_uuid, month) DO UPDATE
|
||||
SET
|
||||
energy_consumed_kw = EXCLUDED.energy_consumed_kw,
|
||||
energy_consumed_a = EXCLUDED.energy_consumed_a,
|
||||
energy_consumed_b = EXCLUDED.energy_consumed_b,
|
||||
energy_consumed_c = EXCLUDED.energy_consumed_c;
|
||||
|
@ -0,0 +1,12 @@
|
||||
select device_id ,
|
||||
product."name" as "device_type",
|
||||
event_time::date as date ,
|
||||
event_time::time as time,
|
||||
code ,
|
||||
value
|
||||
from "device-status-log" dsl
|
||||
join product
|
||||
on dsl.product_id = product.prod_id
|
||||
join device d
|
||||
on d."uuid" = dsl.device_id
|
||||
order by 1,3,4
|
5
libs/common/src/sql/queries/dim_date/dim_date.sql
Normal file
5
libs/common/src/sql/queries/dim_date/dim_date.sql
Normal file
@ -0,0 +1,5 @@
|
||||
SELECT generate_series(
|
||||
DATE '2024-01-01', -- Start date
|
||||
DATE '2065-12-31', -- End date
|
||||
INTERVAL '1 day' -- Step size
|
||||
)::DATE AS daily_date;
|
@ -0,0 +1,66 @@
|
||||
WITH start_date AS (
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.created_at,
|
||||
device.device_tuya_uuid,
|
||||
device.space_device_uuid AS space_id,
|
||||
"device-status-log".event_id,
|
||||
"device-status-log".event_time::timestamp,
|
||||
"device-status-log".code,
|
||||
"device-status-log".value,
|
||||
"device-status-log".log,
|
||||
LAG("device-status-log".event_time::timestamp)
|
||||
OVER (PARTITION BY device.uuid -- Partition only by device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||
LAG("device-status-log".value)
|
||||
OVER (PARTITION BY device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_value
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.cat_name = 'hps'
|
||||
AND "device-status-log".code = 'presence_state'
|
||||
ORDER BY device.uuid, "device-status-log".event_time
|
||||
),
|
||||
|
||||
time_differences AS (
|
||||
SELECT
|
||||
device_id,
|
||||
value,
|
||||
prev_value,
|
||||
event_time,
|
||||
prev_timestamp,
|
||||
event_time::date AS event_date,
|
||||
EXTRACT(EPOCH FROM (event_time - COALESCE(prev_timestamp, event_time))) AS time_diff_in_seconds
|
||||
FROM start_date
|
||||
),
|
||||
|
||||
|
||||
duration as (
|
||||
SELECT
|
||||
device_id,
|
||||
event_date,
|
||||
SUM(CASE WHEN prev_value = 'motion' THEN time_diff_in_seconds ELSE 0 END) AS motion_seconds,
|
||||
SUM(CASE WHEN prev_value = 'presence' THEN time_diff_in_seconds ELSE 0 END) AS presence_seconds,
|
||||
SUM(CASE WHEN prev_value = 'none' THEN time_diff_in_seconds ELSE 0 END) AS none_seconds
|
||||
FROM time_differences
|
||||
WHERE prev_timestamp::date=event_date
|
||||
GROUP BY device_id, event_date
|
||||
ORDER BY device_id, event_date)
|
||||
|
||||
|
||||
, data_final AS(
|
||||
select device_id,
|
||||
event_date,
|
||||
motion_seconds,
|
||||
CONCAT(FLOOR(motion_seconds / 3600), ':',LPAD(FLOOR((motion_seconds % 3600) / 60)::TEXT, 2, '0'), ':',LPAD(FLOOR(motion_seconds % 60)::TEXT, 2, '0')) AS motion_formatted_duration,
|
||||
presence_seconds,
|
||||
CONCAT(FLOOR(presence_seconds / 3600), ':',LPAD(FLOOR((presence_seconds % 3600) / 60)::TEXT, 2, '0'), ':',LPAD(FLOOR(presence_seconds % 60)::TEXT, 2, '0')) AS presence_formatted_duration,
|
||||
none_seconds,
|
||||
CONCAT(FLOOR(none_seconds / 3600), ':',LPAD(FLOOR((none_seconds % 3600) / 60)::TEXT, 2, '0'), ':',LPAD(FLOOR(none_seconds % 60)::TEXT, 2, '0')) AS none_formatted_duration
|
||||
from duration
|
||||
order by 1,2)
|
||||
|
||||
SELECT * FROM data_final
|
@ -0,0 +1,68 @@
|
||||
-- model shows the energy consumed per day per device
|
||||
|
||||
WITH total_energy AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='EnergyConsumed'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_A AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='EnergyConsumedA'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_B AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='EnergyConsumedB'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_C AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='EnergyConsumedC'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
|
||||
SELECT
|
||||
total_energy.device_id,
|
||||
total_energy.date,
|
||||
(total_energy.max_value-total_energy.min_value) as energy_consumed_kW,
|
||||
(energy_phase_A.max_value-energy_phase_A.min_value) as energy_consumed_A,
|
||||
(energy_phase_B.max_value-energy_phase_B.min_value) as energy_consumed_B,
|
||||
(energy_phase_C.max_value-energy_phase_C.min_value) as energy_consumed_C
|
||||
FROM total_energy
|
||||
JOIN energy_phase_A
|
||||
ON total_energy.device_id=energy_phase_A.device_id
|
||||
and total_energy.date=energy_phase_A.date
|
||||
JOIN energy_phase_B
|
||||
ON total_energy.device_id=energy_phase_B.device_id
|
||||
and total_energy.date=energy_phase_B.date
|
||||
JOIN energy_phase_C
|
||||
ON total_energy.device_id=energy_phase_C.device_id
|
||||
and total_energy.date=energy_phase_C.date;
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,91 @@
|
||||
-- Step 1: Get device presence events with previous timestamps
|
||||
WITH start_date AS (
|
||||
SELECT
|
||||
d.uuid AS device_id,
|
||||
d.space_device_uuid AS space_id,
|
||||
l.value,
|
||||
l.event_time::timestamp AS event_time,
|
||||
LAG(l.event_time::timestamp) OVER (PARTITION BY d.uuid ORDER BY l.event_time) AS prev_timestamp
|
||||
FROM device d
|
||||
LEFT JOIN "device-status-log" l
|
||||
ON d.uuid = l.device_id
|
||||
LEFT JOIN product p
|
||||
ON p.uuid = d.product_device_uuid
|
||||
WHERE p.cat_name = 'hps'
|
||||
AND l.code = 'presence_state'
|
||||
),
|
||||
|
||||
-- Step 2: Identify periods when device reports "none"
|
||||
device_none_periods AS (
|
||||
SELECT
|
||||
space_id,
|
||||
device_id,
|
||||
event_time AS empty_from,
|
||||
LEAD(event_time) OVER (PARTITION BY device_id ORDER BY event_time) AS empty_until
|
||||
FROM start_date
|
||||
WHERE value = 'none'
|
||||
),
|
||||
|
||||
-- Step 3: Clip the "none" periods to the edges of each day
|
||||
clipped_device_none_periods AS (
|
||||
SELECT
|
||||
space_id,
|
||||
GREATEST(empty_from, DATE_TRUNC('day', empty_from)) AS clipped_from,
|
||||
LEAST(empty_until, DATE_TRUNC('day', empty_until) + INTERVAL '1 day') AS clipped_until
|
||||
FROM device_none_periods
|
||||
WHERE empty_until IS NOT NULL
|
||||
),
|
||||
|
||||
-- Step 4: Break multi-day periods into daily intervals
|
||||
generated_daily_intervals AS (
|
||||
SELECT
|
||||
space_id,
|
||||
gs::date AS day,
|
||||
GREATEST(clipped_from, gs) AS interval_start,
|
||||
LEAST(clipped_until, gs + INTERVAL '1 day') AS interval_end
|
||||
FROM clipped_device_none_periods,
|
||||
LATERAL generate_series(DATE_TRUNC('day', clipped_from), DATE_TRUNC('day', clipped_until), INTERVAL '1 day') AS gs
|
||||
),
|
||||
|
||||
-- Step 5: Merge overlapping or adjacent intervals per day
|
||||
merged_intervals AS (
|
||||
SELECT
|
||||
space_id,
|
||||
day,
|
||||
interval_start,
|
||||
interval_end
|
||||
FROM (
|
||||
SELECT
|
||||
space_id,
|
||||
day,
|
||||
interval_start,
|
||||
interval_end,
|
||||
LAG(interval_end) OVER (PARTITION BY space_id, day ORDER BY interval_start) AS prev_end
|
||||
FROM generated_daily_intervals
|
||||
) sub
|
||||
WHERE prev_end IS NULL OR interval_start > prev_end
|
||||
),
|
||||
|
||||
-- Step 6: Sum up total missing seconds (device reported "none") per day
|
||||
missing_seconds_per_day AS (
|
||||
SELECT
|
||||
space_id,
|
||||
day AS missing_date,
|
||||
SUM(EXTRACT(EPOCH FROM (interval_end - interval_start))) AS total_missing_seconds
|
||||
FROM merged_intervals
|
||||
GROUP BY space_id, day
|
||||
),
|
||||
|
||||
-- Step 7: Calculate total occupied time per day (86400 - missing)
|
||||
occupied_seconds_per_day AS (
|
||||
SELECT
|
||||
space_id,
|
||||
missing_date as date,
|
||||
86400 - total_missing_seconds AS total_occupied_seconds
|
||||
FROM missing_seconds_per_day
|
||||
)
|
||||
|
||||
-- Final Output
|
||||
SELECT *
|
||||
FROM occupied_seconds_per_day
|
||||
ORDER BY 1,2;
|
@ -0,0 +1,15 @@
|
||||
select dup."uuid" as primary_key,
|
||||
dup.device_uuid,
|
||||
product.name,
|
||||
pt.type,
|
||||
dup.user_uuid as authorized_user_id,
|
||||
dup.created_at::date as permission_creation_date,
|
||||
dup.updated_at::date as permission_update_date
|
||||
from "device-user-permission" dup
|
||||
left join "permission-type" pt
|
||||
on dup.permission_type_uuid =pt."uuid"
|
||||
left join device
|
||||
on device."uuid" =dup.device_uuid
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid;
|
||||
|
@ -0,0 +1,65 @@
|
||||
-- model shows the energy consumed per day per device
|
||||
|
||||
WITH total_energy AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='ActivePower'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_A AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='ActivePowerA'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_B AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='ActivePowerB'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
, energy_phase_C AS (
|
||||
SELECT
|
||||
device_id,
|
||||
event_time::date AS date,
|
||||
MIN(value)::integer AS min_value,
|
||||
MAX(value)::integer AS max_value
|
||||
FROM "device-status-log"
|
||||
where code='ActivePowerC'
|
||||
GROUP BY device_id, date
|
||||
)
|
||||
|
||||
|
||||
SELECT
|
||||
total_energy.device_id,
|
||||
total_energy.date,
|
||||
(total_energy.max_value-total_energy.min_value) as energy_consumed_kW,
|
||||
(energy_phase_A.max_value-energy_phase_A.min_value) as energy_consumed_A,
|
||||
(energy_phase_B.max_value-energy_phase_B.min_value) as energy_consumed_B,
|
||||
(energy_phase_C.max_value-energy_phase_C.min_value) as energy_consumed_C
|
||||
FROM total_energy
|
||||
JOIN energy_phase_A
|
||||
ON total_energy.device_id=energy_phase_A.device_id
|
||||
and total_energy.date=energy_phase_A.date
|
||||
JOIN energy_phase_B
|
||||
ON total_energy.device_id=energy_phase_B.device_id
|
||||
and total_energy.date=energy_phase_B.date
|
||||
JOIN energy_phase_C
|
||||
ON total_energy.device_id=energy_phase_C.device_id
|
||||
and total_energy.date=energy_phase_C.date;
|
||||
|
@ -0,0 +1,75 @@
|
||||
--This model shows the number of times a presence was detected per hour, per day.
|
||||
|
||||
WITH device_logs AS (
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.created_at,
|
||||
device.device_tuya_uuid,
|
||||
device.space_device_uuid AS space_id,
|
||||
"device-status-log".event_id,
|
||||
"device-status-log".event_time::timestamp,
|
||||
"device-status-log".code,
|
||||
"device-status-log".value,
|
||||
"device-status-log".log,
|
||||
LAG("device-status-log".event_time::timestamp)
|
||||
OVER (PARTITION BY device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||
LAG("device-status-log".value)
|
||||
OVER (PARTITION BY device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_value
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.cat_name = 'hps'
|
||||
AND "device-status-log".code = 'presence_state'
|
||||
ORDER BY device.uuid, "device-status-log".event_time
|
||||
),
|
||||
|
||||
presence_detection AS (
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN value = 'motion' AND prev_value = 'none' THEN 1 ELSE 0
|
||||
END AS motion_detected,
|
||||
CASE
|
||||
WHEN value = 'presence' AND prev_value = 'none' THEN 1 ELSE 0
|
||||
END AS presence_detected
|
||||
FROM device_logs
|
||||
),
|
||||
|
||||
presence_detection_summary AS (
|
||||
SELECT
|
||||
pd.device_id,
|
||||
d.subspace_id,
|
||||
pd.space_id,
|
||||
pd.event_time::date AS event_date,
|
||||
EXTRACT(HOUR FROM date_trunc('hour', pd.event_time)) AS event_hour,
|
||||
SUM(motion_detected) AS count_motion_detected,
|
||||
SUM(presence_detected) AS count_presence_detected,
|
||||
SUM(motion_detected + presence_detected) AS count_total_presence_detected
|
||||
FROM presence_detection pd
|
||||
LEFT JOIN device d ON d.uuid = pd.device_id
|
||||
GROUP BY 1, 2, 3, 4, 5
|
||||
),
|
||||
|
||||
all_dates_and_hours AS (
|
||||
SELECT device_id, subspace_id, space_id, event_date, event_hour
|
||||
FROM (
|
||||
SELECT DISTINCT device_id, subspace_id, space_id, event_date
|
||||
FROM presence_detection_summary
|
||||
) d,
|
||||
generate_series(0, 23) AS event_hour
|
||||
)
|
||||
|
||||
SELECT
|
||||
adah.*,
|
||||
COALESCE(pds.count_motion_detected, 0) AS count_motion_detected,
|
||||
COALESCE(pds.count_presence_detected, 0) AS count_presence_detected,
|
||||
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
|
||||
FROM all_dates_and_hours adah
|
||||
LEFT JOIN presence_detection_summary pds
|
||||
ON pds.device_id = adah.device_id
|
||||
AND pds.event_date = adah.event_date
|
||||
AND pds.event_hour = adah.event_hour
|
||||
ORDER BY 1, 4, 5;
|
@ -0,0 +1,71 @@
|
||||
WITH total_energy AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumed'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_A AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedA'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_B AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedB'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_C AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedC'
|
||||
GROUP BY 1,2,3,4,5
|
||||
)
|
||||
|
||||
SELECT
|
||||
t.device_id,
|
||||
t.date,
|
||||
t.event_year::text,
|
||||
t.event_month,
|
||||
t.hour,
|
||||
(t.max_value - t.min_value) AS energy_consumed_kW,
|
||||
(a.max_value - a.min_value) AS energy_consumed_A,
|
||||
(b.max_value - b.min_value) AS energy_consumed_B,
|
||||
(c.max_value - c.min_value) AS energy_consumed_C
|
||||
FROM total_energy t
|
||||
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
|
||||
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
|
||||
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
|
||||
ORDER BY 1,2;
|
@ -0,0 +1,135 @@
|
||||
WITH total_energy AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumed'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_A AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedA'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_B AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedB'
|
||||
GROUP BY 1,2,3,4,5
|
||||
),
|
||||
|
||||
energy_phase_C AS (
|
||||
SELECT
|
||||
log.device_id,
|
||||
log.event_time::date AS date,
|
||||
EXTRACT(HOUR FROM log.event_time) AS hour,
|
||||
TO_CHAR(log.event_time, 'MM-YYYY') AS event_month,
|
||||
EXTRACT(YEAR FROM log.event_time)::int AS event_year,
|
||||
MIN(log.value)::integer AS min_value,
|
||||
MAX(log.value)::integer AS max_value
|
||||
FROM "device-status-log" log
|
||||
WHERE log.code = 'EnergyConsumedC'
|
||||
GROUP BY 1,2,3,4,5
|
||||
)
|
||||
, final_data as (
|
||||
SELECT
|
||||
t.device_id,
|
||||
t.date,
|
||||
t.event_year::text,
|
||||
t.event_month,
|
||||
t.hour,
|
||||
(t.max_value - t.min_value) AS energy_consumed_kW,
|
||||
(a.max_value - a.min_value) AS energy_consumed_A,
|
||||
(b.max_value - b.min_value) AS energy_consumed_B,
|
||||
(c.max_value - c.min_value) AS energy_consumed_C
|
||||
FROM total_energy t
|
||||
JOIN energy_phase_A a ON t.device_id = a.device_id AND t.date = a.date AND t.hour = a.hour
|
||||
JOIN energy_phase_B b ON t.device_id = b.device_id AND t.date = b.date AND t.hour = b.hour
|
||||
JOIN energy_phase_C c ON t.device_id = c.device_id AND t.date = c.date AND t.hour = c.hour
|
||||
ORDER BY 1,2)
|
||||
|
||||
|
||||
INSERT INTO public."power-clamp-energy-consumed-daily"(
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
date
|
||||
)
|
||||
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
|
||||
date
|
||||
FROM final_data
|
||||
GROUP BY device_id, date;
|
||||
|
||||
|
||||
|
||||
INSERT INTO public."power-clamp-energy-consumed-hourly"(
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
date,
|
||||
hour
|
||||
)
|
||||
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
|
||||
date,
|
||||
hour
|
||||
FROM final_data
|
||||
GROUP BY 1,6,7
|
||||
|
||||
|
||||
INSERT INTO public."power-clamp-energy-consumed-monthly"(
|
||||
device_uuid,
|
||||
energy_consumed_kw,
|
||||
energy_consumed_a,
|
||||
energy_consumed_b,
|
||||
energy_consumed_c,
|
||||
month
|
||||
)
|
||||
|
||||
SELECT
|
||||
device_id,
|
||||
SUM(CAST(energy_consumed_kw AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_a AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_b AS NUMERIC))::VARCHAR,
|
||||
SUM(CAST(energy_consumed_c AS NUMERIC))::VARCHAR,
|
||||
TO_CHAR(date, 'MM-YYYY')
|
||||
FROM final_data
|
||||
GROUP BY 1,6;
|
||||
|
@ -0,0 +1,79 @@
|
||||
WITH device_logs AS (
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.created_at,
|
||||
device.device_tuya_uuid,
|
||||
device.space_device_uuid AS space_id,
|
||||
"device-status-log".event_id,
|
||||
"device-status-log".event_time::timestamp AS event_time,
|
||||
"device-status-log".code,
|
||||
"device-status-log".value,
|
||||
"device-status-log".log,
|
||||
LAG("device-status-log".event_time::timestamp)
|
||||
OVER (PARTITION BY device.uuid ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||
LAG("device-status-log".value)
|
||||
OVER (PARTITION BY device.uuid ORDER BY "device-status-log".event_time) AS prev_value
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.cat_name = 'hps'
|
||||
AND "device-status-log".code = 'presence_state'
|
||||
),
|
||||
|
||||
presence_detection AS (
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN value IN ('motion', 'presence') AND prev_value = 'none' THEN 1 ELSE 0
|
||||
END AS presence_started
|
||||
FROM device_logs
|
||||
),
|
||||
|
||||
space_level_presence_events AS (
|
||||
SELECT DISTINCT
|
||||
pd.space_id,
|
||||
pd.event_time::date AS event_date,
|
||||
EXTRACT(HOUR FROM pd.event_time) AS event_hour,
|
||||
pd.event_time
|
||||
FROM presence_detection pd
|
||||
WHERE presence_started = 1
|
||||
),
|
||||
|
||||
space_level_presence_summary AS (
|
||||
SELECT
|
||||
space_id,
|
||||
event_date,
|
||||
event_hour,
|
||||
COUNT(*) AS count_total_presence_detected
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
space_id,
|
||||
event_date,
|
||||
event_hour,
|
||||
event_time
|
||||
FROM space_level_presence_events
|
||||
) deduped
|
||||
GROUP BY space_id, event_date, event_hour
|
||||
),
|
||||
|
||||
all_dates_and_hours AS (
|
||||
SELECT space_id, event_date, event_hour
|
||||
FROM (
|
||||
SELECT DISTINCT space_id, event_date
|
||||
FROM space_level_presence_summary
|
||||
) d
|
||||
CROSS JOIN generate_series(0, 23) AS event_hour
|
||||
)
|
||||
|
||||
SELECT
|
||||
adah.*,
|
||||
COALESCE(pds.count_total_presence_detected, 0) AS count_total_presence_detected
|
||||
FROM all_dates_and_hours adah
|
||||
LEFT JOIN space_level_presence_summary pds
|
||||
ON pds.space_id = adah.space_id
|
||||
AND pds.event_date = adah.event_date
|
||||
AND pds.event_hour = adah.event_hour
|
||||
ORDER BY space_id, event_date, event_hour;
|
||||
|
||||
|
@ -0,0 +1,78 @@
|
||||
-- This model gives the average hourly set and current temperatures per space, per device
|
||||
-- The only issue witht this model is that it does not represent 24 hours/device. which is normal when no changelog is being recorded.
|
||||
--Shall I fill the missing hours
|
||||
WITH avg_set_temp AS (-- average set temperature per device per hour
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.space_device_uuid AS space_id,
|
||||
event_time::date AS date,
|
||||
DATE_PART('hour', event_time) AS hour,
|
||||
AVG("device-status-log".value::INTEGER) AS avg_set_temp
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.name = 'Smart Thermostat'
|
||||
AND "device-status-log".code = 'temp_set'
|
||||
GROUP BY 1,2,3,4
|
||||
)
|
||||
|
||||
, avg_current_temp as (
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.space_device_uuid AS space_id,
|
||||
event_time::date AS date,
|
||||
DATE_PART('hour', event_time) AS hour,
|
||||
AVG("device-status-log".value::INTEGER) AS avg_current_temp
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.name = 'Smart Thermostat'
|
||||
AND "device-status-log".code = 'temp_current'
|
||||
GROUP BY 1,2,3,4
|
||||
)
|
||||
|
||||
, joined_data AS ( -- this will return null values for hours where there was no previously set temperature
|
||||
SELECT
|
||||
current_temp.device_id,
|
||||
current_temp.space_id,
|
||||
current_temp.date,
|
||||
current_temp.hour,
|
||||
set_temp.avg_set_temp,
|
||||
current_temp.avg_current_temp,
|
||||
ROW_NUMBER() OVER (PARTITION BY current_temp.device_id, current_temp.space_id ORDER BY current_temp.date, current_temp.hour) AS row_num
|
||||
FROM avg_current_temp AS current_temp
|
||||
LEFT JOIN avg_set_temp AS set_temp
|
||||
ON set_temp.device_id = current_temp.device_id
|
||||
AND set_temp.space_id = current_temp.space_id
|
||||
AND set_temp.date = current_temp.date
|
||||
AND set_temp.hour = current_temp.hour
|
||||
)
|
||||
|
||||
, filled_data AS (
|
||||
SELECT
|
||||
a.device_id,
|
||||
a.space_id,
|
||||
a.date,
|
||||
a.hour,
|
||||
COALESCE(
|
||||
a.avg_set_temp,
|
||||
(SELECT b.avg_set_temp
|
||||
FROM joined_data b
|
||||
WHERE b.device_id = a.device_id
|
||||
AND b.space_id = a.space_id
|
||||
AND b.row_num < a.row_num
|
||||
AND b.avg_set_temp IS NOT NULL
|
||||
ORDER BY b.row_num DESC
|
||||
LIMIT 1)
|
||||
) AS avg_set_temp,
|
||||
a.avg_current_temp
|
||||
FROM joined_data a
|
||||
)
|
||||
|
||||
SELECT *
|
||||
FROM filled_data
|
||||
ORDER BY 1,3,4;
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This model tracks the timestamp when a presence state went from no-presence --> presence detected, per device.
|
||||
* This model should be used to display the presence logs Talal requested on the platform
|
||||
*/
|
||||
|
||||
WITH device_logs AS (
|
||||
SELECT
|
||||
device.uuid AS device_id,
|
||||
device.created_at,
|
||||
device.device_tuya_uuid,
|
||||
device.space_device_uuid AS space_id,
|
||||
"device-status-log".event_id,
|
||||
"device-status-log".event_time::timestamp,
|
||||
"device-status-log".code,
|
||||
"device-status-log".value,
|
||||
"device-status-log".log,
|
||||
LAG("device-status-log".event_time::timestamp)
|
||||
OVER (PARTITION BY device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_timestamp,
|
||||
LAG("device-status-log".value)
|
||||
OVER (PARTITION BY device.uuid
|
||||
ORDER BY "device-status-log".event_time) AS prev_value
|
||||
FROM device
|
||||
LEFT JOIN "device-status-log"
|
||||
ON device.uuid = "device-status-log".device_id
|
||||
LEFT JOIN product
|
||||
ON product.uuid = device.product_device_uuid
|
||||
WHERE product.cat_name = 'hps' -- presence sensors
|
||||
AND "device-status-log".code = 'presence_state'
|
||||
ORDER BY device.uuid, "device-status-log".event_time
|
||||
)
|
||||
|
||||
, presence_detection AS (
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN value IN ('presence', 'motion') AND prev_value = 'none' THEN 1 -- detects a change in status from no presence to presence or motion
|
||||
ELSE 0
|
||||
END AS presence_detected
|
||||
FROM device_logs
|
||||
)
|
||||
|
||||
SELECT event_time as "time_presence_detected", device_id, space_id
|
||||
FROM presence_detection
|
||||
WHERE presence_detected=1
|
@ -0,0 +1,104 @@
|
||||
Category code,Description
|
||||
dj,Light
|
||||
xdd,Ceiling light
|
||||
fwd,Ambiance light
|
||||
dc,String lights
|
||||
dd,Strip lights
|
||||
gyd,Motion sensor light
|
||||
fsd,Ceiling fan light
|
||||
tyndj,Solar light
|
||||
tgq,Dimmer
|
||||
ykq,Remote control
|
||||
kg,Switch
|
||||
pc,Power strip
|
||||
cz,Socket
|
||||
cjkg,Scene switch
|
||||
ckqdkg,Card switch
|
||||
clkg,Curtain switch
|
||||
ckmkzq,Garage door opener
|
||||
tgkg,Dimmer switch
|
||||
rs,Water heater
|
||||
xfj,Ventilation system
|
||||
bx,Refrigerator
|
||||
yg,Bathtub
|
||||
xy,Washing machine
|
||||
kt,Air conditioner
|
||||
ktkzq,Air conditioner controller
|
||||
bgl,Wall-hung boiler
|
||||
sd,Robot vacuum
|
||||
qn,Heater
|
||||
kj,Air purifier
|
||||
lyj,Drying rack
|
||||
xxj,Diffuser
|
||||
cl,Curtain
|
||||
mc,Door/window controller
|
||||
wk,Thermostat
|
||||
yb,Bathroom heater
|
||||
ggq,Irrigator
|
||||
jsq,Humidifier
|
||||
cs,Dehumidifier
|
||||
fs,Fan
|
||||
js,Water purifier
|
||||
dr,Electric blanket
|
||||
cwtswsq,Pet treat feeder
|
||||
cwwqfsq,Pet ball thrower
|
||||
ntq,HVAC
|
||||
cwwsq,Pet feeder
|
||||
cwysj,Pet fountain
|
||||
sf,Sofa
|
||||
dbl,Electric fireplace
|
||||
tnq,Smart milk kettle
|
||||
msp,Cat toilet
|
||||
mjj,Towel rack
|
||||
sz,Smart indoor garden
|
||||
bh,Smart kettle
|
||||
mb,Bread maker
|
||||
kfj,Coffee maker
|
||||
nnq,Bottle warmer
|
||||
cn,Milk dispenser
|
||||
mzj,Sous vide cooker
|
||||
mg,Rice cabinet
|
||||
dcl,Induction cooker
|
||||
kqzg,Air fryer
|
||||
znfh,Bento box
|
||||
mal,Alarm host
|
||||
sp,Smart camera
|
||||
sgbj,Siren alarm
|
||||
zd,Vibration sensor
|
||||
mcs,Contact sensor
|
||||
rqbj,Gas alarm
|
||||
ywbj,Smoke alarm
|
||||
wsdcg,Temperature and humidity sensor
|
||||
sj,Water leak detector
|
||||
ylcg,Pressure sensor
|
||||
ldcg,Luminance sensor
|
||||
sos,Emergency button
|
||||
pm2.5,PM2.5 detector
|
||||
pir,Human motion sensor
|
||||
cobj,CO detector
|
||||
co2bj,CO2 detector
|
||||
dgnbj,Multi-functional alarm
|
||||
jwbj,Methane detector
|
||||
hps,Human presence sensor
|
||||
ms,Residential lock
|
||||
bxx,Safe box
|
||||
gyms,Business lock
|
||||
jtmspro,Residential lock pro
|
||||
hotelms,Hotel lock
|
||||
ms_category,Lock accessories
|
||||
jtmsbh,Smart lock (keep alive)
|
||||
mk,Access control
|
||||
videolock,Lock with camera
|
||||
photolock,Audio and video lock
|
||||
amy,Massage chair
|
||||
liliao,Physiotherapy product
|
||||
ts,Smart jump rope
|
||||
tzc1,Body fat scale
|
||||
sb,Watch/band
|
||||
zndb,Smart electricity meter
|
||||
znsb,Smart water meter
|
||||
dlq,Circuit breaker
|
||||
ds,TV set
|
||||
tyy,Projector
|
||||
tracker,Tracker
|
||||
znyh,Smart pill box
|
|
9
libs/common/src/type/express/index.d.ts
vendored
Normal file
9
libs/common/src/type/express/index.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import { File } from 'multer';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
file?: File;
|
||||
}
|
||||
}
|
||||
}
|
@ -181,6 +181,49 @@ export class EmailService {
|
||||
);
|
||||
}
|
||||
}
|
||||
async sendOtpEmailWithTemplate(
|
||||
email: string,
|
||||
emailEditData: any,
|
||||
): Promise<void> {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_TOKEN = this.configService.get<string>(
|
||||
'email-config.MAILTRAP_API_TOKEN',
|
||||
);
|
||||
const API_URL = isProduction
|
||||
? SEND_EMAIL_API_URL_PROD
|
||||
: SEND_EMAIL_API_URL_DEV;
|
||||
const TEMPLATE_UUID = this.configService.get<string>(
|
||||
'email-config.MAILTRAP_SEND_OTP_TEMPLATE_UUID',
|
||||
);
|
||||
|
||||
const emailData = {
|
||||
from: {
|
||||
email: this.smtpConfig.sender,
|
||||
},
|
||||
to: [
|
||||
{
|
||||
email: email,
|
||||
},
|
||||
],
|
||||
template_uuid: TEMPLATE_UUID,
|
||||
template_variables: emailEditData,
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.post(API_URL, emailData, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.response?.data?.message ||
|
||||
'Error sending email using Mailtrap template',
|
||||
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
generateUserChangesEmailBody(
|
||||
addedSpaceNames: string[],
|
||||
removedSpaceNames: string[],
|
||||
|
512
package-lock.json
generated
512
package-lock.json
generated
@ -9,6 +9,8 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "^5.0.2",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
@ -17,6 +19,8 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@nestjs/websockets": "^10.3.8",
|
||||
"@tuya/tuya-connector-nodejs": "^2.1.2",
|
||||
@ -26,12 +30,14 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"firebase": "^10.12.5",
|
||||
"google-auth-library": "^9.14.1",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"morgan": "^1.10.0",
|
||||
"nest-winston": "^1.10.2",
|
||||
"nodemailer": "^6.9.10",
|
||||
"onesignal-node": "^3.4.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
@ -39,6 +45,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -47,8 +54,9 @@
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
@ -787,6 +795,17 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||
@ -898,6 +917,19 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fast-csv/format": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-5.0.2.tgz",
|
||||
"integrity": "sha512-fRYcWvI8vs0Zxa/8fXd/QlmQYWWkJqKZPAXM+vksnplb3owQFKTPPh9JqOtD0L3flQw/AZjjXdPkD7Kp/uHm8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isfunction": "^3.0.9",
|
||||
"lodash.isnil": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/analytics": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.8.tgz",
|
||||
@ -2203,6 +2235,17 @@
|
||||
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nestjs/axios": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.0.tgz",
|
||||
"integrity": "sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"axios": "^1.3.1",
|
||||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
|
||||
@ -2552,6 +2595,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/terminus": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-11.0.0.tgz",
|
||||
"integrity": "sha512-c55LOo9YGovmQHtFUMa/vDaxGZ2cglMTZejqgHREaApt/GArTfgYYGwhRXPLq8ZwiQQlLuYB+79e9iA8mlDSLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"boxen": "5.1.2",
|
||||
"check-disk-space": "3.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "*",
|
||||
"@grpc/proto-loader": "*",
|
||||
"@mikro-orm/core": "*",
|
||||
"@mikro-orm/nestjs": "*",
|
||||
"@nestjs/axios": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/microservices": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/mongoose": "^11.0.0",
|
||||
"@nestjs/sequelize": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/typeorm": "^10.0.0 || ^11.0.0",
|
||||
"@prisma/client": "*",
|
||||
"mongoose": "*",
|
||||
"reflect-metadata": "0.1.x || 0.2.x",
|
||||
"rxjs": "7.x",
|
||||
"sequelize": "*",
|
||||
"typeorm": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@grpc/grpc-js": {
|
||||
"optional": true
|
||||
},
|
||||
"@grpc/proto-loader": {
|
||||
"optional": true
|
||||
},
|
||||
"@mikro-orm/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@mikro-orm/nestjs": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/axios": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/microservices": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/mongoose": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/sequelize": {
|
||||
"optional": true
|
||||
},
|
||||
"@nestjs/typeorm": {
|
||||
"optional": true
|
||||
},
|
||||
"@prisma/client": {
|
||||
"optional": true
|
||||
},
|
||||
"mongoose": {
|
||||
"optional": true
|
||||
},
|
||||
"sequelize": {
|
||||
"optional": true
|
||||
},
|
||||
"typeorm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "10.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.15.tgz",
|
||||
@ -2580,6 +2693,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/throttler": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz",
|
||||
"integrity": "sha512-osL67i0PUuwU5nqSuJjtUJZMkxAnYB4VldgYUMGzvYRJDCqGRFMWbsbzm/CkUtPLRL30I8T74Xgt/OQxnYokiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/typeorm": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz",
|
||||
@ -3105,6 +3229,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "1.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
|
||||
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
|
||||
@ -3189,6 +3323,12 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
|
||||
@ -3698,6 +3838,15 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@ -4008,7 +4157,6 @@
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
@ -4363,6 +4511,57 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
||||
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-align": "^3.0.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-boxes": "^2.2.1",
|
||||
"string-width": "^4.2.2",
|
||||
"type-fest": "^0.20.2",
|
||||
"widest-line": "^3.1.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@ -4626,6 +4825,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/check-disk-space": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz",
|
||||
"integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@ -4701,6 +4909,18 @@
|
||||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
|
||||
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@ -4897,6 +5117,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -4915,6 +5145,41 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorspace": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^3.1.3",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -5181,6 +5446,18 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"csv-parser": "bin/csv-parser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@ -5580,6 +5857,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@ -6521,6 +6804,12 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@ -6704,6 +6993,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
@ -9249,6 +9544,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@ -9330,6 +9631,12 @@
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@ -9348,12 +9655,31 @@
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isfunction": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnil": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
@ -9409,6 +9735,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz",
|
||||
@ -9761,6 +10113,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nest-winston": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.10.2.tgz",
|
||||
"integrity": "sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-safe-stringify": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"winston": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
@ -10018,6 +10383,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/onesignal-node": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/onesignal-node/-/onesignal-node-3.4.0.tgz",
|
||||
@ -11386,6 +11760,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@ -11716,6 +12099,21 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@ -11811,6 +12209,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -12332,6 +12739,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -12438,6 +12851,15 @@
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
@ -12657,7 +13079,6 @@
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -13426,6 +13847,91 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
|
||||
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
10
package.json
10
package.json
@ -20,6 +20,8 @@
|
||||
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "^5.0.2",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
@ -28,6 +30,8 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@nestjs/websockets": "^10.3.8",
|
||||
"@tuya/tuya-connector-nodejs": "^2.1.2",
|
||||
@ -37,12 +41,14 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"firebase": "^10.12.5",
|
||||
"google-auth-library": "^9.14.1",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"morgan": "^1.10.0",
|
||||
"nest-winston": "^1.10.2",
|
||||
"nodemailer": "^6.9.10",
|
||||
"onesignal-node": "^3.4.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
@ -50,6 +56,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -58,8 +65,9 @@
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
|
@ -12,7 +12,7 @@ import { UserNotificationModule } from './user-notification/user-notification.mo
|
||||
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
|
||||
import { SceneModule } from './scene/scene.module';
|
||||
import { DoorLockModule } from './door-lock/door.lock.module';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||
import { AutomationModule } from './automation/automation.module';
|
||||
import { RegionModule } from './region/region.module';
|
||||
@ -29,11 +29,24 @@ import { RoleModule } from './role/role.module';
|
||||
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
|
||||
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
|
||||
import { TagModule } from './tags/tags.module';
|
||||
import { ClientModule } from './client/client.module';
|
||||
import { DeviceCommissionModule } from './commission-device/commission-device.module';
|
||||
import { PowerClampModule } from './power-clamp/power-clamp.module';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
||||
import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
load: config,
|
||||
}),
|
||||
/* ThrottlerModule.forRoot({
|
||||
throttlers: [{ ttl: 100000, limit: 30 }],
|
||||
}), */
|
||||
WinstonModule.forRoot(winstonLoggerOptions),
|
||||
ClientModule,
|
||||
AuthenticationModule,
|
||||
UserModule,
|
||||
InviteUserModule,
|
||||
@ -61,12 +74,19 @@ import { TagModule } from './tags/tags.module';
|
||||
TermsConditionsModule,
|
||||
PrivacyPolicyModule,
|
||||
TagModule,
|
||||
DeviceCommissionModule,
|
||||
PowerClampModule,
|
||||
HealthModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor,
|
||||
},
|
||||
/* {
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
}, */
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module';
|
||||
import { CommonModule } from '../../libs/common/src';
|
||||
import { UserAuthController } from './controllers';
|
||||
import { UserAuthService } from './services';
|
||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
||||
import { UserOtpRepository } from '@app/common/modules/user/repositories';
|
||||
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
||||
import { RoleService } from 'src/role/services';
|
||||
import { UserAuthController } from './controllers';
|
||||
import { UserAuthService } from './services';
|
||||
import { AuthService } from '@app/common/auth/services/auth.service';
|
||||
import { EmailService } from '@app/common/util/email.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, UserRepositoryModule, CommonModule],
|
||||
imports: [ConfigModule],
|
||||
controllers: [UserAuthController],
|
||||
providers: [
|
||||
UserAuthService,
|
||||
@ -20,6 +21,9 @@ import { RoleService } from 'src/role/services';
|
||||
UserOtpRepository,
|
||||
RoleTypeRepository,
|
||||
RoleService,
|
||||
AuthService,
|
||||
EmailService,
|
||||
JwtService,
|
||||
],
|
||||
exports: [UserAuthService],
|
||||
})
|
||||
|
@ -19,6 +19,7 @@ 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';
|
||||
import { ClientGuard } from 'src/guards/client.guard';
|
||||
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
@ -29,13 +30,19 @@ export class UserAuthController {
|
||||
constructor(private readonly userAuthService: UserAuthService) {}
|
||||
|
||||
@ResponseMessage('User Registered Successfully')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(ClientGuard)
|
||||
@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);
|
||||
async signUp(@Body() userSignUpDto: UserSignUpDto, @Req() req: any) {
|
||||
const clientUuid = req.client.uuid;
|
||||
const signupUser = await this.userAuthService.signUp(
|
||||
userSignUpDto,
|
||||
clientUuid,
|
||||
);
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
data: {
|
||||
|
@ -34,7 +34,10 @@ export class UserAuthService {
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async signUp(userSignUpDto: UserSignUpDto): Promise<UserEntity> {
|
||||
async signUp(
|
||||
userSignUpDto: UserSignUpDto,
|
||||
clientUuid?: string,
|
||||
): Promise<UserEntity> {
|
||||
const findUser = await this.findUser(userSignUpDto.email);
|
||||
|
||||
if (findUser) {
|
||||
@ -63,6 +66,7 @@ export class UserAuthService {
|
||||
hasAcceptedAppAgreement,
|
||||
password: hashedPassword,
|
||||
roleType: { uuid: spaceMemberRole.uuid },
|
||||
client: { uuid: clientUuid },
|
||||
region: regionUuid
|
||||
? {
|
||||
uuid: regionUuid,
|
||||
@ -160,6 +164,7 @@ export class UserAuthService {
|
||||
role: user.roleType,
|
||||
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
|
||||
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
|
||||
project: user.project,
|
||||
sessionId: session[1].uuid,
|
||||
});
|
||||
return res;
|
||||
@ -176,79 +181,98 @@ export class UserAuthService {
|
||||
otpCode: string;
|
||||
cooldown: number;
|
||||
}> {
|
||||
const otpLimiter = new Date();
|
||||
otpLimiter.setDate(
|
||||
otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'),
|
||||
);
|
||||
const userExists = await this.userRepository.exists({
|
||||
where: {
|
||||
region: data.regionUuid
|
||||
? {
|
||||
uuid: data.regionUuid,
|
||||
}
|
||||
: undefined,
|
||||
email: data.email,
|
||||
isUserVerified: data.type === OtpType.PASSWORD ? true : undefined,
|
||||
},
|
||||
});
|
||||
if (!userExists) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
await this.otpRepository.softDelete({ email: data.email, type: data.type });
|
||||
await this.otpRepository.delete({
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: LessThan(otpLimiter),
|
||||
});
|
||||
const countOfOtp = await this.otpRepository.count({
|
||||
withDeleted: true,
|
||||
where: {
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: MoreThan(otpLimiter),
|
||||
},
|
||||
});
|
||||
const lastOtp = await this.otpRepository.findOne({
|
||||
where: { email: data.email, type: data.type },
|
||||
order: { createdAt: 'DESC' },
|
||||
withDeleted: true,
|
||||
});
|
||||
let cooldown = 30 * Math.pow(2, countOfOtp - 1);
|
||||
if (lastOtp) {
|
||||
const now = new Date();
|
||||
const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt);
|
||||
|
||||
if (timeSinceLastOtp < cooldown) {
|
||||
throw new BadRequestException({
|
||||
message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`,
|
||||
data: {
|
||||
cooldown: cooldown - timeSinceLastOtp,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const otpLimiter = new Date();
|
||||
otpLimiter.setDate(
|
||||
otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'),
|
||||
);
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
region: data.regionUuid ? { uuid: data.regionUuid } : undefined,
|
||||
email: data.email,
|
||||
isUserVerified: data.type === OtpType.PASSWORD ? true : undefined,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
}
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiryTime = new Date();
|
||||
expiryTime.setMinutes(expiryTime.getMinutes() + 10);
|
||||
await this.otpRepository.save({
|
||||
email: data.email,
|
||||
otpCode,
|
||||
expiryTime,
|
||||
type: data.type,
|
||||
});
|
||||
const countOfOtpToReturn = await this.otpRepository.count({
|
||||
withDeleted: true,
|
||||
where: {
|
||||
await this.otpRepository.softDelete({
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: MoreThan(otpLimiter),
|
||||
},
|
||||
});
|
||||
cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1);
|
||||
const subject = 'OTP send successfully';
|
||||
const message = `Your OTP code is ${otpCode}`;
|
||||
this.emailService.sendEmail(data.email, subject, message);
|
||||
return { otpCode, cooldown };
|
||||
});
|
||||
await this.otpRepository.delete({
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: LessThan(otpLimiter),
|
||||
});
|
||||
const countOfOtp = await this.otpRepository.count({
|
||||
withDeleted: true,
|
||||
where: {
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: MoreThan(otpLimiter),
|
||||
},
|
||||
});
|
||||
const lastOtp = await this.otpRepository.findOne({
|
||||
where: { email: data.email, type: data.type },
|
||||
order: { createdAt: 'DESC' },
|
||||
withDeleted: true,
|
||||
});
|
||||
let cooldown = 30 * Math.pow(2, countOfOtp - 1);
|
||||
if (lastOtp) {
|
||||
const now = new Date();
|
||||
const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt);
|
||||
|
||||
if (timeSinceLastOtp < cooldown) {
|
||||
throw new BadRequestException({
|
||||
message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`,
|
||||
data: {
|
||||
cooldown: cooldown - timeSinceLastOtp,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiryTime = new Date();
|
||||
expiryTime.setMinutes(expiryTime.getMinutes() + 10);
|
||||
await this.otpRepository.save({
|
||||
email: data.email,
|
||||
otpCode,
|
||||
expiryTime,
|
||||
type: data.type,
|
||||
});
|
||||
const countOfOtpToReturn = await this.otpRepository.count({
|
||||
withDeleted: true,
|
||||
where: {
|
||||
email: data.email,
|
||||
type: data.type,
|
||||
createdAt: MoreThan(otpLimiter),
|
||||
},
|
||||
});
|
||||
cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1);
|
||||
|
||||
const [otp1, otp2, otp3, otp4, otp5, otp6] = otpCode.split('');
|
||||
|
||||
await this.emailService.sendOtpEmailWithTemplate(data.email, {
|
||||
name: user.firstName,
|
||||
otp1,
|
||||
otp2,
|
||||
otp3,
|
||||
otp4,
|
||||
otp5,
|
||||
otp6,
|
||||
});
|
||||
|
||||
return { otpCode, cooldown };
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
console.error('OTP generation error:', error);
|
||||
throw new BadRequestException(
|
||||
'An unexpected error occurred while generating the OTP.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyOTP(
|
||||
|
@ -17,10 +17,11 @@ import {
|
||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories/project.repository';
|
||||
import { AutomationSpaceController } from './controllers/automation-space.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
|
||||
controllers: [AutomationController],
|
||||
controllers: [AutomationController, AutomationSpaceController],
|
||||
providers: [
|
||||
AutomationService,
|
||||
TuyaService,
|
||||
|
33
src/automation/controllers/automation-space.controller.ts
Normal file
33
src/automation/controllers/automation-space.controller.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { AutomationService } from '../services';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { GetSpaceParam } from '@app/common/dto/get.space.param';
|
||||
|
||||
@ApiTags('Automation Module')
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
path: ControllerRoute.AUTOMATION_SPACE.ROUTE,
|
||||
})
|
||||
export class AutomationSpaceController {
|
||||
constructor(private readonly automationService: AutomationService) {}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('AUTOMATION_VIEW')
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
ControllerRoute.AUTOMATION_SPACE.ACTIONS.GET_AUTOMATION_BY_SPACE_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.AUTOMATION_SPACE.ACTIONS
|
||||
.GET_AUTOMATION_BY_SPACE_DESCRIPTION,
|
||||
})
|
||||
async getAutomationBySpace(@Param() param: GetSpaceParam) {
|
||||
const automation = await this.automationService.getAutomationBySpace(param);
|
||||
return automation;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import {
|
||||
Get,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards,
|
||||
@ -17,10 +18,11 @@ import {
|
||||
UpdateAutomationStatusDto,
|
||||
} from '../dtos/automation.dto';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { AutomationParamDto, SpaceParamDto } from '../dtos';
|
||||
import { AutomationParamDto } from '../dtos';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
|
||||
@ApiTags('Automation Module')
|
||||
@Controller({
|
||||
@ -38,9 +40,14 @@ export class AutomationController {
|
||||
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);
|
||||
async addAutomation(
|
||||
@Param() param: ProjectParam,
|
||||
@Body() addAutomationDto: AddAutomationDto,
|
||||
) {
|
||||
const automation = await this.automationService.addAutomation(
|
||||
param,
|
||||
addAutomationDto,
|
||||
);
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
@ -52,32 +59,14 @@ export class AutomationController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('AUTOMATION_VIEW')
|
||||
@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(PermissionsGuard)
|
||||
@Permissions('AUTOMATION_VIEW')
|
||||
@Get('details/:automationUuid')
|
||||
@Get(':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,
|
||||
);
|
||||
const automation = await this.automationService.getAutomationDetails(param);
|
||||
return automation;
|
||||
}
|
||||
|
||||
@ -113,7 +102,7 @@ export class AutomationController {
|
||||
) {
|
||||
const automation = await this.automationService.updateAutomation(
|
||||
updateAutomationDto,
|
||||
param.automationUuid,
|
||||
param,
|
||||
);
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
@ -126,7 +115,7 @@ export class AutomationController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('AUTOMATION_CONTROL')
|
||||
@Put('status/:automationUuid')
|
||||
@Patch(':automationUuid')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
ControllerRoute.AUTOMATION.ACTIONS.UPDATE_AUTOMATION_STATUS_SUMMARY,
|
||||
@ -139,7 +128,7 @@ export class AutomationController {
|
||||
) {
|
||||
await this.automationService.updateAutomationStatus(
|
||||
updateAutomationStatusDto,
|
||||
param.automationUuid,
|
||||
param,
|
||||
);
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class AutomationParamDto {
|
||||
export class AutomationParamDto extends ProjectParam {
|
||||
@ApiProperty({
|
||||
description: 'TuyaId of the automation',
|
||||
example: 'SfFi2Tbn09btes84',
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class SpaceParamDto {
|
||||
export class SpaceParamDto extends ProjectParam {
|
||||
@ApiProperty({
|
||||
description: 'UUID of the space',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
|
@ -39,6 +39,9 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { AutomationEntity } from '@app/common/modules/automation/entities';
|
||||
import { DeleteTapToRunSceneInterface } from 'src/scene/interface/scene.interface';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { GetSpaceParam } from '@app/common/dto/get.space.param';
|
||||
|
||||
@Injectable()
|
||||
export class AutomationService {
|
||||
@ -51,6 +54,7 @@ export class AutomationService {
|
||||
private readonly sceneDeviceRepository: SceneDeviceRepository,
|
||||
private readonly sceneRepository: SceneRepository,
|
||||
private readonly automationRepository: AutomationRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
) {
|
||||
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
|
||||
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
|
||||
@ -63,8 +67,11 @@ export class AutomationService {
|
||||
}
|
||||
|
||||
async addAutomation(
|
||||
param: ProjectParam,
|
||||
addAutomationDto: AddAutomationDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
const {
|
||||
automationName,
|
||||
@ -74,16 +81,19 @@ export class AutomationService {
|
||||
conditions,
|
||||
spaceUuid,
|
||||
} = addAutomationDto;
|
||||
const space = await this.getSpaceByUuid(spaceUuid);
|
||||
const automation = await this.add({
|
||||
automationName,
|
||||
effectiveTime,
|
||||
decisionExpr,
|
||||
actions,
|
||||
conditions,
|
||||
spaceTuyaId: space.spaceTuyaUuid,
|
||||
spaceUuid,
|
||||
});
|
||||
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
|
||||
const automation = await this.add(
|
||||
{
|
||||
automationName,
|
||||
effectiveTime,
|
||||
decisionExpr,
|
||||
actions,
|
||||
conditions,
|
||||
spaceTuyaId: space.spaceTuyaUuid,
|
||||
spaceUuid,
|
||||
},
|
||||
param.projectUuid,
|
||||
);
|
||||
return new SuccessResponseDto({
|
||||
message: `Successfully created new automation with uuid ${automation.uuid}`,
|
||||
data: automation,
|
||||
@ -102,11 +112,18 @@ export class AutomationService {
|
||||
);
|
||||
}
|
||||
}
|
||||
async createAutomationExternalService(params: AddAutomationParams) {
|
||||
async createAutomationExternalService(
|
||||
params: AddAutomationParams,
|
||||
projectUuid: string,
|
||||
) {
|
||||
try {
|
||||
const formattedActions = await this.prepareActions(params.actions);
|
||||
const formattedActions = await this.prepareActions(
|
||||
params.actions,
|
||||
projectUuid,
|
||||
);
|
||||
const formattedCondition = await this.prepareConditions(
|
||||
params.conditions,
|
||||
projectUuid,
|
||||
);
|
||||
|
||||
const response = await this.tuyaService.createAutomation(
|
||||
@ -142,9 +159,12 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async add(params: AddAutomationParams) {
|
||||
async add(params: AddAutomationParams, projectUuid: string) {
|
||||
try {
|
||||
const response = await this.createAutomationExternalService(params);
|
||||
const response = await this.createAutomationExternalService(
|
||||
params,
|
||||
projectUuid,
|
||||
);
|
||||
|
||||
const automation = await this.automationRepository.save({
|
||||
automationTuyaUuid: response.result.id,
|
||||
@ -169,11 +189,16 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
async getSpaceByUuid(spaceUuid: string) {
|
||||
async getSpaceByUuid(spaceUuid: string, projectUuid: string) {
|
||||
try {
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: {
|
||||
uuid: spaceUuid,
|
||||
community: {
|
||||
project: {
|
||||
uuid: projectUuid,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['community'],
|
||||
});
|
||||
@ -196,15 +221,25 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAutomationBySpace(spaceUuid: string) {
|
||||
async getAutomationBySpace(param: GetSpaceParam) {
|
||||
try {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
// Fetch automation data from the repository
|
||||
const automationData = await this.automationRepository.find({
|
||||
where: {
|
||||
space: { uuid: spaceUuid },
|
||||
space: {
|
||||
uuid: param.spaceUuid,
|
||||
community: {
|
||||
uuid: param.communityUuid,
|
||||
project: {
|
||||
uuid: param.projectUuid,
|
||||
},
|
||||
},
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
relations: ['space'],
|
||||
relations: ['space', 'space.community'],
|
||||
});
|
||||
|
||||
// Safe fetch function to handle individual automation fetching
|
||||
@ -226,6 +261,10 @@ export class AutomationService {
|
||||
name: automationDetails.result.name,
|
||||
status: automationDetails.result.status,
|
||||
type: AUTOMATION_TYPE,
|
||||
spaceId: automation.space.uuid,
|
||||
spaceName: automation.space.spaceName,
|
||||
communityName: automation.space.community.name,
|
||||
communityId: automation.space.community.uuid,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
@ -250,9 +289,10 @@ export class AutomationService {
|
||||
);
|
||||
}
|
||||
}
|
||||
async findAutomationBySpace(spaceUuid: string) {
|
||||
|
||||
async findAutomationBySpace(spaceUuid: string, projectUuid: string) {
|
||||
try {
|
||||
await this.getSpaceByUuid(spaceUuid);
|
||||
await this.getSpaceByUuid(spaceUuid, projectUuid);
|
||||
|
||||
const automationData = await this.automationRepository.find({
|
||||
where: {
|
||||
@ -321,16 +361,21 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async getAutomationDetails(automationUuid: string) {
|
||||
async getAutomationDetails(param: AutomationParamDto) {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
const automation = await this.findAutomation(automationUuid);
|
||||
const automation = await this.findAutomation(
|
||||
param.automationUuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
|
||||
const automationDetails = await this.getAutomation(automation);
|
||||
|
||||
return automationDetails;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching automation details for automationUuid ${automationUuid}:`,
|
||||
`Error fetching automation details for automationUuid ${param.automationUuid}:`,
|
||||
error,
|
||||
);
|
||||
|
||||
@ -372,6 +417,7 @@ export class AutomationService {
|
||||
action.entityId = device.uuid;
|
||||
action.productUuid = device.productDevice.uuid;
|
||||
action.productType = device.productDevice.prodType;
|
||||
action.deviceName = device.name;
|
||||
}
|
||||
} else if (
|
||||
action.actionExecutor !== ActionExecutorEnum.DEVICE_ISSUE &&
|
||||
@ -416,6 +462,7 @@ export class AutomationService {
|
||||
condition.entityId = device.uuid;
|
||||
condition.productUuid = device.productDevice.uuid;
|
||||
condition.productType = device.productDevice.prodType;
|
||||
condition.deviceName = device.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -449,9 +496,15 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
async findAutomation(sceneUuid: string): Promise<AutomationEntity> {
|
||||
async findAutomation(
|
||||
sceneUuid: string,
|
||||
projectUuid: string,
|
||||
): Promise<AutomationEntity> {
|
||||
const automation = await this.automationRepository.findOne({
|
||||
where: { uuid: sceneUuid },
|
||||
where: {
|
||||
uuid: sceneUuid,
|
||||
space: { community: { project: { uuid: projectUuid } } },
|
||||
},
|
||||
relations: ['space'],
|
||||
});
|
||||
|
||||
@ -466,10 +519,17 @@ export class AutomationService {
|
||||
|
||||
async deleteAutomation(param: AutomationParamDto) {
|
||||
const { automationUuid } = param;
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
const automationData = await this.findAutomation(automationUuid);
|
||||
const space = await this.getSpaceByUuid(automationData.space.uuid);
|
||||
const automationData = await this.findAutomation(
|
||||
automationUuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
const space = await this.getSpaceByUuid(
|
||||
automationData.space.uuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
await this.delete(automationData.automationTuyaUuid, space.spaceTuyaUuid);
|
||||
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
|
||||
where: { automationTuyaUuid: automationData.automationTuyaUuid },
|
||||
@ -522,12 +582,16 @@ export class AutomationService {
|
||||
spaceTuyaUuid: string,
|
||||
automationUuid: string,
|
||||
updateAutomationDto: UpdateAutomationDto,
|
||||
projectUuid: string,
|
||||
) {
|
||||
const { automationName, decisionExpr, actions, conditions, effectiveTime } =
|
||||
updateAutomationDto;
|
||||
try {
|
||||
const formattedActions = await this.prepareActions(actions);
|
||||
const formattedCondition = await this.prepareConditions(conditions);
|
||||
const formattedActions = await this.prepareActions(actions, projectUuid);
|
||||
const formattedCondition = await this.prepareConditions(
|
||||
conditions,
|
||||
projectUuid,
|
||||
);
|
||||
const response = await this.tuyaService.updateAutomation(
|
||||
automationUuid,
|
||||
spaceTuyaUuid,
|
||||
@ -564,17 +628,26 @@ export class AutomationService {
|
||||
}
|
||||
async updateAutomation(
|
||||
updateAutomationDto: UpdateAutomationDto,
|
||||
automationUuid: string,
|
||||
param: AutomationParamDto,
|
||||
) {
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
const automation = await this.findAutomation(automationUuid);
|
||||
const space = await this.getSpaceByUuid(automation.space.uuid);
|
||||
const automation = await this.findAutomation(
|
||||
param.automationUuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
const space = await this.getSpaceByUuid(
|
||||
automation.space.uuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
|
||||
const updateTuyaAutomationResponse =
|
||||
await this.updateAutomationExternalService(
|
||||
space.spaceTuyaUuid,
|
||||
automation.automationTuyaUuid,
|
||||
updateAutomationDto,
|
||||
param.projectUuid,
|
||||
);
|
||||
|
||||
if (!updateTuyaAutomationResponse.success) {
|
||||
@ -584,14 +657,14 @@ export class AutomationService {
|
||||
);
|
||||
}
|
||||
const updatedScene = await this.automationRepository.update(
|
||||
{ uuid: automationUuid },
|
||||
{ uuid: param.automationUuid },
|
||||
{
|
||||
space: { uuid: automation.space.uuid },
|
||||
},
|
||||
);
|
||||
return new SuccessResponseDto({
|
||||
data: updatedScene,
|
||||
message: `Automation with ID ${automationUuid} updated successfully`,
|
||||
message: `Automation with ID ${param.automationUuid} updated successfully`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) {
|
||||
@ -606,12 +679,17 @@ export class AutomationService {
|
||||
}
|
||||
async updateAutomationStatus(
|
||||
updateAutomationStatusDto: UpdateAutomationStatusDto,
|
||||
automationUuid: string,
|
||||
param: AutomationParamDto,
|
||||
) {
|
||||
const { isEnable, spaceUuid } = updateAutomationStatusDto;
|
||||
await this.validateProject(param.projectUuid);
|
||||
|
||||
try {
|
||||
const automation = await this.findAutomation(automationUuid);
|
||||
const space = await this.getSpaceByUuid(spaceUuid);
|
||||
const automation = await this.findAutomation(
|
||||
param.automationUuid,
|
||||
param.projectUuid,
|
||||
);
|
||||
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
|
||||
if (!space.spaceTuyaUuid) {
|
||||
throw new HttpException(
|
||||
`Invalid space UUID ${spaceUuid}`,
|
||||
@ -638,7 +716,10 @@ export class AutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareActions(actions: Action[]): Promise<ConvertedAction[]> {
|
||||
private async prepareActions(
|
||||
actions: Action[],
|
||||
projectUuid: string,
|
||||
): Promise<ConvertedAction[]> {
|
||||
const convertedData = convertKeysToSnakeCase(actions) as ConvertedAction[];
|
||||
|
||||
await Promise.all(
|
||||
@ -647,6 +728,7 @@ export class AutomationService {
|
||||
const device = await this.deviceService.getDeviceByDeviceUuid(
|
||||
action.entity_id,
|
||||
false,
|
||||
projectUuid,
|
||||
);
|
||||
if (device) {
|
||||
action.entity_id = device.deviceTuyaUuid;
|
||||
@ -671,7 +753,10 @@ export class AutomationService {
|
||||
action.action_executor === ActionExecutorEnum.RULE_ENABLE
|
||||
) {
|
||||
if (action.action_type === ActionTypeEnum.AUTOMATION) {
|
||||
const automation = await this.findAutomation(action.entity_id);
|
||||
const automation = await this.findAutomation(
|
||||
action.entity_id,
|
||||
projectUuid,
|
||||
);
|
||||
action.entity_id = automation.automationTuyaUuid;
|
||||
}
|
||||
}
|
||||
@ -681,7 +766,10 @@ export class AutomationService {
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
private async prepareConditions(conditions: Condition[]) {
|
||||
private async prepareConditions(
|
||||
conditions: Condition[],
|
||||
projectUuid: string,
|
||||
) {
|
||||
const convertedData = convertKeysToSnakeCase(conditions);
|
||||
await Promise.all(
|
||||
convertedData.map(async (condition) => {
|
||||
@ -689,6 +777,7 @@ export class AutomationService {
|
||||
const device = await this.deviceService.getDeviceByDeviceUuid(
|
||||
condition.entity_id,
|
||||
false,
|
||||
projectUuid,
|
||||
);
|
||||
if (device) {
|
||||
condition.entity_id = device.deviceTuyaUuid;
|
||||
@ -698,4 +787,17 @@ export class AutomationService {
|
||||
);
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
private async validateProject(uuid: string) {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { uuid },
|
||||
});
|
||||
if (!project) {
|
||||
throw new HttpException(
|
||||
`A project with the uuid '${uuid}' doesn't exists.`,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
24
src/client/client.module.ts
Normal file
24
src/client/client.module.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ClientController } from './controllers';
|
||||
import { ClientService } from './services';
|
||||
import { ClientRepositoryModule } from '@app/common/modules/client/client.repository.module';
|
||||
import { ClientRepository } from '@app/common/modules/client/repositories';
|
||||
import { AuthService } from '@app/common/auth/services/auth.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
||||
|
||||
@Module({
|
||||
imports: [ClientRepositoryModule],
|
||||
controllers: [ClientController],
|
||||
providers: [
|
||||
ClientService,
|
||||
ClientRepository,
|
||||
AuthService,
|
||||
JwtService,
|
||||
UserRepository,
|
||||
UserSessionRepository,
|
||||
],
|
||||
exports: [ClientService],
|
||||
})
|
||||
export class ClientModule {}
|
33
src/client/controllers/client.controller.ts
Normal file
33
src/client/controllers/client.controller.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { RegisterClientDto } from '../dtos/register-client.dto';
|
||||
import { ClientService } from '../services';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { ClientTokenDto } from '../dtos/token-client.dto';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
|
||||
@ApiTags('OAuth Clients')
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
path: ControllerRoute.CLIENT.ROUTE,
|
||||
})
|
||||
export class ClientController {
|
||||
constructor(private readonly clientService: ClientService) {}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.CLIENT.ACTIONS.REGISTER_NEW_CLIENT_SUMMARY,
|
||||
description: ControllerRoute.CLIENT.ACTIONS.REGISTER_NEW_CLIENT_DESCRIPTION,
|
||||
})
|
||||
async registerClient(@Body() dto: RegisterClientDto) {
|
||||
return this.clientService.registerClient(dto);
|
||||
}
|
||||
@Post('token')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.CLIENT.ACTIONS.LOGIN_CLIENT_SUMMARY,
|
||||
description: ControllerRoute.CLIENT.ACTIONS.LOGIN_CLIENT_DESCRIPTION,
|
||||
})
|
||||
async login(@Body() dto: ClientTokenDto) {
|
||||
return this.clientService.loginWithClientCredentials(dto);
|
||||
}
|
||||
}
|
1
src/client/controllers/index.ts
Normal file
1
src/client/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './client.controller';
|
1
src/client/dtos/index.ts
Normal file
1
src/client/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './register-client.dto';
|
31
src/client/dtos/register-client.dto.ts
Normal file
31
src/client/dtos/register-client.dto.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class RegisterClientDto {
|
||||
@ApiProperty({
|
||||
example: 'SmartHomeApp',
|
||||
description: 'The name of the client',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://client-app.com/callback',
|
||||
description: 'The redirect URI of the client',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
redirectUri: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ['DEVICE_SINGLE_CONTROL', 'DEVICE_VIEW'],
|
||||
description: 'The scopes of the client',
|
||||
required: true,
|
||||
})
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
scopes: string[];
|
||||
}
|
22
src/client/dtos/token-client.dto.ts
Normal file
22
src/client/dtos/token-client.dto.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ClientTokenDto {
|
||||
@ApiProperty({
|
||||
example: 'abcd1234xyz',
|
||||
description: 'The client ID of the client',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
clientId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'secureSecret123',
|
||||
description: 'The client secret of the client',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
clientSecret: string;
|
||||
}
|
68
src/client/services/client.service.ts
Normal file
68
src/client/services/client.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import { RegisterClientDto } from '../dtos/register-client.dto';
|
||||
import { ClientEntity } from '@app/common/modules/client/entities';
|
||||
import { ClientTokenDto } from '../dtos/token-client.dto';
|
||||
import { AuthService } from '@app/common/auth/services/auth.service';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ClientService {
|
||||
constructor(
|
||||
@InjectRepository(ClientEntity)
|
||||
private clientRepository: Repository<ClientEntity>,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
async loginWithClientCredentials(dto: ClientTokenDto) {
|
||||
const client = await this.validateClient(dto.clientId, dto.clientSecret);
|
||||
const payload = {
|
||||
client: {
|
||||
clientId: client.clientId,
|
||||
uuid: client.uuid,
|
||||
scopes: client.scopes,
|
||||
},
|
||||
};
|
||||
const tokens = await this.authService.getTokens(payload, false, '5m');
|
||||
return new SuccessResponseDto({
|
||||
message: `Client logged in successfully`,
|
||||
data: tokens,
|
||||
statusCode: HttpStatus.CREATED,
|
||||
});
|
||||
}
|
||||
async registerClient(dto: RegisterClientDto) {
|
||||
const clientId = crypto.randomBytes(16).toString('hex');
|
||||
const clientSecret = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const client = this.clientRepository.create({
|
||||
name: dto.name,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri: dto.redirectUri,
|
||||
scopes: dto.scopes,
|
||||
});
|
||||
|
||||
await this.clientRepository.save(client);
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Client registered successfully`,
|
||||
data: { clientId, clientSecret },
|
||||
statusCode: HttpStatus.CREATED,
|
||||
});
|
||||
}
|
||||
|
||||
async validateClient(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<ClientEntity | null> {
|
||||
const client = await this.clientRepository.findOne({
|
||||
where: { clientId, clientSecret },
|
||||
});
|
||||
if (!client) {
|
||||
throw new NotFoundException('Invalid client credentials');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
1
src/client/services/index.ts
Normal file
1
src/client/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './client.service';
|
61
src/commission-device/commission-device.module.ts
Normal file
61
src/commission-device/commission-device.module.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DeviceCommissionController } from './controllers';
|
||||
import { UserRepository } from '@app/common/modules/user/repositories';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { DeviceCommissionService } from './services';
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||
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';
|
||||
import { SceneService } from 'src/scene/services';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import {
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
} from '@app/common/modules/scene/repositories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import {
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
} from '@app/common/modules/power-clamp/repositories';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule],
|
||||
controllers: [DeviceCommissionController],
|
||||
providers: [
|
||||
UserRepository,
|
||||
DeviceRepository,
|
||||
DeviceCommissionService,
|
||||
TuyaService,
|
||||
DeviceService,
|
||||
SceneDeviceRepository,
|
||||
ProductRepository,
|
||||
DeviceStatusFirebaseService,
|
||||
SpaceRepository,
|
||||
SceneService,
|
||||
ProjectRepository,
|
||||
DeviceStatusLogRepository,
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
AutomationRepository,
|
||||
CommunityRepository,
|
||||
SubspaceRepository,
|
||||
PowerClampService,
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
SqlLoaderService,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class DeviceCommissionModule {}
|
@ -0,0 +1,77 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiConsumes,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { diskStorage } from 'multer';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { CommissionDeviceCsvDto } from '../dto';
|
||||
import { DeviceCommissionService } from '../services';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
|
||||
@ApiTags('Commission Devices Module')
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
path: ControllerRoute.DEVICE_COMMISSION.ROUTE,
|
||||
})
|
||||
export class DeviceCommissionController {
|
||||
constructor(private readonly commissionService: DeviceCommissionService) {}
|
||||
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('COMMISSION_DEVICE')
|
||||
@ApiBearerAuth()
|
||||
@Post()
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: diskStorage({
|
||||
destination: './uploads',
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, `${Date.now()}-${file.originalname}`);
|
||||
},
|
||||
}),
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!file.originalname.match(/\.(csv)$/)) {
|
||||
return cb(new Error('Only CSV files are allowed!'), false);
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ApiBody({ type: CommissionDeviceCsvDto })
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE_COMMISSION.ACTIONS.ADD_ALL_DEVICES_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE_COMMISSION.ACTIONS.ADD_ALL_DEVICES_DESCRIPTION,
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async addNewDevice(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Param() param: ProjectParam,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
await this.commissionService.processCsv(param, file.path);
|
||||
return {
|
||||
message: 'CSV file received and processing started',
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
1
src/commission-device/controllers/index.ts
Normal file
1
src/commission-device/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './commission-device.controller';
|
18
src/commission-device/dto/commission-device.dto.ts
Normal file
18
src/commission-device/dto/commission-device.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// dto/commission-device.dto.ts
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, Validate } from 'class-validator';
|
||||
import { FileValidator } from 'src/validators/file.validator';
|
||||
|
||||
export class CommissionDeviceCsvDto {
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'CSV file containing device data',
|
||||
})
|
||||
@IsNotEmpty({ message: 'CSV file is required' })
|
||||
@Validate(FileValidator, ['text/csv', 'application/vnd.ms-excel'], {
|
||||
message: 'Only CSV files are allowed',
|
||||
})
|
||||
file: any;
|
||||
}
|
1
src/commission-device/dto/index.ts
Normal file
1
src/commission-device/dto/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './commission-device.dto';
|
214
src/commission-device/services/commission-device.service.ts
Normal file
214
src/commission-device/services/commission-device.service.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import * as fs from 'fs';
|
||||
import * as csv from 'csv-parser';
|
||||
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { ProjectParam } from '@app/common/dto/project-param.dto';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceCommissionService {
|
||||
constructor(
|
||||
private readonly tuyaService: TuyaService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly communityRepository: CommunityRepository,
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
private readonly subspaceRepository: SubspaceRepository,
|
||||
private readonly deviceRepository: DeviceRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
) {}
|
||||
|
||||
async processCsv(param: ProjectParam, filePath: string) {
|
||||
const successCount = { value: 0 };
|
||||
const failureCount = { value: 0 };
|
||||
|
||||
const projectId = param.projectUuid;
|
||||
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { uuid: projectId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const rows: any[] = [];
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
fs.createReadStream(filePath)
|
||||
.pipe(csv())
|
||||
.on('data', (row) => rows.push(row))
|
||||
.on('end', () => resolve())
|
||||
.on('error', (error) => reject(error));
|
||||
});
|
||||
|
||||
for (const row of rows) {
|
||||
await this.processCsvRow(param, row, successCount, failureCount);
|
||||
}
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Successfully processed CSV file`,
|
||||
data: {
|
||||
successCount: successCount.value,
|
||||
failureCount: failureCount.value,
|
||||
},
|
||||
statusCode: HttpStatus.ACCEPTED,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
'Failed to process CSV file',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processCsvRow(
|
||||
param: ProjectParam,
|
||||
row: any,
|
||||
successCount: { value: number },
|
||||
failureCount: { value: number },
|
||||
) {
|
||||
try {
|
||||
const rawDeviceId = row['Device ID']?.trim();
|
||||
const communityId = row['Community UUID']?.trim();
|
||||
const spaceId = row['Space UUID']?.trim();
|
||||
const subspaceId = row['Subspace UUID']?.trim();
|
||||
const tagName = row['Tag']?.trim();
|
||||
const productName = row['Product Name']?.trim();
|
||||
const projectId = param.projectUuid;
|
||||
let deviceName: string;
|
||||
|
||||
if (!rawDeviceId) {
|
||||
console.error('Missing Device ID in row:', row);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await this.tuyaService.getDeviceDetails(rawDeviceId);
|
||||
if (!device) {
|
||||
console.error(`Device not found for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (device && typeof device === 'object' && 'name' in device) {
|
||||
deviceName = (device as any).name || '';
|
||||
}
|
||||
|
||||
const community = await this.communityRepository.findOne({
|
||||
where: { uuid: communityId, project: { uuid: projectId } },
|
||||
});
|
||||
|
||||
if (!community) {
|
||||
console.error(`Community not found: ${communityId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const tuyaSpaceId = community.externalId;
|
||||
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceId },
|
||||
relations: [
|
||||
'productAllocations',
|
||||
'productAllocations.tags',
|
||||
'productAllocations.product',
|
||||
],
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
console.error(`Space not found: ${spaceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
let subspace: SubspaceEntity | null = null;
|
||||
if (subspaceId?.trim()) {
|
||||
subspace = await this.subspaceRepository.findOne({
|
||||
where: { uuid: subspaceId },
|
||||
relations: [
|
||||
'productAllocations',
|
||||
'productAllocations.tags',
|
||||
'productAllocations.product',
|
||||
],
|
||||
});
|
||||
|
||||
if (!subspace) {
|
||||
console.error(`Subspace not found: ${subspaceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const allocations =
|
||||
subspace?.productAllocations || space.productAllocations;
|
||||
|
||||
const match = allocations
|
||||
.flatMap((pa) =>
|
||||
(pa.tags || []).map((tag) => ({ product: pa.product, tag })),
|
||||
)
|
||||
.find(({ tag }) => tag.name === tagName);
|
||||
|
||||
if (!match) {
|
||||
console.error(`No matching tag found for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.product.name !== productName) {
|
||||
console.error(`Product name mismatch for Device ID: ${rawDeviceId}`);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
const devices = await this.deviceRepository.find({
|
||||
where: {
|
||||
spaceDevice: space,
|
||||
},
|
||||
relations: ['productDevice', 'tag'],
|
||||
});
|
||||
|
||||
if (devices.length > 0) {
|
||||
devices.forEach((device) => {
|
||||
if (device.tag.uuid === match.tag.uuid) {
|
||||
console.error(
|
||||
`Device with same tag already exists: ${device.tag.name}`,
|
||||
);
|
||||
failureCount.value++;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const middlewareDevice = this.deviceRepository.create({
|
||||
deviceTuyaUuid: rawDeviceId,
|
||||
isActive: true,
|
||||
spaceDevice: space,
|
||||
subspace: subspace || null,
|
||||
productDevice: match.product,
|
||||
tag: match.tag,
|
||||
name: deviceName ?? '',
|
||||
});
|
||||
|
||||
await this.deviceRepository.save(middlewareDevice);
|
||||
|
||||
await this.deviceService.transferDeviceInSpacesTuya(
|
||||
rawDeviceId,
|
||||
tuyaSpaceId,
|
||||
);
|
||||
successCount.value++;
|
||||
console.log(
|
||||
`Device ${rawDeviceId} successfully processed and transferred to Tuya space ${tuyaSpaceId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
failureCount.value++;
|
||||
}
|
||||
}
|
||||
}
|
1
src/commission-device/services/index.ts
Normal file
1
src/commission-device/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './commission-device.service';
|
@ -3,13 +3,66 @@ import { CommunityService } from './services/community.service';
|
||||
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 {
|
||||
InviteSpaceRepository,
|
||||
SpaceLinkRepository,
|
||||
SpaceProductAllocationRepository,
|
||||
SpaceRepository,
|
||||
TagRepository,
|
||||
} 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 { SpacePermissionService } from '@app/common/helper/services';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import {
|
||||
SpaceLinkService,
|
||||
SpaceService,
|
||||
SubspaceDeviceService,
|
||||
SubSpaceService,
|
||||
ValidationService,
|
||||
} from 'src/space/services';
|
||||
import { TagService as NewTagService } from 'src/tags/services';
|
||||
import { TagService } from 'src/space/services/tag';
|
||||
import {
|
||||
SpaceModelService,
|
||||
SubSpaceModelService,
|
||||
} from 'src/space-model/services';
|
||||
import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service';
|
||||
import {
|
||||
SubspaceProductAllocationRepository,
|
||||
SubspaceRepository,
|
||||
} from '@app/common/modules/space/repositories/subspace.repository';
|
||||
import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service';
|
||||
import {
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SpaceModelRepository,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
SubspaceModelRepository,
|
||||
} from '@app/common/modules/space-model';
|
||||
import { DeviceRepository } from '@app/common/modules/device/repositories';
|
||||
import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository';
|
||||
import { ProductRepository } from '@app/common/modules/product/repositories';
|
||||
import { SpaceModelProductAllocationService } from 'src/space-model/services/space-model-product-allocation.service';
|
||||
import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service';
|
||||
import { DeviceService } from 'src/device/services';
|
||||
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
|
||||
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
|
||||
import { SceneService } from 'src/scene/services';
|
||||
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
|
||||
import {
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
} from '@app/common/modules/scene/repositories';
|
||||
import { AutomationRepository } from '@app/common/modules/automation/repositories';
|
||||
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
|
||||
import {
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
} from '@app/common/modules/power-clamp/repositories';
|
||||
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule],
|
||||
@ -22,6 +75,45 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
CommunityRepository,
|
||||
SpacePermissionService,
|
||||
ProjectRepository,
|
||||
SpaceService,
|
||||
InviteSpaceRepository,
|
||||
SpaceLinkService,
|
||||
SubSpaceService,
|
||||
ValidationService,
|
||||
NewTagService,
|
||||
SpaceModelService,
|
||||
SpaceProductAllocationService,
|
||||
SpaceLinkRepository,
|
||||
SubspaceRepository,
|
||||
TagService,
|
||||
SubspaceDeviceService,
|
||||
SubspaceProductAllocationService,
|
||||
SpaceModelRepository,
|
||||
DeviceRepository,
|
||||
NewTagRepository,
|
||||
ProductRepository,
|
||||
SubSpaceModelService,
|
||||
SpaceModelProductAllocationService,
|
||||
SpaceProductAllocationRepository,
|
||||
SubspaceProductAllocationRepository,
|
||||
TagRepository,
|
||||
SubspaceModelRepository,
|
||||
SubspaceModelProductAllocationService,
|
||||
SpaceModelProductAllocationRepoitory,
|
||||
SubspaceModelProductAllocationRepoitory,
|
||||
DeviceService,
|
||||
SceneDeviceRepository,
|
||||
DeviceStatusFirebaseService,
|
||||
SceneService,
|
||||
DeviceStatusLogRepository,
|
||||
SceneIconRepository,
|
||||
SceneRepository,
|
||||
AutomationRepository,
|
||||
PowerClampService,
|
||||
PowerClampHourlyRepository,
|
||||
PowerClampDailyRepository,
|
||||
PowerClampMonthlyRepository,
|
||||
SqlLoaderService,
|
||||
],
|
||||
exports: [CommunityService, SpacePermissionService],
|
||||
})
|
||||
|
@ -17,10 +17,10 @@ import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||
// import { CheckUserCommunityGuard } from 'src/guards/user.community.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';
|
||||
import { ProjectParam } from '../dtos';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto';
|
||||
|
||||
@ApiTags('Community Module')
|
||||
@Controller({
|
||||
@ -70,7 +70,7 @@ export class CommunityController {
|
||||
@Get()
|
||||
async getCommunities(
|
||||
@Param() param: ProjectParam,
|
||||
@Query() query: PaginationRequestGetListDto,
|
||||
@Query() query: PaginationRequestWithSearchGetListDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.communityService.getCommunities(param, query);
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos';
|
||||
import { UpdateCommunityNameDto } from '../dtos/update.community.dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import {
|
||||
ExtendedTypeORMCustomModelFindAllQuery,
|
||||
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';
|
||||
@ -13,14 +13,18 @@ import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
|
||||
import { ProjectRepository } from '@app/common/modules/project/repositiories';
|
||||
import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant';
|
||||
import { Not } from 'typeorm';
|
||||
import { ILike, In, Not } from 'typeorm';
|
||||
import { SpaceService } from 'src/space/services';
|
||||
import { SpaceRepository } from '@app/common/modules/space';
|
||||
|
||||
@Injectable()
|
||||
export class CommunityService {
|
||||
constructor(
|
||||
private readonly communityRepository: CommunityRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly spaceService: SpaceService,
|
||||
private readonly tuyaService: TuyaService,
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
) {}
|
||||
|
||||
async createCommunity(
|
||||
@ -82,7 +86,7 @@ export class CommunityService {
|
||||
|
||||
async getCommunities(
|
||||
param: ProjectParam,
|
||||
pageable: Partial<TypeORMCustomModelFindAllQuery>,
|
||||
pageable: Partial<ExtendedTypeORMCustomModelFindAllQuery>,
|
||||
): Promise<BaseResponseDto> {
|
||||
try {
|
||||
const project = await this.validateProject(param.projectUuid);
|
||||
@ -93,11 +97,68 @@ export class CommunityService {
|
||||
name: Not(`${ORPHAN_COMMUNITY_NAME}-${project.name}`),
|
||||
};
|
||||
|
||||
if (pageable.search) {
|
||||
const matchingCommunities = await this.communityRepository.find({
|
||||
where: {
|
||||
project: { uuid: param.projectUuid },
|
||||
name: ILike(`%${pageable.search}%`),
|
||||
},
|
||||
});
|
||||
|
||||
const matchingSpaces = await this.spaceRepository.find({
|
||||
where: {
|
||||
spaceName: ILike(`%${pageable.search}%`),
|
||||
disabled: false,
|
||||
community: { project: { uuid: param.projectUuid } },
|
||||
},
|
||||
relations: ['community'],
|
||||
});
|
||||
|
||||
const spaceCommunityUuids = [
|
||||
...new Set(matchingSpaces.map((space) => space.community.uuid)),
|
||||
];
|
||||
|
||||
const allMatchedCommunityUuids = [
|
||||
...new Set([
|
||||
...matchingCommunities.map((c) => c.uuid),
|
||||
...spaceCommunityUuids,
|
||||
]),
|
||||
];
|
||||
|
||||
pageable.where = {
|
||||
...pageable.where,
|
||||
uuid: In(allMatchedCommunityUuids),
|
||||
};
|
||||
}
|
||||
const customModel = TypeORMCustomModel(this.communityRepository);
|
||||
|
||||
const { baseResponseDto, paginationResponseDto } =
|
||||
await customModel.findAll(pageable);
|
||||
|
||||
if (pageable.includeSpaces) {
|
||||
const communitiesWithSpaces = await Promise.all(
|
||||
baseResponseDto.data.map(async (community: CommunityDto) => {
|
||||
const spaces =
|
||||
await this.spaceService.getSpacesHierarchyForCommunity(
|
||||
{
|
||||
communityUuid: community.uuid,
|
||||
projectUuid: param.projectUuid,
|
||||
},
|
||||
{
|
||||
onlyWithDevices: false,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...community,
|
||||
spaces: spaces.data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
baseResponseDto.data = communitiesWithSpaces;
|
||||
}
|
||||
|
||||
return new PageResponse<CommunityDto>(
|
||||
baseResponseDto,
|
||||
paginationResponseDto,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DeviceService } from '../services/device.service';
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { PermissionsGuard } from 'src/guards/permissions.guard';
|
||||
import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
import { ProjectParam } from '../dtos';
|
||||
import { GetDoorLockDevices, ProjectParam } from '../dtos';
|
||||
|
||||
@ApiTags('Device Module')
|
||||
@Controller({
|
||||
@ -23,7 +23,10 @@ export class DeviceProjectController {
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_SUMMARY,
|
||||
description: ControllerRoute.DEVICE.ACTIONS.GET_ALL_DEVICES_DESCRIPTION,
|
||||
})
|
||||
async getAllDevices(@Param() param: ProjectParam) {
|
||||
return await this.deviceService.getAllDevices(param);
|
||||
async getAllDevices(
|
||||
@Param() param: ProjectParam,
|
||||
@Query() query: GetDoorLockDevices,
|
||||
) {
|
||||
return await this.deviceService.getAllDevices(param, query);
|
||||
}
|
||||
}
|
||||
|
@ -6,29 +6,26 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
Param,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Req,
|
||||
Put,
|
||||
Delete,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import {
|
||||
AddDeviceDto,
|
||||
AddSceneToFourSceneDeviceDto,
|
||||
AssignDeviceToSpaceDto,
|
||||
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 { CheckDeviceGuard } from 'src/guards/device.guard';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard';
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
@ -45,70 +42,101 @@ import { Permissions } from 'src/decorators/permissions.decorator';
|
||||
})
|
||||
export class DeviceController {
|
||||
constructor(private readonly deviceService: DeviceService) {}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard, CheckDeviceGuard)
|
||||
@Permissions('SPACE_DEVICE_ASSIGN_DEVICE_TO_SPACE')
|
||||
@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);
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
message: 'device added successfully',
|
||||
data: device,
|
||||
};
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_BATCH_CONTROL')
|
||||
@Post('batch')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.BATCH_CONTROL_DEVICES_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE.ACTIONS.BATCH_CONTROL_DEVICES_DESCRIPTION,
|
||||
})
|
||||
async batchControlDevices(
|
||||
@Body() batchControlDevicesDto: BatchControlDevicesDto,
|
||||
@Req() req: any,
|
||||
) {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.batchControlDevices(
|
||||
batchControlDevicesDto,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_BATCH_CONTROL')
|
||||
@Get('batch')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.BATCH_STATUS_DEVICES_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE.ACTIONS.BATCH_STATUS_DEVICES_DESCRIPTION,
|
||||
})
|
||||
async batchStatusDevices(
|
||||
@Query() batchStatusDevicesDto: BatchStatusDevicesDto,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.batchStatusDevices(
|
||||
batchStatusDevicesDto,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_VIEW')
|
||||
@Get('user/:userUuid')
|
||||
@Get('gateway/:gatewayUuid/devices')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_USER_SUMMARY,
|
||||
description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_USER_DESCRIPTION,
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_IN_GATEWAY_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_IN_GATEWAY_DESCRIPTION,
|
||||
})
|
||||
async getDevicesByUser(@Param('userUuid') userUuid: string) {
|
||||
return await this.deviceService.getDevicesByUser(userUuid);
|
||||
async getDevicesInGateway(
|
||||
@Param('gatewayUuid') gatewayUuid: string,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getDevicesInGateway(
|
||||
gatewayUuid,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('SPACE_DEVICE_VIEW_DEVICE_IN_SPACE')
|
||||
@Get('space/:spaceUuid')
|
||||
@Permissions('DEVICE_ADD')
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_SPACE_UUID_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE.ACTIONS.GET_DEVICES_BY_SPACE_UUID_DESCRIPTION,
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.ADD_DEVICE_SUMMARY,
|
||||
description: ControllerRoute.DEVICE.ACTIONS.ADD_DEVICE_DESCRIPTION,
|
||||
})
|
||||
async getDevicesByUnitId(@Param('spaceUuid') spaceUuid: string) {
|
||||
return await this.deviceService.getDevicesBySpaceUuid(spaceUuid);
|
||||
async addNewDevice(
|
||||
@Body() addDeviceDto: AddDeviceDto,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.addNewDevice(addDeviceDto, projectUuid);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard, CheckRoomGuard)
|
||||
@Permissions('SUBSPACE_DEVICE_UPDATE_DEVICE_IN_SUBSPACE')
|
||||
@Put('space')
|
||||
@Put(':deviceUuid/space/:spaceUuid')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_IN_ROOM_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.DEVICE.ACTIONS.UPDATE_DEVICE_IN_ROOM_DESCRIPTION,
|
||||
})
|
||||
async updateDeviceInRoom(
|
||||
@Body() updateDeviceInSpaceDto: UpdateDeviceInSpaceDto,
|
||||
) {
|
||||
const device = await this.deviceService.updateDeviceInSpace(
|
||||
async transferDeviceInSpaces(
|
||||
@Param() updateDeviceInSpaceDto: AssignDeviceToSpaceDto,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.transferDeviceInSpaces(
|
||||
updateDeviceInSpaceDto,
|
||||
projectUuid,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
message: 'device updated in room successfully',
|
||||
data: device,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ -122,13 +150,14 @@ export class DeviceController {
|
||||
async getDeviceDetailsByDeviceId(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Req() req: any,
|
||||
) {
|
||||
const userUuid = req.user.uuid;
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getDeviceDetailsByDeviceId(
|
||||
deviceUuid,
|
||||
userUuid,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_UPDATE')
|
||||
@ -140,18 +169,14 @@ export class DeviceController {
|
||||
async updateDevice(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Body() updateDeviceDto: UpdateDeviceDto,
|
||||
) {
|
||||
const device = await this.deviceService.updateDevice(
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.updateDevice(
|
||||
deviceUuid,
|
||||
updateDeviceDto,
|
||||
projectUuid,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
message: 'device updated successfully',
|
||||
data: device,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ -165,8 +190,13 @@ export class DeviceController {
|
||||
})
|
||||
async getDeviceInstructionByDeviceId(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Req() req: any,
|
||||
) {
|
||||
return await this.deviceService.getDeviceInstructionByDeviceId(deviceUuid);
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getDeviceInstructionByDeviceId(
|
||||
deviceUuid,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@ -176,14 +206,18 @@ export class DeviceController {
|
||||
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);
|
||||
async getDevicesInstructionStatus(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getDevicesStatus(deviceUuid, projectUuid);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_SINGLE_CONTROL')
|
||||
@Post(':deviceUuid/control')
|
||||
@Post(':deviceUuid/command')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.CONTROL_DEVICE_SUMMARY,
|
||||
description: ControllerRoute.DEVICE.ACTIONS.CONTROL_DEVICE_DESCRIPTION,
|
||||
@ -191,8 +225,14 @@ export class DeviceController {
|
||||
async controlDevice(
|
||||
@Body() controlDeviceDto: ControlDeviceDto,
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
) {
|
||||
return await this.deviceService.controlDevice(controlDeviceDto, deviceUuid);
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.controlDevice(
|
||||
controlDeviceDto,
|
||||
deviceUuid,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@ -206,29 +246,20 @@ export class DeviceController {
|
||||
async updateDeviceFirmware(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Param('firmwareVersion') firmwareVersion: number,
|
||||
) {
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.updateDeviceFirmware(
|
||||
deviceUuid,
|
||||
firmwareVersion,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_VIEW')
|
||||
@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(PermissionsGuard)
|
||||
@Permissions('DEVICE_VIEW')
|
||||
@Get('report-logs/:deviceUuid')
|
||||
@Get(':deviceUuid/report-logs')
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_LOGS_SUMMARY,
|
||||
description: ControllerRoute.DEVICE.ACTIONS.GET_DEVICE_LOGS_DESCRIPTION,
|
||||
@ -236,69 +267,16 @@ export class DeviceController {
|
||||
async getBuildingChildByUuid(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Query() query: GetDeviceLogsDto,
|
||||
) {
|
||||
return await this.deviceService.getDeviceLogs(deviceUuid, query);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_BATCH_CONTROL')
|
||||
@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,
|
||||
) {
|
||||
return await this.deviceService.batchControlDevices(batchControlDevicesDto);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_BATCH_CONTROL')
|
||||
@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,
|
||||
) {
|
||||
return await this.deviceService.batchStatusDevices(batchStatusDevicesDto);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_DELETE')
|
||||
@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,
|
||||
) {
|
||||
return await this.deviceService.batchFactoryResetDevices(
|
||||
batchFactoryResetDevicesDto,
|
||||
);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard)
|
||||
@Permissions('DEVICE_VIEW')
|
||||
@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,
|
||||
) {
|
||||
return await this.deviceService.getPowerClampInstructionStatus(
|
||||
powerClampUuid,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getDeviceLogs(
|
||||
deviceUuid,
|
||||
query,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard, CheckFourAndSixSceneDeviceTypeGuard)
|
||||
@Permissions('DEVICE_SINGLE_CONTROL')
|
||||
@ -310,18 +288,14 @@ export class DeviceController {
|
||||
async addSceneToSceneDevice(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
|
||||
) {
|
||||
const device = await this.deviceService.addSceneToSceneDevice(
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.addSceneToSceneDevice(
|
||||
deviceUuid,
|
||||
addSceneToFourSceneDeviceDto,
|
||||
projectUuid,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: HttpStatus.CREATED,
|
||||
success: true,
|
||||
message: `scene added successfully to device ${deviceUuid}`,
|
||||
data: device,
|
||||
};
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(PermissionsGuard, CheckFourAndSixSceneDeviceTypeGuard)
|
||||
@ -335,10 +309,13 @@ export class DeviceController {
|
||||
async getScenesBySceneDevice(
|
||||
@Param('deviceUuid') deviceUuid: string,
|
||||
@Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto,
|
||||
) {
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.getScenesBySceneDevice(
|
||||
deviceUuid,
|
||||
getSceneFourSceneDeviceDto,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
@ApiBearerAuth()
|
||||
@ -354,7 +331,13 @@ export class DeviceController {
|
||||
async deleteSceneFromSceneDevice(
|
||||
@Param() param: DeviceSceneParamDto,
|
||||
@Query() query: DeleteSceneFromSceneDeviceDto,
|
||||
@Req() req: any,
|
||||
): Promise<BaseResponseDto> {
|
||||
return await this.deviceService.deleteSceneFromSceneDevice(param, query);
|
||||
const projectUuid = req.user.project.uuid;
|
||||
return await this.deviceService.deleteSceneFromSceneDevice(
|
||||
param,
|
||||
query,
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,16 +10,23 @@ export class AddDeviceDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public deviceTuyaUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'userUuid',
|
||||
description: 'spaceUuid',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public userUuid: string;
|
||||
public spaceUuid: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'deviceName',
|
||||
required: true,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
public deviceName: string;
|
||||
}
|
||||
export class UpdateDeviceInSpaceDto {
|
||||
export class AssignDeviceToSpaceDto {
|
||||
@ApiProperty({
|
||||
description: 'deviceUuid',
|
||||
required: true,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user