Merge branch 'dev'

This commit is contained in:
faris Aljohari
2025-04-29 09:58:05 +03:00
152 changed files with 5720 additions and 1265 deletions

View File

@ -5,4 +5,5 @@ export class AuthInterface {
sessionId: string;
id: number;
role?: object;
project?: object;
}

View File

@ -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',
expiresIn: accessTokenExpiry,
}),
this.jwtService.signAsync(payload, {
isRefreshToken
? this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: '7d',
}),
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);

View File

@ -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');

View File

@ -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');

View File

@ -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,
}),
);

View File

@ -0,0 +1,4 @@
export enum BatchDeviceTypeEnum {
RESET = 'RESET',
COMMAND = 'COMMAND',
}

View File

@ -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 =

View File

@ -0,0 +1,3 @@
export enum DeviceTypeEnum {
DOOR_LOCK = 'DOOR_LOCK',
}

View File

@ -0,0 +1,6 @@
export enum PowerClampEnergyEnum {
ENERGY_CONSUMED = 'EnergyConsumed',
ENERGY_CONSUMED_A = 'EnergyConsumedA',
ENERGY_CONSUMED_B = 'EnergyConsumedB',
ENERGY_CONSUMED_C = 'EnergyConsumedC',
}

View File

@ -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',
],
};

View 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';

View 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',
}

View File

@ -0,0 +1,8 @@
import { AsyncLocalStorage } from 'async_hooks';
export interface RequestContextStore {
requestId?: string;
userId?: string;
}
export const requestContext = new AsyncLocalStorage<RequestContextStore>();

View File

@ -42,12 +42,24 @@ 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) => ({
useFactory: (configService: ConfigService) => {
const winstonLogger = createLogger(winstonLoggerOptions);
const typeOrmLogger = new TypeOrmWinstonLogger(winstonLogger);
return {
name: 'default',
type: 'postgres',
host: configService.get('DB_HOST'),
@ -93,21 +105,29 @@ import { TagEntity } from '../modules/space/entities/tag.entity';
SubspaceModelProductAllocationEntity,
SpaceProductAllocationEntity,
SubspaceProductAllocationEntity,
ClientEntity,
PowerClampHourlyEntity,
PowerClampDailyEntity,
PowerClampMonthlyEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
logging: false,
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: 11_000, // return an error after 11 second if connection could not be established
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 {}

View 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;
}

View 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;
}

View 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;
}

View File

@ -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',

View File

@ -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],

View File

@ -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();

View File

@ -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,

View 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,
);
}
}

View 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);
}
}
}

View 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}`);
}
}
}

View File

@ -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);
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) {

View 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 {}

View 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}`,
});
}
}

View 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(),
}),
],
};

View 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());
}
}

View File

@ -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;

View File

@ -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 {}

View 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[];
}

View File

@ -0,0 +1 @@
export * from './client.dto';

View 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[];
}

View File

@ -0,0 +1 @@
export * from './client.entity';

View File

@ -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());
}
}

View File

@ -0,0 +1 @@
export * from './client.repository';

View File

@ -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);

View File

@ -0,0 +1 @@
export * from './power-clamp.dto';

View 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;
}

View File

@ -0,0 +1 @@
export * from './power-clamp.entity';

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -0,0 +1 @@
export * from './power-clamp.repository';

View File

@ -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());
}
}

View File

@ -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,

View File

@ -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,
})

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View 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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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
1 Category code Description
2 dj Light
3 xdd Ceiling light
4 fwd Ambiance light
5 dc String lights
6 dd Strip lights
7 gyd Motion sensor light
8 fsd Ceiling fan light
9 tyndj Solar light
10 tgq Dimmer
11 ykq Remote control
12 kg Switch
13 pc Power strip
14 cz Socket
15 cjkg Scene switch
16 ckqdkg Card switch
17 clkg Curtain switch
18 ckmkzq Garage door opener
19 tgkg Dimmer switch
20 rs Water heater
21 xfj Ventilation system
22 bx Refrigerator
23 yg Bathtub
24 xy Washing machine
25 kt Air conditioner
26 ktkzq Air conditioner controller
27 bgl Wall-hung boiler
28 sd Robot vacuum
29 qn Heater
30 kj Air purifier
31 lyj Drying rack
32 xxj Diffuser
33 cl Curtain
34 mc Door/window controller
35 wk Thermostat
36 yb Bathroom heater
37 ggq Irrigator
38 jsq Humidifier
39 cs Dehumidifier
40 fs Fan
41 js Water purifier
42 dr Electric blanket
43 cwtswsq Pet treat feeder
44 cwwqfsq Pet ball thrower
45 ntq HVAC
46 cwwsq Pet feeder
47 cwysj Pet fountain
48 sf Sofa
49 dbl Electric fireplace
50 tnq Smart milk kettle
51 msp Cat toilet
52 mjj Towel rack
53 sz Smart indoor garden
54 bh Smart kettle
55 mb Bread maker
56 kfj Coffee maker
57 nnq Bottle warmer
58 cn Milk dispenser
59 mzj Sous vide cooker
60 mg Rice cabinet
61 dcl Induction cooker
62 kqzg Air fryer
63 znfh Bento box
64 mal Alarm host
65 sp Smart camera
66 sgbj Siren alarm
67 zd Vibration sensor
68 mcs Contact sensor
69 rqbj Gas alarm
70 ywbj Smoke alarm
71 wsdcg Temperature and humidity sensor
72 sj Water leak detector
73 ylcg Pressure sensor
74 ldcg Luminance sensor
75 sos Emergency button
76 pm2.5 PM2.5 detector
77 pir Human motion sensor
78 cobj CO detector
79 co2bj CO2 detector
80 dgnbj Multi-functional alarm
81 jwbj Methane detector
82 hps Human presence sensor
83 ms Residential lock
84 bxx Safe box
85 gyms Business lock
86 jtmspro Residential lock pro
87 hotelms Hotel lock
88 ms_category Lock accessories
89 jtmsbh Smart lock (keep alive)
90 mk Access control
91 videolock Lock with camera
92 photolock Audio and video lock
93 amy Massage chair
94 liliao Physiotherapy product
95 ts Smart jump rope
96 tzc1 Body fat scale
97 sb Watch/band
98 zndb Smart electricity meter
99 znsb Smart water meter
100 dlq Circuit breaker
101 ds TV set
102 tyy Projector
103 tracker Tracker
104 znyh Smart pill box

View File

@ -0,0 +1,9 @@
import { File } from 'multer';
declare global {
namespace Express {
interface Request {
file?: File;
}
}
}

View 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
View File

@ -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",

View File

@ -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",

View File

@ -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 {}

View File

@ -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],
})

View File

@ -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: {

View File

@ -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,25 +181,25 @@ export class UserAuthService {
otpCode: string;
cooldown: number;
}> {
try {
const otpLimiter = new Date();
otpLimiter.setDate(
otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'),
);
const userExists = await this.userRepository.exists({
const user = await this.userRepository.findOne({
where: {
region: data.regionUuid
? {
uuid: data.regionUuid,
}
: undefined,
region: data.regionUuid ? { uuid: data.regionUuid } : undefined,
email: data.email,
isUserVerified: data.type === OtpType.PASSWORD ? true : undefined,
},
});
if (!userExists) {
if (!user) {
throw new BadRequestException('User not found');
}
await this.otpRepository.softDelete({ email: data.email, type: data.type });
await this.otpRepository.softDelete({
email: data.email,
type: data.type,
});
await this.otpRepository.delete({
email: data.email,
type: data.type,
@ -245,10 +250,29 @@ export class UserAuthService {
},
});
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);
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(

View File

@ -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,

View 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;
}
}

View File

@ -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,

View File

@ -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',

View File

@ -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',

View File

@ -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,8 +81,9 @@ export class AutomationService {
conditions,
spaceUuid,
} = addAutomationDto;
const space = await this.getSpaceByUuid(spaceUuid);
const automation = await this.add({
const space = await this.getSpaceByUuid(spaceUuid, param.projectUuid);
const automation = await this.add(
{
automationName,
effectiveTime,
decisionExpr,
@ -83,7 +91,9 @@ export class AutomationService {
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;
}
}

View 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 {}

View 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);
}
}

View File

@ -0,0 +1 @@
export * from './client.controller';

1
src/client/dtos/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './register-client.dto';

View 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[];
}

View 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;
}

View 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;
}
}

View File

@ -0,0 +1 @@
export * from './client.service';

View 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 {}

View File

@ -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,
};
}
}

View File

@ -0,0 +1 @@
export * from './commission-device.controller';

View 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;
}

View File

@ -0,0 +1 @@
export * from './commission-device.dto';

View 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++;
}
}
}

View File

@ -0,0 +1 @@
export * from './commission-device.service';

View File

@ -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],
})

View File

@ -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);
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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,
);
}
}

View File

@ -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