From fa3cb578dfa72897de08b433c91b3d1f470a960a Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 8 May 2025 13:11:29 +0300 Subject: [PATCH 01/56] param moved --- .../procedure_insert_fact_daily_space_occupancy.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql index b3f121b..0a172a8 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql @@ -56,6 +56,8 @@ presence_detection_summary AS ( SUM(motion_detected + presence_detected) AS count_total_presence_detected FROM presence_detection pd LEFT JOIN device d ON d.uuid = pd.device_id + JOIN params P ON TRUE + AND (P.event_date IS NULL OR pd.event_time::date = P.event_date) GROUP BY 1, 2, 3, 4, 5 ), From f8269df3fba94a0ec9a4f0bfb048f53086e7d065 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 8 May 2025 13:31:43 +0300 Subject: [PATCH 02/56] removed param from first CTE --- .../procedure_insert_fact_daily_space_occupancy.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql index 0a172a8..b539aa0 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql @@ -30,7 +30,6 @@ device_logs AS ( WHERE product.cat_name = 'hps' AND "device-status-log".code = 'presence_state' AND device.uuid::text = P.device_id - AND (P.event_date IS NULL OR "device-status-log".event_time::date = P.event_date) ), presence_detection AS ( From 8030644fee6a511d86ecf16a3188252467e6db17 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Fri, 9 May 2025 13:11:14 +0300 Subject: [PATCH 03/56] refactor: rename presence sensor entities and update related references --- libs/common/src/database/database.module.ts | 8 +++-- .../modules/device/entities/device.entity.ts | 6 ++-- .../entities/presence-sensor.entity.ts | 33 +++++++++++++++++-- .../presence-sensor.repository.module.ts | 12 +++++-- .../presence-sensor.repository.ts | 15 +++++++-- .../modules/space/entities/space.entity.ts | 4 +++ ...dure_insert_fact_daily_space_occupancy.sql | 2 +- ...dure_select_fact_daily_space_occupancy.sql | 2 +- ...ice_presence_detected_insert_statement.sql | 2 +- 9 files changed, 68 insertions(+), 16 deletions(-) diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index f65cb1d..183fbcc 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -51,7 +51,10 @@ import { PowerClampHourlyEntity, PowerClampMonthlyEntity, } from '../modules/power-clamp/entities/power-clamp.entity'; -import { PresenceSensorDailyEntity } from '../modules/presence-sensor/entities'; +import { + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, +} from '../modules/presence-sensor/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -110,7 +113,8 @@ import { PresenceSensorDailyEntity } from '../modules/presence-sensor/entities'; PowerClampHourlyEntity, PowerClampDailyEntity, PowerClampMonthlyEntity, - PresenceSensorDailyEntity, + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index 4dc9519..a172862 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -18,7 +18,7 @@ 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'; -import { PresenceSensorDailyEntity } from '../../presence-sensor/entities'; +import { PresenceSensorDailyDeviceEntity } from '../../presence-sensor/entities'; @Entity({ name: 'device' }) @Unique(['deviceTuyaUuid']) @@ -83,8 +83,8 @@ export class DeviceEntity extends AbstractEntity { public tag: NewTagEntity; @OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device) powerClampHourly: PowerClampHourlyEntity[]; - @OneToMany(() => PresenceSensorDailyEntity, (sensor) => sensor.device) - presenceSensorDaily: PresenceSensorDailyEntity[]; + @OneToMany(() => PresenceSensorDailyDeviceEntity, (sensor) => sensor.device) + presenceSensorDaily: PresenceSensorDailyDeviceEntity[]; constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/modules/presence-sensor/entities/presence-sensor.entity.ts b/libs/common/src/modules/presence-sensor/entities/presence-sensor.entity.ts index 710b245..facbf8c 100644 --- a/libs/common/src/modules/presence-sensor/entities/presence-sensor.entity.ts +++ b/libs/common/src/modules/presence-sensor/entities/presence-sensor.entity.ts @@ -2,10 +2,11 @@ import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { PresenceSensorDto } from '../dtos'; import { DeviceEntity } from '../../device/entities/device.entity'; +import { SpaceEntity } from '../../space/entities/space.entity'; -@Entity({ name: 'presence-sensor-daily-detection' }) +@Entity({ name: 'presence-sensor-daily-device-detection' }) @Unique(['deviceUuid', 'eventDate']) -export class PresenceSensorDailyEntity extends AbstractEntity { +export class PresenceSensorDailyDeviceEntity extends AbstractEntity { @Column({ nullable: false }) public deviceUuid: string; @@ -24,7 +25,33 @@ export class PresenceSensorDailyEntity extends AbstractEntity @ManyToOne(() => DeviceEntity, (device) => device.presenceSensorDaily) device: DeviceEntity; - constructor(partial: Partial) { + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} +@Entity({ name: 'presence-sensor-daily-space-detection' }) +@Unique(['spaceUuid', 'eventDate']) +export class PresenceSensorDailySpaceEntity extends AbstractEntity { + @Column({ nullable: false }) + public spaceUuid: string; + + @Column({ nullable: false, type: 'date' }) + public eventDate: string; + + @Column({ nullable: false }) + public CountMotionDetected: number; + + @Column({ nullable: false }) + public CountPresenceDetected: number; + + @Column({ nullable: false }) + public CountTotalPresenceDetected: number; + + @ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily) + space: SpaceEntity; + + constructor(partial: Partial) { super(); Object.assign(this, partial); } diff --git a/libs/common/src/modules/presence-sensor/presence-sensor.repository.module.ts b/libs/common/src/modules/presence-sensor/presence-sensor.repository.module.ts index 54849b2..fc460c8 100644 --- a/libs/common/src/modules/presence-sensor/presence-sensor.repository.module.ts +++ b/libs/common/src/modules/presence-sensor/presence-sensor.repository.module.ts @@ -1,11 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { PresenceSensorDailyEntity } from './entities/presence-sensor.entity'; +import { + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, +} from './entities/presence-sensor.entity'; @Module({ providers: [], exports: [], controllers: [], - imports: [TypeOrmModule.forFeature([PresenceSensorDailyEntity])], + imports: [ + TypeOrmModule.forFeature([ + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, + ]), + ], }) export class PresenceSensorRepositoryModule {} diff --git a/libs/common/src/modules/presence-sensor/repositories/presence-sensor.repository.ts b/libs/common/src/modules/presence-sensor/repositories/presence-sensor.repository.ts index 2dcd8bc..146eb59 100644 --- a/libs/common/src/modules/presence-sensor/repositories/presence-sensor.repository.ts +++ b/libs/common/src/modules/presence-sensor/repositories/presence-sensor.repository.ts @@ -1,10 +1,19 @@ import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; -import { PresenceSensorDailyEntity } from '../entities'; +import { + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, +} from '../entities'; @Injectable() -export class PresenceSensorDailyRepository extends Repository { +export class PresenceSensorDailyDeviceRepository extends Repository { constructor(private dataSource: DataSource) { - super(PresenceSensorDailyEntity, dataSource.createEntityManager()); + super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class PresenceSensorDailySpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager()); } } diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 9087d80..5a7b4d2 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -10,6 +10,7 @@ import { SpaceModelEntity } from '../../space-model'; import { InviteUserSpaceEntity } from '../../Invite-user/entities'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SubspaceEntity } from './subspace/subspace.entity'; +import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -111,6 +112,9 @@ export class SpaceEntity extends AbstractEntity { ) public productAllocations: SpaceProductAllocationEntity[]; + @OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space) + presenceSensorDaily: PresenceSensorDailySpaceEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql index b539aa0..0fca3a1 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql @@ -94,7 +94,7 @@ daily_aggregates AS ( GROUP BY device_id, event_date ) -INSERT INTO public."presence-sensor-daily-detection" ( +INSERT INTO public."presence-sensor-daily-device-detection" ( device_uuid, event_date, count_motion_detected, diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql index 4b084c4..e5f189b 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql @@ -11,7 +11,7 @@ WITH params AS ( A.count_motion_detected, A.count_presence_detected, A.count_total_presence_detected - FROM public."presence-sensor-daily-detection" AS A + FROM public."presence-sensor-daily-device-detection" AS A JOIN params P ON TRUE WHERE A.device_uuid::text = ANY(P.device_ids) AND (P.month IS NULL diff --git a/libs/common/src/sql/queries/fact_hourly_device_presence_detected/fact_daily_device_presence_detected_insert_statement.sql b/libs/common/src/sql/queries/fact_hourly_device_presence_detected/fact_daily_device_presence_detected_insert_statement.sql index ace575c..4affd7c 100644 --- a/libs/common/src/sql/queries/fact_hourly_device_presence_detected/fact_daily_device_presence_detected_insert_statement.sql +++ b/libs/common/src/sql/queries/fact_hourly_device_presence_detected/fact_daily_device_presence_detected_insert_statement.sql @@ -85,7 +85,7 @@ daily_aggregate AS ( GROUP BY device_id, event_date ) -INSERT INTO public."presence-sensor-daily-detection" ( +INSERT INTO public."presence-sensor-daily-device-detection" ( device_uuid, event_date, count_motion_detected, From 2ba5700fdd3f7205cc4534279c37db8e9eab4f79 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Fri, 9 May 2025 15:24:39 +0300 Subject: [PATCH 04/56] space occupancy API --- ...re_select_fact_daily_device_occupancy.sql} | 0 ...re_update_fact_daily_device_occupancy.sql} | 0 .../proceduce_select_fact_space_occupancy.sql | 12 ++ ..._insert_all_fact_space_occupancy_count.sql | 95 +++++++++++++++ .../procedure_update_fact_space_occupancy.sql | 113 ++++++++++++++++++ 5 files changed, 220 insertions(+) rename libs/common/src/sql/procedures/{fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql => fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql} (100%) rename libs/common/src/sql/procedures/{fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql => fact_device_occupancy_count/procedure_update_fact_daily_device_occupancy.sql} (100%) create mode 100644 libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql create mode 100644 libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql create mode 100644 libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql similarity index 100% rename from libs/common/src/sql/procedures/fact_space_occupancy/procedure_select_fact_daily_space_occupancy.sql rename to libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql diff --git a/libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_update_fact_daily_device_occupancy.sql similarity index 100% rename from libs/common/src/sql/procedures/fact_space_occupancy/procedure_insert_fact_daily_space_occupancy.sql rename to libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_update_fact_daily_device_occupancy.sql diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql new file mode 100644 index 0000000..1faaa1e --- /dev/null +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql @@ -0,0 +1,12 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date, + $4::uuid AS space_id +) + +select psdsd.* +from public."presence-sensor-daily-space-detection" psdsd +JOIN params P ON true +where psdsd.space_uuid = P.space_id +and (P.event_date IS NULL or psdsd.event_date::date = P.event_date) +ORDER BY 1,2 diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql new file mode 100644 index 0000000..e0d348d --- /dev/null +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql @@ -0,0 +1,95 @@ +WITH device_logs AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".value, + 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' +), + +-- 1. All 'none' → presence or motion +presence_transitions AS ( + SELECT + space_id, + event_time, + event_time::date AS event_date, + value + FROM device_logs + WHERE (value = 'motion' OR value = 'presence') AND prev_value = 'none' +), + +-- 2. Cluster events per space_id within 30s +clustered_events AS ( + SELECT + space_id, + event_time, + event_date, + value, + SUM(new_cluster_flag) OVER (PARTITION BY space_id ORDER BY event_time) AS cluster_id + FROM ( + SELECT *, + CASE + WHEN event_time - LAG(event_time) OVER (PARTITION BY space_id ORDER BY event_time) > INTERVAL '30 seconds' + THEN 1 ELSE 0 + END AS new_cluster_flag + FROM presence_transitions + ) marked +), + +-- 3. Determine dominant type (motion vs presence) per cluster +cluster_type AS ( + SELECT + space_id, + event_date, + cluster_id, + COUNT(*) FILTER (WHERE value = 'motion') AS motion_count, + COUNT(*) FILTER (WHERE value = 'presence') AS presence_count, + CASE + WHEN COUNT(*) FILTER (WHERE value = 'motion') > COUNT(*) FILTER (WHERE value = 'presence') THEN 'motion' + ELSE 'presence' + END AS dominant_type + FROM clustered_events + GROUP BY space_id, event_date, cluster_id +), + +-- 4. Count clusters by dominant type +summary AS ( + SELECT + space_id, + event_date, + COUNT(*) FILTER (WHERE dominant_type = 'motion') AS count_motion_detected, + COUNT(*) FILTER (WHERE dominant_type = 'presence') AS count_presence_detected, + COUNT(*) AS count_total_presence_detected + FROM cluster_type + GROUP BY space_id, event_date +) + +-- 5. Output +, final_table as ( +SELECT * +FROM summary +ORDER BY space_id, event_date) + + +INSERT INTO public."presence-sensor-daily-space-detection" ( + space_uuid, + event_date, + count_motion_detected, + count_presence_detected, + count_total_presence_detected +) +SELECT + space_id, + event_date, + count_motion_detected, + count_presence_detected, + count_total_presence_detected +FROM final_table; \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql new file mode 100644 index 0000000..b166cb5 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql @@ -0,0 +1,113 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date, + $4::uuid AS space_id +), + +device_logs AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".value, + 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' +), + +-- 1. All 'none' → presence or motion +presence_transitions AS ( + SELECT + space_id, + event_time, + event_time::date AS event_date, + value + FROM device_logs + WHERE (value = 'motion' OR value = 'presence') AND prev_value = 'none' +), + +-- 2. Cluster events per space_id within 30s +clustered_events AS ( + SELECT + space_id, + event_time, + event_date, + value, + SUM(new_cluster_flag) OVER (PARTITION BY space_id ORDER BY event_time) AS cluster_id + FROM ( + SELECT *, + CASE + WHEN event_time - LAG(event_time) OVER (PARTITION BY space_id ORDER BY event_time) > INTERVAL '30 seconds' + THEN 1 ELSE 0 + END AS new_cluster_flag + FROM presence_transitions + ) marked +), + +-- 3. Determine dominant type (motion vs presence) per cluster +cluster_type AS ( + SELECT + space_id, + event_date, + cluster_id, + COUNT(*) FILTER (WHERE value = 'motion') AS motion_count, + COUNT(*) FILTER (WHERE value = 'presence') AS presence_count, + CASE + WHEN COUNT(*) FILTER (WHERE value = 'motion') > COUNT(*) FILTER (WHERE value = 'presence') THEN 'motion' + ELSE 'presence' + END AS dominant_type + FROM clustered_events + GROUP BY space_id, event_date, cluster_id +), + +-- 4. Count clusters by dominant type +summary AS ( + SELECT + space_id, + event_date, + COUNT(*) FILTER (WHERE dominant_type = 'motion') AS count_motion_detected, + COUNT(*) FILTER (WHERE dominant_type = 'presence') AS count_presence_detected, + COUNT(*) AS count_total_presence_detected + FROM cluster_type + GROUP BY space_id, event_date +) + +-- 5. Output +, final_table as ( +SELECT summary.space_id, + summary.event_date, + count_motion_detected, + count_presence_detected, + count_total_presence_detected +FROM summary +JOIN params P ON true +where summary.space_id = P.space_id +and (P.event_date IS NULL or summary.event_date::date = P.event_date) +ORDER BY space_id, event_date) + + +INSERT INTO public."presence-sensor-daily-space-detection" ( + space_uuid, + event_date, + count_motion_detected, + count_presence_detected, + count_total_presence_detected +) +SELECT + space_id, + event_date, + count_motion_detected, + count_presence_detected, + count_total_presence_detected +FROM final_table +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + count_motion_detected = EXCLUDED.count_motion_detected, + count_presence_detected = EXCLUDED.count_presence_detected, + count_total_presence_detected = EXCLUDED.count_total_presence_detected; From 3535c1d8c591289a2a31a1983b3dc0bd06af7b68 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Fri, 9 May 2025 15:47:05 +0300 Subject: [PATCH 05/56] month param --- .../proceduce_select_fact_space_occupancy.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql index 1faaa1e..ac6756d 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql @@ -1,12 +1,12 @@ WITH params AS ( SELECT - TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date, - $4::uuid AS space_id + $1::uuid AS space_id, + TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month ) select psdsd.* from public."presence-sensor-daily-space-detection" psdsd JOIN params P ON true where psdsd.space_uuid = P.space_id -and (P.event_date IS NULL or psdsd.event_date::date = P.event_date) -ORDER BY 1,2 +AND (P.event_month IS NULL OR TO_CHAR(psdsd.event_date, 'YYYY-MM') = TO_CHAR(P.event_month, 'YYYY-MM')) +ORDER BY space_uuid, event_date \ No newline at end of file From 9c3abdd08a04dd36e84f16eb897ff1230af481bb Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Fri, 9 May 2025 16:25:30 +0300 Subject: [PATCH 06/56] added on conflict --- .../procedure_insert_all_fact_space_occupancy_count.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql index e0d348d..32a7283 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_insert_all_fact_space_occupancy_count.sql @@ -92,4 +92,9 @@ SELECT count_motion_detected, count_presence_detected, count_total_presence_detected -FROM final_table; \ No newline at end of file +FROM final_table +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + count_motion_detected = EXCLUDED.count_motion_detected, + count_presence_detected = EXCLUDED.count_presence_detected, + count_total_presence_detected = EXCLUDED.count_total_presence_detected; \ No newline at end of file From fb5084ba3aa9471aaaa12e2827153d2e82a10c48 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 11 May 2025 01:59:26 +0300 Subject: [PATCH 07/56] feat: add groupByDevice option to GetPowerClampBySpaceDto and update service logic --- src/power-clamp/dto/get-power-clamp.dto.ts | 23 ++++- .../services/power-clamp.service.ts | 84 +++++++++++++------ 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/power-clamp/dto/get-power-clamp.dto.ts b/src/power-clamp/dto/get-power-clamp.dto.ts index d53b681..1121b96 100644 --- a/src/power-clamp/dto/get-power-clamp.dto.ts +++ b/src/power-clamp/dto/get-power-clamp.dto.ts @@ -1,5 +1,13 @@ +import { BooleanValues } from '@app/common/constants/boolean-values.enum'; import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsDateString, Matches, IsNotEmpty } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { + IsOptional, + IsDateString, + Matches, + IsNotEmpty, + IsBoolean, +} from 'class-validator'; export class GetPowerClampDto { @ApiPropertyOptional({ @@ -42,4 +50,17 @@ export class GetPowerClampBySpaceDto { @IsDateString() @IsNotEmpty() public monthDate: string; + + @ApiPropertyOptional({ + example: true, + description: 'Whether to group results by device or not', + required: false, + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform((value) => { + return value.obj.groupByDevice === BooleanValues.TRUE; + }) + public groupByDevice?: boolean = false; } diff --git a/src/power-clamp/services/power-clamp.service.ts b/src/power-clamp/services/power-clamp.service.ts index 24bca95..e7e0df4 100644 --- a/src/power-clamp/services/power-clamp.service.ts +++ b/src/power-clamp/services/power-clamp.service.ts @@ -45,7 +45,7 @@ export class PowerClampService { params: ResourceParamsDto, query: GetPowerClampBySpaceDto, ): Promise { - const { monthDate } = query; + const { monthDate, groupByDevice } = query; const { spaceUuid, communityUuid } = params; try { @@ -63,44 +63,78 @@ export class PowerClampService { if (!devices?.length) { return this.buildResponse( - 'No power clamp devices found for the specified criteria', + `No devices found for ${spaceUuid ? 'space' : 'community'}`, [], ); } - // Filter and prepare device UUIDs - const deviceUuids = devices - .filter((device) => device.productDevice?.prodType === ProductType.PC) - .map((device) => device.uuid); + // Filter power clamp devices + const powerClampDevices = devices.filter( + (device) => device.productDevice?.prodType === ProductType.PC, + ); - if (deviceUuids.length === 0) { + if (powerClampDevices.length === 0) { return this.buildResponse( - 'No power clamp devices (PC type) found for the specified criteria', + `No power clamp devices found for ${spaceUuid ? 'space' : 'community'}`, [], ); } - // Execute procedure const formattedMonthDate = toMMYYYY(monthDate); - const data = await this.executeProcedure( - 'fact_daily_space_energy_consumed_procedure', - [formattedMonthDate, deviceUuids.join(',')], - ); - // Format and filter data - const formattedData = data.map((item) => ({ - ...item, - date: new Date(item.date).toLocaleDateString('en-CA'), // YYYY-MM-DD - })); + if (groupByDevice) { + // Handle per-device response + const deviceDataPromises = powerClampDevices.map(async (device) => { + const data = await this.executeProcedure( + 'fact_daily_space_energy_consumed_procedure', + [formattedMonthDate, device.uuid], + ); - const resultData = monthDate - ? filterByMonth(formattedData, monthDate) - : formattedData; + const formattedData = data.map((item) => ({ + ...item, + date: new Date(item.date).toLocaleDateString('en-CA'), // YYYY-MM-DD + })); - return this.buildResponse( - `Power clamp data fetched successfully for ${spaceUuid ? 'space' : 'community'}`, - resultData, - ); + const filteredData = monthDate + ? filterByMonth(formattedData, monthDate) + : formattedData; + + return { + deviceUuid: device.uuid, + deviceName: device.name || `Power Clamp Device`, + data: filteredData, + }; + }); + + const deviceDataResults = await Promise.all(deviceDataPromises); + + return this.buildResponse( + `Power clamp data fetched successfully for ${spaceUuid ? 'space' : 'community'}`, + deviceDataResults, + ); + } else { + // Original behavior - all devices together + const deviceUuids = powerClampDevices.map((device) => device.uuid); + const data = await this.executeProcedure( + 'fact_daily_space_energy_consumed_procedure', + [formattedMonthDate, deviceUuids.join(',')], + ); + + // Format and filter data + const formattedData = data.map((item) => ({ + ...item, + date: new Date(item.date).toLocaleDateString('en-CA'), // YYYY-MM-DD + })); + + const resultData = monthDate + ? filterByMonth(formattedData, monthDate) + : formattedData; + + return this.buildResponse( + `Power clamp data fetched successfully for ${spaceUuid ? 'space' : 'community'}`, + resultData, + ); + } } catch (error) { console.error('Error fetching power clamp data', { error, From 7ec4171e1a67ee1d81a56ee79eb76089c3a26459 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 12 May 2025 02:15:57 +0300 Subject: [PATCH 08/56] feat: add occupancy module with controller, service, and related DTOs for heat map data retrieval --- libs/common/src/constants/controller-route.ts | 11 +++ .../src/constants/presence.sensor.enum.ts | 4 ++ .../devices-status/devices-status.module.ts | 2 + .../services/devices-status.service.ts | 20 ++++++ .../src/helper/services/occupancy.service.ts | 49 +++++++++++++ .../proceduce_select_fact_space_occupancy.sql | 13 ++-- .../procedure_update_fact_space_occupancy.sql | 4 +- src/app.module.ts | 6 +- .../commission-device.module.ts | 2 + src/community/community.module.ts | 2 + src/door-lock/door.lock.module.ts | 2 + src/group/group.module.ts | 2 + src/invite-user/invite-user.module.ts | 2 + src/occupancy/controllers/index.ts | 1 + .../controllers/occupancy.controller.ts | 46 +++++++++++++ src/occupancy/dto/get-occupancy.dto.ts | 15 ++++ src/occupancy/dto/occupancy-params.dto.ts | 7 ++ src/occupancy/occupancy.module.ts | 11 +++ src/occupancy/services/index.ts | 1 + src/occupancy/services/occupancy.service.ts | 69 +++++++++++++++++++ src/power-clamp/power-clamp.module.ts | 2 + src/project/project.module.ts | 2 + src/space-model/space-model.module.ts | 2 + src/space/space.module.ts | 2 + .../visitor-password.module.ts | 2 + 25 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 libs/common/src/constants/presence.sensor.enum.ts create mode 100644 libs/common/src/helper/services/occupancy.service.ts create mode 100644 src/occupancy/controllers/index.ts create mode 100644 src/occupancy/controllers/occupancy.controller.ts create mode 100644 src/occupancy/dto/get-occupancy.dto.ts create mode 100644 src/occupancy/dto/occupancy-params.dto.ts create mode 100644 src/occupancy/occupancy.module.ts create mode 100644 src/occupancy/services/index.ts create mode 100644 src/occupancy/services/occupancy.service.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 852c4a8..8fdffbe 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -504,6 +504,17 @@ export class ControllerRoute { 'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.'; }; }; + + static Occupancy = class { + public static readonly ROUTE = 'occupancy'; + + static ACTIONS = class { + public static readonly GET_OCCUPANCY_HEAT_MAP_SUMMARY = + 'Get occupancy heat map data'; + public static readonly GET_OCCUPANCY_HEAT_MAP_DESCRIPTION = + 'This endpoint retrieves the occupancy heat map data based on the provided parameters.'; + }; + }; static DEVICE = class { public static readonly ROUTE = 'devices'; diff --git a/libs/common/src/constants/presence.sensor.enum.ts b/libs/common/src/constants/presence.sensor.enum.ts new file mode 100644 index 0000000..d2f9e50 --- /dev/null +++ b/libs/common/src/constants/presence.sensor.enum.ts @@ -0,0 +1,4 @@ +export enum PresenceSensorEnum { + PRESENCE_STATE = 'presence_state', + SENSITIVITY = 'sensitivity', +} diff --git a/libs/common/src/firebase/devices-status/devices-status.module.ts b/libs/common/src/firebase/devices-status/devices-status.module.ts index 217a4d8..784b801 100644 --- a/libs/common/src/firebase/devices-status/devices-status.module.ts +++ b/libs/common/src/firebase/devices-status/devices-status.module.ts @@ -10,6 +10,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ providers: [ @@ -21,6 +22,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], controllers: [DeviceStatusFirebaseController], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index 739d846..f01b0be 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -21,6 +21,8 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log 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'; +import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -29,6 +31,7 @@ export class DeviceStatusFirebaseService { private readonly configService: ConfigService, private readonly deviceRepository: DeviceRepository, private readonly powerClampService: PowerClampService, + private readonly occupancyService: OccupancyService, private deviceStatusLogRepository: DeviceStatusLogRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -240,6 +243,23 @@ export class DeviceStatusFirebaseService { } } + if ( + addDeviceStatusDto.productType === ProductType.CPS || + addDeviceStatusDto.productType === ProductType.WPS + ) { + const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]); + + const occupancyStatus = addDeviceStatusDto?.log?.properties?.find( + (status) => occupancyCodes.has(status.code), + ); + + if (occupancyStatus) { + await this.occupancyService.updateOccupancySensorHistoricalData( + addDeviceStatusDto.deviceUuid, + ); + } + } + // Return the updated data const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); diff --git a/libs/common/src/helper/services/occupancy.service.ts b/libs/common/src/helper/services/occupancy.service.ts new file mode 100644 index 0000000..3e8fc7c --- /dev/null +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -0,0 +1,49 @@ +import { DeviceRepository } from '@app/common/modules/device/repositories'; +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 OccupancyService { + constructor( + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, + private readonly deviceRepository: DeviceRepository, + ) {} + + async updateOccupancySensorHistoricalData(deviceUuid: string): Promise { + try { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations: ['spaceDevice'], + }); + + await this.executeProcedure('procedure_update_fact_space_occupancy', [ + dateStr, + device.spaceDevice?.uuid, + ]); + } catch (err) { + console.error('Failed to insert or update occupancy data:', err); + throw err; + } + } + + private async executeProcedure( + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery( + 'fact_space_occupancy_count', + procedureFileName, + ); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } + + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql index ac6756d..113eb07 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/proceduce_select_fact_space_occupancy.sql @@ -1,12 +1,15 @@ WITH params AS ( SELECT $1::uuid AS space_id, - TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month + TO_DATE(NULLIF($2, ''), 'YYYY') AS event_year ) -select psdsd.* -from public."presence-sensor-daily-space-detection" psdsd +SELECT psdsd.* +FROM public."presence-sensor-daily-space-detection" psdsd JOIN params P ON true -where psdsd.space_uuid = P.space_id -AND (P.event_month IS NULL OR TO_CHAR(psdsd.event_date, 'YYYY-MM') = TO_CHAR(P.event_month, 'YYYY-MM')) +WHERE psdsd.space_uuid = P.space_id +AND ( + P.event_year IS NULL + OR TO_CHAR(psdsd.event_date, 'YYYY') = TO_CHAR(P.event_year, 'YYYY') +) ORDER BY space_uuid, event_date \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql index b166cb5..cc727c0 100644 --- a/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_space_occupancy_count/procedure_update_fact_space_occupancy.sql @@ -1,7 +1,7 @@ WITH params AS ( SELECT - TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date, - $4::uuid AS space_id + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id ), device_logs AS ( diff --git a/src/app.module.ts b/src/app.module.ts index 704d6b3..e674880 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -37,12 +37,13 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; +import { OccupancyModule } from './occupancy/occupancy.module'; @Module({ imports: [ ConfigModule.forRoot({ load: config, }), - /* ThrottlerModule.forRoot({ + /* ThrottlerModule.forRoot({ throttlers: [{ ttl: 100000, limit: 30 }], }), */ WinstonModule.forRoot(winstonLoggerOptions), @@ -77,13 +78,14 @@ import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston DeviceCommissionModule, PowerClampModule, HealthModule, + OccupancyModule, ], providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }, - /* { + /* { provide: APP_GUARD, useClass: ThrottlerGuard, }, */ diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 71c93bd..8821410 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -28,6 +28,7 @@ import { } 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'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -55,6 +56,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [], }) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index 289ece4..a6f64df 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -63,6 +63,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], @@ -114,6 +115,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [CommunityService, SpacePermissionService], }) diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index adbfceb..459a4fb 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -27,6 +27,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -52,6 +53,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index a903373..8380793 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -25,6 +25,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], @@ -49,6 +50,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [GroupService], }) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index fdf5251..fb1ba8d 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -83,6 +83,7 @@ import { } 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'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], @@ -152,6 +153,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [InviteUserService], }) diff --git a/src/occupancy/controllers/index.ts b/src/occupancy/controllers/index.ts new file mode 100644 index 0000000..c1973ca --- /dev/null +++ b/src/occupancy/controllers/index.ts @@ -0,0 +1 @@ +export * from './occupancy.controller'; diff --git a/src/occupancy/controllers/occupancy.controller.ts b/src/occupancy/controllers/occupancy.controller.ts new file mode 100644 index 0000000..d6aa9b1 --- /dev/null +++ b/src/occupancy/controllers/occupancy.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiParam, +} from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { OccupancyService } from '../services/occupancy.service'; +import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SpaceParamsDto } from '../dto/occupancy-params.dto'; + +@ApiTags('Occupancy Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.Occupancy.ROUTE, +}) +export class OccupancyController { + constructor(private readonly occupancyService: OccupancyService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('heat-map/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY, + description: + ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getOccupancyHeatMapDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetOccupancyHeatMapBySpaceDto, + ): Promise { + return await this.occupancyService.getOccupancyHeatMapDataBySpace( + params, + query, + ); + } +} diff --git a/src/occupancy/dto/get-occupancy.dto.ts b/src/occupancy/dto/get-occupancy.dto.ts new file mode 100644 index 0000000..26ea0a5 --- /dev/null +++ b/src/occupancy/dto/get-occupancy.dto.ts @@ -0,0 +1,15 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Matches, IsNotEmpty } from 'class-validator'; + +export class GetOccupancyHeatMapBySpaceDto { + @ApiPropertyOptional({ + description: 'Input year in YYYY format to filter the data', + example: '2025', + required: false, + }) + @IsNotEmpty() + @Matches(/^\d{4}$/, { + message: 'Year must be in YYYY format', + }) + year: string; +} diff --git a/src/occupancy/dto/occupancy-params.dto.ts b/src/occupancy/dto/occupancy-params.dto.ts new file mode 100644 index 0000000..6e26d4f --- /dev/null +++ b/src/occupancy/dto/occupancy-params.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class SpaceParamsDto { + @IsUUID('4', { message: 'Invalid UUID format' }) + @IsNotEmpty() + spaceUuid: string; +} diff --git a/src/occupancy/occupancy.module.ts b/src/occupancy/occupancy.module.ts new file mode 100644 index 0000000..6293858 --- /dev/null +++ b/src/occupancy/occupancy.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { OccupancyController } from './controllers'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from './services'; +@Module({ + imports: [ConfigModule], + controllers: [OccupancyController], + providers: [OccupancyService, SqlLoaderService], +}) +export class OccupancyModule {} diff --git a/src/occupancy/services/index.ts b/src/occupancy/services/index.ts new file mode 100644 index 0000000..d8dc93e --- /dev/null +++ b/src/occupancy/services/index.ts @@ -0,0 +1 @@ +export * from './occupancy.service'; diff --git a/src/occupancy/services/occupancy.service.ts b/src/occupancy/services/occupancy.service.ts new file mode 100644 index 0000000..59afe18 --- /dev/null +++ b/src/occupancy/services/occupancy.service.ts @@ -0,0 +1,69 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { SpaceParamsDto } from '../dto/occupancy-params.dto'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { DataSource } from 'typeorm'; +import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; + +@Injectable() +export class OccupancyService { + constructor( + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, + ) {} + + async getOccupancyHeatMapDataBySpace( + params: SpaceParamsDto, + query: GetOccupancyHeatMapBySpaceDto, + ): Promise { + const { year } = query; + const { spaceUuid } = params; + + try { + const data = await this.executeProcedure( + 'proceduce_select_fact_space_occupancy', + [spaceUuid, year], + ); + const formattedData = data.map((item) => ({ + ...item, + event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD + })); + return this.buildResponse( + `Occupancy heat map data fetched successfully for ${spaceUuid ? 'space' : 'community'}`, + formattedData, + ); + } catch (error) { + console.error('Failed to fetch occupancy heat map data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch occupancy heat map data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private buildResponse(message: string, data: any[]) { + return new SuccessResponseDto({ + message, + data, + statusCode: HttpStatus.OK, + }); + } + private async executeProcedure( + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery( + 'fact_space_occupancy_count', + procedureFileName, + ); + return await this.dataSource.query(query, params); + } + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 3f0595c..e85ea1b 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -60,6 +60,7 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; 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 { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule], controllers: [PowerClampController], @@ -109,6 +110,7 @@ import { SubspaceModelProductAllocationService } from 'src/space-model/services/ SubspaceModelProductAllocationService, SpaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory, + OccupancyService, ], exports: [PowerClamp], }) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index e52294d..fd9acb3 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -67,6 +67,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -124,6 +125,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [ProjectService, CqrsModule], }) diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index e5b4426..be57bb3 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -65,6 +65,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; const CommandHandlers = [ PropogateUpdateSpaceModelHandler, @@ -124,6 +125,7 @@ const CommandHandlers = [ PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [CqrsModule, SpaceModelService], }) diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 13b4ecc..f74d993 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -90,6 +90,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; export const CommandHandlers = [DisableSpaceHandler]; @@ -166,6 +167,7 @@ export const CommandHandlers = [DisableSpaceHandler]; PowerClampMonthlyRepository, PowerClampService, SqlLoaderService, + OccupancyService, ], exports: [SpaceService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 061f79d..91f9c2e 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -29,6 +29,7 @@ import { PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -55,6 +56,7 @@ import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service PowerClampDailyRepository, PowerClampMonthlyRepository, SqlLoaderService, + OccupancyService, ], exports: [VisitorPasswordService], }) From 799fcb6fb9ef677b53c26582d48cebef061f2026 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 May 2025 03:06:43 +0300 Subject: [PATCH 09/56] feat: add DEVICE_SPACE_COMMUNITY route and controller for device retrieval by space or community --- libs/common/src/constants/controller-route.ts | 9 ++ src/automation/automation.module.ts | 2 + .../device-space-community.controller.ts | 52 +++++++ src/device/device.module.ts | 9 +- src/device/dtos/get.device.dto.ts | 25 +++- src/device/services/device.service.ts | 141 ++++++++++++++++++ src/door-lock/door.lock.module.ts | 2 + src/group/group.module.ts | 2 + src/scene/scene.module.ts | 2 + .../visitor-password.module.ts | 2 + 10 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/device/controllers/device-space-community.controller.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 8fdffbe..56c9bb4 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -624,6 +624,15 @@ export class ControllerRoute { 'This endpoint retrieves all devices in the system.'; }; }; + static DEVICE_SPACE_COMMUNITY = class { + public static readonly ROUTE = 'devices/recursive-child'; + static ACTIONS = class { + public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY = + 'Get all devices by space or community with recursive child'; + public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION = + 'This endpoint retrieves all devices in the system by space or community with recursive child.'; + }; + }; static DEVICE_PERMISSION = class { public static readonly ROUTE = 'device-permission'; diff --git a/src/automation/automation.module.ts b/src/automation/automation.module.ts index c90d106..d24bdb2 100644 --- a/src/automation/automation.module.ts +++ b/src/automation/automation.module.ts @@ -18,6 +18,7 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito 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'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -35,6 +36,7 @@ import { AutomationSpaceController } from './controllers/automation-space.contro SceneDeviceRepository, AutomationRepository, ProjectRepository, + CommunityRepository, ], exports: [AutomationService], }) diff --git a/src/device/controllers/device-space-community.controller.ts b/src/device/controllers/device-space-community.controller.ts new file mode 100644 index 0000000..e19f1f5 --- /dev/null +++ b/src/device/controllers/device-space-community.controller.ts @@ -0,0 +1,52 @@ +import { DeviceService } from '../services/device.service'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiQuery, +} 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 { GetDevicesBySpaceOrCommunityDto } from '../dtos'; + +@ApiTags('Device Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.DEVICE_SPACE_COMMUNITY.ROUTE, +}) +export class DeviceSpaceOrCommunityController { + constructor(private readonly deviceService: DeviceService) {} + + @ApiBearerAuth() + @UseGuards(PermissionsGuard) + @Permissions('DEVICE_VIEW') + @Get() + @ApiOperation({ + summary: + ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS + .GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY, + description: + ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS + .GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION, + }) + @ApiQuery({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: false, + }) + @ApiQuery({ + name: 'communityUuid', + description: 'UUID of the Community', + required: false, + }) + async getAllDevicesBySpaceOrCommunityWithChild( + @Query() query: GetDevicesBySpaceOrCommunityDto, + ) { + return await this.deviceService.getAllDevicesBySpaceOrCommunityWithChild( + query, + ); + } +} diff --git a/src/device/device.module.ts b/src/device/device.module.ts index fdd3350..6a5a469 100644 --- a/src/device/device.module.ts +++ b/src/device/device.module.ts @@ -22,6 +22,8 @@ import { SceneDeviceRepository } from '@app/common/modules/scene-device/reposito import { AutomationRepository } from '@app/common/modules/automation/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { DeviceProjectController } from './controllers/device-project.controller'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceSpaceOrCommunityController } from './controllers/device-space-community.controller'; @Module({ imports: [ ConfigModule, @@ -30,7 +32,11 @@ import { DeviceProjectController } from './controllers/device-project.controller DeviceRepositoryModule, DeviceStatusFirebaseModule, ], - controllers: [DeviceController, DeviceProjectController], + controllers: [ + DeviceController, + DeviceProjectController, + DeviceSpaceOrCommunityController, + ], providers: [ DeviceService, ProductRepository, @@ -46,6 +52,7 @@ import { DeviceProjectController } from './controllers/device-project.controller SceneRepository, SceneDeviceRepository, AutomationRepository, + CommunityRepository, ], exports: [DeviceService], }) diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 9080062..1ffc109 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -1,6 +1,12 @@ import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; export class GetDeviceBySpaceUuidDto { @ApiProperty({ @@ -44,3 +50,20 @@ export class GetDoorLockDevices { @IsOptional() public deviceType: DeviceTypeEnum; } +export class GetDevicesBySpaceOrCommunityDto { + @ApiProperty({ + description: 'Device Product Type', + example: 'PC', + required: true, + }) + @IsString() + @IsNotEmpty() + public deviceType: string; + @IsUUID('4', { message: 'Invalid space UUID format' }) + @IsOptional() + spaceUuid?: string; + + @IsUUID('4', { message: 'Invalid community UUID format' }) + @IsOptional() + communityUuid?: string; +} diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 59cee2f..1e0a8a0 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -31,6 +31,7 @@ import { import { GetDeviceBySpaceUuidDto, GetDeviceLogsDto, + GetDevicesBySpaceOrCommunityDto, GetDoorLockDevices, } from '../dtos/get.device.dto'; import { @@ -65,6 +66,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; import { ProjectParam } from '../dtos'; import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Injectable() export class DeviceService { @@ -80,6 +82,7 @@ export class DeviceService { private readonly sceneService: SceneService, private readonly tuyaService: TuyaService, private readonly projectRepository: ProjectRepository, + private readonly communityRepository: CommunityRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); const secretKey = this.configService.get('auth-config.SECRET_KEY'); @@ -1722,4 +1725,142 @@ export class DeviceService { statusCode: HttpStatus.OK, }); } + async getAllDevicesBySpaceOrCommunityWithChild( + query: GetDevicesBySpaceOrCommunityDto, + ): Promise { + try { + const { spaceUuid, communityUuid, deviceType } = query; + if (!spaceUuid && !communityUuid) { + throw new BadRequestException( + 'Either spaceUuid or communityUuid must be provided', + ); + } + + // Get devices based on space or community + const devices = spaceUuid + ? await this.getAllDevicesBySpace(spaceUuid) + : await this.getAllDevicesByCommunity(communityUuid); + + if (!devices?.length) { + return new SuccessResponseDto({ + message: `No devices found for ${spaceUuid ? 'space' : 'community'}`, + data: [], + statusCode: HttpStatus.CREATED, + }); + } + + const devicesFilterd = devices.filter( + (device) => device.productDevice?.prodType === deviceType, + ); + + if (devicesFilterd.length === 0) { + return new SuccessResponseDto({ + message: `No ${deviceType} devices found for ${spaceUuid ? 'space' : 'community'}`, + data: [], + statusCode: HttpStatus.CREATED, + }); + } + + return new SuccessResponseDto({ + message: `Devices fetched successfully`, + data: devicesFilterd, + statusCode: HttpStatus.OK, + }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async getAllDevicesBySpace(spaceUuid: string): Promise { + const space = await this.spaceRepository.findOne({ + where: { uuid: spaceUuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (!space) { + throw new NotFoundException('Space not found'); + } + + const allDevices: DeviceEntity[] = [...space.devices]; + + // Recursive fetch function + const fetchChildren = async (parentSpace: SpaceEntity) => { + const children = await this.spaceRepository.find({ + where: { parent: { uuid: parentSpace.uuid } }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + for (const child of children) { + allDevices.push(...child.devices); + + if (child.children.length > 0) { + await fetchChildren(child); + } + } + }; + + // Start recursive fetch + await fetchChildren(space); + + return allDevices; + } + async getAllDevicesByCommunity( + communityUuid: string, + ): Promise { + // Fetch the community and its top-level spaces + const community = await this.communityRepository.findOne({ + where: { uuid: communityUuid }, + relations: [ + 'spaces', + 'spaces.children', + 'spaces.devices', + 'spaces.devices.productDevice', + ], + }); + + if (!community) { + throw new NotFoundException('Community not found'); + } + + const allDevices: DeviceEntity[] = []; + + // Recursive fetch function for spaces + const fetchSpaceDevices = async (space: SpaceEntity) => { + if (space.devices && space.devices.length > 0) { + allDevices.push(...space.devices); + } + + if (space.children && space.children.length > 0) { + for (const childSpace of space.children) { + const fullChildSpace = await this.spaceRepository.findOne({ + where: { uuid: childSpace.uuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (fullChildSpace) { + await fetchSpaceDevices(fullChildSpace); + } + } + } + }; + + // Start recursive fetch for all top-level spaces + for (const space of community.spaces) { + const fullSpace = await this.spaceRepository.findOne({ + where: { uuid: space.uuid }, + relations: ['children', 'devices', 'devices.productDevice'], + }); + + if (fullSpace) { + await fetchSpaceDevices(fullSpace); + } + } + + return allDevices; + } } diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index 459a4fb..3f2cee7 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -28,6 +28,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -54,6 +55,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index 8380793..b50bac9 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -26,6 +26,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], @@ -51,6 +52,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [GroupService], }) diff --git a/src/scene/scene.module.ts b/src/scene/scene.module.ts index 0fe961b..7243d9c 100644 --- a/src/scene/scene.module.ts +++ b/src/scene/scene.module.ts @@ -16,6 +16,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service 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'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], @@ -32,6 +33,7 @@ import { ProjectRepository } from '@app/common/modules/project/repositiories'; ProjectRepository, SceneDeviceRepository, AutomationRepository, + CommunityRepository, ], exports: [SceneService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 91f9c2e..6924713 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -30,6 +30,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -57,6 +58,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + CommunityRepository, ], exports: [VisitorPasswordService], }) From 4aa3d04478289f3728fb0a7aef9604b33f5df839 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 13 May 2025 03:19:21 +0300 Subject: [PATCH 10/56] feat: update device and space services to use productType instead of deviceType and add query support for device retrieval by product type --- src/device/dtos/get.device.dto.ts | 2 +- src/device/services/device.service.ts | 6 ++--- .../controllers/space-device.controller.ts | 6 +++-- src/space/dtos/device.space.dto.ts | 13 ++++++++++ src/space/services/space-device.service.ts | 26 ++++++++++++++++--- 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 src/space/dtos/device.space.dto.ts diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 1ffc109..868b1e1 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -58,7 +58,7 @@ export class GetDevicesBySpaceOrCommunityDto { }) @IsString() @IsNotEmpty() - public deviceType: string; + public productType: string; @IsUUID('4', { message: 'Invalid space UUID format' }) @IsOptional() spaceUuid?: string; diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 1e0a8a0..717affd 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1729,7 +1729,7 @@ export class DeviceService { query: GetDevicesBySpaceOrCommunityDto, ): Promise { try { - const { spaceUuid, communityUuid, deviceType } = query; + const { spaceUuid, communityUuid, productType } = query; if (!spaceUuid && !communityUuid) { throw new BadRequestException( 'Either spaceUuid or communityUuid must be provided', @@ -1750,12 +1750,12 @@ export class DeviceService { } const devicesFilterd = devices.filter( - (device) => device.productDevice?.prodType === deviceType, + (device) => device.productDevice?.prodType === productType, ); if (devicesFilterd.length === 0) { return new SuccessResponseDto({ - message: `No ${deviceType} devices found for ${spaceUuid ? 'space' : 'community'}`, + message: `No ${productType} devices found for ${spaceUuid ? 'space' : 'community'}`, data: [], statusCode: HttpStatus.CREATED, }); diff --git a/src/space/controllers/space-device.controller.ts b/src/space/controllers/space-device.controller.ts index 7009531..4474d3e 100644 --- a/src/space/controllers/space-device.controller.ts +++ b/src/space/controllers/space-device.controller.ts @@ -1,11 +1,12 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { GetSpaceParam } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SpaceDeviceService } from '../services'; import { PermissionsGuard } from 'src/guards/permissions.guard'; import { Permissions } from 'src/decorators/permissions.decorator'; +import { GetDevicesBySpaceDto } from '../dtos/device.space.dto'; @ApiTags('Space Module') @Controller({ @@ -26,7 +27,8 @@ export class SpaceDeviceController { @Get() async listDevicesInSpace( @Param() params: GetSpaceParam, + @Query() query: GetDevicesBySpaceDto, ): Promise { - return await this.spaceDeviceService.listDevicesInSpace(params); + return await this.spaceDeviceService.listDevicesInSpace(params, query); } } diff --git a/src/space/dtos/device.space.dto.ts b/src/space/dtos/device.space.dto.ts new file mode 100644 index 0000000..e49dea1 --- /dev/null +++ b/src/space/dtos/device.space.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class GetDevicesBySpaceDto { + @ApiProperty({ + description: 'Device Product Type', + example: 'PC', + required: false, + }) + @IsString() + @IsOptional() + public productType?: string; +} diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index b257b1e..49d965d 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -17,6 +17,7 @@ import { DeviceService } from 'src/device/services'; import { SpaceRepository } from '@app/common/modules/space'; import { DeviceEntity } from '@app/common/modules/device/entities'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { GetDevicesBySpaceDto } from '../dtos/device.space.dto'; @Injectable() export class SpaceDeviceService { @@ -27,9 +28,12 @@ export class SpaceDeviceService { private readonly spaceRepository: SpaceRepository, ) {} - async listDevicesInSpace(params: GetSpaceParam): Promise { + async listDevicesInSpace( + params: GetSpaceParam, + query: GetDevicesBySpaceDto, + ): Promise { const { spaceUuid, communityUuid, projectUuid } = params; - + const { productType } = query; try { // Validate community, project, and fetch space including devices in a single query const space = await this.validationService.fetchSpaceDevices(spaceUuid); @@ -51,7 +55,23 @@ export class SpaceDeviceService { const detailedDevices = (await Promise.allSettled(deviceDetailsPromises)) .filter((result) => result.status === 'fulfilled' && result.value) .map((result) => (result as PromiseFulfilledResult).value); - + console.log('detailedDevices', detailedDevices); + if (productType) { + const devicesFilterd = detailedDevices.filter( + (device) => device.productType === productType, + ); + if (devicesFilterd.length === 0) { + return new SuccessResponseDto({ + message: `No ${productType} devices found for ${spaceUuid ? 'space' : 'community'}`, + data: [], + statusCode: HttpStatus.CREATED, + }); + } + return new SuccessResponseDto({ + data: devicesFilterd, + message: 'Successfully retrieved list of devices.', + }); + } return new SuccessResponseDto({ data: detailedDevices, message: 'Successfully retrieved list of devices.', From 67331aa92a1c7fadd37e2558a0ca9585f67bd016 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 14 May 2025 13:00:09 +0300 Subject: [PATCH 11/56] feat: update DEVICE_SPACE_COMMUNITY route and add validation for spaceUuid and communityUuid in DTO --- libs/common/src/constants/controller-route.ts | 4 +++- src/device/controllers/device-space-community.controller.ts | 2 +- src/device/dtos/get.device.dto.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 56c9bb4..7afbaa5 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -625,10 +625,12 @@ export class ControllerRoute { }; }; static DEVICE_SPACE_COMMUNITY = class { - public static readonly ROUTE = 'devices/recursive-child'; + public static readonly ROUTE = 'devices-space-community'; + static ACTIONS = class { public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_SUMMARY = 'Get all devices by space or community with recursive child'; + public static readonly GET_ALL_DEVICES_BY_SPACE_OR_COMMUNITY_WITH_RECURSIVE_CHILD_DESCRIPTION = 'This endpoint retrieves all devices in the system by space or community with recursive child.'; }; diff --git a/src/device/controllers/device-space-community.controller.ts b/src/device/controllers/device-space-community.controller.ts index e19f1f5..bcad77e 100644 --- a/src/device/controllers/device-space-community.controller.ts +++ b/src/device/controllers/device-space-community.controller.ts @@ -23,7 +23,7 @@ export class DeviceSpaceOrCommunityController { @ApiBearerAuth() @UseGuards(PermissionsGuard) @Permissions('DEVICE_VIEW') - @Get() + @Get('recursive-child') @ApiOperation({ summary: ControllerRoute.DEVICE_SPACE_COMMUNITY.ACTIONS diff --git a/src/device/dtos/get.device.dto.ts b/src/device/dtos/get.device.dto.ts index 868b1e1..84c9d64 100644 --- a/src/device/dtos/get.device.dto.ts +++ b/src/device/dtos/get.device.dto.ts @@ -6,6 +6,7 @@ import { IsOptional, IsString, IsUUID, + ValidateIf, } from 'class-validator'; export class GetDeviceBySpaceUuidDto { @@ -66,4 +67,8 @@ export class GetDevicesBySpaceOrCommunityDto { @IsUUID('4', { message: 'Invalid community UUID format' }) @IsOptional() communityUuid?: string; + + @ValidateIf((o) => !o.spaceUuid && !o.communityUuid) + @IsNotEmpty({ message: 'Either spaceUuid or communityUuid must be provided' }) + requireEither?: never; // This ensures at least one of them is provided } From c06be4736c39153d75c6bf038652b4ff15bcae81 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 14 May 2025 13:34:29 +0300 Subject: [PATCH 12/56] occupancy duration procedures --- ...ert_all_daily_spacy_occupancy_duration.sql | 109 ++++++++++++++++ ..._select_daily_space_occupancy_duration.sql | 15 +++ ..._update_daily_space_occupancy_duration.sql | 117 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql new file mode 100644 index 0000000..10e03df --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql @@ -0,0 +1,109 @@ +-- 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 event_date, + 86400 - total_missing_seconds AS total_occupied_seconds, + (86400 - total_missing_seconds)/86400*100 as occupancy_prct + FROM missing_seconds_per_day +) + +-- Final Output +, final_data as ( +SELECT space_id, + event_date, + occupancy_prct +FROM occupied_seconds_per_day +ORDER BY 1,2 +) + +INSERT INTO public."space-daily-occupancy-duration" ( + space_uuid, + event_date, + occupancy_percentage +) +select space_id, + event_date, + occupancy_prct +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + occupancy_percentage = EXCLUDED.occupancy_percentage; \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql new file mode 100644 index 0000000..115caec --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql @@ -0,0 +1,15 @@ +WITH params AS ( + SELECT + $1::uuid AS space_uuid, + TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date +) + +SELECT sdod.* +FROM public."space-daily-occupancy-duration" AS sdod +JOIN params P ON true +WHERE sdod.space_uuid = P.space_uuid +AND ( + P.event_year IS NULL + OR sdod.event_date = P.event_year +) +ORDER BY space_uuid, event_date; diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql new file mode 100644 index 0000000..f50ff75 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql @@ -0,0 +1,117 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id +) + +, 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 event_date, + 86400 - total_missing_seconds AS total_occupied_seconds, + (86400 - total_missing_seconds)/86400*100 as occupancy_prct + FROM missing_seconds_per_day +) + +-- Final Output +, final_data as ( +SELECT occupied_seconds_per_day.space_id, + occupied_seconds_per_day.event_date, + occupied_seconds_per_day.occupancy_prct +FROM occupied_seconds_per_day +join params p on true + and p.space_uuid = occupied_seconds_per_day.space_id + and p.event_date = occupied_seconds_per_day.event_date +ORDER BY 1,2 +) + +INSERT INTO public."space-daily-occupancy-duration" ( + space_uuid, + event_date, + occupancy_percentage +) +select space_id, + event_date, + occupancy_prct +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + occupancy_percentage = EXCLUDED.occupancy_percentage; From 92ee6ee951b43fdd924acb61f87c4e9d0e93159f Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 14 May 2025 14:11:36 +0300 Subject: [PATCH 13/56] bug fix --- .../procedure_update_daily_space_occupancy_duration.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql index f50ff75..92b3d02 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql @@ -1,3 +1,4 @@ + WITH params AS ( SELECT TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, @@ -98,7 +99,7 @@ SELECT occupied_seconds_per_day.space_id, occupied_seconds_per_day.occupancy_prct FROM occupied_seconds_per_day join params p on true - and p.space_uuid = occupied_seconds_per_day.space_id + and p.space_id = occupied_seconds_per_day.space_id and p.event_date = occupied_seconds_per_day.event_date ORDER BY 1,2 ) From 1bb3803229616586f3b2903ba1db0fa4dd4489fb Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 14 May 2025 15:03:52 +0300 Subject: [PATCH 14/56] wording --- .../procedure_update_daily_space_occupancy_duration.sql | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql index 92b3d02..d3c1e88 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql @@ -1,4 +1,3 @@ - WITH params AS ( SELECT TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, @@ -88,7 +87,7 @@ occupied_seconds_per_day AS ( space_id, missing_date as event_date, 86400 - total_missing_seconds AS total_occupied_seconds, - (86400 - total_missing_seconds)/86400*100 as occupancy_prct + (86400 - total_missing_seconds)/86400*100 as occupancy_percentage FROM missing_seconds_per_day ) @@ -96,7 +95,7 @@ occupied_seconds_per_day AS ( , final_data as ( SELECT occupied_seconds_per_day.space_id, occupied_seconds_per_day.event_date, - occupied_seconds_per_day.occupancy_prct + occupied_seconds_per_day.occupancy_percentage FROM occupied_seconds_per_day join params p on true and p.space_id = occupied_seconds_per_day.space_id @@ -111,7 +110,7 @@ INSERT INTO public."space-daily-occupancy-duration" ( ) select space_id, event_date, - occupancy_prct + occupancy_percentage FROM final_data ON CONFLICT (space_uuid, event_date) DO UPDATE SET From b50d7682f37824cc02caf97e2fa70ca366e55efc Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 14 May 2025 15:50:28 +0300 Subject: [PATCH 15/56] updates --- ...ert_all_daily_spacy_occupancy_duration.sql | 8 ++++- ..._select_daily_space_occupancy_duration.sql | 29 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql index 10e03df..9c3315f 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql @@ -90,6 +90,7 @@ occupied_seconds_per_day AS ( , final_data as ( SELECT space_id, event_date, + total_occupied_seconds, occupancy_prct FROM occupied_seconds_per_day ORDER BY 1,2 @@ -98,12 +99,17 @@ ORDER BY 1,2 INSERT INTO public."space-daily-occupancy-duration" ( space_uuid, event_date, + occupied_seconds, occupancy_percentage ) select space_id, event_date, + total_occupied_seconds, occupancy_prct FROM final_data ON CONFLICT (space_uuid, event_date) DO UPDATE SET - occupancy_percentage = EXCLUDED.occupancy_percentage; \ No newline at end of file + occupancy_percentage = EXCLUDED.occupancy_percentage; + + + \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql index 115caec..6cbf98e 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql @@ -1,15 +1,26 @@ WITH params AS ( SELECT $1::uuid AS space_uuid, - TO_DATE(NULLIF($2, ''), 'YYYY-MM-DD') AS event_date + TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month ) -SELECT sdod.* -FROM public."space-daily-occupancy-duration" AS sdod -JOIN params P ON true -WHERE sdod.space_uuid = P.space_uuid -AND ( - P.event_year IS NULL - OR sdod.event_date = P.event_year +, step1 AS ( + SELECT + space_uuid, + TO_CHAR(event_date, 'YYYY-MM') AS event_month, + DATE_PART('day', (date_trunc('month', event_date) + INTERVAL '1 month - 1 day')) AS days_in_month, + SUM(occupied_seconds) AS occupied_seconds + FROM public."space-daily-occupancy-duration" AS sdod + GROUP BY space_uuid, event_month, days_in_month ) -ORDER BY space_uuid, event_date; + +SELECT + step1.space_uuid, + step1.event_month, + occupied_seconds / (days_in_month * 86400.0) * 100 AS occupancy_percentage +FROM step1 +JOIN params P ON step1.space_uuid = P.space_uuid +WHERE P.event_month IS NULL + OR step1.event_month = TO_CHAR(P.event_month, 'YYYY-MM') +ORDER BY step1.space_uuid, step1.event_month; + From e253d1ca03c49312e098fac2f651863e64e6da0d Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 15 May 2025 10:08:02 +0300 Subject: [PATCH 16/56] refactor: optimize device retrieval by avoiding duplicate space visits in getAllDevicesByCommunity --- src/community/services/community.service.ts | 31 +++++++++------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index cb166e7..542926c 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -313,7 +313,6 @@ export class CommunityService { async getAllDevicesByCommunity( communityUuid: string, ): Promise { - // Fetch the community and its top-level spaces const community = await this.communityRepository.findOne({ where: { uuid: communityUuid }, relations: [ @@ -329,37 +328,33 @@ export class CommunityService { } const allDevices: DeviceEntity[] = []; + const visitedSpaceUuids = new Set(); - // Recursive fetch function for spaces + // Recursive fetch function with visited check const fetchSpaceDevices = async (space: SpaceEntity) => { - if (space.devices && space.devices.length > 0) { + if (visitedSpaceUuids.has(space.uuid)) return; + visitedSpaceUuids.add(space.uuid); + + if (space.devices?.length) { allDevices.push(...space.devices); } - if (space.children && space.children.length > 0) { - for (const childSpace of space.children) { - const fullChildSpace = await this.spaceRepository.findOne({ - where: { uuid: childSpace.uuid }, + if (space.children?.length) { + for (const child of space.children) { + const fullChild = await this.spaceRepository.findOne({ + where: { uuid: child.uuid }, relations: ['children', 'devices', 'devices.productDevice'], }); - if (fullChildSpace) { - await fetchSpaceDevices(fullChildSpace); + if (fullChild) { + await fetchSpaceDevices(fullChild); } } } }; - // Start recursive fetch for all top-level spaces for (const space of community.spaces) { - const fullSpace = await this.spaceRepository.findOne({ - where: { uuid: space.uuid }, - relations: ['children', 'devices', 'devices.productDevice'], - }); - - if (fullSpace) { - await fetchSpaceDevices(fullSpace); - } + await fetchSpaceDevices(space); } return allDevices; From 8750da7e6211100e42404c74f4552505a975d232 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 15 May 2025 10:09:29 +0300 Subject: [PATCH 17/56] feat: optimize getAllDevicesByCommunity to prevent duplicate space processing --- src/device/services/device.service.ts | 31 +++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 717affd..4d4efbb 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1812,7 +1812,6 @@ export class DeviceService { async getAllDevicesByCommunity( communityUuid: string, ): Promise { - // Fetch the community and its top-level spaces const community = await this.communityRepository.findOne({ where: { uuid: communityUuid }, relations: [ @@ -1828,37 +1827,33 @@ export class DeviceService { } const allDevices: DeviceEntity[] = []; + const visitedSpaceUuids = new Set(); - // Recursive fetch function for spaces + // Recursive fetch function with visited check const fetchSpaceDevices = async (space: SpaceEntity) => { - if (space.devices && space.devices.length > 0) { + if (visitedSpaceUuids.has(space.uuid)) return; + visitedSpaceUuids.add(space.uuid); + + if (space.devices?.length) { allDevices.push(...space.devices); } - if (space.children && space.children.length > 0) { - for (const childSpace of space.children) { - const fullChildSpace = await this.spaceRepository.findOne({ - where: { uuid: childSpace.uuid }, + if (space.children?.length) { + for (const child of space.children) { + const fullChild = await this.spaceRepository.findOne({ + where: { uuid: child.uuid }, relations: ['children', 'devices', 'devices.productDevice'], }); - if (fullChildSpace) { - await fetchSpaceDevices(fullChildSpace); + if (fullChild) { + await fetchSpaceDevices(fullChild); } } } }; - // Start recursive fetch for all top-level spaces for (const space of community.spaces) { - const fullSpace = await this.spaceRepository.findOne({ - where: { uuid: space.uuid }, - relations: ['children', 'devices', 'devices.productDevice'], - }); - - if (fullSpace) { - await fetchSpaceDevices(fullSpace); - } + await fetchSpaceDevices(space); } return allDevices; From 56e78683b33d7fb751fc841659ccda0b91579b3b Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 15 May 2025 12:28:38 +0300 Subject: [PATCH 18/56] adjusted select --- ..._select_daily_space_occupancy_duration.sql | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql index 6cbf98e..f279e91 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_select_daily_space_occupancy_duration.sql @@ -4,23 +4,14 @@ WITH params AS ( TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month ) -, step1 AS ( - SELECT - space_uuid, - TO_CHAR(event_date, 'YYYY-MM') AS event_month, - DATE_PART('day', (date_trunc('month', event_date) + INTERVAL '1 month - 1 day')) AS days_in_month, - SUM(occupied_seconds) AS occupied_seconds - FROM public."space-daily-occupancy-duration" AS sdod - GROUP BY space_uuid, event_month, days_in_month -) - -SELECT - step1.space_uuid, - step1.event_month, - occupied_seconds / (days_in_month * 86400.0) * 100 AS occupancy_percentage -FROM step1 -JOIN params P ON step1.space_uuid = P.space_uuid -WHERE P.event_month IS NULL - OR step1.event_month = TO_CHAR(P.event_month, 'YYYY-MM') -ORDER BY step1.space_uuid, step1.event_month; +SELECT sdo.space_uuid, +event_date, +occupancy_percentage, +occupied_seconds +FROM public."space-daily-occupancy-duration" as sdo +JOIN params P ON true +where (sdo.space_uuid = P.space_uuid +OR P.event_month IS null) +AND TO_CHAR(sdo.event_date, 'YYYY-MM') = TO_CHAR(P.event_month, 'YYYY-MM') +ORDER BY sdo.space_uuid, sdo.event_date; From 180d16eeb1911cdb53234cba5c8c64785fac5e19 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 18 May 2025 17:40:41 +0300 Subject: [PATCH 19/56] feat: add occupancy duration data retrieval and update procedures --- .../services/devices-status.service.ts | 3 ++ .../src/helper/services/occupancy.service.ts | 35 ++++++++++---- .../controllers/occupancy.controller.ts | 27 ++++++++++- src/occupancy/dto/get-occupancy.dto.ts | 12 +++++ src/occupancy/services/occupancy.service.ts | 46 ++++++++++++++++--- 5 files changed, 108 insertions(+), 15 deletions(-) diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index f01b0be..18c752c 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -257,6 +257,9 @@ export class DeviceStatusFirebaseService { await this.occupancyService.updateOccupancySensorHistoricalData( addDeviceStatusDto.deviceUuid, ); + await this.occupancyService.updateOccupancySensorHistoricalDurationData( + addDeviceStatusDto.deviceUuid, + ); } } diff --git a/libs/common/src/helper/services/occupancy.service.ts b/libs/common/src/helper/services/occupancy.service.ts index 3e8fc7c..ea99b7c 100644 --- a/libs/common/src/helper/services/occupancy.service.ts +++ b/libs/common/src/helper/services/occupancy.service.ts @@ -11,7 +11,27 @@ export class OccupancyService { private readonly dataSource: DataSource, private readonly deviceRepository: DeviceRepository, ) {} + async updateOccupancySensorHistoricalDurationData( + deviceUuid: string, + ): Promise { + try { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations: ['spaceDevice'], + }); + await this.executeProcedure( + 'fact_daily_space_occupancy_duration', + 'procedure_update_daily_space_occupancy_duration', + [dateStr, device.spaceDevice?.uuid], + ); + } catch (err) { + console.error('Failed to insert or update occupancy duration data:', err); + throw err; + } + } async updateOccupancySensorHistoricalData(deviceUuid: string): Promise { try { const now = new Date(); @@ -21,10 +41,11 @@ export class OccupancyService { relations: ['spaceDevice'], }); - await this.executeProcedure('procedure_update_fact_space_occupancy', [ - dateStr, - device.spaceDevice?.uuid, - ]); + await this.executeProcedure( + 'fact_space_occupancy_count', + 'procedure_update_fact_space_occupancy', + [dateStr, device.spaceDevice?.uuid], + ); } catch (err) { console.error('Failed to insert or update occupancy data:', err); throw err; @@ -32,13 +53,11 @@ export class OccupancyService { } private async executeProcedure( + procedureFolderName: string, procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery( - 'fact_space_occupancy_count', - procedureFileName, - ); + const query = this.loadQuery(procedureFolderName, procedureFileName); await this.dataSource.query(query, params); console.log(`Procedure ${procedureFileName} executed successfully.`); } diff --git a/src/occupancy/controllers/occupancy.controller.ts b/src/occupancy/controllers/occupancy.controller.ts index d6aa9b1..c8ff3aa 100644 --- a/src/occupancy/controllers/occupancy.controller.ts +++ b/src/occupancy/controllers/occupancy.controller.ts @@ -9,7 +9,10 @@ import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { ControllerRoute } from '@app/common/constants/controller-route'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { OccupancyService } from '../services/occupancy.service'; -import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto'; +import { + GetOccupancyDurationBySpaceDto, + GetOccupancyHeatMapBySpaceDto, +} from '../dto/get-occupancy.dto'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SpaceParamsDto } from '../dto/occupancy-params.dto'; @@ -43,4 +46,26 @@ export class OccupancyController { query, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('duration/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_SUMMARY, + description: + ControllerRoute.Occupancy.ACTIONS.GET_OCCUPANCY_HEAT_MAP_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getOccupancyDurationDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetOccupancyDurationBySpaceDto, + ): Promise { + return await this.occupancyService.getOccupancyDurationDataBySpace( + params, + query, + ); + } } diff --git a/src/occupancy/dto/get-occupancy.dto.ts b/src/occupancy/dto/get-occupancy.dto.ts index 26ea0a5..7e2fb38 100644 --- a/src/occupancy/dto/get-occupancy.dto.ts +++ b/src/occupancy/dto/get-occupancy.dto.ts @@ -13,3 +13,15 @@ export class GetOccupancyHeatMapBySpaceDto { }) year: string; } +export class GetOccupancyDurationBySpaceDto { + @ApiPropertyOptional({ + description: 'Month and year in format YYYY-MM', + example: '2025-03', + required: true, + }) + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { + message: 'monthDate must be in YYYY-MM format', + }) + @IsNotEmpty() + monthDate: string; +} diff --git a/src/occupancy/services/occupancy.service.ts b/src/occupancy/services/occupancy.service.ts index 59afe18..6eec631 100644 --- a/src/occupancy/services/occupancy.service.ts +++ b/src/occupancy/services/occupancy.service.ts @@ -1,5 +1,8 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { GetOccupancyHeatMapBySpaceDto } from '../dto/get-occupancy.dto'; +import { + GetOccupancyDurationBySpaceDto, + GetOccupancyHeatMapBySpaceDto, +} from '../dto/get-occupancy.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SpaceParamsDto } from '../dto/occupancy-params.dto'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; @@ -14,6 +17,38 @@ export class OccupancyService { private readonly dataSource: DataSource, ) {} + async getOccupancyDurationDataBySpace( + params: SpaceParamsDto, + query: GetOccupancyDurationBySpaceDto, + ): Promise { + const { monthDate } = query; + const { spaceUuid } = params; + + try { + const data = await this.executeProcedure( + 'fact_daily_space_occupancy_duration', + 'procedure_select_daily_space_occupancy_duration', + [spaceUuid, monthDate], + ); + const formattedData = data.map((item) => ({ + ...item, + event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD + })); + return this.buildResponse( + `Occupancy duration data fetched successfully for ${spaceUuid} space`, + formattedData, + ); + } catch (error) { + console.error('Failed to fetch occupancy duration data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch occupancy duration data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getOccupancyHeatMapDataBySpace( params: SpaceParamsDto, query: GetOccupancyHeatMapBySpaceDto, @@ -23,6 +58,7 @@ export class OccupancyService { try { const data = await this.executeProcedure( + 'fact_space_occupancy_count', 'proceduce_select_fact_space_occupancy', [spaceUuid, year], ); @@ -31,7 +67,7 @@ export class OccupancyService { event_date: new Date(item.event_date).toLocaleDateString('en-CA'), // YYYY-MM-DD })); return this.buildResponse( - `Occupancy heat map data fetched successfully for ${spaceUuid ? 'space' : 'community'}`, + `Occupancy heat map data fetched successfully for ${spaceUuid} space`, formattedData, ); } catch (error) { @@ -54,13 +90,11 @@ export class OccupancyService { }); } private async executeProcedure( + procedureFolderName: string, procedureFileName: string, params: (string | number | null)[], ): Promise { - const query = this.loadQuery( - 'fact_space_occupancy_count', - procedureFileName, - ); + const query = this.loadQuery(procedureFolderName, procedureFileName); return await this.dataSource.query(query, params); } private loadQuery(folderName: string, fileName: string): string { From f7fd96afa1ec37592c8f1b0a15d591ec6c270025 Mon Sep 17 00:00:00 2001 From: Mhd Zayd Skaff Date: Tue, 20 May 2025 14:40:48 +0300 Subject: [PATCH 20/56] fix: remove unused params from get all projects api --- src/project/controllers/project.controller.ts | 14 ++++++-------- src/project/dto/list-project.dto.ts | 7 +++++++ 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/project/dto/list-project.dto.ts diff --git a/src/project/controllers/project.controller.ts b/src/project/controllers/project.controller.ts index 24d5a44..3b2de61 100644 --- a/src/project/controllers/project.controller.ts +++ b/src/project/controllers/project.controller.ts @@ -1,4 +1,6 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { Body, Controller, @@ -13,7 +15,6 @@ import { Res, UseGuards, } from '@nestjs/common'; -import { Response } from 'express'; import { ApiBearerAuth, ApiOperation, @@ -21,11 +22,10 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { ProjectService } from '../services'; +import { Response } from 'express'; import { CreateProjectDto, GetProjectParam } from '../dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; +import { ListProjectsDto } from '../dto/list-project.dto'; +import { ProjectService } from '../services'; @ApiTags('Project Module') @Controller({ @@ -80,9 +80,7 @@ export class ProjectController { description: ControllerRoute.PROJECT.ACTIONS.LIST_PROJECTS_DESCRIPTION, }) @Get() - async list( - @Query() query: PaginationRequestGetListDto, - ): Promise { + async list(@Query() query: ListProjectsDto): Promise { return this.projectService.listProjects(query); } diff --git a/src/project/dto/list-project.dto.ts b/src/project/dto/list-project.dto.ts new file mode 100644 index 0000000..344c8c4 --- /dev/null +++ b/src/project/dto/list-project.dto.ts @@ -0,0 +1,7 @@ +import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; +import { PickType } from '@nestjs/swagger'; + +export class ListProjectsDto extends PickType(PaginationRequestGetListDto, [ + 'page', + 'size', +]) {} From fe891030aad1e24f6412d4fa0b9fb34189037252 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 22 May 2025 13:10:05 +0300 Subject: [PATCH 21/56] input year, output month --- ...e_select_fact_monthly_device_occupancy.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_monthly_device_occupancy.sql diff --git a/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_monthly_device_occupancy.sql b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_monthly_device_occupancy.sql new file mode 100644 index 0000000..ad1481f --- /dev/null +++ b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_monthly_device_occupancy.sql @@ -0,0 +1,21 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($2, ''), 'YYYY') AS year, + string_to_array(NULLIF($4, ''), ',') AS device_ids +) + +SELECT + A.device_uuid, + TO_CHAR(date_trunc('month', A.event_date), 'YYYY-MM') AS event_month, + SUM(A.count_motion_detected) AS total_motion_detected, + SUM(A.count_presence_detected) AS total_presence_detected, + SUM(A.count_total_presence_detected) AS total_overall_presence +FROM public."presence-sensor-daily-device-detection" AS A +JOIN params P ON TRUE +WHERE A.device_uuid::text = ANY(P.device_ids) + AND ( + P.year IS NULL + OR date_trunc('year', A.event_date) = P.year + ) +GROUP BY 1,2 +ORDER BY 1,2; From a7c4bf1c3d279d7f92b89e23356c5bf12fff02d3 Mon Sep 17 00:00:00 2001 From: khuss Date: Thu, 22 May 2025 07:28:50 -0400 Subject: [PATCH 22/56] AQI score calculations and air quality model --- .../fact_daily_device_aqi_score.sql | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql new file mode 100644 index 0000000..45603be --- /dev/null +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -0,0 +1,144 @@ +-- Function to calculate AQI +CREATE OR REPLACE FUNCTION calculate_aqi(p_pollutant TEXT, concentration NUMERIC) +RETURNS NUMERIC AS $$ +DECLARE + c_low NUMERIC; + c_high NUMERIC; + i_low INT; + i_high INT; +BEGIN + SELECT v.c_low, v.c_high, v.i_low, v.i_high + INTO c_low, c_high, i_low, i_high + FROM ( + VALUES + -- PM2.5 + ('pm25', 0.0, 12.0, 0, 50), + ('pm25', 12.1, 35.4, 51, 100), + ('pm25', 35.5, 55.4, 101, 150), + ('pm25', 55.5, 150.4, 151, 200), + ('pm25', 150.5, 250.4, 201, 300), + ('pm25', 250.5, 500.4, 301, 500), + + -- PM10 + ('pm10', 0, 54, 0, 50), + ('pm10', 55, 154, 51, 100), + ('pm10', 155, 254, 101, 150), + ('pm10', 255, 354, 151, 200), + + -- VOC + ('voc_value', 0, 200, 0, 50), + ('voc_value', 201, 400, 51, 100), + ('voc_value', 401, 600, 101, 150), + ('voc_value', 601, 1000, 151, 200), + + -- CH2O + ('ch2o_value', 0, 2, 0, 50), + ('ch2o_value', 2.1, 4, 51, 100), + ('ch2o_value', 4.1, 6, 101, 150), + + -- CO2 + ('co2_value', 350, 1000, 0, 50), + ('co2_value', 1001, 1250, 51, 100), + ('co2_value', 1251, 1500, 101, 150), + ('co2_value', 1501, 2000, 151, 200) + ) AS v(pollutant, c_low, c_high, i_low, i_high) + WHERE v.pollutant = LOWER(p_pollutant) + AND concentration BETWEEN v.c_low AND v.c_high + LIMIT 1; + + -- Linear interpolation + RETURN ROUND(((i_high - i_low) * (concentration - c_low) / (c_high - c_low)) + i_low); +END; +$$ LANGUAGE plpgsql; + +-- CTE for device + status log + space +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.created_at, + 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 + 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 = 'hjjcy' +), + +-- Aggregate air sensor data per device per day +air_data AS ( + SELECT + DATE_TRUNC('day', event_time) AS date, + device_id, + space_id, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + + -- PM2.5 + MIN(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_min, + MAX(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_max, + AVG(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_avg, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + + -- Humidity + MIN(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_low, + MAX(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_high, + AVG(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_avg, + + -- Temperature + MIN(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_low, + MAX(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_high, + AVG(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_avg, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg + + FROM device_space + GROUP BY date, device_id, space_id +) + +-- Final select with AQI calculation +SELECT + date, + device_id, + space_id, + voc_min, voc_max, voc_avg, + pm1_min, pm1_max, pm1_avg, + pm25_min, pm25_max, pm25_avg, + pm10_min, pm10_max, pm10_avg, + ch2o_min, ch2o_max, ch2o_avg, + humidity_low, humidity_high, humidity_avg, + temp_low, temp_high, temp_avg, + co2_min, co2_max, co2_avg, + GREATEST( + calculate_aqi('pm25', pm25_avg), + calculate_aqi('pm10', pm10_avg), + calculate_aqi('voc_value', voc_avg), + calculate_aqi('co2_value', co2_avg), + calculate_aqi('ch2o_value', ch2o_avg) + ) AS overall_AQI +FROM air_data; From 25967d02f953ef00050c45b06b43b27e954f34af Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Wed, 28 May 2025 13:55:56 +0300 Subject: [PATCH 23/56] model adjustments --- ...ert_all_daily_spacy_occupancy_duration.sql | 168 +++++++++-------- ..._update_daily_space_occupancy_duration.sql | 172 +++++++++--------- ...ure_select_fact_daily_device_occupancy.sql | 3 +- .../fact_daily_space_presence_duration.sql | 157 ++++++++-------- 4 files changed, 244 insertions(+), 256 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql index 9c3315f..f7a0378 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql @@ -1,100 +1,93 @@ --- 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' +WITH presence_logs AS ( + SELECT + d.space_device_uuid AS space_id, + l.device_id, + l.event_time, + l.value, + LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time + FROM device d + JOIN "device-status-log" l ON d.uuid = l.device_id + JOIN product p ON p.uuid = d.product_device_uuid + WHERE l.code = 'presence_state' + AND p.cat_name = 'hps' ), --- 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' +-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') +presence_intervals AS ( + SELECT + space_id, + prev_time AS start_time, + event_time AS end_time + FROM presence_logs + WHERE value = 'none' + AND prev_value = 'presence' + AND prev_time IS NOT NULL ), --- 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 +-- Split intervals across days +split_intervals AS ( + SELECT + space_id, + generate_series( + date_trunc('day', start_time), + date_trunc('day', end_time), + interval '1 day' + )::date AS event_date, + GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, + LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end + FROM presence_intervals ), --- 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 +-- Mark and group overlapping intervals per space per day +ordered_intervals AS ( + SELECT + space_id, + event_date, + interval_start, + interval_end, + LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end + FROM split_intervals ), --- Step 5: Merge overlapping or adjacent intervals per day +grouped_intervals AS ( + SELECT *, + SUM(CASE + WHEN prev_end IS NULL OR interval_start > prev_end THEN 1 + ELSE 0 + END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp + FROM ordered_intervals +), + +-- Merge overlapping intervals per group 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 + SELECT + space_id, + event_date, + MIN(interval_start) AS merged_start, + MAX(interval_end) AS merged_end + FROM grouped_intervals + GROUP BY space_id, event_date, grp ), --- 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 -), +-- Sum durations of merged intervals +summed_intervals AS ( + SELECT + space_id, + event_date, + SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds + FROM merged_intervals + GROUP BY space_id, event_date + ), --- Step 7: Calculate total occupied time per day (86400 - missing) -occupied_seconds_per_day AS ( - SELECT - space_id, - missing_date as event_date, - 86400 - total_missing_seconds AS total_occupied_seconds, - (86400 - total_missing_seconds)/86400*100 as occupancy_prct - FROM missing_seconds_per_day -) +final_data AS ( +SELECT + space_id, + event_date, + LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, + ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage +FROM summed_intervals +ORDER BY space_id, event_date) --- Final Output -, final_data as ( -SELECT space_id, - event_date, - total_occupied_seconds, - occupancy_prct -FROM occupied_seconds_per_day -ORDER BY 1,2 -) INSERT INTO public."space-daily-occupancy-duration" ( space_uuid, @@ -104,12 +97,13 @@ INSERT INTO public."space-daily-occupancy-duration" ( ) select space_id, event_date, - total_occupied_seconds, - occupancy_prct + occupied_seconds, + occupancy_percentage FROM final_data ON CONFLICT (space_uuid, event_date) DO UPDATE SET - occupancy_percentage = EXCLUDED.occupancy_percentage; + occupancy_percentage = EXCLUDED.occupancy_percentage, + occupied_seconds = EXCLUDED.occupied_seconds; \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql index d3c1e88..e669864 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_update_daily_space_occupancy_duration.sql @@ -2,116 +2,108 @@ WITH params AS ( SELECT TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, $2::uuid AS space_id -) - -, 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' +presence_logs AS ( + SELECT + d.space_device_uuid AS space_id, + l.device_id, + l.event_time, + l.value, + LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time + FROM device d + JOIN "device-status-log" l ON d.uuid = l.device_id + JOIN product p ON p.uuid = d.product_device_uuid + WHERE l.code = 'presence_state' + AND p.cat_name = 'hps' ), --- 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 +presence_intervals AS ( + SELECT + space_id, + prev_time AS start_time, + event_time AS end_time + FROM presence_logs + WHERE value = 'none' AND prev_time 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 +split_intervals AS ( + SELECT + space_id, + generate_series( + date_trunc('day', start_time), + date_trunc('day', end_time), + interval '1 day' + )::date AS event_date, + GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, + LEAST(end_time, date_trunc('day', end_time) + INTERVAL '1 day') AS interval_end + FROM presence_intervals +), + +ordered_intervals AS ( + SELECT + space_id, + event_date, + interval_start, + interval_end, + LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end + FROM split_intervals +), + +grouped_intervals AS ( + SELECT *, + SUM(CASE + WHEN prev_end IS NULL OR interval_start > prev_end THEN 1 + ELSE 0 + END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp + FROM ordered_intervals ), --- 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 + SELECT + space_id, + event_date, + MIN(interval_start) AS merged_start, + MAX(interval_end) AS merged_end + FROM grouped_intervals + GROUP BY space_id, event_date, grp ), --- 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 +summed_intervals AS ( + SELECT + space_id, + event_date, + SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds + FROM merged_intervals + GROUP BY space_id, event_date ), --- Step 7: Calculate total occupied time per day (86400 - missing) -occupied_seconds_per_day AS ( - SELECT - space_id, - missing_date as event_date, - 86400 - total_missing_seconds AS total_occupied_seconds, - (86400 - total_missing_seconds)/86400*100 as occupancy_percentage - FROM missing_seconds_per_day -) - --- Final Output -, final_data as ( -SELECT occupied_seconds_per_day.space_id, - occupied_seconds_per_day.event_date, - occupied_seconds_per_day.occupancy_percentage -FROM occupied_seconds_per_day -join params p on true - and p.space_id = occupied_seconds_per_day.space_id - and p.event_date = occupied_seconds_per_day.event_date -ORDER BY 1,2 +final_data AS ( + SELECT + s.space_id, + s.event_date, + LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, + ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage + FROM summed_intervals s + JOIN params p + ON p.space_id = s.space_id + AND p.event_date = s.event_date ) INSERT INTO public."space-daily-occupancy-duration" ( space_uuid, event_date, + occupied_seconds, occupancy_percentage ) -select space_id, - event_date, - occupancy_percentage +SELECT + space_id, + event_date, + occupied_seconds, + occupancy_percentage FROM final_data ON CONFLICT (space_uuid, event_date) DO UPDATE SET - occupancy_percentage = EXCLUDED.occupancy_percentage; + occupancy_percentage = EXCLUDED.occupancy_percentage, + occupied_seconds = EXCLUDED.occupied_seconds; + diff --git a/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql index e5f189b..6632568 100644 --- a/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql +++ b/libs/common/src/sql/procedures/fact_device_occupancy_count/procedure_select_fact_daily_device_occupancy.sql @@ -16,4 +16,5 @@ WITH params AS ( WHERE A.device_uuid::text = ANY(P.device_ids) AND (P.month IS NULL OR date_trunc('month', A.event_date) = P.month - ) \ No newline at end of file + ); + \ No newline at end of file diff --git a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql index 296f7d8..90a01c8 100644 --- a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql +++ b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql @@ -1,91 +1,92 @@ --- 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' +WITH presence_logs AS ( + SELECT + d.space_device_uuid AS space_id, + l.device_id, + l.event_time, + l.value, + LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time + FROM device d + JOIN "device-status-log" l ON d.uuid = l.device_id + JOIN product p ON p.uuid = d.product_device_uuid + WHERE l.code = 'presence_state' + AND p.cat_name = 'hps' ), --- 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' +-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') +presence_intervals AS ( + SELECT + space_id, + prev_time AS start_time, + event_time AS end_time + FROM presence_logs + WHERE value = 'none' + AND prev_value = 'presence' + AND prev_time IS NOT NULL ), --- 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 +-- Split intervals across days +split_intervals AS ( + SELECT + space_id, + generate_series( + date_trunc('day', start_time), + date_trunc('day', end_time), + interval '1 day' + )::date AS event_date, + GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, + LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end + FROM presence_intervals ), --- 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 +-- Mark and group overlapping intervals per space per day +ordered_intervals AS ( + SELECT + space_id, + event_date, + interval_start, + interval_end, + LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end + FROM split_intervals ), --- Step 5: Merge overlapping or adjacent intervals per day +grouped_intervals AS ( + SELECT *, + SUM(CASE + WHEN prev_end IS NULL OR interval_start > prev_end THEN 1 + ELSE 0 + END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp + FROM ordered_intervals +), + +-- Merge overlapping intervals per group 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 + SELECT + space_id, + event_date, + MIN(interval_start) AS merged_start, + MAX(interval_end) AS merged_end + FROM grouped_intervals + GROUP BY space_id, event_date, grp ), --- 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 +-- Sum durations of merged intervals +summed_intervals AS ( + SELECT + space_id, + event_date, + SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds + FROM merged_intervals + GROUP BY space_id, event_date ) --- Final Output -SELECT * -FROM occupied_seconds_per_day -ORDER BY 1,2; +-- Final output with capped seconds and percentage +SELECT + space_id, + event_date, + LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, + ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage +FROM summed_intervals +ORDER BY space_id, event_date; + + + From efdf9181592bed554f5ddb84edadb1aa22099e21 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 28 May 2025 20:42:56 -0400 Subject: [PATCH 24/56] fixed pm25 code + date format. Added average AQI level from device --- .../fact_daily_device_aqi_score.sql | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql index 45603be..4e5ad1d 100644 --- a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -51,6 +51,18 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- Function to convert Tuya AQI level (e.g., level_0, level_1) to numeric value +CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT) +RETURNS NUMERIC AS $$ +BEGIN + -- Extract the number from the string, default to NULL if not found + RETURN CAST(regexp_replace(level_text, '[^0-9]', '', 'g') AS NUMERIC); +EXCEPTION WHEN others THEN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + + -- CTE for device + status log + space WITH device_space AS ( SELECT @@ -58,7 +70,7 @@ WITH device_space AS ( device.created_at, device.space_device_uuid AS space_id, "device-status-log".event_id, - "device-status-log".event_time::timestamp, + "device-status-log".event_time::date, "device-status-log".code, "device-status-log".value, "device-status-log".log @@ -73,7 +85,7 @@ WITH device_space AS ( -- Aggregate air sensor data per device per day air_data AS ( SELECT - DATE_TRUNC('day', event_time) AS date, + event_time AS date, device_id, space_id, @@ -88,9 +100,9 @@ air_data AS ( AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, -- PM2.5 - MIN(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_min, - MAX(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_max, - AVG(CASE WHEN code = 'pm25' THEN value::numeric END) AS pm25_avg, + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, -- PM10 MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, @@ -115,7 +127,11 @@ air_data AS ( -- CO2 MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + + -- AQI + AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index + FROM device_space GROUP BY date, device_id, space_id @@ -140,5 +156,7 @@ SELECT calculate_aqi('voc_value', voc_avg), calculate_aqi('co2_value', co2_avg), calculate_aqi('ch2o_value', ch2o_avg) - ) AS overall_AQI + ) AS overall_AQI, + air_quality_index as avg_device_index FROM air_data; + From 90fc44ab539bf97887363b33d3bfef4cbbd5a4b5 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 28 May 2025 21:14:01 -0400 Subject: [PATCH 25/56] Air quality and AQI for space model --- .../fact_daily_space_aqi_score.sql | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql diff --git a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql new file mode 100644 index 0000000..3ffe42a --- /dev/null +++ b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql @@ -0,0 +1,54 @@ + WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.created_at, + device.space_device_uuid AS space_id, + "device-status-log".event_time::date, + "device-status-log".code, + "device-status-log".value, + "device-status-log".log + 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 = 'hjjcy' +), + +average_pollutants as( +SELECT +event_time, +space_id, +AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, +AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, +AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, +AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, +AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, +AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, +AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index + +FROM device_space +--WHERE code IN ('pm25_value', 'pm10') +GROUP BY space_id, event_time +) + +SELECT + event_time::date as date, + space_id, + pm1_avg, + pm25_avg, + pm10_avg, + voc_avg, + ch2o_avg, + co2_avg, + --calculate_aqi('pm25', pm25_avg) AS aqi_pm25, + --calculate_aqi('pm10', pm10_avg) AS aqi_pm10, + GREATEST( + calculate_aqi('pm25', pm25_avg), + calculate_aqi('pm10', pm10_avg), + calculate_aqi('voc_value', voc_avg), + calculate_aqi('co2_value', co2_avg), + calculate_aqi('ch2o_value', ch2o_avg) + ) AS overall_AQI, + air_quality_index as avg_space_device_aqi +FROM average_pollutants; \ No newline at end of file From dd54af5f46bd9a7e7c97892cad1d35594bfca020 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 29 May 2025 13:38:50 +0300 Subject: [PATCH 26/56] fix --- .../fact_daily_space_presence_duration.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql index 90a01c8..3d4c339 100644 --- a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql +++ b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql @@ -4,7 +4,8 @@ WITH presence_logs AS ( l.device_id, l.event_time, l.value, - LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time + LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time, + LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value FROM device d JOIN "device-status-log" l ON d.uuid = l.device_id JOIN product p ON p.uuid = d.product_device_uuid From bfd92fdd87146918c5a57da6fc62476233eaabc9 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Thu, 29 May 2025 15:05:06 +0300 Subject: [PATCH 27/56] fix --- .../fact_daily_space_presence_duration.sql | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql index 3d4c339..d05820a 100644 --- a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql +++ b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql @@ -87,7 +87,4 @@ SELECT LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage FROM summed_intervals -ORDER BY space_id, event_date; - - - +ORDER BY space_id, event_date; \ No newline at end of file From 684205053d63832b82f8e404952f612666c65bd8 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Fri, 30 May 2025 10:51:26 +0300 Subject: [PATCH 28/56] testing fix --- .../fact_daily_space_presence_duration.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql index d05820a..74e20c6 100644 --- a/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql +++ b/libs/common/src/sql/queries/fact_daily_space_presence_duration/fact_daily_space_presence_duration.sql @@ -5,7 +5,7 @@ WITH presence_logs AS ( l.event_time, l.value, LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time, - LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value + LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value FROM device d JOIN "device-status-log" l ON d.uuid = l.device_id JOIN product p ON p.uuid = d.product_device_uuid From 3ac48183bda4b69eaa6a88e4ac4c404325ff9212 Mon Sep 17 00:00:00 2001 From: Dona Maria Absi <49731027+DonaAbsi@users.noreply.github.com> Date: Fri, 30 May 2025 11:27:12 +0300 Subject: [PATCH 29/56] procedure fix test --- .../procedure_insert_all_daily_spacy_occupancy_duration.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql index f7a0378..d0bb0d4 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_occupancy_duration/procedure_insert_all_daily_spacy_occupancy_duration.sql @@ -4,7 +4,8 @@ WITH presence_logs AS ( l.device_id, l.event_time, l.value, - LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time + LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time, + LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value FROM device d JOIN "device-status-log" l ON d.uuid = l.device_id JOIN product p ON p.uuid = d.product_device_uuid From 2fee8c055ef085990f6c3d9a6c126c7fe0f037a5 Mon Sep 17 00:00:00 2001 From: khuss Date: Sun, 1 Jun 2025 16:03:07 -0400 Subject: [PATCH 30/56] device model for aqi updated with hourly to daily logic getting max, min, avergae and percentage of categorical values for each aqi bracket --- .../fact_daily_device_aqi_score.sql | 242 +++++++++++++----- 1 file changed, 171 insertions(+), 71 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql index 4e5ad1d..6b919d0 100644 --- a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -51,7 +51,7 @@ BEGIN END; $$ LANGUAGE plpgsql; --- Function to convert Tuya AQI level (e.g., level_0, level_1) to numeric value +-- Function to convert Tuya AQI level (e.g., level_1, level_2) to numeric value CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT) RETURNS NUMERIC AS $$ BEGIN @@ -62,101 +62,201 @@ EXCEPTION WHEN others THEN END; $$ LANGUAGE plpgsql; - -- CTE for device + status log + space WITH device_space AS ( SELECT device.uuid AS device_id, - device.created_at, device.space_device_uuid AS space_id, - "device-status-log".event_id, - "device-status-log".event_time::date, + "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, - "device-status-log".value, - "device-status-log".log + "device-status-log".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 = 'hjjcy' + WHERE product.cat_name = 'hjjcy' ), --- Aggregate air sensor data per device per day -air_data AS ( +-- Getting the hourly pollutants max min avg for each device +average_pollutants AS ( SELECT - event_time AS date, + date_trunc('hour', event_time) AS event_hour, device_id, space_id, - -- VOC - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + -- AVG READINGS AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - - -- PM1 - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - - -- PM2.5 - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - - -- PM10 - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - - -- CH2O - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - - -- Humidity - MIN(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_low, - MAX(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_high, - AVG(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_avg, - - -- Temperature - MIN(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_low, - MAX(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_high, - AVG(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_avg, - - -- CO2 - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - - -- AQI - AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index - + + -- MIN READINGS + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + + -- MAX READINGS + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY date, device_id, space_id + GROUP BY device_id, space_id, event_hour +), + + +-- Fill NULLs due to missing log values +filled_pollutants AS ( + SELECT + *, + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_filled_avg, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_filled_avg, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_filled_avg, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_filled_avg, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_filled_avg, + + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_filled, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_filled, + + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_filled, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_filled + + FROM average_pollutants +), +-- Calculate max, min, avg hourly AQI for each device +hourly_results AS ( + SELECT + device_id, + space_id, + event_hour, + pm25_filled_avg, + pm10_filled_avg, + pm25_max_filled, + pm10_max_filled, + pm25_min_filled, + pm10_min_filled, + + GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_filled), + calculate_aqi('pm10', pm10_max_filled) + ) AS hourly_max_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_min_filled), + calculate_aqi('pm10', pm10_min_filled) + ) AS hourly_min_aqi, + + CASE + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 50 THEN 'Good' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 100 THEN 'Moderate' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 150 THEN 'Unhealthy for Sensitive Groups' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 200 THEN 'Unhealthy' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 300 THEN 'Very Unhealthy' + ELSE 'Hazardous' + END AS aqi_category + + FROM filled_pollutants + ORDER BY device_id, event_hour +), + +-- counting how many categories/hours are there in total (in case device was disconnected could be less than 24) +daily_category_counts AS ( + SELECT + space_id, + device_id, + date_trunc('day', event_hour) AS event_day, + aqi_category, + COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_day, aqi_category +), + +-- Aggregate Total Counts per Day +daily_totals AS ( + select + device_id, + space_id, + event_day, + SUM(category_count) AS total_count + FROM daily_category_counts + GROUP BY device_id, space_id, event_day +), +-- Calculate the daily of the daily min max avg AQI values +daily_averages AS ( + select + device_id, + space_id, + date_trunc('day', event_hour) AS event_day, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi + FROM hourly_results + GROUP BY device_id, space_id, event_day +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.device_id, + dt.space_id, + dt.event_day, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.device_id = dcc.device_id AND dt.event_day = dcc.event_day + GROUP BY dt.device_id, dt.space_id, dt.event_day, dt.total_count ) --- Final select with AQI calculation -SELECT - date, - device_id, - space_id, - voc_min, voc_max, voc_avg, - pm1_min, pm1_max, pm1_avg, - pm25_min, pm25_max, pm25_avg, - pm10_min, pm10_max, pm10_avg, - ch2o_min, ch2o_max, ch2o_avg, - humidity_low, humidity_high, humidity_avg, - temp_low, temp_high, temp_avg, - co2_min, co2_max, co2_avg, - GREATEST( - calculate_aqi('pm25', pm25_avg), - calculate_aqi('pm10', pm10_avg), - calculate_aqi('voc_value', voc_avg), - calculate_aqi('co2_value', co2_avg), - calculate_aqi('ch2o_value', ch2o_avg) - ) AS overall_AQI, - air_quality_index as avg_device_index -FROM air_data; +-- Final Output +SELECT + p.device_id, + p.space_id, + p.event_day, + p.good_percentage, + p.moderate_percentage, + p.unhealthy_sensitive_percentage, + p.unhealthy_percentage, + p.very_unhealthy_percentage, + p.hazardous_percentage, + a.daily_avg_aqi, + a.daily_max_aqi, + a.daily_min_aqi +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.device_id = a.device_id AND p.event_day = a.event_day +ORDER BY p.space_id, p.event_day; From 5b0135ba805d8e313b481cc70ce104db7c62a927 Mon Sep 17 00:00:00 2001 From: khuss Date: Sun, 1 Jun 2025 16:09:17 -0400 Subject: [PATCH 31/56] AQI space model updated with new hourly to daily logic for calculations and categorization of aqi brackets --- .../fact_daily_space_aqi_score.sql | 234 +++++++++++++++--- 1 file changed, 197 insertions(+), 37 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql index 3ffe42a..ccee7f4 100644 --- a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql @@ -1,9 +1,11 @@ - WITH device_space AS ( +-- CTE for device + status log + space + +WITH device_space AS ( SELECT device.uuid AS device_id, device.created_at, device.space_device_uuid AS space_id, - "device-status-log".event_time::date, + "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, "device-status-log".value, "device-status-log".log @@ -12,43 +14,201 @@ ON device.uuid = "device-status-log".device_id LEFT JOIN product ON product.uuid = device.product_device_uuid - WHERE product.cat_name = 'hjjcy' + WHERE product.cat_name = 'hjjcy' ), -average_pollutants as( -SELECT -event_time, -space_id, -AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, -AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, -AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, -AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, -AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, -AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, -AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index +-- Getting the hourly pollutants max min avg for each space +average_pollutants AS ( + SELECT + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- AVG READINGS + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + AVG(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_avg, + + -- MIN READINGS + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + MIN(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_min, + + -- MAX READINGS + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, + MAX(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_max -FROM device_space ---WHERE code IN ('pm25_value', 'pm10') -GROUP BY space_id, event_time + + FROM device_space + GROUP BY space_id, event_hour +), + +-- Fill NULLs due to missing log values +filled_pollutants AS ( + SELECT + *, + -- Forward-fill nulls using LAG() over partitioned data + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_filled_avg, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_filled_avg, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_filled_avg, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_filled_avg, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_filled_avg, + COALESCE(air_quality_index_avg, LAG(air_quality_index_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_filled_avg, + + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_filled, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_filled, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_filled, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_filled, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_filled, + COALESCE(air_quality_index_min, LAG(air_quality_index_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_min_filled, + + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_filled, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_filled, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_filled, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_filled, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_filled, + COALESCE(air_quality_index_max, LAG(air_quality_index_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_max_filled + + + FROM average_pollutants +), + +-- Calculate max, min, avg hourly AQI for each space +hourly_results as ( + SELECT + space_id, + event_hour, + --voc_filled, + pm25_filled_avg, + pm10_filled_avg, + -- co2_filled, + --ch2o_filled, + --aqi_filled, + pm25_max_filled, + pm10_max_filled, + pm25_min_filled, + pm10_min_filled, + air_quality_index_avg, + GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + --calculate_aqi('voc_value', voc_filled_avg), + --calculate_aqi('co2_value', co2_filled_avg), + --calculate_aqi('ch2o_value', ch2o_filled_avg) + -- Add more AQI sources as needed + ) AS hourly_avg_aqi, + GREATEST( + calculate_aqi('pm25', pm25_max_filled), + calculate_aqi('pm10', pm10_max_filled) + ) AS hourly_max_aqi, + GREATEST( + calculate_aqi('pm25', pm25_min_filled), + calculate_aqi('pm10', pm10_min_filled) + ) AS hourly_min_aqi, + case + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 50 then 'Good' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 100 then 'Moderate' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 150 then 'Unhealthy for Sensitive Groups' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 200 then 'Unhealthy' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 300 then 'Very Unhealthy' + else 'Hazardous' + end as aqi_category + + FROM filled_pollutants + ORDER BY space_id, event_hour +), + +-- counting how many categories/hours are there in total (in case device(s) was disconnected could be less than 24) +daily_category_counts AS ( + SELECT + space_id, + date_trunc('day', event_hour) AS event_day, + aqi_category, + COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_day, aqi_category +), + +-- Aggregate Total Counts per Day +daily_totals AS ( + SELECT + space_id, + event_day, + SUM(category_count) AS total_count + FROM daily_category_counts + GROUP BY space_id, event_day +), + +-- Calculate the daily of the daily min max avg AQI values +daily_averages AS ( + SELECT + space_id, + date_trunc('day', event_hour) AS event_day, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi + FROM hourly_results + GROUP BY space_id, event_day +), + +-- Pivot Categories into Columns +daily_percentages AS ( + SELECT + dt.space_id, + dt.event_day, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_day = dcc.event_day + GROUP BY dt.space_id, dt.event_day, dt.total_count ) -SELECT - event_time::date as date, - space_id, - pm1_avg, - pm25_avg, - pm10_avg, - voc_avg, - ch2o_avg, - co2_avg, - --calculate_aqi('pm25', pm25_avg) AS aqi_pm25, - --calculate_aqi('pm10', pm10_avg) AS aqi_pm10, - GREATEST( - calculate_aqi('pm25', pm25_avg), - calculate_aqi('pm10', pm10_avg), - calculate_aqi('voc_value', voc_avg), - calculate_aqi('co2_value', co2_avg), - calculate_aqi('ch2o_value', ch2o_avg) - ) AS overall_AQI, - air_quality_index as avg_space_device_aqi -FROM average_pollutants; \ No newline at end of file +-- Final Output +SELECT + p.space_id, + p.event_day, + p.good_percentage, + p.moderate_percentage, + p.unhealthy_sensitive_percentage, + p.unhealthy_percentage, + p.very_unhealthy_percentage, + p.hazardous_percentage, + a.daily_avg_aqi, + a.daily_max_aqi, + a.daily_min_aqi +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_day = a.event_day +ORDER BY p.space_id, p.event_day; \ No newline at end of file From 191d0dfaf62aacb9a4ed12acfa35bb6b361f50ab Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:16:51 -0600 Subject: [PATCH 32/56] Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity --- .../entities/subspace/subspace-product-allocation.entity.ts | 2 +- libs/common/src/modules/tag/entities/tag.entity.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts index d0d3e9a..0bc2918 100644 --- a/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts +++ b/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts @@ -14,7 +14,7 @@ import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.e import { SubspaceProductAllocationDto } from '../../dtos/subspace-product-allocation.dto'; @Entity({ name: 'subspace_product_allocation' }) -@Unique(['subspace', 'product']) +// @Unique(['subspace', 'product']) export class SubspaceProductAllocationEntity extends AbstractEntity { @Column({ type: 'uuid', diff --git a/libs/common/src/modules/tag/entities/tag.entity.ts b/libs/common/src/modules/tag/entities/tag.entity.ts index 99a2265..53eca79 100644 --- a/libs/common/src/modules/tag/entities/tag.entity.ts +++ b/libs/common/src/modules/tag/entities/tag.entity.ts @@ -25,7 +25,7 @@ export class NewTagEntity extends AbstractEntity { name: string; @ManyToOne(() => ProductEntity, (product) => product.newTags, { - nullable: false, + nullable: true, onDelete: 'CASCADE', }) public product: ProductEntity; From 0fe6c807311ca83127bfba4dc1f402f1100036f1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:56:08 -0600 Subject: [PATCH 33/56] Add utility function to associate space UUID with devices in community and device services --- libs/common/src/util/device-utils.ts | 11 +++++++++++ src/community/services/community.service.ts | 3 ++- src/device/services/device.service.ts | 8 +++++--- src/space/services/space-device.service.ts | 6 ++++-- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 libs/common/src/util/device-utils.ts diff --git a/libs/common/src/util/device-utils.ts b/libs/common/src/util/device-utils.ts new file mode 100644 index 0000000..9a930ae --- /dev/null +++ b/libs/common/src/util/device-utils.ts @@ -0,0 +1,11 @@ +import { DeviceEntity } from '../modules/device/entities'; + +export function addSpaceUuidToDevices( + devices: DeviceEntity[], + spaceUuid: string, +): DeviceEntity[] { + return devices.map((device) => { + (device as any).spaceUuid = spaceUuid; + return device; + }); +} diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 542926c..f3b7f28 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -23,6 +23,7 @@ import { SpaceService } from 'src/space/services'; import { SpaceRepository } from '@app/common/modules/space'; import { DeviceEntity } from '@app/common/modules/device/entities'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; @Injectable() export class CommunityService { @@ -336,7 +337,7 @@ export class CommunityService { visitedSpaceUuids.add(space.uuid); if (space.devices?.length) { - allDevices.push(...space.devices); + allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); } if (space.children?.length) { diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 4d4efbb..f502816 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -67,6 +67,7 @@ import { ProjectParam } from '../dtos'; import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; @Injectable() export class DeviceService { @@ -1786,7 +1787,8 @@ export class DeviceService { throw new NotFoundException('Space not found'); } - const allDevices: DeviceEntity[] = [...space.devices]; + const allDevices: DeviceEntity[] = []; + allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); // Recursive fetch function const fetchChildren = async (parentSpace: SpaceEntity) => { @@ -1796,7 +1798,7 @@ export class DeviceService { }); for (const child of children) { - allDevices.push(...child.devices); + allDevices.push(...addSpaceUuidToDevices(child.devices, child.uuid)); if (child.children.length > 0) { await fetchChildren(child); @@ -1835,7 +1837,7 @@ export class DeviceService { visitedSpaceUuids.add(space.uuid); if (space.devices?.length) { - allDevices.push(...space.devices); + allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); } if (space.children?.length) { diff --git a/src/space/services/space-device.service.ts b/src/space/services/space-device.service.ts index 49d965d..8f8a2cc 100644 --- a/src/space/services/space-device.service.ts +++ b/src/space/services/space-device.service.ts @@ -18,6 +18,7 @@ import { SpaceRepository } from '@app/common/modules/space'; import { DeviceEntity } from '@app/common/modules/device/entities'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { GetDevicesBySpaceDto } from '../dtos/device.space.dto'; +import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; @Injectable() export class SpaceDeviceService { @@ -160,7 +161,8 @@ export class SpaceDeviceService { throw new NotFoundException('Space not found'); } - const allDevices: DeviceEntity[] = [...space.devices]; + const allDevices: DeviceEntity[] = []; + allDevices.push(...addSpaceUuidToDevices(space.devices, space.uuid)); // Recursive fetch function const fetchChildren = async (parentSpace: SpaceEntity) => { @@ -170,7 +172,7 @@ export class SpaceDeviceService { }); for (const child of children) { - allDevices.push(...child.devices); + allDevices.push(...addSpaceUuidToDevices(child.devices, child.uuid)); if (child.children.length > 0) { await fetchChildren(child); From 35ce13a67f10e5c37c43ac6aca42bdd35a327d03 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 3 Jun 2025 09:47:24 +0300 Subject: [PATCH 34/56] fix: return proper error on login API (#386) --- libs/common/src/auth/services/auth.service.ts | 29 ++++++++++--------- .../role-type/entities/role.type.entity.ts | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 41ef028..db191a5 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,18 +1,18 @@ +import { PlatformType } from '@app/common/constants/platform-type.enum'; +import { RoleType } from '@app/common/constants/role.type.enum'; import { BadRequestException, Injectable, UnauthorizedException, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as argon2 from 'argon2'; -import { HelperHashService } from '../../helper/services'; -import { UserRepository } from '../../../../common/src/modules/user/repositories'; -import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; -import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; -import { ConfigService } from '@nestjs/config'; import { OAuth2Client } from 'google-auth-library'; -import { PlatformType } from '@app/common/constants/platform-type.enum'; -import { RoleType } from '@app/common/constants/role.type.enum'; +import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; +import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; +import { UserRepository } from '../../../../common/src/modules/user/repositories'; +import { HelperHashService } from '../../helper/services'; @Injectable() export class AuthService { @@ -40,16 +40,17 @@ export class AuthService { }, relations: ['roleType', 'project'], }); - if ( - platform === PlatformType.WEB && - (user.roleType.type === RoleType.SPACE_OWNER || - user.roleType.type === RoleType.SPACE_MEMBER) - ) { - throw new UnauthorizedException('Access denied for web platform'); - } if (!user) { throw new BadRequestException('Invalid credentials'); } + if ( + platform === PlatformType.WEB && + [RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes( + user.roleType.type as RoleType, + ) + ) { + throw new UnauthorizedException('Access denied for web platform'); + } if (!user.isUserVerified) { throw new BadRequestException('User is not verified'); diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index b7289a3..79d5acf 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -12,6 +12,7 @@ export class RoleTypeEntity extends AbstractEntity { nullable: false, enum: Object.values(RoleType), }) + // why is this ts-type string not enum? type: string; @OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, { nullable: true, From c39129f75b4cf0296a96f65cc9c10489d9feee17 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:38:44 -0600 Subject: [PATCH 35/56] Add Weather module with controller, service, and DTO for fetching weather details --- libs/common/src/constants/controller-route.ts | 9 +++++ libs/common/src/util/calculate.aqi.ts | 18 +++++++++ src/app.module.ts | 2 + src/config/index.ts | 3 +- src/config/weather.open.config.ts | 8 ++++ src/weather/controllers/index.ts | 1 + src/weather/controllers/weather.controller.ts | 28 +++++++++++++ src/weather/dto/get.weather.dto.ts | 21 ++++++++++ src/weather/services/index.ts | 1 + src/weather/services/weather.service.ts | 39 +++++++++++++++++++ src/weather/weather.module.ts | 12 ++++++ 11 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/util/calculate.aqi.ts create mode 100644 src/config/weather.open.config.ts create mode 100644 src/weather/controllers/index.ts create mode 100644 src/weather/controllers/weather.controller.ts create mode 100644 src/weather/dto/get.weather.dto.ts create mode 100644 src/weather/services/index.ts create mode 100644 src/weather/services/weather.service.ts create mode 100644 src/weather/weather.module.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 7afbaa5..064deba 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -465,7 +465,16 @@ export class ControllerRoute { 'This endpoint retrieves the terms and conditions for the application.'; }; }; + static WEATHER = class { + public static readonly ROUTE = 'weather'; + static ACTIONS = class { + public static readonly FETCH_WEATHER_DETAILS_SUMMARY = + 'Fetch Weather Details'; + public static readonly FETCH_WEATHER_DETAILS_DESCRIPTION = + 'This endpoint retrieves the current weather details for a specified location like temperature, humidity, etc.'; + }; + }; static PRIVACY_POLICY = class { public static readonly ROUTE = 'policy'; diff --git a/libs/common/src/util/calculate.aqi.ts b/libs/common/src/util/calculate.aqi.ts new file mode 100644 index 0000000..7911ad6 --- /dev/null +++ b/libs/common/src/util/calculate.aqi.ts @@ -0,0 +1,18 @@ +export function calculateAQI(pm2_5: number): number { + const breakpoints = [ + { pmLow: 0.0, pmHigh: 12.0, aqiLow: 0, aqiHigh: 50 }, + { pmLow: 12.1, pmHigh: 35.4, aqiLow: 51, aqiHigh: 100 }, + { pmLow: 35.5, pmHigh: 55.4, aqiLow: 101, aqiHigh: 150 }, + { pmLow: 55.5, pmHigh: 150.4, aqiLow: 151, aqiHigh: 200 }, + { pmLow: 150.5, pmHigh: 250.4, aqiLow: 201, aqiHigh: 300 }, + { pmLow: 250.5, pmHigh: 500.4, aqiLow: 301, aqiHigh: 500 }, + ]; + + const bp = breakpoints.find((b) => pm2_5 >= b.pmLow && pm2_5 <= b.pmHigh); + if (!bp) return pm2_5 > 500.4 ? 500 : 0; // Handle out-of-range values + + return Math.round( + ((bp.aqiHigh - bp.aqiLow) / (bp.pmHigh - bp.pmLow)) * (pm2_5 - bp.pmLow) + + bp.aqiLow, + ); +} diff --git a/src/app.module.ts b/src/app.module.ts index e674880..bde273d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -38,6 +38,7 @@ import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { OccupancyModule } from './occupancy/occupancy.module'; +import { WeatherModule } from './weather/weather.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -79,6 +80,7 @@ import { OccupancyModule } from './occupancy/occupancy.module'; PowerClampModule, HealthModule, OccupancyModule, + WeatherModule, ], providers: [ { diff --git a/src/config/index.ts b/src/config/index.ts index d7d0014..b1d9833 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,5 @@ import AuthConfig from './auth.config'; import AppConfig from './app.config'; import JwtConfig from './jwt.config'; -export default [AuthConfig, AppConfig, JwtConfig]; +import WeatherOpenConfig from './weather.open.config'; +export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig]; diff --git a/src/config/weather.open.config.ts b/src/config/weather.open.config.ts new file mode 100644 index 0000000..2182490 --- /dev/null +++ b/src/config/weather.open.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'openweather-config', + (): Record => ({ + OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY, + }), +); diff --git a/src/weather/controllers/index.ts b/src/weather/controllers/index.ts new file mode 100644 index 0000000..8157369 --- /dev/null +++ b/src/weather/controllers/index.ts @@ -0,0 +1 @@ +export * from './weather.controller'; diff --git a/src/weather/controllers/weather.controller.ts b/src/weather/controllers/weather.controller.ts new file mode 100644 index 0000000..280e09d --- /dev/null +++ b/src/weather/controllers/weather.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; // Assuming this is where the routes are defined +import { WeatherService } from '../services'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; + +@ApiTags('Weather Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.WEATHER.ROUTE, // use the static route constant +}) +export class WeatherController { + constructor(private readonly weatherService: WeatherService) {} + + @Get() + @ApiOperation({ + summary: ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_SUMMARY, + description: + ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_DESCRIPTION, + }) + async fetchWeatherDetails( + @Query() query: GetWeatherDetailsDto, + ): Promise { + return await this.weatherService.fetchWeatherDetails(query); + } +} diff --git a/src/weather/dto/get.weather.dto.ts b/src/weather/dto/get.weather.dto.ts new file mode 100644 index 0000000..86aefdd --- /dev/null +++ b/src/weather/dto/get.weather.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber } from 'class-validator'; + +export class GetWeatherDetailsDto { + @ApiProperty({ + description: 'Latitude coordinate', + example: 35.6895, + }) + @IsNumber() + @Type(() => Number) + lat: number; + + @ApiProperty({ + description: 'Longitude coordinate', + example: 139.6917, + }) + @IsNumber() + @Type(() => Number) + lon: number; +} diff --git a/src/weather/services/index.ts b/src/weather/services/index.ts new file mode 100644 index 0000000..9b2cb64 --- /dev/null +++ b/src/weather/services/index.ts @@ -0,0 +1 @@ +export * from './weather.service'; diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts new file mode 100644 index 0000000..11e1677 --- /dev/null +++ b/src/weather/services/weather.service.ts @@ -0,0 +1,39 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; +import { calculateAQI } from '@app/common/util/calculate.aqi'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; + +@Injectable() +export class WeatherService { + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) {} + + async fetchWeatherDetails( + query: GetWeatherDetailsDto, + ): Promise { + const { lat, lon } = query; + const weatherApiKey = this.configService.get( + 'OPEN_WEATHER_MAP_API_KEY', + ); + const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + + const response = await firstValueFrom(this.httpService.get(url)); + const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) + + return new SuccessResponseDto({ + message: `Weather details fetched successfully`, + data: { + aqi: calculateAQI(pm2_5), // Converted AQI (0-500) + temperature: response.data.current.temp_c, + humidity: response.data.current.humidity, + }, + statusCode: HttpStatus.OK, + }); + } +} diff --git a/src/weather/weather.module.ts b/src/weather/weather.module.ts new file mode 100644 index 0000000..eb65b5b --- /dev/null +++ b/src/weather/weather.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; // <-- Import this! +import { WeatherController } from './controllers'; +import { WeatherService } from './services'; + +@Module({ + imports: [ConfigModule, HttpModule], + controllers: [WeatherController], + providers: [WeatherService], +}) +export class WeatherModule {} From ab3efedc35cbae2bd61117e5be7a88b1e9055805 Mon Sep 17 00:00:00 2001 From: khuss Date: Tue, 3 Jun 2025 20:52:27 -0400 Subject: [PATCH 36/56] device model updated to include the fixes and final columns --- .../fact_daily_device_aqi_score.sql | 370 +++++++++++------- 1 file changed, 235 insertions(+), 135 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql index 6b919d0..53e68ee 100644 --- a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -26,43 +26,60 @@ BEGIN ('pm10', 255, 354, 151, 200), -- VOC - ('voc_value', 0, 200, 0, 50), - ('voc_value', 201, 400, 51, 100), - ('voc_value', 401, 600, 101, 150), - ('voc_value', 601, 1000, 151, 200), + ('voc', 0, 200, 0, 50), + ('voc', 201, 400, 51, 100), + ('voc', 401, 600, 101, 150), + ('voc', 601, 1000, 151, 200), -- CH2O - ('ch2o_value', 0, 2, 0, 50), - ('ch2o_value', 2.1, 4, 51, 100), - ('ch2o_value', 4.1, 6, 101, 150), + ('ch2o', 0, 2, 0, 50), + ('ch2o', 2.1, 4, 51, 100), + ('ch2o', 4.1, 6, 101, 150), -- CO2 - ('co2_value', 350, 1000, 0, 50), - ('co2_value', 1001, 1250, 51, 100), - ('co2_value', 1251, 1500, 101, 150), - ('co2_value', 1501, 2000, 151, 200) + ('co2', 350, 1000, 0, 50), + ('co2', 1001, 1250, 51, 100), + ('co2', 1251, 1500, 101, 150), + ('co2', 1501, 2000, 151, 200) ) AS v(pollutant, c_low, c_high, i_low, i_high) WHERE v.pollutant = LOWER(p_pollutant) AND concentration BETWEEN v.c_low AND v.c_high LIMIT 1; - -- Linear interpolation RETURN ROUND(((i_high - i_low) * (concentration - c_low) / (c_high - c_low)) + i_low); END; $$ LANGUAGE plpgsql; --- Function to convert Tuya AQI level (e.g., level_1, level_2) to numeric value + +-- Function to classify AQI +CREATE OR REPLACE FUNCTION classify_aqi(aqi NUMERIC) +RETURNS TEXT AS $$ +BEGIN + RETURN CASE + WHEN aqi BETWEEN 0 AND 50 THEN 'Good' + WHEN aqi BETWEEN 51 AND 100 THEN 'Moderate' + WHEN aqi BETWEEN 101 AND 150 THEN 'Unhealthy for Sensitive Groups' + WHEN aqi BETWEEN 151 AND 200 THEN 'Unhealthy' + WHEN aqi BETWEEN 201 AND 300 THEN 'Very Unhealthy' + WHEN aqi >= 301 THEN 'Hazardous' + ELSE NULL + END; +END; +$$ LANGUAGE plpgsql; + + +-- Function to convert AQI level string to number CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT) RETURNS NUMERIC AS $$ BEGIN - -- Extract the number from the string, default to NULL if not found RETURN CAST(regexp_replace(level_text, '[^0-9]', '', 'g') AS NUMERIC); EXCEPTION WHEN others THEN RETURN NULL; END; $$ LANGUAGE plpgsql; --- CTE for device + status log + space + +-- Query Pipeline Starts Here WITH device_space AS ( SELECT device.uuid AS device_id, @@ -78,149 +95,160 @@ WITH device_space AS ( WHERE product.cat_name = 'hjjcy' ), --- Getting the hourly pollutants max min avg for each device average_pollutants AS ( SELECT + event_time::date AS event_date, date_trunc('hour', event_time) AS event_hour, device_id, space_id, - -- AVG READINGS - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - - -- MIN READINGS - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + -- PM1 MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - - -- MAX READINGS - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY device_id, space_id, event_hour + GROUP BY device_id, space_id, event_hour, event_date ), - --- Fill NULLs due to missing log values filled_pollutants AS ( SELECT *, - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_filled_avg, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_filled_avg, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_filled_avg, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_filled_avg, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_filled_avg, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_avg_f, - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_filled, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_filled, - - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_filled, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_filled + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_min_f, + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_max_f FROM average_pollutants ), --- Calculate max, min, avg hourly AQI for each device + hourly_results AS ( SELECT device_id, space_id, + event_date, event_hour, - pm25_filled_avg, - pm10_filled_avg, - pm25_max_filled, - pm10_max_filled, - pm25_min_filled, - pm10_min_filled, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) ) AS hourly_avg_aqi, GREATEST( - calculate_aqi('pm25', pm25_max_filled), - calculate_aqi('pm10', pm10_max_filled) + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) ) AS hourly_max_aqi, - GREATEST( - calculate_aqi('pm25', pm25_min_filled), - calculate_aqi('pm10', pm10_min_filled) - ) AS hourly_min_aqi, - - CASE - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 50 THEN 'Good' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 100 THEN 'Moderate' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 150 THEN 'Unhealthy for Sensitive Groups' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 200 THEN 'Unhealthy' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 300 THEN 'Very Unhealthy' - ELSE 'Hazardous' - END AS aqi_category + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category FROM filled_pollutants - ORDER BY device_id, event_hour ), --- counting how many categories/hours are there in total (in case device was disconnected could be less than 24) daily_category_counts AS ( - SELECT - space_id, - device_id, - date_trunc('day', event_hour) AS event_day, - aqi_category, - COUNT(*) AS category_count + SELECT device_id, space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count FROM hourly_results - GROUP BY device_id, space_id, event_day, aqi_category -), + GROUP BY device_id, space_id, event_date, aqi_category + + UNION ALL + + SELECT device_id, space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, pm25_category + + UNION ALL + + SELECT device_id, space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, pm10_category + + UNION ALL + + SELECT device_id, space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, voc_category + + UNION ALL + + SELECT device_id, space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, co2_category + + UNION ALL + + SELECT device_id, space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, ch2o_category +), --- Aggregate Total Counts per Day daily_totals AS ( - select - device_id, + SELECT + device_id, space_id, - event_day, + event_date, SUM(category_count) AS total_count FROM daily_category_counts - GROUP BY device_id, space_id, event_day -), --- Calculate the daily of the daily min max avg AQI values -daily_averages AS ( - select - device_id, - space_id, - date_trunc('day', event_hour) AS event_day, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi - FROM hourly_results - GROUP BY device_id, space_id, event_day + where pollutant = 'aqi' + GROUP BY device_id, space_id, event_date ), -- Pivot Categories into Columns @@ -228,35 +256,107 @@ daily_percentages AS ( select dt.device_id, dt.space_id, - dt.event_day, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage FROM daily_totals dt LEFT JOIN daily_category_counts dcc - ON dt.device_id = dcc.device_id AND dt.event_day = dcc.event_day - GROUP BY dt.device_id, dt.space_id, dt.event_day, dt.total_count -) + ON dt.device_id = dcc.device_id AND dt.event_date = dcc.event_date + GROUP BY dt.device_id, dt.space_id, dt.event_date, dt.total_count +), --- Final Output +daily_averages AS ( + SELECT + device_id, + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY device_id, space_id, event_date +) SELECT p.device_id, p.space_id, - p.event_day, - p.good_percentage, - p.moderate_percentage, - p.unhealthy_sensitive_percentage, - p.unhealthy_percentage, - p.very_unhealthy_percentage, - p.hazardous_percentage, - a.daily_avg_aqi, - a.daily_max_aqi, - a.daily_min_aqi + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o FROM daily_percentages p LEFT JOIN daily_averages a - ON p.device_id = a.device_id AND p.event_day = a.event_day -ORDER BY p.space_id, p.event_day; + ON p.device_id = a.device_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date; + From 3ad81864d139d3fc2a509bc577f6c0feffc9e336 Mon Sep 17 00:00:00 2001 From: khuss Date: Tue, 3 Jun 2025 21:05:34 -0400 Subject: [PATCH 37/56] updated space models to include suggested fixes, update final logic and column names --- .../fact_daily_space_aqi_score.sql | 383 ++++++++++-------- 1 file changed, 222 insertions(+), 161 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql index ccee7f4..a74e66b 100644 --- a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql @@ -1,14 +1,11 @@ --- CTE for device + status log + space - +-- Query Pipeline Starts Here WITH device_space AS ( SELECT device.uuid AS device_id, - device.created_at, device.space_device_uuid AS space_id, "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, - "device-status-log".value, - "device-status-log".log + "device-status-log".value FROM device LEFT JOIN "device-status-log" ON device.uuid = "device-status-log".device_id @@ -17,198 +14,262 @@ WITH device_space AS ( WHERE product.cat_name = 'hjjcy' ), --- Getting the hourly pollutants max min avg for each space average_pollutants AS ( SELECT + event_time::date AS event_date, date_trunc('hour', event_time) AS event_hour, space_id, - - -- AVG READINGS - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - AVG(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_avg, - - -- MIN READINGS - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - MIN(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_min, - - -- MAX READINGS - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, - MAX(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_max + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY space_id, event_hour + GROUP BY space_id, event_hour, event_date ), --- Fill NULLs due to missing log values filled_pollutants AS ( SELECT *, - -- Forward-fill nulls using LAG() over partitioned data - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_filled_avg, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_filled_avg, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_filled_avg, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_filled_avg, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_filled_avg, - COALESCE(air_quality_index_avg, LAG(air_quality_index_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_filled_avg, - - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_filled, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_filled, - COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_filled, - COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_filled, - COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_filled, - COALESCE(air_quality_index_min, LAG(air_quality_index_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_min_filled, - - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_filled, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_filled, - COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_filled, - COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_filled, - COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_filled, - COALESCE(air_quality_index_max, LAG(air_quality_index_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_max_filled + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f FROM average_pollutants ), --- Calculate max, min, avg hourly AQI for each space -hourly_results as ( - SELECT - space_id, - event_hour, - --voc_filled, - pm25_filled_avg, - pm10_filled_avg, - -- co2_filled, - --ch2o_filled, - --aqi_filled, - pm25_max_filled, - pm10_max_filled, - pm25_min_filled, - pm10_min_filled, - air_quality_index_avg, - GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - --calculate_aqi('voc_value', voc_filled_avg), - --calculate_aqi('co2_value', co2_filled_avg), - --calculate_aqi('ch2o_value', ch2o_filled_avg) - -- Add more AQI sources as needed - ) AS hourly_avg_aqi, - GREATEST( - calculate_aqi('pm25', pm25_max_filled), - calculate_aqi('pm10', pm10_max_filled) - ) AS hourly_max_aqi, - GREATEST( - calculate_aqi('pm25', pm25_min_filled), - calculate_aqi('pm10', pm10_min_filled) - ) AS hourly_min_aqi, - case - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 50 then 'Good' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 100 then 'Moderate' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 150 then 'Unhealthy for Sensitive Groups' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 200 then 'Unhealthy' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 300 then 'Very Unhealthy' - else 'Hazardous' - end as aqi_category - - FROM filled_pollutants - ORDER BY space_id, event_hour +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants ), --- counting how many categories/hours are there in total (in case device(s) was disconnected could be less than 24) daily_category_counts AS ( - SELECT - space_id, - date_trunc('day', event_hour) AS event_day, - aqi_category, - COUNT(*) AS category_count + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count FROM hourly_results - GROUP BY space_id, event_day, aqi_category -), + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), --- Aggregate Total Counts per Day daily_totals AS ( SELECT space_id, - event_day, + event_date, SUM(category_count) AS total_count FROM daily_category_counts - GROUP BY space_id, event_day -), - --- Calculate the daily of the daily min max avg AQI values -daily_averages AS ( - SELECT - space_id, - date_trunc('day', event_hour) AS event_day, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi - FROM hourly_results - GROUP BY space_id, event_day + where pollutant = 'aqi' + GROUP BY space_id, event_date ), -- Pivot Categories into Columns daily_percentages AS ( - SELECT + select dt.space_id, - dt.event_day, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage FROM daily_totals dt LEFT JOIN daily_category_counts dcc - ON dt.space_id = dcc.space_id AND dt.event_day = dcc.event_day - GROUP BY dt.space_id, dt.event_day, dt.total_count -) + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), --- Final Output +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +) SELECT p.space_id, - p.event_day, - p.good_percentage, - p.moderate_percentage, - p.unhealthy_sensitive_percentage, - p.unhealthy_percentage, - p.very_unhealthy_percentage, - p.hazardous_percentage, - a.daily_avg_aqi, - a.daily_max_aqi, - a.daily_min_aqi + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o FROM daily_percentages p LEFT JOIN daily_averages a - ON p.space_id = a.space_id AND p.event_day = a.event_day -ORDER BY p.space_id, p.event_day; \ No newline at end of file + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date; + + From ef2245eae1eec5725f49b64cbfe0ce9b9a714eb0 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 3 Jun 2025 23:37:52 -0600 Subject: [PATCH 38/56] Add AQI space daily pollutant stats module and related entities, DTOs, and repositories --- libs/common/src/database/database.module.ts | 2 + .../src/modules/aqi/aqi.repository.module.ts | 11 +++ libs/common/src/modules/aqi/dtos/aqi.dto.ts | 82 +++++++++++++++++ libs/common/src/modules/aqi/dtos/index.ts | 1 + .../src/modules/aqi/entities/aqi.entity.ts | 88 +++++++++++++++++++ libs/common/src/modules/aqi/entities/index.ts | 1 + .../src/modules/aqi/repositories/index.ts | 1 + .../presence-sensor.repository.ts | 19 ++++ 8 files changed, 205 insertions(+) create mode 100644 libs/common/src/modules/aqi/aqi.repository.module.ts create mode 100644 libs/common/src/modules/aqi/dtos/aqi.dto.ts create mode 100644 libs/common/src/modules/aqi/dtos/index.ts create mode 100644 libs/common/src/modules/aqi/entities/aqi.entity.ts create mode 100644 libs/common/src/modules/aqi/entities/index.ts create mode 100644 libs/common/src/modules/aqi/repositories/index.ts create mode 100644 libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 183fbcc..d25dbd8 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -55,6 +55,7 @@ import { PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, } from '../modules/presence-sensor/entities'; +import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -115,6 +116,7 @@ import { PowerClampMonthlyEntity, PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, + AqiSpaceDailyPollutantStatsEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/aqi/aqi.repository.module.ts b/libs/common/src/modules/aqi/aqi.repository.module.ts new file mode 100644 index 0000000..1fc820d --- /dev/null +++ b/libs/common/src/modules/aqi/aqi.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AqiSpaceDailyPollutantStatsEntity } from './entities/aqi.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([AqiSpaceDailyPollutantStatsEntity])], +}) +export class AqiRepositoryModule {} diff --git a/libs/common/src/modules/aqi/dtos/aqi.dto.ts b/libs/common/src/modules/aqi/dtos/aqi.dto.ts new file mode 100644 index 0000000..1745d60 --- /dev/null +++ b/libs/common/src/modules/aqi/dtos/aqi.dto.ts @@ -0,0 +1,82 @@ +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class AqiSpaceDailyPollutantStatsDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsNotEmpty() + @IsString() + spaceUuid: string; + + @IsNotEmpty() + @IsString() + eventDay: string; + + @IsNotEmpty() + @IsNumber() + eventHour: number; + + @IsNumber() + pm1Min: number; + + @IsNumber() + pm1Avg: number; + + @IsNumber() + pm1Max: number; + + @IsNumber() + pm10Min: number; + + @IsNumber() + pm10Avg: number; + + @IsNumber() + pm10Max: number; + + @IsNumber() + pm25Min: number; + + @IsNumber() + pm25Avg: number; + + @IsNumber() + pm25Max: number; + + @IsNumber() + ch2oMin: number; + + @IsNumber() + ch2oAvg: number; + + @IsNumber() + ch2oMax: number; + + @IsNumber() + vocMin: number; + + @IsNumber() + vocAvg: number; + + @IsNumber() + vocMax: number; + + @IsNumber() + co2Min: number; + + @IsNumber() + co2Avg: number; + + @IsNumber() + co2Max: number; + + @IsNumber() + aqiMin: number; + + @IsNumber() + aqiAvg: number; + + @IsNumber() + aqiMax: number; +} diff --git a/libs/common/src/modules/aqi/dtos/index.ts b/libs/common/src/modules/aqi/dtos/index.ts new file mode 100644 index 0000000..da89122 --- /dev/null +++ b/libs/common/src/modules/aqi/dtos/index.ts @@ -0,0 +1 @@ +export * from './aqi.dto'; diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts new file mode 100644 index 0000000..701bcdd --- /dev/null +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -0,0 +1,88 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from '../../space/entities/space.entity'; +import { AqiSpaceDailyPollutantStatsDto } from '../dtos'; + +@Entity({ name: 'space-daily-pollutant-stats' }) +@Unique(['spaceUuid', 'eventDay', 'eventHour']) +export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity { + @Column({ nullable: false }) + public spaceUuid: string; + + @ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily) + space: SpaceEntity; + + @Column({ nullable: false }) + public eventDay: string; + + @Column({ nullable: false }) + public eventHour: number; + + @Column('float', { nullable: true }) + public pm1Min: number; + + @Column('float', { nullable: true }) + public pm1Avg: number; + + @Column('float', { nullable: true }) + public pm1Max: number; + + @Column('float', { nullable: true }) + public pm10Min: number; + + @Column('float', { nullable: true }) + public pm10Avg: number; + + @Column('float', { nullable: true }) + public pm10Max: number; + + @Column('float', { nullable: true }) + public pm25Min: number; + + @Column('float', { nullable: true }) + public pm25Avg: number; + + @Column('float', { nullable: true }) + public pm25Max: number; + + @Column('float', { nullable: true }) + public ch2oMin: number; + + @Column('float', { nullable: true }) + public ch2oAvg: number; + + @Column('float', { nullable: true }) + public ch2oMax: number; + + @Column('float', { nullable: true }) + public vocMin: number; + + @Column('float', { nullable: true }) + public vocAvg: number; + + @Column('float', { nullable: true }) + public vocMax: number; + + @Column('float', { nullable: true }) + public co2Min: number; + + @Column('float', { nullable: true }) + public co2Avg: number; + + @Column('float', { nullable: true }) + public co2Max: number; + + @Column('float', { nullable: true }) + public aqiMin: number; + + @Column('float', { nullable: true }) + public aqiAvg: number; + + @Column('float', { nullable: true }) + public aqiMax: number; + + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/aqi/entities/index.ts b/libs/common/src/modules/aqi/entities/index.ts new file mode 100644 index 0000000..655a0e7 --- /dev/null +++ b/libs/common/src/modules/aqi/entities/index.ts @@ -0,0 +1 @@ +export * from './aqi.entity'; diff --git a/libs/common/src/modules/aqi/repositories/index.ts b/libs/common/src/modules/aqi/repositories/index.ts new file mode 100644 index 0000000..8b64ee8 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/index.ts @@ -0,0 +1 @@ +export * from './presence-sensor.repository'; diff --git a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts new file mode 100644 index 0000000..146eb59 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts @@ -0,0 +1,19 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { + PresenceSensorDailyDeviceEntity, + PresenceSensorDailySpaceEntity, +} from '../entities'; + +@Injectable() +export class PresenceSensorDailyDeviceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager()); + } +} +@Injectable() +export class PresenceSensorDailySpaceRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager()); + } +} From cd3e9016f24c9771c5a698d07d7fc5ace97eb454 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:08:33 -0600 Subject: [PATCH 39/56] fix: improve error handling in fetchWeatherDetails method --- src/space/services/space.service.ts | 31 +++++++++--------- src/weather/services/weather.service.ts | 43 +++++++++++++++---------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 246f583..c001394 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -213,8 +213,8 @@ export class SpaceService { { incomingConnectionDisabled: false }, ) .leftJoinAndSelect('space.productAllocations', 'productAllocations') - .leftJoinAndSelect('productAllocations.tags', 'tags') - .leftJoinAndSelect('tags.product', 'tagProduct') + // .leftJoinAndSelect('productAllocations.tags', 'tags') + // .leftJoinAndSelect('productAllocations.product', 'product') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -225,8 +225,11 @@ export class SpaceService { 'subspaces.productAllocations', 'subspaceProductAllocations', ) - .leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTags') - .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') + // .leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTag') + // .leftJoinAndSelect( + // 'subspaceProductAllocations.product', + // 'subspaceProduct', + // ) .leftJoinAndSelect('space.spaceModel', 'spaceModel') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { @@ -264,9 +267,7 @@ export class SpaceService { }), ); } - - const transformedSpaces = spaces.map(this.transformSpace); - const spaceHierarchy = this.buildSpaceHierarchy(transformedSpaces); + const spaceHierarchy = this.buildSpaceHierarchy(spaces); return new SuccessResponseDto({ message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`, @@ -326,13 +327,13 @@ export class SpaceService { 'incomingConnections.disabled = :incomingConnectionDisabled', { incomingConnectionDisabled: false }, ) - .leftJoinAndSelect( - 'space.tags', - 'tags', - 'tags.disabled = :tagDisabled', - { tagDisabled: false }, - ) - .leftJoinAndSelect('tags.product', 'tagProduct') + // .leftJoinAndSelect( + // 'space.tags', + // 'tags', + // 'tags.disabled = :tagDisabled', + // { tagDisabled: false }, + // ) + // .leftJoinAndSelect('tags.product', 'tagProduct') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -345,7 +346,7 @@ export class SpaceService { 'subspaceTags.disabled = :subspaceTagsDisabled', { subspaceTagsDisabled: false }, ) - .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') + // .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { orphanSpaceName: ORPHAN_SPACE_NAME, diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index 11e1677..d534057 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { firstValueFrom } from 'rxjs'; @@ -17,23 +17,32 @@ export class WeatherService { async fetchWeatherDetails( query: GetWeatherDetailsDto, ): Promise { - const { lat, lon } = query; - const weatherApiKey = this.configService.get( - 'OPEN_WEATHER_MAP_API_KEY', - ); - const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + try { + const { lat, lon } = query; + const weatherApiKey = this.configService.get( + 'OPEN_WEATHER_MAP_API_KEY', + ); + const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; - const response = await firstValueFrom(this.httpService.get(url)); - const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) + const response = await firstValueFrom(this.httpService.get(url)); + const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) - return new SuccessResponseDto({ - message: `Weather details fetched successfully`, - data: { - aqi: calculateAQI(pm2_5), // Converted AQI (0-500) - temperature: response.data.current.temp_c, - humidity: response.data.current.humidity, - }, - statusCode: HttpStatus.OK, - }); + return new SuccessResponseDto({ + message: `Weather details fetched successfully`, + data: { + aqi: calculateAQI(pm2_5), // Converted AQI (0-500) + temperature: response.data.current.temp_c, + humidity: response.data.current.humidity, + }, + statusCode: HttpStatus.OK, + }); + } catch (error) { + console.log(`Error fetching weather data: ${error}`); + + throw new HttpException( + `Wrong latitude or longitude provided`, + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } From ea021ad2280c113b8981627cf0e3d4e2913533fb Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:09:49 -0600 Subject: [PATCH 40/56] fix: update error message for invalid latitude and longitude in fetchWeatherDetails method --- src/weather/services/weather.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index d534057..aa4ea0d 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -40,7 +40,7 @@ export class WeatherService { console.log(`Error fetching weather data: ${error}`); throw new HttpException( - `Wrong latitude or longitude provided`, + `Api can't handle these lat and lon values`, error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } From 43dfaaa90d0e2775fbf979edbe282161b489f800 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:32:01 -0600 Subject: [PATCH 41/56] fix: utilize WEATHER_API_URL in WeatherService for dynamic API endpoint --- src/config/weather.open.config.ts | 1 + src/weather/services/weather.service.ts | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config/weather.open.config.ts b/src/config/weather.open.config.ts index 2182490..94b77d5 100644 --- a/src/config/weather.open.config.ts +++ b/src/config/weather.open.config.ts @@ -4,5 +4,6 @@ export default registerAs( 'openweather-config', (): Record => ({ OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY, + WEATHER_API_URL: process.env.WEATHER_API_URL, }), ); diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index aa4ea0d..3463b5c 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -9,10 +9,13 @@ import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @Injectable() export class WeatherService { + private readonly weatherApiUrl: string; constructor( private readonly configService: ConfigService, private readonly httpService: HttpService, - ) {} + ) { + this.weatherApiUrl = this.configService.get('WEATHER_API_URL'); + } async fetchWeatherDetails( query: GetWeatherDetailsDto, @@ -22,7 +25,7 @@ export class WeatherService { const weatherApiKey = this.configService.get( 'OPEN_WEATHER_MAP_API_KEY', ); - const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + const url = `${this.weatherApiUrl}/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; const response = await firstValueFrom(this.httpService.get(url)); const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) From 466863e71f00961fc947b46ca44ad87f76496785 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 16:33:55 -0400 Subject: [PATCH 42/56] Procedures insert-all, update, and select for daily space air quality stats --- .../proceduce_select_daily_space_aqi.sql | 374 ++++++++++++++++++ .../proceduce_update_daily_space_aqi.sql | 374 ++++++++++++++++++ .../procedure_insert_all_daily_space_aqi.sql | 367 +++++++++++++++++ 3 files changed, 1115 insertions(+) create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql new file mode 100644 index 0000000..993f3d0 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -0,0 +1,374 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id +), + +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".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 = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + + diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql new file mode 100644 index 0000000..993f3d0 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -0,0 +1,374 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id +), + +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".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 = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + + diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql new file mode 100644 index 0000000..aeb211b --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql @@ -0,0 +1,367 @@ +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".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 = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + From 80e89dd035d2acb356bc412bec51b5343fffe2aa Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 17:20:25 -0400 Subject: [PATCH 43/56] fix name of snapshot table --- .../proceduce_select_daily_space_aqi.sql | 401 ++---------------- .../proceduce_update_daily_space_aqi.sql | 2 +- .../procedure_insert_all_daily_space_aqi.sql | 2 +- 3 files changed, 35 insertions(+), 370 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql index 993f3d0..4c4c6ab 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -1,374 +1,39 @@ -WITH params AS ( - SELECT - TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, - $2::uuid AS space_id -), - --- Query Pipeline Starts Here -WITH device_space AS ( - SELECT - device.uuid AS device_id, - device.space_device_uuid AS space_id, - "device-status-log".event_time::timestamp AS event_time, - "device-status-log".code, - "device-status-log".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 = 'hjjcy' -), - -average_pollutants AS ( - SELECT - event_time::date AS event_date, - date_trunc('hour', event_time) AS event_hour, - space_id, - - -- PM1 - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, - - -- PM25 - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, - - -- PM10 - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, - - -- VOC - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, - - -- CH2O - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, - - -- CO2 - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max - - FROM device_space - GROUP BY space_id, event_hour, event_date -), - -filled_pollutants AS ( - SELECT - *, - -- AVG - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, - - -- MIN - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, - COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, - COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, - COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, - - -- MAX - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, - COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, - COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, - COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f - FROM average_pollutants -), - -hourly_results AS ( +ITH params AS ( SELECT - space_id, - event_date, - event_hour, - pm1_min, pm1_avg, pm1_max, - pm25_min_f, pm25_avg_f, pm25_max_f, - pm10_min_f, pm10_avg_f, pm10_max_f, - voc_min_f, voc_avg_f, voc_max_f, - co2_min_f, co2_avg_f, co2_max_f, - ch2o_min_f, ch2o_avg_f, ch2o_max_f, - - GREATEST( - calculate_aqi('pm25', pm25_min_f), - calculate_aqi('pm10', pm10_min_f) - ) AS hourly_min_aqi, - - GREATEST( - calculate_aqi('pm25', pm25_avg_f), - calculate_aqi('pm10', pm10_avg_f) - ) AS hourly_avg_aqi, - - GREATEST( - calculate_aqi('pm25', pm25_max_f), - calculate_aqi('pm10', pm10_max_f) - ) AS hourly_max_aqi, - - classify_aqi(GREATEST( - calculate_aqi('pm25', pm25_avg_f), - calculate_aqi('pm10', pm10_avg_f) - )) AS aqi_category, - - classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, - classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, - classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, - classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, - classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category - - FROM filled_pollutants -), - -daily_category_counts AS ( - SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, aqi_category - - UNION ALL - - SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, pm25_category - - UNION ALL - - SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, pm10_category - - UNION ALL - - SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, voc_category - - UNION ALL - - SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, co2_category - - UNION ALL - - SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, ch2o_category -), - -daily_totals AS ( - SELECT - space_id, - event_date, - SUM(category_count) AS total_count - FROM daily_category_counts - where pollutant = 'aqi' - GROUP BY space_id, event_date -), - --- Pivot Categories into Columns -daily_percentages AS ( - select - dt.space_id, - dt.event_date, - -- AQI CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, - -- PM25 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, - -- PM10 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, - -- VOC CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, - -- CO2 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, - -- CH20 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage - FROM daily_totals dt - LEFT JOIN daily_category_counts dcc - ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date - GROUP BY dt.space_id, dt.event_date, dt.total_count -), - -daily_averages AS ( - SELECT - space_id, - event_date, - -- AQI - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - -- PM25 - ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, - ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, - ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, - -- PM10 - ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, - ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, - ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, - -- VOC - ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, - ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, - ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, - -- CO2 - ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, - ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, - ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, - -- CH2O - ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, - ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, - ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o - - FROM hourly_results - GROUP BY space_id, event_date -), - -final_data as( -SELECT - p.space_id, - p.event_date, - p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, - a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, - p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, - a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, - p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, - a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, - p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, - a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, - p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, - a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, - p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, - a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o -FROM daily_percentages p -LEFT JOIN daily_averages a - ON p.space_id = a.space_id AND p.event_date = a.event_date -ORDER BY p.space_id, p.event_date) - - -INSERT INTO public.space_daily_pollutant_stats ( - space_uuid, - event_date, - good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, - daily_avg_aqi, daily_max_aqi, daily_min_aqi, - good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, - daily_avg_pm25, daily_max_pm25, daily_min_pm25, - good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, - daily_avg_pm10, daily_max_pm10, daily_min_pm10, - good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, - daily_avg_voc, daily_max_voc, daily_min_voc, - good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, - daily_avg_co2, daily_max_co2, daily_min_co2, - good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, - daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o + $1::uuid AS space_uuid, + TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month ) + SELECT - space_id, - event_date, - good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, - daily_avg_aqi, daily_max_aqi, daily_min_aqi, - good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, - daily_avg_pm25, daily_max_pm25, daily_min_pm25, - good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, - daily_avg_pm10, daily_max_pm10, daily_min_pm10, - good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, - daily_avg_voc, daily_max_voc, daily_min_voc, - good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, - daily_avg_co2, daily_max_co2, daily_min_co2, - good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, - daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o -FROM final_data -ON CONFLICT (space_uuid, event_date) DO UPDATE -SET - good_aqi_percentage = EXCLUDED.good_aqi_percentage, - moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, - unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, - unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, - very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, - hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, - daily_avg_aqi = EXCLUDED.daily_avg_aqi, - daily_max_aqi = EXCLUDED.daily_max_aqi, - daily_min_aqi = EXCLUDED.daily_min_aqi, - good_pm25_percentage = EXCLUDED.good_pm25_percentage, - moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, - unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, - unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, - very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, - hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, - daily_avg_pm25 = EXCLUDED.daily_avg_pm25, - daily_max_pm25 = EXCLUDED.daily_max_pm25, - daily_min_pm25 = EXCLUDED.daily_min_pm25, - good_pm10_percentage = EXCLUDED.good_pm10_percentage, - moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, - unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, - unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, - very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, - hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, - daily_avg_pm10 = EXCLUDED.daily_avg_pm10, - daily_max_pm10 = EXCLUDED.daily_max_pm10, - daily_min_pm10 = EXCLUDED.daily_min_pm10, - good_voc_percentage = EXCLUDED.good_voc_percentage, - moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, - unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, - unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, - very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, - hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, - daily_avg_voc = EXCLUDED.daily_avg_voc, - daily_max_voc = EXCLUDED.daily_max_voc, - daily_min_voc = EXCLUDED.daily_min_voc, - good_co2_percentage = EXCLUDED.good_co2_percentage, - moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, - unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, - unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, - very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, - hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, - daily_avg_co2 = EXCLUDED.daily_avg_co2, - daily_max_co2 = EXCLUDED.daily_max_co2, - daily_min_co2 = EXCLUDED.daily_min_co2, - good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, - moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, - unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, - unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, - very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, - hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, - daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, - daily_max_ch2o = EXCLUDED.daily_max_ch2o, - daily_min_ch2o = EXCLUDED.daily_min_ch2o; + sdp.space_uuid, + sdp.event_date, + sdp.good_aqi_percentage, sdp.moderate_aqi_percentage, sdp.unhealthy_sensitive_aqi_percentage, sdp.unhealthy_aqi_percentage, + sdp.very_unhealthy_aqi_percentage, sdp.hazardous_aqi_percentage, + sdp.daily_avg_aqi, sdp.daily_max_aqi, sdp.daily_min_aqi, + sdp.good_pm25_percentage, sdp.moderate_pm25_percentage, sdp.unhealthy_sensitive_pm25_percentage, sdp.unhealthy_pm25_percentage, + sdp.very_unhealthy_pm25_percentage, sdp.hazardous_pm25_percentage, + sdp.daily_avg_pm25, sdp.daily_max_pm25, sdp.daily_min_pm25, + sdp.good_pm10_percentage, sdp.moderate_pm10_percentage, sdp.unhealthy_sensitive_pm10_percentage, sdp.unhealthy_pm10_percentage, + sdp.very_unhealthy_pm10_percentage, sdp.hazardous_pm10_percentage, + sdp.daily_avg_pm10, sdp.daily_max_pm10, sdp.daily_min_pm10, + sdp.good_voc_percentage, sdp.moderate_voc_percentage, sdp.unhealthy_sensitive_voc_percentage, sdp.unhealthy_voc_percentage, + sdp.very_unhealthy_voc_percentage, sdp.hazardous_voc_percentage, + sdp.daily_avg_voc, sdp.daily_max_voc, sdp.daily_min_voc, + + sdp.good_co2_percentage, sdp.moderate_co2_percentage, sdp.unhealthy_sensitive_co2_percentage, sdp.unhealthy_co2_percentage, + sdp.very_unhealthy_co2_percentage, sdp.hazardous_co2_percentage, + sdp.daily_avg_co2, sdp.daily_max_co2, sdp.daily_min_co2, + + sdp.good_ch2o_percentage, sdp.moderate_ch2o_percentage, sdp.unhealthy_sensitive_ch2o_percentage, sdp.unhealthy_ch2o_percentage, + sdp.very_unhealthy_ch2o_percentage, sdp.hazardous_ch2o_percentage, + sdp.daily_avg_ch2o, sdp.daily_max_ch2o, sdp.daily_min_ch2o + +FROM public."space-daily-pollutant-stats" AS sdp +CROSS JOIN params p +WHERE + (p.space_uuid IS NULL OR sdp.space_uuid = p.space_uuid) + AND (p.event_month IS NULL OR TO_CHAR(sdp.event_date, 'YYYY-MM') = TO_CHAR(p.event_month, 'YYYY-MM')) +ORDER BY sdp.space_uuid, sdp.event_date; \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql index 993f3d0..e2466f2 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -281,7 +281,7 @@ LEFT JOIN daily_averages a ORDER BY p.space_id, p.event_date) -INSERT INTO public.space_daily_pollutant_stats ( +INSERT INTO public."space-daily-pollutant-stats" ( space_uuid, event_date, good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql index aeb211b..0ed029b 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql @@ -275,7 +275,7 @@ LEFT JOIN daily_averages a ORDER BY p.space_id, p.event_date) -INSERT INTO public.space_daily_pollutant_stats ( +INSERT INTO public."space-daily-pollutant-stats" ( space_uuid, event_date, good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, From 0d6de2df43455711ab0061c91a2776ac3a040fd5 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:24:13 -0600 Subject: [PATCH 44/56] Refactor AqiSpaceDailyPollutantStatsEntity: update unique constraint and rename event fields for clarity --- .../src/modules/aqi/entities/aqi.entity.ts | 150 ++++++++++++++---- 1 file changed, 123 insertions(+), 27 deletions(-) diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts index 701bcdd..fed3881 100644 --- a/libs/common/src/modules/aqi/entities/aqi.entity.ts +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -4,7 +4,7 @@ import { SpaceEntity } from '../../space/entities/space.entity'; import { AqiSpaceDailyPollutantStatsDto } from '../dtos'; @Entity({ name: 'space-daily-pollutant-stats' }) -@Unique(['spaceUuid', 'eventDay', 'eventHour']) +@Unique(['spaceUuid', 'eventDate']) export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity { @Column({ nullable: false }) public spaceUuid: string; @@ -12,74 +12,170 @@ export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity SpaceEntity, (space) => space.presenceSensorDaily) space: SpaceEntity; - @Column({ nullable: false }) - public eventDay: string; - - @Column({ nullable: false }) - public eventHour: number; + @Column({ type: 'date', nullable: false }) + public eventDate: Date; @Column('float', { nullable: true }) - public pm1Min: number; + public goodAqiPercentage: number; @Column('float', { nullable: true }) - public pm1Avg: number; + public moderateAqiPercentage: number; @Column('float', { nullable: true }) - public pm1Max: number; + public unhealthySensitiveAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Min: number; + public unhealthyAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Avg: number; + public veryUnhealthyAqiPercentage: number; @Column('float', { nullable: true }) - public pm10Max: number; + public hazardousAqiPercentage: number; @Column('float', { nullable: true }) - public pm25Min: number; + public dailyAvgAqi: number; @Column('float', { nullable: true }) - public pm25Avg: number; + public dailyMaxAqi: number; @Column('float', { nullable: true }) - public pm25Max: number; + public dailyMinAqi: number; @Column('float', { nullable: true }) - public ch2oMin: number; + public goodPm25Percentage: number; @Column('float', { nullable: true }) - public ch2oAvg: number; + public moderatePm25Percentage: number; @Column('float', { nullable: true }) - public ch2oMax: number; + public unhealthySensitivePm25Percentage: number; @Column('float', { nullable: true }) - public vocMin: number; + public unhealthyPm25Percentage: number; @Column('float', { nullable: true }) - public vocAvg: number; + public veryUnhealthyPm25Percentage: number; @Column('float', { nullable: true }) - public vocMax: number; + public hazardousPm25Percentage: number; @Column('float', { nullable: true }) - public co2Min: number; + public dailyAvgPm25: number; @Column('float', { nullable: true }) - public co2Avg: number; + public dailyMaxPm25: number; @Column('float', { nullable: true }) - public co2Max: number; + public dailyMinPm25: number; @Column('float', { nullable: true }) - public aqiMin: number; + public goodPm10Percentage: number; @Column('float', { nullable: true }) - public aqiAvg: number; + public moderatePm10Percentage: number; @Column('float', { nullable: true }) - public aqiMax: number; + public unhealthySensitivePm10Percentage: number; + + @Column('float', { nullable: true }) + public unhealthyPm10Percentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyPm10Percentage: number; + + @Column('float', { nullable: true }) + public hazardousPm10Percentage: number; + + @Column('float', { nullable: true }) + public dailyAvgPm10: number; + + @Column('float', { nullable: true }) + public dailyMaxPm10: number; + + @Column('float', { nullable: true }) + public dailyMinPm10: number; + + @Column('float', { nullable: true }) + public goodVocPercentage: number; + + @Column('float', { nullable: true }) + public moderateVocPercentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveVocPercentage: number; + + @Column('float', { nullable: true }) + public unhealthyVocPercentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyVocPercentage: number; + + @Column('float', { nullable: true }) + public hazardousVocPercentage: number; + + @Column('float', { nullable: true }) + public dailyAvgVoc: number; + + @Column('float', { nullable: true }) + public dailyMaxVoc: number; + + @Column('float', { nullable: true }) + public dailyMinVoc: number; + + @Column('float', { nullable: true }) + public goodCo2Percentage: number; + + @Column('float', { nullable: true }) + public moderateCo2Percentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveCo2Percentage: number; + + @Column('float', { nullable: true }) + public unhealthyCo2Percentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyCo2Percentage: number; + + @Column('float', { nullable: true }) + public hazardousCo2Percentage: number; + + @Column('float', { nullable: true }) + public dailyAvgCo2: number; + + @Column('float', { nullable: true }) + public dailyMaxCo2: number; + + @Column('float', { nullable: true }) + public dailyMinCo2: number; + + @Column('float', { nullable: true }) + public goodCh2oPercentage: number; + + @Column('float', { nullable: true }) + public moderateCh2oPercentage: number; + + @Column('float', { nullable: true }) + public unhealthySensitiveCh2oPercentage: number; + + @Column('float', { nullable: true }) + public unhealthyCh2oPercentage: number; + + @Column('float', { nullable: true }) + public veryUnhealthyCh2oPercentage: number; + + @Column('float', { nullable: true }) + public hazardousCh2oPercentage: number; + + @Column('float', { nullable: true }) + public dailyAvgCh2o: number; + + @Column('float', { nullable: true }) + public dailyMaxCh2o: number; + + @Column('float', { nullable: true }) + public dailyMinCh2o: number; constructor(partial: Partial) { super(); From ee0261d102015af5cce07b04edf5a5996fcb1ed9 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 17:32:50 -0400 Subject: [PATCH 45/56] Fix typos procedure select and update --- .../fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql | 2 +- .../fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql index 4c4c6ab..83fc840 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -1,4 +1,4 @@ -ITH params AS ( +WITH params AS ( SELECT $1::uuid AS space_uuid, TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql index e2466f2..04fb661 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -5,7 +5,7 @@ WITH params AS ( ), -- Query Pipeline Starts Here -WITH device_space AS ( +device_space AS ( SELECT device.uuid AS device_id, device.space_device_uuid AS space_id, From 7eb13088acb0916cfa965376e67a217ac4542c8c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 9 Jun 2025 04:50:58 -0600 Subject: [PATCH 46/56] Refactor DeviceStatusLogEntity: update unique constraint and primary key definition --- .../entities/device-status-log.entity.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index b40c393..595db05 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -1,16 +1,16 @@ import { SourceType } from '@app/common/constants/source-type.enum'; -import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; +import { Entity, Column, Unique, PrimaryGeneratedColumn } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime']) +@Unique('device_event_time_unique', ['deviceId', 'eventTime']) export class DeviceStatusLogEntity { - @Column({ type: 'int', generated: true, unsigned: true }) + @PrimaryGeneratedColumn() id: number; @Column({ type: 'text' }) eventId: string; - @PrimaryColumn({ type: 'timestamptz' }) + @Column({ type: 'timestamptz' }) eventTime: Date; @Column({ From c1065126aab117ee508cca9d65dd4e7b5bbf456c Mon Sep 17 00:00:00 2001 From: faljawhary Date: Tue, 10 Jun 2025 01:03:45 -0600 Subject: [PATCH 47/56] =?UTF-8?q?Revert=20"Refactor=20DeviceStatusLogEntit?= =?UTF-8?q?y:=20update=20unique=20constraint=20and=20primary=20=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/device-status-log.entity.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index 595db05..b40c393 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -1,16 +1,16 @@ import { SourceType } from '@app/common/constants/source-type.enum'; -import { Entity, Column, Unique, PrimaryGeneratedColumn } from 'typeorm'; +import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('device_event_time_unique', ['deviceId', 'eventTime']) +@Unique('event_time_idx', ['eventTime']) export class DeviceStatusLogEntity { - @PrimaryGeneratedColumn() + @Column({ type: 'int', generated: true, unsigned: true }) id: number; @Column({ type: 'text' }) eventId: string; - @Column({ type: 'timestamptz' }) + @PrimaryColumn({ type: 'timestamptz' }) eventTime: Date; @Column({ From b6321c253058f18ebe5d78a6d8909b042e6ff5db Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:18:58 -0600 Subject: [PATCH 48/56] Refactor DeviceStatusLogEntity: update unique constraint to include deviceId --- .../device-status-log/entities/device-status-log.entity.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index b40c393..a97565a 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,15 +2,15 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime']) +@Unique('event_time_device_idx', ['eventTime', 'deviceId']) export class DeviceStatusLogEntity { - @Column({ type: 'int', generated: true, unsigned: true }) + @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number; @Column({ type: 'text' }) eventId: string; - @PrimaryColumn({ type: 'timestamptz' }) + @Column({ type: 'timestamptz' }) eventTime: Date; @Column({ From 0793441e06534b2b1ade526c1516ed5478c1deaf Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:24:33 -0600 Subject: [PATCH 49/56] Refactor DeviceStatusLogEntity: correct unique constraint name for event time and device ID --- .../device-status-log/entities/device-status-log.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index a97565a..3b55e31 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,7 +2,7 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_device_idx', ['eventTime', 'deviceId']) +@Unique('event_time_idx', ['eventTime', 'deviceId']) export class DeviceStatusLogEntity { @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number; From 97e14e70f7cb4d452383b383723935fb04d46fbb Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:41:41 -0600 Subject: [PATCH 50/56] Refactor DeviceStatusLogEntity: expand unique constraint to include code and value --- .../device-status-log/entities/device-status-log.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index 3b55e31..07fc8d2 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,7 +2,7 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime', 'deviceId']) +@Unique('event_time_idx', ['eventTime', 'deviceId', 'code', 'value']) export class DeviceStatusLogEntity { @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number; From c86be27576e6f4ea9c0fd4c32cc8ccf78a474998 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:19:34 -0600 Subject: [PATCH 51/56] Add AQI module and related services, controllers, and DTOs - Introduced AqiModule with AqiService and AqiController for handling AQI data. - Added DTOs for AQI requests: GetAqiDailyBySpaceDto and GetAqiPollutantBySpaceDto. - Implemented AqiDataService for managing AQI sensor historical data. - Updated existing modules to include AqiDataService where necessary. - Defined new routes for AQI data retrieval in ControllerRoute. --- libs/common/src/constants/controller-route.ts | 14 ++ .../common/src/constants/product-type.enum.ts | 1 + .../devices-status/devices-status.module.ts | 2 + .../services/devices-status.service.ts | 8 +- .../src/helper/services/aqi.data.service.ts | 47 ++++++ .../aqi/repositories/aqi.repository.ts | 10 ++ .../src/modules/aqi/repositories/index.ts | 2 +- .../presence-sensor.repository.ts | 19 --- src/app.module.ts | 2 + src/aqi/aqi.module.ts | 11 ++ src/aqi/controllers/aqi.controller.ts | 64 ++++++++ src/aqi/controllers/index.ts | 1 + src/aqi/dto/aqi-params.dto.ts | 7 + src/aqi/dto/get-aqi.dto.ts | 36 +++++ src/aqi/services/aqi.service.ts | 146 ++++++++++++++++++ src/aqi/services/index.ts | 1 + .../commission-device.module.ts | 2 + src/community/community.module.ts | 2 + src/door-lock/door.lock.module.ts | 2 + src/group/group.module.ts | 2 + src/invite-user/invite-user.module.ts | 2 + src/power-clamp/power-clamp.module.ts | 2 + src/project/project.module.ts | 2 + src/space-model/space-model.module.ts | 2 + src/space/space.module.ts | 2 + .../visitor-password.module.ts | 2 + 26 files changed, 370 insertions(+), 21 deletions(-) create mode 100644 libs/common/src/helper/services/aqi.data.service.ts create mode 100644 libs/common/src/modules/aqi/repositories/aqi.repository.ts delete mode 100644 libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts create mode 100644 src/aqi/aqi.module.ts create mode 100644 src/aqi/controllers/aqi.controller.ts create mode 100644 src/aqi/controllers/index.ts create mode 100644 src/aqi/dto/aqi-params.dto.ts create mode 100644 src/aqi/dto/get-aqi.dto.ts create mode 100644 src/aqi/services/aqi.service.ts create mode 100644 src/aqi/services/index.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 064deba..cc37eee 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -524,6 +524,20 @@ export class ControllerRoute { 'This endpoint retrieves the occupancy heat map data based on the provided parameters.'; }; }; + static AQI = class { + public static readonly ROUTE = 'aqi'; + + static ACTIONS = class { + public static readonly GET_AQI_RANGE_DATA_SUMMARY = 'Get AQI range data'; + public static readonly GET_AQI_RANGE_DATA_DESCRIPTION = + 'This endpoint retrieves the AQI (Air Quality Index) range data based on the provided parameters.'; + + public static readonly GET_AQI_DISTRIBUTION_DATA_SUMMARY = + 'Get AQI distribution data'; + public static readonly GET_AQI_DISTRIBUTION_DATA_DESCRIPTION = + 'This endpoint retrieves the AQI (Air Quality Index) distribution data based on the provided parameters.'; + }; + }; static DEVICE = class { public static readonly ROUTE = 'devices'; diff --git a/libs/common/src/constants/product-type.enum.ts b/libs/common/src/constants/product-type.enum.ts index fd0cc6f..1bb1394 100644 --- a/libs/common/src/constants/product-type.enum.ts +++ b/libs/common/src/constants/product-type.enum.ts @@ -19,4 +19,5 @@ export enum ProductType { FOUR_S = '4S', SIX_S = '6S', SOS = 'SOS', + AQI = 'AQI', } diff --git a/libs/common/src/firebase/devices-status/devices-status.module.ts b/libs/common/src/firebase/devices-status/devices-status.module.ts index 784b801..52f6123 100644 --- a/libs/common/src/firebase/devices-status/devices-status.module.ts +++ b/libs/common/src/firebase/devices-status/devices-status.module.ts @@ -11,6 +11,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ providers: [ @@ -23,6 +24,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], controllers: [DeviceStatusFirebaseController], exports: [DeviceStatusFirebaseService, DeviceStatusLogRepository], diff --git a/libs/common/src/firebase/devices-status/services/devices-status.service.ts b/libs/common/src/firebase/devices-status/services/devices-status.service.ts index 18c752c..4b0b0f7 100644 --- a/libs/common/src/firebase/devices-status/services/devices-status.service.ts +++ b/libs/common/src/firebase/devices-status/services/devices-status.service.ts @@ -23,6 +23,7 @@ import { PowerClampService } from '@app/common/helper/services/power.clamp.servi import { PowerClampEnergyEnum } from '@app/common/constants/power.clamp.enargy.enum'; import { PresenceSensorEnum } from '@app/common/constants/presence.sensor.enum'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Injectable() export class DeviceStatusFirebaseService { private tuya: TuyaContext; @@ -32,6 +33,7 @@ export class DeviceStatusFirebaseService { private readonly deviceRepository: DeviceRepository, private readonly powerClampService: PowerClampService, private readonly occupancyService: OccupancyService, + private readonly aqiDataService: AqiDataService, private deviceStatusLogRepository: DeviceStatusLogRepository, ) { const accessKey = this.configService.get('auth-config.ACCESS_KEY'); @@ -262,7 +264,11 @@ export class DeviceStatusFirebaseService { ); } } - + if (addDeviceStatusDto.productType === ProductType.AQI) { + await this.aqiDataService.updateAQISensorHistoricalData( + addDeviceStatusDto.deviceUuid, + ); + } // Return the updated data const snapshot: DataSnapshot = await get(dataRef); return snapshot.val(); diff --git a/libs/common/src/helper/services/aqi.data.service.ts b/libs/common/src/helper/services/aqi.data.service.ts new file mode 100644 index 0000000..3e19b6c --- /dev/null +++ b/libs/common/src/helper/services/aqi.data.service.ts @@ -0,0 +1,47 @@ +import { DeviceRepository } from '@app/common/modules/device/repositories'; +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 AqiDataService { + constructor( + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, + private readonly deviceRepository: DeviceRepository, + ) {} + async updateAQISensorHistoricalData(deviceUuid: string): Promise { + try { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-CA'); // YYYY-MM-DD + const device = await this.deviceRepository.findOne({ + where: { uuid: deviceUuid }, + relations: ['spaceDevice'], + }); + + await this.executeProcedure( + 'fact_daily_space_aqi', + 'proceduce_update_daily_space_aqi', + [dateStr, device.spaceDevice?.uuid], + ); + } catch (err) { + console.error('Failed to insert or update aqi data:', err); + throw err; + } + } + + private async executeProcedure( + procedureFolderName: string, + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery(procedureFolderName, procedureFileName); + await this.dataSource.query(query, params); + console.log(`Procedure ${procedureFileName} executed successfully.`); + } + + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/libs/common/src/modules/aqi/repositories/aqi.repository.ts b/libs/common/src/modules/aqi/repositories/aqi.repository.ts new file mode 100644 index 0000000..4f02490 --- /dev/null +++ b/libs/common/src/modules/aqi/repositories/aqi.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { AqiSpaceDailyPollutantStatsEntity } from '../entities'; + +@Injectable() +export class AqiSpaceDailyPollutantStatsRepository extends Repository { + constructor(private dataSource: DataSource) { + super(AqiSpaceDailyPollutantStatsEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/aqi/repositories/index.ts b/libs/common/src/modules/aqi/repositories/index.ts index 8b64ee8..d1c0488 100644 --- a/libs/common/src/modules/aqi/repositories/index.ts +++ b/libs/common/src/modules/aqi/repositories/index.ts @@ -1 +1 @@ -export * from './presence-sensor.repository'; +export * from './aqi.repository'; diff --git a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts b/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts deleted file mode 100644 index 146eb59..0000000 --- a/libs/common/src/modules/aqi/repositories/presence-sensor.repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Injectable } from '@nestjs/common'; -import { - PresenceSensorDailyDeviceEntity, - PresenceSensorDailySpaceEntity, -} from '../entities'; - -@Injectable() -export class PresenceSensorDailyDeviceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(PresenceSensorDailyDeviceEntity, dataSource.createEntityManager()); - } -} -@Injectable() -export class PresenceSensorDailySpaceRepository extends Repository { - constructor(private dataSource: DataSource) { - super(PresenceSensorDailySpaceEntity, dataSource.createEntityManager()); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index bde273d..8d4b6e0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -39,6 +39,7 @@ import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { OccupancyModule } from './occupancy/occupancy.module'; import { WeatherModule } from './weather/weather.module'; +import { AqiModule } from './aqi/aqi.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -81,6 +82,7 @@ import { WeatherModule } from './weather/weather.module'; HealthModule, OccupancyModule, WeatherModule, + AqiModule, ], providers: [ { diff --git a/src/aqi/aqi.module.ts b/src/aqi/aqi.module.ts new file mode 100644 index 0000000..fdf3a5f --- /dev/null +++ b/src/aqi/aqi.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { AqiService } from './services'; +import { AqiController } from './controllers'; +@Module({ + imports: [ConfigModule], + controllers: [AqiController], + providers: [AqiService, SqlLoaderService], +}) +export class AqiModule {} diff --git a/src/aqi/controllers/aqi.controller.ts b/src/aqi/controllers/aqi.controller.ts new file mode 100644 index 0000000..32d1d6c --- /dev/null +++ b/src/aqi/controllers/aqi.controller.ts @@ -0,0 +1,64 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiParam, +} from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; +import { AqiService } from '../services/aqi.service'; +import { + GetAqiDailyBySpaceDto, + GetAqiPollutantBySpaceDto, +} from '../dto/get-aqi.dto'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SpaceParamsDto } from '../dto/aqi-params.dto'; + +@ApiTags('AQI Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.AQI.ROUTE, +}) +export class AqiController { + constructor(private readonly aqiService: AqiService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('range/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_SUMMARY, + description: ControllerRoute.AQI.ACTIONS.GET_AQI_RANGE_DATA_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getAQIRangeDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetAqiDailyBySpaceDto, + ): Promise { + return await this.aqiService.getAQIRangeDataBySpace(params, query); + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('distribution/space/:spaceUuid') + @ApiOperation({ + summary: ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_SUMMARY, + description: + ControllerRoute.AQI.ACTIONS.GET_AQI_DISTRIBUTION_DATA_DESCRIPTION, + }) + @ApiParam({ + name: 'spaceUuid', + description: 'UUID of the Space', + required: true, + }) + async getAQIDistributionDataBySpace( + @Param() params: SpaceParamsDto, + @Query() query: GetAqiPollutantBySpaceDto, + ): Promise { + return await this.aqiService.getAQIDistributionDataBySpace(params, query); + } +} diff --git a/src/aqi/controllers/index.ts b/src/aqi/controllers/index.ts new file mode 100644 index 0000000..ffd182e --- /dev/null +++ b/src/aqi/controllers/index.ts @@ -0,0 +1 @@ +export * from './aqi.controller'; diff --git a/src/aqi/dto/aqi-params.dto.ts b/src/aqi/dto/aqi-params.dto.ts new file mode 100644 index 0000000..6e26d4f --- /dev/null +++ b/src/aqi/dto/aqi-params.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class SpaceParamsDto { + @IsUUID('4', { message: 'Invalid UUID format' }) + @IsNotEmpty() + spaceUuid: string; +} diff --git a/src/aqi/dto/get-aqi.dto.ts b/src/aqi/dto/get-aqi.dto.ts new file mode 100644 index 0000000..53d49ec --- /dev/null +++ b/src/aqi/dto/get-aqi.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Matches, IsNotEmpty, IsString } from 'class-validator'; + +export class GetAqiDailyBySpaceDto { + @ApiProperty({ + description: 'Month and year in format YYYY-MM', + example: '2025-03', + required: true, + }) + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { + message: 'monthDate must be in YYYY-MM format', + }) + @IsNotEmpty() + monthDate: string; +} +export class GetAqiPollutantBySpaceDto { + @ApiProperty({ + description: 'Pollutant Type', + enum: ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o'], + example: 'aqi', + required: true, + }) + @IsString() + @IsNotEmpty() + public pollutantType: string; + @ApiProperty({ + description: 'Month and year in format YYYY-MM', + example: '2025-03', + required: true, + }) + @Matches(/^\d{4}-(0[1-9]|1[0-2])$/, { + message: 'monthDate must be in YYYY-MM format', + }) + @IsNotEmpty() + monthDate: string; +} diff --git a/src/aqi/services/aqi.service.ts b/src/aqi/services/aqi.service.ts new file mode 100644 index 0000000..a8435ed --- /dev/null +++ b/src/aqi/services/aqi.service.ts @@ -0,0 +1,146 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + GetAqiDailyBySpaceDto, + GetAqiPollutantBySpaceDto, +} from '../dto/get-aqi.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { SpaceParamsDto } from '../dto/aqi-params.dto'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { DataSource } from 'typeorm'; +import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; + +@Injectable() +export class AqiService { + constructor( + private readonly sqlLoader: SqlLoaderService, + private readonly dataSource: DataSource, + ) {} + async getAQIDistributionDataBySpace( + params: SpaceParamsDto, + query: GetAqiPollutantBySpaceDto, + ): Promise { + const { monthDate, pollutantType } = query; + const { spaceUuid } = params; + + // Validate pollutantType against the allowed values + const allowedPollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + if (!allowedPollutants.includes(pollutantType.toLowerCase())) { + throw new HttpException( + `Invalid pollutant type. Allowed values: ${allowedPollutants.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + try { + const data = await this.executeProcedure( + 'fact_daily_space_aqi', + 'proceduce_select_daily_space_aqi', + [spaceUuid, monthDate], + ); + + const categories = [ + 'good', + 'moderate', + 'unhealthy_sensitive', + 'unhealthy', + 'very_unhealthy', + 'hazardous', + ]; + + const transformedData = data.map((item) => { + const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD + + const categoryData = categories.map((category) => { + const key = `${category}_${pollutantType.toLowerCase()}_percentage`; + return { + type: category, + percentage: item[key] ?? 0, + }; + }); + + return { date, data: categoryData }; + }); + + const response = this.buildResponse( + `AQI distribution data fetched successfully for ${spaceUuid} space and pollutant ${pollutantType}`, + transformedData, + ); + return response; + } catch (error) { + console.error('Failed to fetch AQI distribution data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch AQI distribution data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getAQIRangeDataBySpace( + params: SpaceParamsDto, + query: GetAqiDailyBySpaceDto, + ): Promise { + const { monthDate } = query; + const { spaceUuid } = params; + + try { + const data = await this.executeProcedure( + 'fact_daily_space_aqi', + 'proceduce_select_daily_space_aqi', + [spaceUuid, monthDate], + ); + + // Define pollutants dynamically + const pollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + + const transformedData = data.map((item) => { + const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD + const dailyData = pollutants.map((type) => ({ + type, + min: item[`daily_min_${type}`], + max: item[`daily_max_${type}`], + average: item[`daily_avg_${type}`], + })); + return { date, data: dailyData }; + }); + + const response = this.buildResponse( + `AQI data fetched successfully for ${spaceUuid} space`, + transformedData, + ); + return convertKeysToCamelCase(response); + } catch (error) { + console.error('Failed to fetch AQI data', { + error, + spaceUuid, + }); + throw new HttpException( + error.response?.message || 'Failed to fetch AQI data', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private buildResponse(message: string, data: any[]) { + return new SuccessResponseDto({ + message, + data, + statusCode: HttpStatus.OK, + }); + } + private async executeProcedure( + procedureFolderName: string, + procedureFileName: string, + params: (string | number | null)[], + ): Promise { + const query = this.loadQuery(procedureFolderName, procedureFileName); + return await this.dataSource.query(query, params); + } + private loadQuery(folderName: string, fileName: string): string { + return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH); + } +} diff --git a/src/aqi/services/index.ts b/src/aqi/services/index.ts new file mode 100644 index 0000000..f0ba82c --- /dev/null +++ b/src/aqi/services/index.ts @@ -0,0 +1 @@ +export * from './aqi.service'; diff --git a/src/commission-device/commission-device.module.ts b/src/commission-device/commission-device.module.ts index 8821410..3306705 100644 --- a/src/commission-device/commission-device.module.ts +++ b/src/commission-device/commission-device.module.ts @@ -29,6 +29,7 @@ import { import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule], @@ -57,6 +58,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [], }) diff --git a/src/community/community.module.ts b/src/community/community.module.ts index a6f64df..edaddbb 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -64,6 +64,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, SpaceRepositoryModule, UserRepositoryModule], @@ -116,6 +117,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [CommunityService, SpacePermissionService], }) diff --git a/src/door-lock/door.lock.module.ts b/src/door-lock/door.lock.module.ts index 3f2cee7..c2eaad1 100644 --- a/src/door-lock/door.lock.module.ts +++ b/src/door-lock/door.lock.module.ts @@ -29,6 +29,7 @@ import { import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [DoorLockController], @@ -56,6 +57,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [DoorLockService], }) diff --git a/src/group/group.module.ts b/src/group/group.module.ts index b50bac9..443ac31 100644 --- a/src/group/group.module.ts +++ b/src/group/group.module.ts @@ -27,6 +27,7 @@ import { import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule], controllers: [GroupController], @@ -53,6 +54,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [GroupService], }) diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index fb1ba8d..a42922c 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -84,6 +84,7 @@ import { import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], @@ -154,6 +155,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [InviteUserService], }) diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index e85ea1b..9910dd9 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -61,6 +61,7 @@ import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repos 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 { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule], controllers: [PowerClampController], @@ -111,6 +112,7 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service' SpaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory, OccupancyService, + AqiDataService, ], exports: [PowerClamp], }) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index fd9acb3..306a6c2 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -68,6 +68,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -126,6 +127,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [ProjectService, CqrsModule], }) diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index be57bb3..cdb713c 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -66,6 +66,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; const CommandHandlers = [ PropogateUpdateSpaceModelHandler, @@ -126,6 +127,7 @@ const CommandHandlers = [ PowerClampMonthlyRepository, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [CqrsModule, SpaceModelService], }) diff --git a/src/space/space.module.ts b/src/space/space.module.ts index f74d993..0b33696 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -91,6 +91,7 @@ import { } from '@app/common/modules/power-clamp/repositories'; import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; export const CommandHandlers = [DisableSpaceHandler]; @@ -168,6 +169,7 @@ export const CommandHandlers = [DisableSpaceHandler]; PowerClampService, SqlLoaderService, OccupancyService, + AqiDataService, ], exports: [SpaceService], }) diff --git a/src/vistor-password/visitor-password.module.ts b/src/vistor-password/visitor-password.module.ts index 6924713..c66ba39 100644 --- a/src/vistor-password/visitor-password.module.ts +++ b/src/vistor-password/visitor-password.module.ts @@ -31,6 +31,7 @@ import { import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; import { OccupancyService } from '@app/common/helper/services/occupancy.service'; import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], controllers: [VisitorPasswordController], @@ -59,6 +60,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' SqlLoaderService, OccupancyService, CommunityRepository, + AqiDataService, ], exports: [VisitorPasswordService], }) From 2cb77504ca27572f8a5321ee184d884ed4b9e2c1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:28:00 -0600 Subject: [PATCH 52/56] Add PollutantType enum and update AQI-related entities and services to use it --- libs/common/src/constants/pollutants.enum.ts | 8 ++ .../src/modules/aqi/entities/aqi.entity.ts | 110 +++++++++--------- .../modules/space/entities/space.entity.ts | 4 + src/aqi/dto/get-aqi.dto.ts | 5 +- src/aqi/services/aqi.service.ts | 12 +- 5 files changed, 72 insertions(+), 67 deletions(-) create mode 100644 libs/common/src/constants/pollutants.enum.ts diff --git a/libs/common/src/constants/pollutants.enum.ts b/libs/common/src/constants/pollutants.enum.ts new file mode 100644 index 0000000..a6c88ea --- /dev/null +++ b/libs/common/src/constants/pollutants.enum.ts @@ -0,0 +1,8 @@ +export enum PollutantType { + AQI = 'aqi', + PM25 = 'pm25', + PM10 = 'pm10', + VOC = 'voc', + CO2 = 'co2', + CH2O = 'ch2o', +} diff --git a/libs/common/src/modules/aqi/entities/aqi.entity.ts b/libs/common/src/modules/aqi/entities/aqi.entity.ts index fed3881..b4fb2bc 100644 --- a/libs/common/src/modules/aqi/entities/aqi.entity.ts +++ b/libs/common/src/modules/aqi/entities/aqi.entity.ts @@ -9,173 +9,173 @@ export class AqiSpaceDailyPollutantStatsEntity extends AbstractEntity SpaceEntity, (space) => space.presenceSensorDaily) + @ManyToOne(() => SpaceEntity, (space) => space.aqiSensorDaily) space: SpaceEntity; @Column({ type: 'date', nullable: false }) public eventDate: Date; @Column('float', { nullable: true }) - public goodAqiPercentage: number; + public goodAqiPercentage?: number; @Column('float', { nullable: true }) - public moderateAqiPercentage: number; + public moderateAqiPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveAqiPercentage: number; + public unhealthySensitiveAqiPercentage?: number; @Column('float', { nullable: true }) - public unhealthyAqiPercentage: number; + public unhealthyAqiPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyAqiPercentage: number; + public veryUnhealthyAqiPercentage?: number; @Column('float', { nullable: true }) - public hazardousAqiPercentage: number; + public hazardousAqiPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgAqi: number; + public dailyAvgAqi?: number; @Column('float', { nullable: true }) - public dailyMaxAqi: number; + public dailyMaxAqi?: number; @Column('float', { nullable: true }) - public dailyMinAqi: number; + public dailyMinAqi?: number; @Column('float', { nullable: true }) - public goodPm25Percentage: number; + public goodPm25Percentage?: number; @Column('float', { nullable: true }) - public moderatePm25Percentage: number; + public moderatePm25Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitivePm25Percentage: number; + public unhealthySensitivePm25Percentage?: number; @Column('float', { nullable: true }) - public unhealthyPm25Percentage: number; + public unhealthyPm25Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyPm25Percentage: number; + public veryUnhealthyPm25Percentage?: number; @Column('float', { nullable: true }) - public hazardousPm25Percentage: number; + public hazardousPm25Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgPm25: number; + public dailyAvgPm25?: number; @Column('float', { nullable: true }) - public dailyMaxPm25: number; + public dailyMaxPm25?: number; @Column('float', { nullable: true }) - public dailyMinPm25: number; + public dailyMinPm25?: number; @Column('float', { nullable: true }) - public goodPm10Percentage: number; + public goodPm10Percentage?: number; @Column('float', { nullable: true }) - public moderatePm10Percentage: number; + public moderatePm10Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitivePm10Percentage: number; + public unhealthySensitivePm10Percentage?: number; @Column('float', { nullable: true }) - public unhealthyPm10Percentage: number; + public unhealthyPm10Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyPm10Percentage: number; + public veryUnhealthyPm10Percentage?: number; @Column('float', { nullable: true }) - public hazardousPm10Percentage: number; + public hazardousPm10Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgPm10: number; + public dailyAvgPm10?: number; @Column('float', { nullable: true }) - public dailyMaxPm10: number; + public dailyMaxPm10?: number; @Column('float', { nullable: true }) - public dailyMinPm10: number; + public dailyMinPm10?: number; @Column('float', { nullable: true }) - public goodVocPercentage: number; + public goodVocPercentage?: number; @Column('float', { nullable: true }) - public moderateVocPercentage: number; + public moderateVocPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveVocPercentage: number; + public unhealthySensitiveVocPercentage?: number; @Column('float', { nullable: true }) - public unhealthyVocPercentage: number; + public unhealthyVocPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyVocPercentage: number; + public veryUnhealthyVocPercentage?: number; @Column('float', { nullable: true }) - public hazardousVocPercentage: number; + public hazardousVocPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgVoc: number; + public dailyAvgVoc?: number; @Column('float', { nullable: true }) - public dailyMaxVoc: number; + public dailyMaxVoc?: number; @Column('float', { nullable: true }) - public dailyMinVoc: number; + public dailyMinVoc?: number; @Column('float', { nullable: true }) - public goodCo2Percentage: number; + public goodCo2Percentage?: number; @Column('float', { nullable: true }) - public moderateCo2Percentage: number; + public moderateCo2Percentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveCo2Percentage: number; + public unhealthySensitiveCo2Percentage?: number; @Column('float', { nullable: true }) - public unhealthyCo2Percentage: number; + public unhealthyCo2Percentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyCo2Percentage: number; + public veryUnhealthyCo2Percentage?: number; @Column('float', { nullable: true }) - public hazardousCo2Percentage: number; + public hazardousCo2Percentage?: number; @Column('float', { nullable: true }) - public dailyAvgCo2: number; + public dailyAvgCo2?: number; @Column('float', { nullable: true }) - public dailyMaxCo2: number; + public dailyMaxCo2?: number; @Column('float', { nullable: true }) - public dailyMinCo2: number; + public dailyMinCo2?: number; @Column('float', { nullable: true }) - public goodCh2oPercentage: number; + public goodCh2oPercentage?: number; @Column('float', { nullable: true }) - public moderateCh2oPercentage: number; + public moderateCh2oPercentage?: number; @Column('float', { nullable: true }) - public unhealthySensitiveCh2oPercentage: number; + public unhealthySensitiveCh2oPercentage?: number; @Column('float', { nullable: true }) - public unhealthyCh2oPercentage: number; + public unhealthyCh2oPercentage?: number; @Column('float', { nullable: true }) - public veryUnhealthyCh2oPercentage: number; + public veryUnhealthyCh2oPercentage?: number; @Column('float', { nullable: true }) - public hazardousCh2oPercentage: number; + public hazardousCh2oPercentage?: number; @Column('float', { nullable: true }) - public dailyAvgCh2o: number; + public dailyAvgCh2o?: number; @Column('float', { nullable: true }) - public dailyMaxCh2o: number; + public dailyMaxCh2o?: number; @Column('float', { nullable: true }) - public dailyMinCh2o: number; + public dailyMinCh2o?: number; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 5a7b4d2..e5e3b12 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -11,6 +11,7 @@ import { InviteUserSpaceEntity } from '../../Invite-user/entities'; import { SpaceProductAllocationEntity } from './space-product-allocation.entity'; import { SubspaceEntity } from './subspace/subspace.entity'; import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; +import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -115,6 +116,9 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => PresenceSensorDailySpaceEntity, (sensor) => sensor.space) presenceSensorDaily: PresenceSensorDailySpaceEntity[]; + @OneToMany(() => AqiSpaceDailyPollutantStatsEntity, (aqi) => aqi.space) + aqiSensorDaily: AqiSpaceDailyPollutantStatsEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/aqi/dto/get-aqi.dto.ts b/src/aqi/dto/get-aqi.dto.ts index 53d49ec..c25cb3a 100644 --- a/src/aqi/dto/get-aqi.dto.ts +++ b/src/aqi/dto/get-aqi.dto.ts @@ -1,3 +1,4 @@ +import { PollutantType } from '@app/common/constants/pollutants.enum'; import { ApiProperty } from '@nestjs/swagger'; import { Matches, IsNotEmpty, IsString } from 'class-validator'; @@ -16,8 +17,8 @@ export class GetAqiDailyBySpaceDto { export class GetAqiPollutantBySpaceDto { @ApiProperty({ description: 'Pollutant Type', - enum: ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o'], - example: 'aqi', + enum: PollutantType, + example: PollutantType.AQI, required: true, }) @IsString() diff --git a/src/aqi/services/aqi.service.ts b/src/aqi/services/aqi.service.ts index a8435ed..b583faf 100644 --- a/src/aqi/services/aqi.service.ts +++ b/src/aqi/services/aqi.service.ts @@ -10,6 +10,7 @@ import { DataSource } from 'typeorm'; import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { PollutantType } from '@app/common/constants/pollutants.enum'; @Injectable() export class AqiService { @@ -24,15 +25,6 @@ export class AqiService { const { monthDate, pollutantType } = query; const { spaceUuid } = params; - // Validate pollutantType against the allowed values - const allowedPollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; - if (!allowedPollutants.includes(pollutantType.toLowerCase())) { - throw new HttpException( - `Invalid pollutant type. Allowed values: ${allowedPollutants.join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - try { const data = await this.executeProcedure( 'fact_daily_space_aqi', @@ -95,7 +87,7 @@ export class AqiService { ); // Define pollutants dynamically - const pollutants = ['aqi', 'pm25', 'pm10', 'voc', 'co2', 'ch2o']; + const pollutants = Object.values(PollutantType); const transformedData = data.map((item) => { const date = new Date(item.event_date).toLocaleDateString('en-CA'); // YYYY-MM-DD From 8503ee728d1a05088a30ddf4f8bab0417df2f490 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Wed, 11 Jun 2025 13:15:21 +0300 Subject: [PATCH 53/56] Refactor/space management (#404) * refactor: reducing used queries on get communities (#385) * refactor: fix create space logic (#394) * Remove unique constraint on subspace and product in SubspaceProductAllocationEntity; update product relation to nullable in NewTagEntity * refactor: fix create space logic * device model updated to include the fixes and final columns * updated space models to include suggested fixes, update final logic and column names * task: removing old references of the old tag-product relation * task: remove old use of tags * task: remove old tag & tag model usage * refactor: delete space * task: remove unused functions * fix lint rule --- libs/common/src/database/database.module.ts | 76 +- .../modules/device/entities/device.entity.ts | 4 +- .../product/entities/product.entity.ts | 14 +- .../modules/space-model/dtos/tag-model.dto.ts | 21 - .../src/modules/space-model/entities/index.ts | 3 +- .../space-model-product-allocation.entity.ts | 21 +- .../entities/space-model.entity.ts | 12 +- ...ubspace-model-product-allocation.entity.ts | 15 +- .../subspace-model/subspace-model.entity.ts | 6 +- .../space-model/entities/tag-model.entity.ts | 38 - .../repositories/space-model.repository.ts | 10 +- .../space-model.repository.module.ts | 8 +- .../space-product-allocation.entity.ts | 16 +- .../subspace-product-allocation.entity.ts | 20 +- .../entities/subspace/subspace.entity.ts | 4 - .../src/modules/space/entities/tag.entity.ts | 41 - .../space/repositories/space.repository.ts | 12 +- .../modules/space/space.repository.module.ts | 2 - .../src/modules/tag/entities/tag.entity.ts | 25 +- package.json | 1 + src/app.module.ts | 61 +- .../services/commission-device.service.ts | 48 +- src/community/community.module.ts | 3 +- src/community/services/community.service.ts | 96 +-- src/device/services/device.service.ts | 250 ++---- src/invite-user/invite-user.module.ts | 134 ++- src/power-clamp/power-clamp.module.ts | 96 ++- src/project/project.module.ts | 102 ++- src/project/services/project.service.ts | 86 +- src/space-model/dtos/index.ts | 5 +- .../modify-subspace-model.dto.ts | 16 +- .../tag-model-dtos/create-tag-model.dto.ts | 28 - src/space-model/dtos/tag-model-dtos/index.ts | 3 - .../tag-model-dtos/modify-tag-model.dto.ts | 46 - .../tag-model-dtos/update-tag-model.dto.ts | 21 - .../dtos/update-space-model.dto.ts | 47 +- ...-space-model-product-allocation.handler.ts | 154 ++-- .../handlers/propogate-subspace-handler.ts | 205 +++-- src/space-model/interfaces/index.ts | 3 +- .../interfaces/modify-subspace.interface.ts | 24 - .../interfaces/single-subspace.interface.ts | 6 +- .../space-model-product-allocation.service.ts | 484 ++++++----- .../services/space-model.service.ts | 193 ++--- ...bspace-model-product-allocation.service.ts | 788 +++++++++--------- .../subspace/subspace-model.service.ts | 191 ++--- src/space-model/space-model.module.ts | 92 +- src/space/dtos/add.space.dto.ts | 24 +- src/space/dtos/create-allocations.dto.ts | 29 + .../dtos/subspace/modify.subspace.dto.ts | 47 +- src/space/dtos/tag/modify-tag.dto.ts | 28 +- src/space/dtos/update.space.dto.ts | 12 +- src/space/handlers/disable-space.handler.ts | 15 +- .../interfaces/single-subspace.interface.ts | 9 - .../update-subspace-allocation.dto.ts | 6 + .../product-allocation.service.ts | 62 ++ .../space-product-allocation.service.ts | 560 ++----------- .../services/space-validation.service.ts | 121 +-- src/space/services/space.service.ts | 259 +++--- .../subspace/subspace-device.service.ts | 25 +- .../subspace-product-allocation.service.ts | 645 +++++--------- .../services/subspace/subspace.service.ts | 318 +++---- src/space/services/tag/tag.service.ts | 597 +------------ src/space/space.module.ts | 147 ++-- src/tags/services/tags.service.ts | 251 ++---- 64 files changed, 2314 insertions(+), 4372 deletions(-) delete mode 100644 libs/common/src/modules/space-model/dtos/tag-model.dto.ts delete mode 100644 libs/common/src/modules/space-model/entities/tag-model.entity.ts delete mode 100644 libs/common/src/modules/space/entities/tag.entity.ts delete mode 100644 src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts delete mode 100644 src/space-model/dtos/tag-model-dtos/index.ts delete mode 100644 src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts delete mode 100644 src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts delete mode 100644 src/space-model/interfaces/modify-subspace.interface.ts create mode 100644 src/space/dtos/create-allocations.dto.ts delete mode 100644 src/space/interfaces/single-subspace.interface.ts create mode 100644 src/space/interfaces/update-subspace-allocation.dto.ts create mode 100644 src/space/services/product-allocation/product-allocation.service.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index d25dbd8..2187c72 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -1,51 +1,30 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SnakeNamingStrategy } from './strategies'; -import { UserEntity } from '../modules/user/entities/user.entity'; -import { UserSessionEntity } from '../modules/session/entities/session.entity'; -import { UserOtpEntity } from '../modules/user/entities'; -import { ProductEntity } from '../modules/product/entities'; import { DeviceEntity } from '../modules/device/entities'; import { PermissionTypeEntity } from '../modules/permission/entities'; +import { ProductEntity } from '../modules/product/entities'; +import { UserSessionEntity } from '../modules/session/entities/session.entity'; +import { UserOtpEntity } from '../modules/user/entities'; +import { UserEntity } from '../modules/user/entities/user.entity'; +import { SnakeNamingStrategy } from './strategies'; -import { UserSpaceEntity } from '../modules/user/entities'; -import { DeviceUserPermissionEntity } from '../modules/device/entities'; -import { RoleTypeEntity } from '../modules/role-type/entities'; -import { UserNotificationEntity } from '../modules/user/entities'; -import { DeviceNotificationEntity } from '../modules/device/entities'; -import { RegionEntity } from '../modules/region/entities'; -import { TimeZoneEntity } from '../modules/timezone/entities'; -import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; +import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger'; +import { createLogger } from 'winston'; +import { winstonLoggerOptions } from '../logger/services/winston.logger'; +import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; +import { AutomationEntity } from '../modules/automation/entities'; +import { ClientEntity } from '../modules/client/entities'; import { CommunityEntity } from '../modules/community/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; -import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; -import { SceneDeviceEntity } from '../modules/scene-device/entities'; -import { ProjectEntity } from '../modules/project/entities'; import { - SpaceModelEntity, - SubspaceModelEntity, - TagModel, - SpaceModelProductAllocationEntity, - SubspaceModelProductAllocationEntity, -} from '../modules/space-model/entities'; + DeviceNotificationEntity, + DeviceUserPermissionEntity, +} from '../modules/device/entities'; import { InviteUserEntity, InviteUserSpaceEntity, } from '../modules/Invite-user/entities'; -import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity'; -import { AutomationEntity } from '../modules/automation/entities'; -import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity'; -import { NewTagEntity } from '../modules/tag/entities/tag.entity'; -import { SpaceEntity } from '../modules/space/entities/space.entity'; -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, @@ -55,7 +34,30 @@ import { PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, } from '../modules/presence-sensor/entities'; -import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; +import { ProjectEntity } from '../modules/project/entities'; +import { RegionEntity } from '../modules/region/entities'; +import { RoleTypeEntity } from '../modules/role-type/entities'; +import { SceneDeviceEntity } from '../modules/scene-device/entities'; +import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; +import { + SpaceModelEntity, + SpaceModelProductAllocationEntity, + SubspaceModelEntity, + SubspaceModelProductAllocationEntity, +} from '../modules/space-model/entities'; +import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity'; +import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity'; +import { SpaceProductAllocationEntity } from '../modules/space/entities/space-product-allocation.entity'; +import { SpaceEntity } from '../modules/space/entities/space.entity'; +import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity'; +import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity'; +import { NewTagEntity } from '../modules/tag/entities/tag.entity'; +import { TimeZoneEntity } from '../modules/timezone/entities'; +import { + UserNotificationEntity, + UserSpaceEntity, +} from '../modules/user/entities'; +import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -86,7 +88,6 @@ import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; SpaceEntity, SpaceLinkEntity, SubspaceEntity, - TagEntity, UserSpaceEntity, DeviceUserPermissionEntity, RoleTypeEntity, @@ -101,7 +102,6 @@ import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities'; SceneDeviceEntity, SpaceModelEntity, SubspaceModelEntity, - TagModel, InviteUserEntity, InviteUserSpaceEntity, InviteSpaceEntity, diff --git a/libs/common/src/modules/device/entities/device.entity.ts b/libs/common/src/modules/device/entities/device.entity.ts index a172862..431222d 100644 --- a/libs/common/src/modules/device/entities/device.entity.ts +++ b/libs/common/src/modules/device/entities/device.entity.ts @@ -78,8 +78,8 @@ export class DeviceEntity extends AbstractEntity { @OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {}) sceneDevices: SceneDeviceEntity[]; - @OneToMany(() => NewTagEntity, (tag) => tag.devices) - // @JoinTable({ name: 'device_tags' }) + @ManyToOne(() => NewTagEntity, (tag) => tag.devices) + @JoinColumn({ name: 'tag_uuid' }) public tag: NewTagEntity; @OneToMany(() => PowerClampHourlyEntity, (powerClamp) => powerClamp.device) powerClampHourly: PowerClampHourlyEntity[]; diff --git a/libs/common/src/modules/product/entities/product.entity.ts b/libs/common/src/modules/product/entities/product.entity.ts index 209b7b1..5f4909d 100644 --- a/libs/common/src/modules/product/entities/product.entity.ts +++ b/libs/common/src/modules/product/entities/product.entity.ts @@ -1,10 +1,7 @@ import { Column, Entity, OneToMany } from 'typeorm'; -import { ProductDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { DeviceEntity } from '../../device/entities'; -import { TagModel } from '../../space-model'; -import { TagEntity } from '../../space/entities/tag.entity'; -import { NewTagEntity } from '../../tag/entities'; +import { ProductDto } from '../dtos'; @Entity({ name: 'product' }) export class ProductEntity extends AbstractEntity { @Column({ @@ -28,15 +25,6 @@ export class ProductEntity extends AbstractEntity { }) public prodType: string; - @OneToMany(() => NewTagEntity, (tag) => tag.product, { cascade: true }) - public newTags: NewTagEntity[]; - - @OneToMany(() => TagModel, (tag) => tag.product) - tagModels: TagModel[]; - - @OneToMany(() => TagEntity, (tag) => tag.product) - tags: TagEntity[]; - @OneToMany( () => DeviceEntity, (devicesProductEntity) => devicesProductEntity.productDevice, diff --git a/libs/common/src/modules/space-model/dtos/tag-model.dto.ts b/libs/common/src/modules/space-model/dtos/tag-model.dto.ts deleted file mode 100644 index f98f160..0000000 --- a/libs/common/src/modules/space-model/dtos/tag-model.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class TagModelDto { - @IsString() - @IsNotEmpty() - public uuid: string; - - @IsString() - @IsNotEmpty() - public name: string; - - @IsString() - @IsNotEmpty() - public productUuid: string; - - @IsString() - spaceModelUuid: string; - - @IsString() - subspaceModelUuid: string; -} diff --git a/libs/common/src/modules/space-model/entities/index.ts b/libs/common/src/modules/space-model/entities/index.ts index 56de1b5..a820c01 100644 --- a/libs/common/src/modules/space-model/entities/index.ts +++ b/libs/common/src/modules/space-model/entities/index.ts @@ -1,4 +1,3 @@ +export * from './space-model-product-allocation.entity'; export * from './space-model.entity'; export * from './subspace-model'; -export * from './tag-model.entity'; -export * from './space-model-product-allocation.entity'; diff --git a/libs/common/src/modules/space-model/entities/space-model-product-allocation.entity.ts b/libs/common/src/modules/space-model/entities/space-model-product-allocation.entity.ts index c487bde..2337625 100644 --- a/libs/common/src/modules/space-model/entities/space-model-product-allocation.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-model-product-allocation.entity.ts @@ -1,18 +1,12 @@ -import { - Entity, - Column, - ManyToOne, - ManyToMany, - JoinTable, - OneToMany, -} from 'typeorm'; -import { SpaceModelEntity } from './space-model.entity'; -import { NewTagEntity } from '../../tag/entities/tag.entity'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ProductEntity } from '../../product/entities/product.entity'; import { SpaceProductAllocationEntity } from '../../space/entities/space-product-allocation.entity'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { NewTagEntity } from '../../tag/entities/tag.entity'; +import { SpaceModelEntity } from './space-model.entity'; @Entity({ name: 'space_model_product_allocation' }) +@Unique(['spaceModel', 'product', 'tag']) export class SpaceModelProductAllocationEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -31,9 +25,8 @@ export class SpaceModelProductAllocationEntity extends AbstractEntity ProductEntity, { nullable: false, onDelete: 'CASCADE' }) public product: ProductEntity; - @ManyToMany(() => NewTagEntity, { cascade: true, onDelete: 'CASCADE' }) - @JoinTable({ name: 'space_model_product_tags' }) - public tags: NewTagEntity[]; + @ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) + public tag: NewTagEntity; @OneToMany( () => SpaceProductAllocationEntity, diff --git a/libs/common/src/modules/space-model/entities/space-model.entity.ts b/libs/common/src/modules/space-model/entities/space-model.entity.ts index 94b6130..222906e 100644 --- a/libs/common/src/modules/space-model/entities/space-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/space-model.entity.ts @@ -1,11 +1,10 @@ -import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { SpaceModelDto } from '../dtos'; -import { SubspaceModelEntity } from './subspace-model'; import { ProjectEntity } from '../../project/entities'; -import { TagModel } from './tag-model.entity'; -import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity'; import { SpaceEntity } from '../../space/entities/space.entity'; +import { SpaceModelDto } from '../dtos'; +import { SpaceModelProductAllocationEntity } from './space-model-product-allocation.entity'; +import { SubspaceModelEntity } from './subspace-model'; @Entity({ name: 'space-model' }) export class SpaceModelEntity extends AbstractEntity { @@ -49,9 +48,6 @@ export class SpaceModelEntity extends AbstractEntity { }) public spaces: SpaceEntity[]; - @OneToMany(() => TagModel, (tag) => tag.spaceModel) - tags: TagModel[]; - @OneToMany( () => SpaceModelProductAllocationEntity, (allocation) => allocation.spaceModel, diff --git a/libs/common/src/modules/space-model/entities/subspace-model/subspace-model-product-allocation.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model-product-allocation.entity.ts index f955723..7069330 100644 --- a/libs/common/src/modules/space-model/entities/subspace-model/subspace-model-product-allocation.entity.ts +++ b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model-product-allocation.entity.ts @@ -1,11 +1,12 @@ -import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm'; -import { SubspaceModelEntity } from './subspace-model.entity'; +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; import { ProductEntity } from '@app/common/modules/product/entities/product.entity'; import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { SubspaceModelProductAllocationDto } from '../../dtos/subspace-model/subspace-model-product-allocation.dto'; -import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { SubspaceModelEntity } from './subspace-model.entity'; @Entity({ name: 'subspace_model_product_allocation' }) +@Unique(['subspaceModel', 'product', 'tag']) export class SubspaceModelProductAllocationEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -27,12 +28,8 @@ export class SubspaceModelProductAllocationEntity extends AbstractEntity ProductEntity, { nullable: false, onDelete: 'CASCADE' }) public product: ProductEntity; - @ManyToMany(() => NewTagEntity, (tag) => tag.subspaceModelAllocations, { - cascade: true, - onDelete: 'CASCADE', - }) - @JoinTable({ name: 'subspace_model_product_tags' }) - public tags: NewTagEntity[]; + @ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) + public tag: NewTagEntity; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts index 932c855..c0045b3 100644 --- a/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts +++ b/libs/common/src/modules/space-model/entities/subspace-model/subspace-model.entity.ts @@ -1,10 +1,9 @@ import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; import { SubSpaceModelDto } from '../../dtos'; import { SpaceModelEntity } from '../space-model.entity'; -import { TagModel } from '../tag-model.entity'; import { SubspaceModelProductAllocationEntity } from './subspace-model-product-allocation.entity'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; @Entity({ name: 'subspace-model' }) export class SubspaceModelEntity extends AbstractEntity { @@ -41,9 +40,6 @@ export class SubspaceModelEntity extends AbstractEntity { }) public disabled: boolean; - @OneToMany(() => TagModel, (tag) => tag.subspaceModel) - tags: TagModel[]; - @OneToMany( () => SubspaceModelProductAllocationEntity, (allocation) => allocation.subspaceModel, diff --git a/libs/common/src/modules/space-model/entities/tag-model.entity.ts b/libs/common/src/modules/space-model/entities/tag-model.entity.ts deleted file mode 100644 index e2a70ee..0000000 --- a/libs/common/src/modules/space-model/entities/tag-model.entity.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; -import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { TagModelDto } from '../dtos/tag-model.dto'; -import { SpaceModelEntity } from './space-model.entity'; -import { SubspaceModelEntity } from './subspace-model'; -import { ProductEntity } from '../../product/entities'; -import { TagEntity } from '../../space/entities/tag.entity'; - -@Entity({ name: 'tag_model' }) -export class TagModel extends AbstractEntity { - @Column({ type: 'varchar', length: 255 }) - tag: string; - - @ManyToOne(() => ProductEntity, (product) => product.tagModels, { - nullable: false, - }) - @JoinColumn({ name: 'product_id' }) - product: ProductEntity; - - @ManyToOne(() => SpaceModelEntity, (space) => space.tags, { nullable: true }) - @JoinColumn({ name: 'space_model_id' }) - spaceModel: SpaceModelEntity; - - @ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.tags, { - nullable: true, - }) - @JoinColumn({ name: 'subspace_model_id' }) - subspaceModel: SubspaceModelEntity; - - @Column({ - nullable: false, - default: false, - }) - public disabled: boolean; - - @OneToMany(() => TagEntity, (tag) => tag.model) - tags: TagEntity[]; -} diff --git a/libs/common/src/modules/space-model/repositories/space-model.repository.ts b/libs/common/src/modules/space-model/repositories/space-model.repository.ts index 5523e7c..b77e351 100644 --- a/libs/common/src/modules/space-model/repositories/space-model.repository.ts +++ b/libs/common/src/modules/space-model/repositories/space-model.repository.ts @@ -1,11 +1,10 @@ -import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; import { SpaceModelEntity, SpaceModelProductAllocationEntity, SubspaceModelEntity, SubspaceModelProductAllocationEntity, - TagModel, } from '../entities'; @Injectable() @@ -21,13 +20,6 @@ export class SubspaceModelRepository extends Repository { } } -@Injectable() -export class TagModelRepository extends Repository { - constructor(private dataSource: DataSource) { - super(TagModel, dataSource.createEntityManager()); - } -} - @Injectable() export class SpaceModelProductAllocationRepoitory extends Repository { constructor(private dataSource: DataSource) { diff --git a/libs/common/src/modules/space-model/space-model.repository.module.ts b/libs/common/src/modules/space-model/space-model.repository.module.ts index 9a35d88..19519dc 100644 --- a/libs/common/src/modules/space-model/space-model.repository.module.ts +++ b/libs/common/src/modules/space-model/space-model.repository.module.ts @@ -1,13 +1,11 @@ -import { TypeOrmModule } from '@nestjs/typeorm'; -import { SpaceModelEntity, SubspaceModelEntity, TagModel } from './entities'; import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SpaceModelEntity, SubspaceModelEntity } from './entities'; @Module({ providers: [], exports: [], controllers: [], - imports: [ - TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity, TagModel]), - ], + imports: [TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity])], }) export class SpaceModelRepositoryModule {} diff --git a/libs/common/src/modules/space/entities/space-product-allocation.entity.ts b/libs/common/src/modules/space/entities/space-product-allocation.entity.ts index f8cb2fd..c9cd7e2 100644 --- a/libs/common/src/modules/space/entities/space-product-allocation.entity.ts +++ b/libs/common/src/modules/space/entities/space-product-allocation.entity.ts @@ -1,12 +1,13 @@ -import { Entity, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm'; -import { SpaceEntity } from './space.entity'; -import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity'; -import { ProductEntity } from '../../product/entities/product.entity'; -import { NewTagEntity } from '../../tag/entities/tag.entity'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ProductEntity } from '../../product/entities/product.entity'; +import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity'; +import { NewTagEntity } from '../../tag/entities/tag.entity'; import { SpaceProductAllocationDto } from '../dtos/space-product-allocation.dto'; +import { SpaceEntity } from './space.entity'; @Entity({ name: 'space_product_allocation' }) +@Unique(['space', 'product', 'tag'], {}) export class SpaceProductAllocationEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -30,9 +31,8 @@ export class SpaceProductAllocationEntity extends AbstractEntity ProductEntity, { nullable: false, onDelete: 'CASCADE' }) public product: ProductEntity; - @ManyToMany(() => NewTagEntity) - @JoinTable({ name: 'space_product_tags' }) - public tags: NewTagEntity[]; + @ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) + public tag: NewTagEntity; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts index 0bc2918..23b3111 100644 --- a/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts +++ b/libs/common/src/modules/space/entities/subspace/subspace-product-allocation.entity.ts @@ -1,20 +1,13 @@ -import { - Entity, - Column, - ManyToOne, - ManyToMany, - JoinTable, - Unique, -} from 'typeorm'; -import { SubspaceEntity } from './subspace.entity'; +import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; import { ProductEntity } from '@app/common/modules/product/entities'; import { SubspaceModelProductAllocationEntity } from '@app/common/modules/space-model'; import { NewTagEntity } from '@app/common/modules/tag/entities/tag.entity'; -import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity'; +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; import { SubspaceProductAllocationDto } from '../../dtos/subspace-product-allocation.dto'; +import { SubspaceEntity } from './subspace.entity'; @Entity({ name: 'subspace_product_allocation' }) -// @Unique(['subspace', 'product']) +@Unique(['subspace', 'product', 'tag']) export class SubspaceProductAllocationEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -38,9 +31,8 @@ export class SubspaceProductAllocationEntity extends AbstractEntity ProductEntity, { nullable: false, onDelete: 'CASCADE' }) public product: ProductEntity; - @ManyToMany(() => NewTagEntity) - @JoinTable({ name: 'subspace_product_tags' }) - public tags: NewTagEntity[]; + @ManyToOne(() => NewTagEntity, { nullable: true, onDelete: 'CASCADE' }) + public tag: NewTagEntity; constructor(partial: Partial) { super(); diff --git a/libs/common/src/modules/space/entities/subspace/subspace.entity.ts b/libs/common/src/modules/space/entities/subspace/subspace.entity.ts index 4ac19c0..885292b 100644 --- a/libs/common/src/modules/space/entities/subspace/subspace.entity.ts +++ b/libs/common/src/modules/space/entities/subspace/subspace.entity.ts @@ -4,7 +4,6 @@ import { SubspaceModelEntity } from '@app/common/modules/space-model'; import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { SubspaceDto } from '../../dtos'; import { SpaceEntity } from '../space.entity'; -import { TagEntity } from '../tag.entity'; import { SubspaceProductAllocationEntity } from './subspace-product-allocation.entity'; @Entity({ name: 'subspace' }) @@ -43,9 +42,6 @@ export class SubspaceEntity extends AbstractEntity { }) subSpaceModel?: SubspaceModelEntity; - @OneToMany(() => TagEntity, (tag) => tag.subspace) - tags: TagEntity[]; - @OneToMany( () => SubspaceProductAllocationEntity, (allocation) => allocation.subspace, diff --git a/libs/common/src/modules/space/entities/tag.entity.ts b/libs/common/src/modules/space/entities/tag.entity.ts deleted file mode 100644 index cfa895c..0000000 --- a/libs/common/src/modules/space/entities/tag.entity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Entity, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm'; -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 { DeviceEntity } from '../../device/entities'; -import { SubspaceEntity } from './subspace/subspace.entity'; - -@Entity({ name: 'tag' }) -export class TagEntity extends AbstractEntity { - @Column({ type: 'varchar', length: 255, nullable: true }) - tag: string; - - @ManyToOne(() => TagModel, (model) => model.tags, { - nullable: true, - }) - model: TagModel; - - @ManyToOne(() => ProductEntity, (product) => product.tags, { - nullable: false, - }) - product: ProductEntity; - - @ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, { - nullable: true, - }) - @JoinColumn({ name: 'subspace_id' }) - subspace: SubspaceEntity; - - @Column({ - nullable: false, - default: false, - }) - public disabled: boolean; - - @OneToOne(() => DeviceEntity, (device) => device.tag, { - nullable: true, - }) - @JoinColumn({ name: 'device_id' }) - device: DeviceEntity; -} diff --git a/libs/common/src/modules/space/repositories/space.repository.ts b/libs/common/src/modules/space/repositories/space.repository.ts index 2462749..a89a314 100644 --- a/libs/common/src/modules/space/repositories/space.repository.ts +++ b/libs/common/src/modules/space/repositories/space.repository.ts @@ -1,10 +1,9 @@ -import { DataSource, Repository } from 'typeorm'; import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; import { InviteSpaceEntity } from '../entities/invite-space.entity'; import { SpaceLinkEntity } from '../entities/space-link.entity'; -import { SpaceEntity } from '../entities/space.entity'; -import { TagEntity } from '../entities/tag.entity'; import { SpaceProductAllocationEntity } from '../entities/space-product-allocation.entity'; +import { SpaceEntity } from '../entities/space.entity'; @Injectable() export class SpaceRepository extends Repository { @@ -20,13 +19,6 @@ export class SpaceLinkRepository extends Repository { } } -@Injectable() -export class TagRepository extends Repository { - constructor(private dataSource: DataSource) { - super(TagEntity, dataSource.createEntityManager()); - } -} - @Injectable() export class InviteSpaceRepository extends Repository { constructor(private dataSource: DataSource) { diff --git a/libs/common/src/modules/space/space.repository.module.ts b/libs/common/src/modules/space/space.repository.module.ts index 597e967..694d848 100644 --- a/libs/common/src/modules/space/space.repository.module.ts +++ b/libs/common/src/modules/space/space.repository.module.ts @@ -6,7 +6,6 @@ import { SpaceProductAllocationEntity } from './entities/space-product-allocatio import { SpaceEntity } from './entities/space.entity'; import { SubspaceProductAllocationEntity } from './entities/subspace/subspace-product-allocation.entity'; import { SubspaceEntity } from './entities/subspace/subspace.entity'; -import { TagEntity } from './entities/tag.entity'; @Module({ providers: [], @@ -16,7 +15,6 @@ import { TagEntity } from './entities/tag.entity'; TypeOrmModule.forFeature([ SpaceEntity, SubspaceEntity, - TagEntity, InviteSpaceEntity, SpaceProductAllocationEntity, SubspaceProductAllocationEntity, diff --git a/libs/common/src/modules/tag/entities/tag.entity.ts b/libs/common/src/modules/tag/entities/tag.entity.ts index 53eca79..c6b26cb 100644 --- a/libs/common/src/modules/tag/entities/tag.entity.ts +++ b/libs/common/src/modules/tag/entities/tag.entity.ts @@ -1,11 +1,10 @@ -import { Entity, Column, ManyToOne, Unique, ManyToMany } from 'typeorm'; -import { ProductEntity } from '../../product/entities'; -import { ProjectEntity } from '../../project/entities'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; -import { NewTagDto } from '../dtos/tag.dto'; +import { DeviceEntity } from '../../device/entities/device.entity'; +import { ProjectEntity } from '../../project/entities'; import { SpaceModelProductAllocationEntity } from '../../space-model/entities/space-model-product-allocation.entity'; import { SubspaceModelProductAllocationEntity } from '../../space-model/entities/subspace-model/subspace-model-product-allocation.entity'; -import { DeviceEntity } from '../../device/entities/device.entity'; +import { NewTagDto } from '../dtos/tag.dto'; @Entity({ name: 'new_tag' }) @Unique(['name', 'project']) @@ -24,31 +23,25 @@ export class NewTagEntity extends AbstractEntity { }) name: string; - @ManyToOne(() => ProductEntity, (product) => product.newTags, { - nullable: true, - onDelete: 'CASCADE', - }) - public product: ProductEntity; - @ManyToOne(() => ProjectEntity, (project) => project.tags, { nullable: false, onDelete: 'CASCADE', }) public project: ProjectEntity; - @ManyToMany( + @OneToMany( () => SpaceModelProductAllocationEntity, - (allocation) => allocation.tags, + (allocation) => allocation.tag, ) public spaceModelAllocations: SpaceModelProductAllocationEntity[]; - @ManyToMany( + @OneToMany( () => SubspaceModelProductAllocationEntity, - (allocation) => allocation.tags, + (allocation) => allocation.tag, ) public subspaceModelAllocations: SubspaceModelProductAllocationEntity[]; - @ManyToOne(() => DeviceEntity, (device) => device.tag) + @OneToMany(() => DeviceEntity, (device) => device.tag) public devices: DeviceEntity[]; constructor(partial: Partial) { diff --git a/package.json b/package.json index 7e535ed..eaec865 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "npm run test && node dist/main", "start:dev": "npm run test && npx nest start --watch", + "dev": "npx nest start --watch", "start:debug": "npm run test && npx nest start --debug --watch", "start:prod": "npm run test && node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/src/app.module.ts b/src/app.module.ts index 8d4b6e0..ce64932 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,45 +1,44 @@ +import { SeederModule } from '@app/common/seed/seeder.module'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import config from './config'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { WinstonModule } from 'nest-winston'; import { AuthenticationModule } from './auth/auth.module'; -import { UserModule } from './users/user.module'; -import { GroupModule } from './group/group.module'; -import { DeviceModule } from './device/device.module'; -import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; -import { CommunityModule } from './community/community.module'; -import { SeederModule } from '@app/common/seed/seeder.module'; -import { UserNotificationModule } from './user-notification/user-notification.module'; -import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; -import { SceneModule } from './scene/scene.module'; -import { DoorLockModule } from './door-lock/door.lock.module'; -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'; -import { TimeZoneModule } from './timezone/timezone.module'; -import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; -import { ScheduleModule } from './schedule/schedule.module'; -import { SpaceModule } from './space/space.module'; -import { ProductModule } from './product'; -import { ProjectModule } from './project'; -import { SpaceModelModule } from './space-model'; -import { InviteUserModule } from './invite-user/invite-user.module'; -import { PermissionModule } from './permission/permission.module'; -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 { CommunityModule } from './community/community.module'; +import config from './config'; +import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module'; +import { DeviceModule } from './device/device.module'; +import { DoorLockModule } from './door-lock/door.lock.module'; +import { GroupModule } from './group/group.module'; import { HealthModule } from './health/health.module'; +import { LoggingInterceptor } from './interceptors/logging.interceptor'; +import { InviteUserModule } from './invite-user/invite-user.module'; +import { PermissionModule } from './permission/permission.module'; +import { PowerClampModule } from './power-clamp/power-clamp.module'; +import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module'; +import { ProductModule } from './product'; +import { ProjectModule } from './project'; +import { RegionModule } from './region/region.module'; +import { RoleModule } from './role/role.module'; +import { SceneModule } from './scene/scene.module'; +import { ScheduleModule } from './schedule/schedule.module'; +import { SpaceModelModule } from './space-model'; +import { SpaceModule } from './space/space.module'; +import { TagModule } from './tags/tags.module'; +import { TermsConditionsModule } from './terms-conditions/terms-conditions.module'; +import { TimeZoneModule } from './timezone/timezone.module'; +import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module'; +import { UserNotificationModule } from './user-notification/user-notification.module'; +import { UserModule } from './users/user.module'; +import { VisitorPasswordModule } from './vistor-password/visitor-password.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; +import { AqiModule } from './aqi/aqi.module'; import { OccupancyModule } from './occupancy/occupancy.module'; import { WeatherModule } from './weather/weather.module'; -import { AqiModule } from './aqi/aqi.module'; @Module({ imports: [ ConfigModule.forRoot({ diff --git a/src/commission-device/services/commission-device.service.ts b/src/commission-device/services/commission-device.service.ts index d118e2c..ac2aae5 100644 --- a/src/commission-device/services/commission-device.service.ts +++ b/src/commission-device/services/commission-device.service.ts @@ -1,17 +1,19 @@ -import * as fs from 'fs'; import * as csv from 'csv-parser'; +import * as fs from 'fs'; +import { ProjectParam } from '@app/common/dto/project-param.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SpaceRepository } from '@app/common/modules/space'; +import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; +import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; 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 { @@ -118,7 +120,7 @@ export class DeviceCommissionService { where: { uuid: spaceId }, relations: [ 'productAllocations', - 'productAllocations.tags', + 'productAllocations.tag', 'productAllocations.product', ], }); @@ -135,7 +137,7 @@ export class DeviceCommissionService { where: { uuid: subspaceId }, relations: [ 'productAllocations', - 'productAllocations.tags', + 'productAllocations.tag', 'productAllocations.product', ], }); @@ -151,19 +153,23 @@ export class DeviceCommissionService { subspace?.productAllocations || space.productAllocations; const match = allocations - .flatMap((pa) => - (pa.tags || []).map((tag) => ({ product: pa.product, tag })), + .map( + ({ + product, + tag, + }: + | SpaceProductAllocationEntity + | SubspaceProductAllocationEntity) => ({ product, tag }), ) - .find(({ tag }) => tag.name === tagName); + .find( + ({ tag, product }) => + tag.name === tagName && product.name === productName, + ); 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}`); + console.error( + `No matching tag-product combination found for Device ID: ${rawDeviceId}`, + ); failureCount.value++; return; } diff --git a/src/community/community.module.ts b/src/community/community.module.ts index edaddbb..ea69401 100644 --- a/src/community/community.module.ts +++ b/src/community/community.module.ts @@ -8,7 +8,6 @@ import { 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'; @@ -87,6 +86,7 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SpaceProductAllocationService, SpaceLinkRepository, SubspaceRepository, + // Todo: find out why this is needed TagService, SubspaceDeviceService, SubspaceProductAllocationService, @@ -98,7 +98,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SpaceModelProductAllocationService, SpaceProductAllocationRepository, SubspaceProductAllocationRepository, - TagRepository, SubspaceModelRepository, SubspaceModelProductAllocationService, SpaceModelProductAllocationRepoitory, diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index f3b7f28..081c0ac 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,29 +1,30 @@ -import { - Injectable, - HttpException, - HttpStatus, - NotFoundException, -} from '@nestjs/common'; -import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos'; -import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; +import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { ExtendedTypeORMCustomModelFindAllQuery, TypeORMCustomModel, } from '@app/common/models/typeOrmCustom.model'; -import { PageResponse } from '@app/common/dto/pagination.response.dto'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; import { CommunityDto } from '@app/common/modules/community/dtos'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant'; -import { ILike, In, Not } from 'typeorm'; -import { SpaceService } from 'src/space/services'; -import { SpaceRepository } from '@app/common/modules/space'; +import { CommunityEntity } from '@app/common/modules/community/entities'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; import { DeviceEntity } from '@app/common/modules/device/entities'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SpaceRepository } from '@app/common/modules/space'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; +import { + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { SpaceService } from 'src/space/services'; +import { SelectQueryBuilder } from 'typeorm'; +import { AddCommunityDto, GetCommunityParams, ProjectParam } from '../dtos'; +import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; @Injectable() export class CommunityService { @@ -93,56 +94,35 @@ export class CommunityService { } async getCommunities( - param: ProjectParam, + { projectUuid }: ProjectParam, pageable: Partial, ): Promise { try { - const project = await this.validateProject(param.projectUuid); - - pageable.modelName = 'community'; - pageable.where = { - project: { uuid: param.projectUuid }, - name: Not(`${ORPHAN_COMMUNITY_NAME}-${project.name}`), - }; + const project = await this.validateProject(projectUuid); + /** + * TODO: removing this breaks the code (should be fixed when refactoring @see TypeORMCustomModel + */ + pageable.where = {}; + let qb: undefined | SelectQueryBuilder = undefined; 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), - }; + qb = this.communityRepository + .createQueryBuilder('c') + .leftJoin('c.spaces', 's') + .where('c.project = :projectUuid', { projectUuid }) + .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) + .andWhere('s.disabled = false') + .andWhere( + `c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`, + ) + .distinct(true); } const customModel = TypeORMCustomModel(this.communityRepository); const { baseResponseDto, paginationResponseDto } = - await customModel.findAll(pageable); + await customModel.findAll({ ...pageable, modelName: 'community' }, qb); + // todo: refactor this to minimize the number of queries if (pageable.includeSpaces) { const communitiesWithSpaces = await Promise.all( baseResponseDto.data.map(async (community: CommunityDto) => { @@ -150,7 +130,7 @@ export class CommunityService { await this.spaceService.getSpacesHierarchyForCommunity( { communityUuid: community.uuid, - projectUuid: param.projectUuid, + projectUuid: projectUuid, }, { onlyWithDevices: false, diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index f502816..d2ac4e7 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -1,39 +1,46 @@ -import { ORPHAN_SPACE_NAME } from './../../../libs/common/src/constants/orphan-constant'; -import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository'; +import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; +import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; +import { BatteryStatus } from '@app/common/constants/battery-status.enum'; +import { DeviceStatuses } from '@app/common/constants/device-status.enum'; +import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; +import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { ProductType } from '@app/common/constants/product-type.enum'; +import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceEntity } from '@app/common/modules/device/entities'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProjectEntity } from '@app/common/modules/project/entities'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { SpaceRepository } from '@app/common/modules/space/repositories'; +import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; import { - Injectable, - HttpException, - HttpStatus, - NotFoundException, BadRequestException, forwardRef, + HttpException, + HttpStatus, Inject, + Injectable, + NotFoundException, } from '@nestjs/common'; -import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { ConfigService } from '@nestjs/config'; +import { TuyaContext } from '@tuya/tuya-connector-nodejs'; +import { AddAutomationDto } from 'src/automation/dtos'; +import { SceneService } from 'src/scene/services'; +import { In, Not, QueryRunner } from 'typeorm'; +import { ProjectParam } from '../dtos'; import { AddDeviceDto, AddSceneToFourSceneDeviceDto, - UpdateDeviceDto, AssignDeviceToSpaceDto, + UpdateDeviceDto, } from '../dtos/add.device.dto'; -import { - DeviceInstructionResponse, - GetDeviceDetailsFunctionsInterface, - GetDeviceDetailsFunctionsStatusInterface, - GetDeviceDetailsInterface, - GetMacAddressInterface, - GetPowerClampFunctionsStatusInterface, - controlDeviceInterface, - getDeviceLogsInterface, - updateDeviceFirmwareInterface, -} from '../interfaces/get.device.interface'; -import { - GetDeviceBySpaceUuidDto, - GetDeviceLogsDto, - GetDevicesBySpaceOrCommunityDto, - GetDoorLockDevices, -} from '../dtos/get.device.dto'; import { BatchControlDevicesDto, BatchFactoryResetDevicesDto, @@ -41,33 +48,29 @@ import { ControlDeviceDto, GetSceneFourSceneDeviceDto, } from '../dtos/control.device.dto'; -import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { In, Not, QueryRunner } from 'typeorm'; -import { ProductType } from '@app/common/constants/product-type.enum'; -import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; -import { DeviceStatuses } from '@app/common/constants/device-status.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; -import { BatteryStatus } from '@app/common/constants/battery-status.enum'; -import { SceneService } from 'src/scene/services'; -import { AddAutomationDto } from 'src/automation/dtos'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; -import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum'; -import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum'; -import { DeviceSceneParamDto } from '../dtos/device.param.dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto'; -import { DeviceEntity } from '@app/common/modules/device/entities'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { ProjectParam } from '../dtos'; -import { BatchDeviceTypeEnum } from '@app/common/constants/batch-device.enum'; -import { DeviceTypeEnum } from '@app/common/constants/device-type.enum'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { addSpaceUuidToDevices } from '@app/common/util/device-utils'; +import { DeviceSceneParamDto } from '../dtos/device.param.dto'; +import { + GetDeviceLogsDto, + GetDevicesBySpaceOrCommunityDto, + GetDoorLockDevices, +} from '../dtos/get.device.dto'; +import { + controlDeviceInterface, + DeviceInstructionResponse, + GetDeviceDetailsFunctionsInterface, + GetDeviceDetailsFunctionsStatusInterface, + GetDeviceDetailsInterface, + getDeviceLogsInterface, + GetMacAddressInterface, + GetPowerClampFunctionsStatusInterface, + updateDeviceFirmwareInterface, +} from '../interfaces/get.device.interface'; +import { + ORPHAN_COMMUNITY_NAME, + ORPHAN_SPACE_NAME, +} from './../../../libs/common/src/constants/orphan-constant'; +import { ProductRepository } from './../../../libs/common/src/modules/product/repositories/product.repository'; @Injectable() export class DeviceService { @@ -198,46 +201,6 @@ export class DeviceService { } } - async getDevicesBySpaceId( - getDeviceBySpaceUuidDto: GetDeviceBySpaceUuidDto, - ): Promise { - try { - const devices = await this.deviceRepository.find({ - where: { - spaceDevice: { uuid: getDeviceBySpaceUuidDto.spaceUuid }, - isActive: true, - }, - relations: [ - 'spaceDevice', - 'productDevice', - 'permission', - 'permission.permissionType', - ], - }); - const devicesData = await Promise.all( - devices.map(async (device) => { - return { - haveRoom: device.spaceDevice ? true : false, - productUuid: device.productDevice.uuid, - productType: device.productDevice.prodType, - permissionType: device.permission[0].permissionType.type, - ...(await this.getDeviceDetailsByDeviceIdTuya( - device.deviceTuyaUuid, - )), - uuid: device.uuid, - } as GetDeviceDetailsInterface; - }), - ); - - return devicesData; - } catch (error) { - // Handle the error here - throw new HttpException( - 'Error fetching devices by space', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } async transferDeviceInSpaces( assignDeviceToSpaceDto: AssignDeviceToSpaceDto, projectUuid: string, @@ -1199,39 +1162,6 @@ export class DeviceService { } } - async getFullSpaceHierarchy( - space: SpaceEntity, - ): Promise<{ uuid: string; spaceName: string }[]> { - try { - // Fetch only the relevant spaces, starting with the target space - const targetSpace = await this.spaceRepository.findOne({ - where: { uuid: space.uuid }, - relations: ['parent', 'children'], - }); - - // Fetch only the ancestors of the target space - const ancestors = await this.fetchAncestors(targetSpace); - - // Optionally, fetch descendants if required - const descendants = await this.fetchDescendants(targetSpace); - - const fullHierarchy = [...ancestors, targetSpace, ...descendants].map( - (space) => ({ - uuid: space.uuid, - spaceName: space.spaceName, - }), - ); - - return fullHierarchy; - } catch (error) { - console.error('Error fetching space hierarchy:', error.message); - throw new HttpException( - 'Error fetching space hierarchy', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - async getPowerClampInstructionStatus(deviceDetails: any) { try { const deviceStatus = await this.getPowerClampInstructionStatusTuya( @@ -1331,27 +1261,6 @@ export class DeviceService { return ancestors.reverse(); } - private async fetchDescendants(space: SpaceEntity): Promise { - const descendants: SpaceEntity[] = []; - - // Fetch the immediate children of the current space - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['children'], // To continue fetching downwards - }); - - for (const child of children) { - // Add the child to the descendants list - descendants.push(child); - - // Recursively fetch the child's descendants - const childDescendants = await this.fetchDescendants(child); - descendants.push(...childDescendants); - } - - return descendants; - } - async addSceneToSceneDevice( deviceUuid: string, addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto, @@ -1654,22 +1563,6 @@ export class DeviceService { } } - async moveDevicesToSpace( - targetSpace: SpaceEntity, - deviceIds: string[], - ): Promise { - if (!deviceIds || deviceIds.length === 0) { - throw new HttpException( - 'No device IDs provided for transfer', - HttpStatus.BAD_REQUEST, - ); - } - - await this.deviceRepository.update( - { uuid: In(deviceIds) }, - { spaceDevice: targetSpace }, - ); - } async getDoorLockDevices(projectUuid: string) { await this.validateProject(projectUuid); @@ -1860,4 +1753,39 @@ export class DeviceService { return allDevices; } + + async addDevicesToOrphanSpace( + space: SpaceEntity, + project: ProjectEntity, + queryRunner: QueryRunner, + ) { + const spaceRepository = queryRunner.manager.getRepository(SpaceEntity); + const deviceRepository = queryRunner.manager.getRepository(DeviceEntity); + try { + const orphanSpace = await spaceRepository.findOne({ + where: { + community: { + name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, + }, + spaceName: ORPHAN_SPACE_NAME, + }, + }); + + if (!orphanSpace) { + throw new HttpException( + `Orphan space not found in community ${project.name}`, + HttpStatus.NOT_FOUND, + ); + } + + await deviceRepository.update( + { uuid: In(space.devices.map((device) => device.uuid)) }, + { spaceDevice: orphanSpace }, + ); + } catch (error) { + throw new Error( + `Failed to add devices to orphan spaces: ${error.message}`, + ); + } + } } diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index a42922c..d28ff10 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -1,18 +1,75 @@ import { Module } from '@nestjs/common'; -import { InviteUserService } from './services/invite-user.service'; -import { InviteUserController } from './controllers/invite-user.controller'; import { ConfigModule } from '@nestjs/config'; +import { InviteUserController } from './controllers/invite-user.controller'; +import { InviteUserService } from './services/invite-user.service'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; import { - UserRepository, - UserSpaceRepository, -} from '@app/common/modules/user/repositories'; + DeviceRepository, + DeviceUserPermissionRepository, +} from '@app/common/modules/device/repositories'; import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module'; import { InviteUserRepository, InviteUserSpaceRepository, } from '@app/common/modules/Invite-user/repositiories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { + PowerClampDailyRepository, + PowerClampHourlyRepository, + PowerClampMonthlyRepository, +} from '@app/common/modules/power-clamp/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { RegionRepository } from '@app/common/modules/region/repositories'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { + InviteSpaceRepository, + SpaceLinkRepository, + SpaceProductAllocationRepository, + SpaceRepository, +} from '@app/common/modules/space'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; +import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; import { EmailService } from '@app/common/util/email.service'; +import { CommunityModule } from 'src/community/community.module'; +import { CommunityService } from 'src/community/services'; +import { DeviceService } from 'src/device/services'; +import { ProjectUserService } from 'src/project/services/project-user.service'; +import { SceneService } from 'src/scene/services'; +import { + SpaceModelService, + SubSpaceModelService, +} from 'src/space-model/services'; +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 { SpaceLinkService, SpaceService, @@ -21,70 +78,11 @@ import { SubSpaceService, ValidationService, } from 'src/space/services'; -import { CommunityService } from 'src/community/services'; -import { - InviteSpaceRepository, - SpaceLinkRepository, - SpaceProductAllocationRepository, - SpaceRepository, - TagRepository, -} from '@app/common/modules/space'; -import { SpaceModelRepository } from '@app/common/modules/space-model'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { UserService, UserSpaceService } from 'src/users/services'; -import { UserDevicePermissionService } from 'src/user-device-permission/services'; -import { - DeviceRepository, - DeviceUserPermissionRepository, -} from '@app/common/modules/device/repositories'; -import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; -import { ProjectUserService } from 'src/project/services/project-user.service'; -import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; -import { RegionRepository } from '@app/common/modules/region/repositories'; -import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; -import { CommunityModule } from 'src/community/community.module'; -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, - SubspaceModelProductAllocationRepoitory, - SubspaceModelRepository, -} from '@app/common/modules/space-model'; -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 { - 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'; -import { OccupancyService } from '@app/common/helper/services/occupancy.service'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { TagService as NewTagService } from 'src/tags/services'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { UserService, UserSpaceService } from 'src/users/services'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule], @@ -125,7 +123,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SpaceProductAllocationService, SpaceLinkRepository, SubspaceRepository, - TagService, SubspaceDeviceService, SubspaceProductAllocationService, SpaceModelRepository, @@ -136,7 +133,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SpaceModelProductAllocationService, SpaceProductAllocationRepository, SubspaceProductAllocationRepository, - TagRepository, SubspaceModelRepository, SubspaceModelProductAllocationService, SpaceModelProductAllocationRepoitory, diff --git a/src/power-clamp/power-clamp.module.ts b/src/power-clamp/power-clamp.module.ts index 9910dd9..6a36393 100644 --- a/src/power-clamp/power-clamp.module.ts +++ b/src/power-clamp/power-clamp.module.ts @@ -1,13 +1,52 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { PowerClampService as PowerClamp } from './services/power-clamp.service'; -import { PowerClampController } from './controllers'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; import { PowerClampDailyRepository, PowerClampHourlyRepository, PowerClampMonthlyRepository, } from '@app/common/modules/power-clamp/repositories'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { + InviteSpaceRepository, + SpaceLinkRepository, + SpaceProductAllocationRepository, + SpaceRepository, +} from '@app/common/modules/space'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { CommunityService } from 'src/community/services'; +import { DeviceService } from 'src/device/services'; +import { SceneService } from 'src/scene/services'; +import { + SpaceModelService, + SubSpaceModelService, +} from 'src/space-model/services'; +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 { SpaceDeviceService, SpaceLinkService, @@ -16,51 +55,11 @@ import { SubSpaceService, ValidationService, } from 'src/space/services'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { DeviceService } from 'src/device/services'; -import { - InviteSpaceRepository, - SpaceLinkRepository, - SpaceProductAllocationRepository, - SpaceRepository, - TagRepository, -} from '@app/common/modules/space'; -import { CommunityService } from 'src/community/services'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { - SpaceModelProductAllocationRepoitory, - SpaceModelRepository, - SubspaceModelProductAllocationRepoitory, - SubspaceModelRepository, -} from '@app/common/modules/space-model'; -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 { SceneService } from 'src/scene/services'; -import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; -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 { TagService } from 'src/tags/services'; -import { - SpaceModelService, - SubSpaceModelService, -} from 'src/space-model/services'; import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service'; -import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; -import { - SubspaceProductAllocationRepository, - SubspaceRepository, -} from '@app/common/modules/space/repositories/subspace.repository'; import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-product-allocation.service'; -import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; -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 { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { TagService } from 'src/tags/services'; +import { PowerClampController } from './controllers'; +import { PowerClampService as PowerClamp } from './services/power-clamp.service'; import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; @Module({ imports: [ConfigModule], @@ -106,7 +105,6 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; SpaceModelProductAllocationService, SpaceProductAllocationRepository, SubspaceProductAllocationRepository, - TagRepository, SubspaceModelRepository, SubspaceModelProductAllocationService, SpaceModelProductAllocationRepoitory, diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 306a6c2..2434fde 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -1,24 +1,58 @@ -import { Global, Module } from '@nestjs/common'; -import { CqrsModule } from '@nestjs/cqrs'; -import { ProjectController } from './controllers'; -import { ProjectService } from './services'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; +import { + PowerClampDailyRepository, + PowerClampHourlyRepository, + PowerClampMonthlyRepository, +} from '@app/common/modules/power-clamp/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { CreateOrphanSpaceHandler } from './handler'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; import { InviteSpaceRepository, SpaceLinkRepository, SpaceProductAllocationRepository, SpaceRepository, - TagRepository, } from '@app/common/modules/space'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; -import { ProjectUserController } from './controllers/project-user.controller'; -import { ProjectUserService } from './services/project-user.service'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; import { UserRepository, UserSpaceRepository, } from '@app/common/modules/user/repositories'; +import { Global, Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { CommunityService } from 'src/community/services'; +import { DeviceService } from 'src/device/services'; +import { SceneService } from 'src/scene/services'; +import { + SpaceModelService, + SubSpaceModelService, +} from 'src/space-model/services'; +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 { SpaceLinkService, SpaceService, @@ -26,49 +60,14 @@ import { SubSpaceService, ValidationService, } from 'src/space/services'; -import { TagService } from 'src/tags/services'; -import { - SpaceModelService, - SubSpaceModelService, -} from 'src/space-model/services'; -import { DeviceService } from 'src/device/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 { CommunityService } from 'src/community/services'; -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 { 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 { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { SubspaceModelProductAllocationService } from 'src/space-model/services/subspace/subspace-model-product-allocation.service'; -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 { - PowerClampDailyRepository, - PowerClampHourlyRepository, - PowerClampMonthlyRepository, -} from '@app/common/modules/power-clamp/repositories'; -import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; -import { OccupancyService } from '@app/common/helper/services/occupancy.service'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { TagService } from 'src/tags/services'; +import { ProjectController } from './controllers'; +import { ProjectUserController } from './controllers/project-user.controller'; +import { CreateOrphanSpaceHandler } from './handler'; +import { ProjectService } from './services'; +import { ProjectUserService } from './services/project-user.service'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -112,7 +111,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; DeviceStatusFirebaseService, SceneService, TuyaService, - TagRepository, SubspaceModelRepository, SubspaceModelProductAllocationService, SpaceModelProductAllocationRepoitory, diff --git a/src/project/services/project.service.ts b/src/project/services/project.service.ts index f771c3a..645554e 100644 --- a/src/project/services/project.service.ts +++ b/src/project/services/project.service.ts @@ -1,4 +1,17 @@ +import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { ProjectDto } from '@app/common/modules/project/dtos'; +import { ProjectEntity } from '@app/common/modules/project/entities'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { format } from '@fast-csv/format'; import { forwardRef, HttpException, @@ -6,24 +19,11 @@ import { Inject, Injectable, } from '@nestjs/common'; -import { CreateProjectDto, GetProjectParam } from '../dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { ProjectEntity } from '@app/common/modules/project/entities'; -import { - TypeORMCustomModel, - TypeORMCustomModelFindAllQuery, -} from '@app/common/models/typeOrmCustom.model'; -import { ProjectDto } from '@app/common/modules/project/dtos'; -import { PageResponse } from '@app/common/dto/pagination.response.dto'; import { CommandBus } from '@nestjs/cqrs'; -import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command'; -import { UserRepository } from '@app/common/modules/user/repositories'; -import { format } from '@fast-csv/format'; -import { PassThrough } from 'stream'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SpaceService } from 'src/space/services'; -import { ORPHAN_COMMUNITY_NAME } from '@app/common/constants/orphan-constant'; +import { PassThrough } from 'stream'; +import { CreateOrphanSpaceCommand } from '../command/create-orphan-space-command'; +import { CreateProjectDto, GetProjectParam } from '../dto'; @Injectable() export class ProjectService { @@ -236,11 +236,11 @@ export class ProjectService { 'communities.spaces.parent', 'communities.spaces.productAllocations', 'communities.spaces.productAllocations.product', - 'communities.spaces.productAllocations.tags', + 'communities.spaces.productAllocations.tag', 'communities.spaces.subspaces', 'communities.spaces.subspaces.productAllocations', 'communities.spaces.subspaces.productAllocations.product', - 'communities.spaces.subspaces.productAllocations.tags', + 'communities.spaces.subspaces.productAllocations.tag', ], }); @@ -303,52 +303,38 @@ export class ProjectService { if (subspace.disabled) continue; for (const productAllocation of subspace.productAllocations || []) { - for (const tag of productAllocation.tags || []) { - csvStream.write({ - 'Device ID': '', - 'Community Name': space.community?.name || '', - 'Space Name': space.spaceName, - 'Space Location': spaceLocation, - 'Subspace Name': subspace.subspaceName || '', - Tag: tag.name, - 'Product Name': productAllocation.product.name || '', - 'Community UUID': space.community?.uuid || '', - 'Space UUID': space.uuid, - 'Subspace UUID': subspace.uuid, - }); - } - } - } - - for (const productAllocation of space.productAllocations || []) { - for (const tag of productAllocation.tags || []) { csvStream.write({ 'Device ID': '', 'Community Name': space.community?.name || '', 'Space Name': space.spaceName, 'Space Location': spaceLocation, - 'Subspace Name': '', - Tag: tag.name, + 'Subspace Name': subspace.subspaceName || '', + Tag: productAllocation.tag.name, 'Product Name': productAllocation.product.name || '', 'Community UUID': space.community?.uuid || '', 'Space UUID': space.uuid, - 'Subspace UUID': '', + 'Subspace UUID': subspace.uuid, }); } } + + for (const productAllocation of space.productAllocations || []) { + csvStream.write({ + 'Device ID': '', + 'Community Name': space.community?.name || '', + 'Space Name': space.spaceName, + 'Space Location': spaceLocation, + 'Subspace Name': '', + Tag: productAllocation.tag.name, + 'Product Name': productAllocation.product.name || '', + 'Community UUID': space.community?.uuid || '', + 'Space UUID': space.uuid, + 'Subspace UUID': '', + }); + } } csvStream.end(); return stream; } - - getSpaceLocation(space: SpaceEntity): string { - const names = []; - let current = space.parent; - while (current) { - names.unshift(current.spaceName); - current = current.parent; - } - return names.join(' > '); - } } diff --git a/src/space-model/dtos/index.ts b/src/space-model/dtos/index.ts index a49d8b0..f710b34 100644 --- a/src/space-model/dtos/index.ts +++ b/src/space-model/dtos/index.ts @@ -1,7 +1,6 @@ export * from './create-space-model.dto'; +export * from './link-space-model.dto'; export * from './project-param.dto'; -export * from './update-space-model.dto'; export * from './space-model-param'; export * from './subspaces-model-dtos'; -export * from './tag-model-dtos'; -export * from './link-space-model.dto'; +export * from './update-space-model.dto'; diff --git a/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts index cbf021c..f044dea 100644 --- a/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts +++ b/src/space-model/dtos/subspaces-model-dtos/modify-subspace-model.dto.ts @@ -1,14 +1,14 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { - IsString, - IsOptional, IsArray, - ValidateNested, IsEnum, + IsOptional, + IsString, + ValidateNested, } from 'class-validator'; -import { ModifyTagModelDto } from '../tag-model-dtos'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; export class ModifySubspaceModelDto { @ApiProperty({ @@ -37,11 +37,11 @@ export class ModifySubspaceModelDto { @ApiPropertyOptional({ description: 'List of tag modifications (add/update/delete) for the subspace', - type: [ModifyTagModelDto], + type: [ModifyTagDto], }) @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => ModifyTagModelDto) - tags?: ModifyTagModelDto[]; + @Type(() => ModifyTagDto) + tags?: ModifyTagDto[]; } diff --git a/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts deleted file mode 100644 index 1cb7473..0000000 --- a/src/space-model/dtos/tag-model-dtos/create-tag-model.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -export class CreateTagModelDto { - @ApiProperty({ - description: 'Tag associated with the space or subspace models', - example: 'Temperature Control', - }) - @IsNotEmpty() - @IsString() - tag: string; - - @ApiPropertyOptional({ - description: 'UUID of the tag model (required for update/delete)', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsOptional() - @IsString() - uuid?: string; - - @ApiProperty({ - description: 'ID of the product associated with the tag', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsNotEmpty() - @IsString() - productUuid: string; -} diff --git a/src/space-model/dtos/tag-model-dtos/index.ts b/src/space-model/dtos/tag-model-dtos/index.ts deleted file mode 100644 index a0f136d..0000000 --- a/src/space-model/dtos/tag-model-dtos/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './create-tag-model.dto'; -export * from './update-tag-model.dto'; -export * from './modify-tag-model.dto'; diff --git a/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts deleted file mode 100644 index 0bc9fc0..0000000 --- a/src/space-model/dtos/tag-model-dtos/modify-tag-model.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsOptional, IsEnum, IsUUID } from 'class-validator'; - -export class ModifyTagModelDto { - @ApiProperty({ - description: 'Action to perform: add, update, or delete', - example: ModifyAction.ADD, - }) - @IsEnum(ModifyAction) - action: ModifyAction; - - @ApiPropertyOptional({ - description: 'UUID of the new tag', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsOptional() - @IsUUID() - newTagUuid: string; - - @ApiPropertyOptional({ - description: - 'UUID of an existing tag (required for update/delete, optional for add)', - example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef', - }) - @IsOptional() - @IsUUID() - tagUuid?: string; - - @ApiPropertyOptional({ - description: 'Name of the tag (required for add/update)', - example: 'Temperature Sensor', - }) - @IsOptional() - @IsString() - name?: string; - - @ApiPropertyOptional({ - description: - 'UUID of the product associated with the tag (required for add)', - example: 'c789a91e-549a-4753-9006-02f89e8170e0', - }) - @IsOptional() - @IsUUID() - productUuid?: string; -} diff --git a/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts b/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts deleted file mode 100644 index ca5612f..0000000 --- a/src/space-model/dtos/tag-model-dtos/update-tag-model.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; - -export class UpdateTagModelDto { - @ApiProperty({ - description: 'UUID of the tag to be updated', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsNotEmpty() - @IsUUID() - uuid: string; - - @ApiProperty({ - description: 'Updated name of the tag', - example: 'Updated Tag Name', - required: false, - }) - @IsOptional() - @IsString() - tag?: string; -} diff --git a/src/space-model/dtos/update-space-model.dto.ts b/src/space-model/dtos/update-space-model.dto.ts index d1110ea..6e33a3b 100644 --- a/src/space-model/dtos/update-space-model.dto.ts +++ b/src/space-model/dtos/update-space-model.dto.ts @@ -1,48 +1,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; import { Type } from 'class-transformer'; +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator'; import { DeleteSubspaceModelDto, ModifySubspaceModelDto, UpdateSubspaceModelDto, } from './subspaces-model-dtos'; -import { ModifyTagModelDto } from './tag-model-dtos'; - -export class ModifySubspacesModelDto { - @ApiProperty({ - description: 'List of subspaces to add', - type: [CreateSubspaceModelDto], - required: false, - }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateSubspaceModelDto) - add?: CreateSubspaceModelDto[]; - - @ApiProperty({ - description: 'List of subspaces to add', - type: [CreateSubspaceModelDto], - required: false, - }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateSubspaceModelDto) - update?: UpdateSubspaceModelDto[]; - - @ApiProperty({ - description: 'List of subspaces to delete', - type: [DeleteSubspaceModelDto], - required: false, - }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => DeleteSubspaceModelDto) - delete?: DeleteSubspaceModelDto[]; -} +import { CreateSubspaceModelDto } from './subspaces-model-dtos/create-subspace-model.dto'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; export class UpdateSpaceModelDto { @ApiProperty({ @@ -66,11 +31,11 @@ export class UpdateSpaceModelDto { @ApiPropertyOptional({ description: 'List of tag modifications (add/update/delete) for the space model', - type: [ModifyTagModelDto], + type: [ModifyTagDto], }) @IsOptional() @IsArray() @ValidateNested({ each: true }) - @Type(() => ModifyTagModelDto) - tags?: ModifyTagModelDto[]; + @Type(() => ModifyTagDto) + tags?: ModifyTagDto[]; } diff --git a/src/space-model/handlers/propogate-space-model-product-allocation.handler.ts b/src/space-model/handlers/propogate-space-model-product-allocation.handler.ts index 51f2623..5da4c98 100644 --- a/src/space-model/handlers/propogate-space-model-product-allocation.handler.ts +++ b/src/space-model/handlers/propogate-space-model-product-allocation.handler.ts @@ -1,11 +1,11 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { PropogateUpdateSpaceModelProductAllocationCommand } from '../commands'; import { SpaceProductAllocationRepository } from '@app/common/modules/space'; import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model'; import { - SubspaceRepository, SubspaceProductAllocationRepository, + SubspaceRepository, } from '@app/common/modules/space/repositories/subspace.repository'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { PropogateUpdateSpaceModelProductAllocationCommand } from '../commands'; @CommandHandler(PropogateUpdateSpaceModelProductAllocationCommand) export class PropogateUpdateSpaceModelProductAllocationHandler @@ -31,89 +31,89 @@ export class PropogateUpdateSpaceModelProductAllocationHandler console.log(`Processing ${updatedAllocations.length} allocations...`); - for (const allocation of updatedAllocations) { - try { - if (allocation.allocation) { - const spaceAllocations = await this.spaceProductRepository.find({ - where: { uuid: allocation.allocation.uuid }, - relations: ['tags'], - }); + // for (const allocation of updatedAllocations) { + // try { + // if (allocation.allocation) { + // const spaceAllocations = await this.spaceProductRepository.find({ + // where: { uuid: allocation.allocation.uuid }, + // relations: ['tags'], + // }); - if (!spaceAllocations || spaceAllocations.length === 0) { - console.warn( - `No space allocations found for UUID: ${allocation.allocation.uuid}`, - ); - continue; - } + // if (!spaceAllocations || spaceAllocations.length === 0) { + // console.warn( + // `No space allocations found for UUID: ${allocation.allocation.uuid}`, + // ); + // continue; + // } - if (allocation.tagsAdded?.length) { - for (const spaceAllocation of spaceAllocations) { - spaceAllocation.tags.push(...allocation.tagsAdded); - } - await this.spaceProductRepository.save(spaceAllocations); - console.log( - `Added tags to ${spaceAllocations.length} space allocations.`, - ); - } + // if (allocation.tagsAdded?.length) { + // for (const spaceAllocation of spaceAllocations) { + // spaceAllocation.tags.push(...allocation.tagsAdded); + // } + // await this.spaceProductRepository.save(spaceAllocations); + // console.log( + // `Added tags to ${spaceAllocations.length} space allocations.`, + // ); + // } - if (allocation.tagsRemoved?.length) { - const tagsToRemoveUUIDs = new Set( - allocation.tagsRemoved.map((tag) => tag.uuid), - ); + // if (allocation.tagsRemoved?.length) { + // const tagsToRemoveUUIDs = new Set( + // allocation.tagsRemoved.map((tag) => tag.uuid), + // ); - for (const spaceAllocation of spaceAllocations) { - spaceAllocation.tags = spaceAllocation.tags.filter( - (tag) => !tagsToRemoveUUIDs.has(tag.uuid), - ); - } - await this.spaceProductRepository.save(spaceAllocations); - console.log( - `Removed tags from ${spaceAllocations.length} space allocations.`, - ); - } - } + // for (const spaceAllocation of spaceAllocations) { + // spaceAllocation.tags = spaceAllocation.tags.filter( + // (tag) => !tagsToRemoveUUIDs.has(tag.uuid), + // ); + // } + // await this.spaceProductRepository.save(spaceAllocations); + // console.log( + // `Removed tags from ${spaceAllocations.length} space allocations.`, + // ); + // } + // } - if (allocation.deletedAllocation) { - const spaceAllocations = await this.spaceProductRepository.find({ - where: { uuid: allocation.deletedAllocation.uuid }, - relations: ['tags'], - }); + // if (allocation.deletedAllocation) { + // const spaceAllocations = await this.spaceProductRepository.find({ + // where: { uuid: allocation.deletedAllocation.uuid }, + // relations: ['tags'], + // }); - if (!spaceAllocations || spaceAllocations.length === 0) { - console.warn( - `No space allocations found to delete for UUID: ${allocation.deletedAllocation.uuid}`, - ); - continue; - } + // if (!spaceAllocations || spaceAllocations.length === 0) { + // console.warn( + // `No space allocations found to delete for UUID: ${allocation.deletedAllocation.uuid}`, + // ); + // continue; + // } - await this.spaceProductRepository.remove(spaceAllocations); - console.log( - `Deleted ${spaceAllocations.length} space allocations.`, - ); - } + // await this.spaceProductRepository.remove(spaceAllocations); + // console.log( + // `Deleted ${spaceAllocations.length} space allocations.`, + // ); + // } - if (allocation.newAllocation) { - const newAllocations = spaces.map((space) => - this.spaceProductRepository.create({ - space, - product: allocation.newAllocation.product, - tags: allocation.newAllocation.tags, - inheritedFromModel: allocation.newAllocation, - }), - ); + // if (allocation.newAllocation) { + // const newAllocations = spaces.map((space) => + // this.spaceProductRepository.create({ + // space, + // product: allocation.newAllocation.product, + // tag: allocation.newAllocation.tag, + // inheritedFromModel: allocation.newAllocation, + // }), + // ); - await this.spaceProductRepository.save(newAllocations); - console.log( - `Created ${newAllocations.length} new space allocations.`, - ); - } - } catch (error) { - console.error( - `Error processing allocation update: ${JSON.stringify(allocation)}`, - error, - ); - } - } + // await this.spaceProductRepository.save(newAllocations); + // console.log( + // `Created ${newAllocations.length} new space allocations.`, + // ); + // } + // } catch (error) { + // console.error( + // `Error processing allocation update: ${JSON.stringify(allocation)}`, + // error, + // ); + // } + // } console.log('Finished processing all allocations.'); } catch (error) { diff --git a/src/space-model/handlers/propogate-subspace-handler.ts b/src/space-model/handlers/propogate-subspace-handler.ts index d651d85..a5218a4 100644 --- a/src/space-model/handlers/propogate-subspace-handler.ts +++ b/src/space-model/handlers/propogate-subspace-handler.ts @@ -1,15 +1,14 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { PropogateUpdateSpaceModelCommand } from '../commands'; +import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { SpaceProductAllocationRepository } from '@app/common/modules/space'; +import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SubspaceProductAllocationRepository, SubspaceRepository, } from '@app/common/modules/space/repositories/subspace.repository'; -import { SubspaceModelProductAllocationRepoitory } from '@app/common/modules/space-model'; -import { In } from 'typeorm'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { PropogateUpdateSpaceModelCommand } from '../commands'; import { ISingleSubspaceModel } from '../interfaces'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { IUpdatedAllocations } from '../interfaces/subspace-product-allocation-update-result.interface'; @CommandHandler(PropogateUpdateSpaceModelCommand) @@ -72,48 +71,48 @@ export class PropogateUpdateSpaceModelHandler for (const allocation of allocations) { if (!allocation) continue; - if (allocation.allocation) { - try { - const subspaceAllocations = - await this.subspaceProductRepository.find({ - where: { - inheritedFromModel: { uuid: allocation.allocation.uuid }, - }, - relations: ['tags'], - }); + // if (allocation.allocation) { + // try { + // const subspaceAllocations = + // await this.subspaceProductRepository.find({ + // where: { + // inheritedFromModel: { uuid: allocation.allocation.uuid }, + // }, + // relations: ['tags'], + // }); - if (!subspaceAllocations || subspaceAllocations.length === 0) - continue; + // if (!subspaceAllocations || subspaceAllocations.length === 0) + // continue; - if (allocation.tagsAdded?.length) { - for (const subspaceAllocation of subspaceAllocations) { - subspaceAllocation.tags.push(...allocation.tagsAdded); - } - await this.subspaceProductRepository.save(subspaceAllocations); - console.log( - `Added tags to ${subspaceAllocations.length} subspace allocations.`, - ); - } + // if (allocation.tagsAdded?.length) { + // for (const subspaceAllocation of subspaceAllocations) { + // subspaceAllocation.tags.push(...allocation.tagsAdded); + // } + // await this.subspaceProductRepository.save(subspaceAllocations); + // console.log( + // `Added tags to ${subspaceAllocations.length} subspace allocations.`, + // ); + // } - if (allocation.tagsRemoved?.length) { - const tagsToRemoveUUIDs = allocation.tagsRemoved.map( - (tag) => tag.uuid, - ); + // if (allocation.tagsRemoved?.length) { + // const tagsToRemoveUUIDs = allocation.tagsRemoved.map( + // (tag) => tag.uuid, + // ); - for (const subspaceAllocation of subspaceAllocations) { - subspaceAllocation.tags = subspaceAllocation.tags.filter( - (tag) => !tagsToRemoveUUIDs.includes(tag.uuid), - ); - } - await this.subspaceProductRepository.save(subspaceAllocations); - console.log( - `Removed tags from ${subspaceAllocations.length} subspace allocations.`, - ); - } - } catch (error) { - console.error('Error processing allocation update:', error); - } - } + // for (const subspaceAllocation of subspaceAllocations) { + // subspaceAllocation.tags = subspaceAllocation.tags.filter( + // (tag) => !tagsToRemoveUUIDs.includes(tag.uuid), + // ); + // } + // await this.subspaceProductRepository.save(subspaceAllocations); + // console.log( + // `Removed tags from ${subspaceAllocations.length} subspace allocations.`, + // ); + // } + // } catch (error) { + // console.error('Error processing allocation update:', error); + // } + // } if (allocation.newAllocation) { try { @@ -127,7 +126,7 @@ export class PropogateUpdateSpaceModelHandler const newAllocations = subspaces.map((subspace) => this.subspaceProductRepository.create({ product: allocation.newAllocation.product, - tags: allocation.newAllocation.tags, + tag: allocation.newAllocation.tag, subspace, inheritedFromModel: allocation.newAllocation, }), @@ -198,7 +197,7 @@ export class PropogateUpdateSpaceModelHandler const subspaceAllocation = this.subspaceProductRepository.create({ subspace: subspace, product: allocation.product, - tags: allocation.tags, + tag: allocation.tag, inheritedFromModel: allocation, }); await this.subspaceProductRepository.save(subspaceAllocation); @@ -211,67 +210,59 @@ export class PropogateUpdateSpaceModelHandler subspaceModel: ISingleSubspaceModel, spaces: SpaceEntity[], ) { - const subspaces = await this.subspaceRepository.find({ - where: { - subSpaceModel: { uuid: subspaceModel.subspaceModel.uuid }, - disabled: false, - }, - relations: [ - 'productAllocations', - 'productAllocations.product', - 'productAllocations.tags', - ], - }); - - if (!subspaces.length) { - return; - } - - const allocationUuidsToRemove = subspaces.flatMap((subspace) => - subspace.productAllocations.map((allocation) => allocation.uuid), - ); - - if (allocationUuidsToRemove.length) { - await this.subspaceProductRepository.delete(allocationUuidsToRemove); - } - - await this.subspaceRepository.update( - { uuid: In(subspaces.map((s) => s.uuid)) }, - { disabled: true }, - ); - - const relocatedAllocations = subspaceModel.relocatedAllocations || []; - - if (!relocatedAllocations.length) { - return; - } - - for (const space of spaces) { - for (const { allocation, tags = [] } of relocatedAllocations) { - const spaceAllocation = await this.spaceProductRepository.findOne({ - where: { - inheritedFromModel: { uuid: allocation.uuid }, - space: { uuid: space.uuid }, - }, - relations: ['tags'], - }); - - if (spaceAllocation) { - if (tags.length) { - spaceAllocation.tags.push(...tags); - await this.spaceProductRepository.save(spaceAllocation); - } - } else { - const newSpaceAllocation = this.spaceProductRepository.create({ - space, - inheritedFromModel: allocation, - tags: allocation.tags, - product: allocation.product, - }); - await this.spaceProductRepository.save(newSpaceAllocation); - } - } - } + // const subspaces = await this.subspaceRepository.find({ + // where: { + // subSpaceModel: { uuid: subspaceModel.subspaceModel.uuid }, + // disabled: false, + // }, + // relations: [ + // 'productAllocations', + // 'productAllocations.product', + // 'productAllocations.tags', + // ], + // }); + // if (!subspaces.length) { + // return; + // } + // const allocationUuidsToRemove = subspaces.flatMap((subspace) => + // subspace.productAllocations.map((allocation) => allocation.uuid), + // ); + // if (allocationUuidsToRemove.length) { + // await this.subspaceProductRepository.delete(allocationUuidsToRemove); + // } + // await this.subspaceRepository.update( + // { uuid: In(subspaces.map((s) => s.uuid)) }, + // { disabled: true }, + // ); + // const relocatedAllocations = subspaceModel.relocatedAllocations || []; + // if (!relocatedAllocations.length) { + // return; + // } + // for (const space of spaces) { + // for (const { allocation, tags = [] } of relocatedAllocations) { + // const spaceAllocation = await this.spaceProductRepository.findOne({ + // where: { + // inheritedFromModel: { uuid: allocation.uuid }, + // space: { uuid: space.uuid }, + // }, + // relations: ['tags'], + // }); + // if (spaceAllocation) { + // if (tags.length) { + // spaceAllocation.tags.push(...tags); + // await this.spaceProductRepository.save(spaceAllocation); + // } + // } else { + // const newSpaceAllocation = this.spaceProductRepository.create({ + // space, + // inheritedFromModel: allocation, + // tag: allocation.tag, + // product: allocation.product, + // }); + // await this.spaceProductRepository.save(newSpaceAllocation); + // } + // } + // } } async updateSubspaceModel(subspaceModel: ISingleSubspaceModel) { diff --git a/src/space-model/interfaces/index.ts b/src/space-model/interfaces/index.ts index 61561f8..b1ba78f 100644 --- a/src/space-model/interfaces/index.ts +++ b/src/space-model/interfaces/index.ts @@ -1,4 +1,3 @@ -export * from './update-subspace.interface'; -export * from './modify-subspace.interface'; export * from './single-subspace.interface'; export * from './space-product-allocation.interface'; +export * from './update-subspace.interface'; diff --git a/src/space-model/interfaces/modify-subspace.interface.ts b/src/space-model/interfaces/modify-subspace.interface.ts deleted file mode 100644 index 8969baf..0000000 --- a/src/space-model/interfaces/modify-subspace.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SubspaceModelEntity, TagModel } from '@app/common/modules/space-model'; - -export interface ModifyspaceModelPayload { - modifiedSubspaceModels?: ModifySubspaceModelPayload; - modifiedTags?: ModifiedTagsModelPayload; -} - -export interface ModifySubspaceModelPayload { - addedSubspaceModels?: SubspaceModelEntity[]; - updatedSubspaceModels?: UpdatedSubspaceModelPayload[]; - deletedSubspaceModels?: string[]; -} - -export interface UpdatedSubspaceModelPayload { - subspaceName?: string; - modifiedTags?: ModifiedTagsModelPayload; - subspaceModelUuid: string; -} - -export interface ModifiedTagsModelPayload { - added?: TagModel[]; - updated?: TagModel[]; - deleted?: string[]; -} diff --git a/src/space-model/interfaces/single-subspace.interface.ts b/src/space-model/interfaces/single-subspace.interface.ts index 76fce50..bc1c8f0 100644 --- a/src/space-model/interfaces/single-subspace.interface.ts +++ b/src/space-model/interfaces/single-subspace.interface.ts @@ -1,11 +1,11 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { SpaceModelProductAllocationEntity, SubspaceModelEntity, } from '@app/common/modules/space-model'; -import { ModifyTagModelDto } from '../dtos'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { NewTagEntity } from '@app/common/modules/tag'; import { IUpdatedAllocations } from './subspace-product-allocation-update-result.interface'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; export interface IRelocatedAllocation { allocation: SpaceModelProductAllocationEntity; @@ -14,7 +14,7 @@ export interface IRelocatedAllocation { export interface ISingleSubspaceModel { subspaceModel: SubspaceModelEntity; action: ModifyAction; - tags?: ModifyTagModelDto[]; + tags?: ModifyTagDto[]; relocatedAllocations?: IRelocatedAllocation[]; } diff --git a/src/space-model/services/space-model-product-allocation.service.ts b/src/space-model/services/space-model-product-allocation.service.ts index d8f78a6..6f8b25b 100644 --- a/src/space-model/services/space-model-product-allocation.service.ts +++ b/src/space-model/services/space-model-product-allocation.service.ts @@ -1,20 +1,19 @@ -import { In, QueryRunner } from 'typeorm'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { In, QueryRunner } from 'typeorm'; +import { ProductEntity } from '@app/common/modules/product/entities'; +import { ProjectEntity } from '@app/common/modules/project/entities'; +import { SpaceRepository } from '@app/common/modules/space'; import { SpaceModelEntity, SpaceModelProductAllocationEntity, SpaceModelProductAllocationRepoitory, - SubspaceModelProductAllocationEntity, } from '@app/common/modules/space-model'; -import { TagService as NewTagService } from 'src/tags/services'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { ModifySubspaceModelDto, ModifyTagModelDto } from '../dtos'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { NewTagEntity } from '@app/common/modules/tag'; -import { ProductEntity } from '@app/common/modules/product/entities'; -import { SpaceRepository } from '@app/common/modules/space'; -import { ProjectEntity } from '@app/common/modules/project/entities'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; +import { ProcessTagDto } from 'src/tags/dtos'; +import { TagService as NewTagService } from 'src/tags/services'; +import { ModifySubspaceModelDto } from '../dtos'; import { IUpdatedSpaceAllocations } from '../interfaces'; @Injectable() @@ -32,225 +31,220 @@ export class SpaceModelProductAllocationService { queryRunner?: QueryRunner, modifySubspaceModels?: ModifySubspaceModelDto[], ): Promise { - try { - if (!tags.length) return []; + // try { + if (!tags.length) return []; - const allocationUpdates: IUpdatedSpaceAllocations[] = []; + const allocationUpdates: IUpdatedSpaceAllocations[] = []; + return allocationUpdates; + // const processedTags = await this.tagService.processTags( + // tags, + // projectUuid, + // queryRunner, + // ); - const processedTags = await this.tagService.processTags( - tags, - projectUuid, - queryRunner, - ); + // const productAllocations: SpaceModelProductAllocationEntity[] = []; + // const existingAllocations = new Map< + // string, + // SpaceModelProductAllocationEntity + // >(); - const productAllocations: SpaceModelProductAllocationEntity[] = []; - const existingAllocations = new Map< - string, - SpaceModelProductAllocationEntity - >(); + // for (const tag of processedTags) { + // let isTagNeeded = true; - for (const tag of processedTags) { - let isTagNeeded = true; + // if (modifySubspaceModels) { + // const relatedSubspaces = await queryRunner.manager.find( + // SubspaceModelProductAllocationEntity, + // { + // where: { + // product: { uuid: tag.product.uuid }, + // subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, + // tags: { uuid: tag.uuid }, + // }, + // relations: ['subspaceModel', 'tags'], + // }, + // ); - if (modifySubspaceModels) { - const relatedSubspaces = await queryRunner.manager.find( - SubspaceModelProductAllocationEntity, - { - where: { - product: { uuid: tag.product.uuid }, - subspaceModel: { spaceModel: { uuid: spaceModel.uuid } }, - tags: { uuid: tag.uuid }, - }, - relations: ['subspaceModel', 'tags'], - }, - ); + // for (const subspaceWithTag of relatedSubspaces) { + // const modifyingSubspace = modifySubspaceModels.find( + // (subspace) => + // subspace.action === ModifyAction.UPDATE && + // subspace.uuid === subspaceWithTag.subspaceModel.uuid, + // ); - for (const subspaceWithTag of relatedSubspaces) { - const modifyingSubspace = modifySubspaceModels.find( - (subspace) => - subspace.action === ModifyAction.UPDATE && - subspace.uuid === subspaceWithTag.subspaceModel.uuid, - ); + // if ( + // modifyingSubspace && + // modifyingSubspace.tags && + // modifyingSubspace.tags.some( + // (subspaceTag) => + // subspaceTag.action === ModifyAction.DELETE && + // subspaceTag.tagUuid === tag.uuid, + // ) + // ) { + // isTagNeeded = true; + // break; + // } + // } + // } - if ( - modifyingSubspace && - modifyingSubspace.tags && - modifyingSubspace.tags.some( - (subspaceTag) => - subspaceTag.action === ModifyAction.DELETE && - subspaceTag.tagUuid === tag.uuid, - ) - ) { - isTagNeeded = true; - break; - } - } - } + // if (isTagNeeded) { + // const hasTags = await this.validateTagWithinSpaceModel( + // queryRunner, + // tag, + // spaceModel, + // ); - if (isTagNeeded) { - const hasTags = await this.validateTagWithinSpaceModel( - queryRunner, - tag, - spaceModel, - ); + // if (hasTags) continue; - if (hasTags) continue; + // let allocation = existingAllocations.get(tag.product.uuid); + // if (!allocation) { + // allocation = await this.getAllocationByProduct( + // tag.product, + // spaceModel, + // queryRunner, + // ); + // if (allocation) { + // existingAllocations.set(tag.product.uuid, allocation); + // } + // } - let allocation = existingAllocations.get(tag.product.uuid); - if (!allocation) { - allocation = await this.getAllocationByProduct( - tag.product, - spaceModel, - queryRunner, - ); - if (allocation) { - existingAllocations.set(tag.product.uuid, allocation); - } - } + // if (!allocation) { + // allocation = this.createNewAllocation(spaceModel, tag, queryRunner); + // productAllocations.push(allocation); + // allocationUpdates.push({ + // newAllocation: allocation, + // }); + // } else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) { + // allocation.tags.push(tag); + // allocationUpdates.push({ + // allocation: allocation, + // tagsAdded: [tag], + // }); + // await this.saveAllocation(allocation, queryRunner); + // } + // } + // } - if (!allocation) { - allocation = this.createNewAllocation(spaceModel, tag, queryRunner); - productAllocations.push(allocation); - allocationUpdates.push({ - newAllocation: allocation, - }); - } else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) { - allocation.tags.push(tag); - allocationUpdates.push({ - allocation: allocation, - tagsAdded: [tag], - }); - await this.saveAllocation(allocation, queryRunner); - } - } - } + // if (productAllocations.length > 0) { + // await this.saveAllocations(productAllocations, queryRunner); + // } - if (productAllocations.length > 0) { - await this.saveAllocations(productAllocations, queryRunner); - } - - return allocationUpdates; - } catch (error) { - throw this.handleError(error, 'Failed to create product allocations'); - } + // return allocationUpdates; + // } catch (error) { + // throw this.handleError(error, 'Failed to create product allocations'); + // } } async updateProductAllocations( - dtos: ModifyTagModelDto[], + dtos: ModifyTagDto[], project: ProjectEntity, spaceModel: SpaceModelEntity, queryRunner: QueryRunner, modifySubspaceModels?: ModifySubspaceModelDto[], ): Promise { - try { - const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD); - const deleteDtos = dtos.filter( - (dto) => dto.action === ModifyAction.DELETE, - ); - - const addTagDtos: ProcessTagDto[] = addDtos.map((dto) => ({ - name: dto.name, - productUuid: dto.productUuid, - uuid: dto.newTagUuid, - })); - - // Process added tags - const processedTags = await this.tagService.processTags( - addTagDtos, - project.uuid, - queryRunner, - ); - - const addTagUuidMap = new Map(); - processedTags.forEach((tag, index) => { - addTagUuidMap.set(tag.uuid, addDtos[index]); - }); - - const addTagUuids = new Set(processedTags.map((tag) => tag.uuid)); - const deleteTagUuids = new Set(deleteDtos.map((dto) => dto.tagUuid)); - - const tagsToIgnore = new Set( - [...addTagUuids].filter((uuid) => deleteTagUuids.has(uuid)), - ); - - // Filter out tags that are added and deleted in the same request - const filteredDtos = dtos.filter( - (dto) => - !( - tagsToIgnore.has(dto.tagUuid) || - (dto.action === ModifyAction.ADD && - tagsToIgnore.has( - [...addTagUuidMap.keys()].find( - (uuid) => addTagUuidMap.get(uuid) === dto, - ), - )) - ), - ); - - // Process add and delete actions concurrently - const [updatedAllocations, deletedAllocations] = await Promise.all([ - this.processAddActions( - filteredDtos, - project.uuid, - spaceModel, - queryRunner, - modifySubspaceModels, - ), - this.processDeleteActions(filteredDtos, queryRunner, spaceModel), - ]); - - // Combine results and return - return [...updatedAllocations, ...deletedAllocations]; - } catch (error) { - throw this.handleError(error, 'Error while updating product allocations'); - } + const allocationUpdates: IUpdatedSpaceAllocations[] = []; + return allocationUpdates; + // try { + // const addDtos = dtos.filter((dto) => dto.action === ModifyAction.ADD); + // const deleteDtos = dtos.filter( + // (dto) => dto.action === ModifyAction.DELETE, + // ); + // const addTagDtos: ProcessTagDto[] = addDtos.map((dto) => ({ + // name: dto.name, + // productUuid: dto.productUuid, + // uuid: dto.newTagUuid, + // })); + // // Process added tags + // const processedTags = await this.tagService.processTags( + // addTagDtos, + // project.uuid, + // queryRunner, + // ); + // const addTagUuidMap = new Map(); + // processedTags.forEach((tag, index) => { + // addTagUuidMap.set(tag.uuid, addDtos[index]); + // }); + // const addTagUuids = new Set(processedTags.map((tag) => tag.uuid)); + // const deleteTagUuids = new Set(deleteDtos.map((dto) => dto.tagUuid)); + // const tagsToIgnore = new Set( + // [...addTagUuids].filter((uuid) => deleteTagUuids.has(uuid)), + // ); + // // Filter out tags that are added and deleted in the same request + // const filteredDtos = dtos.filter( + // (dto) => + // !( + // tagsToIgnore.has(dto.tagUuid) || + // (dto.action === ModifyAction.ADD && + // tagsToIgnore.has( + // [...addTagUuidMap.keys()].find( + // (uuid) => addTagUuidMap.get(uuid) === dto, + // ), + // )) + // ), + // ); + // // Process add and delete actions concurrently + // const [updatedAllocations, deletedAllocations] = await Promise.all([ + // this.processAddActions( + // filteredDtos, + // project.uuid, + // spaceModel, + // queryRunner, + // modifySubspaceModels, + // ), + // this.processDeleteActions(filteredDtos, queryRunner, spaceModel), + // ]); + // // Combine results and return + // return [...updatedAllocations, ...deletedAllocations]; + // } catch (error) { + // throw this.handleError(error, 'Error while updating product allocations'); + // } } private async processAddActions( - dtos: ModifyTagModelDto[], + dtos: ModifyTagDto[], projectUuid: string, spaceModel: SpaceModelEntity, queryRunner: QueryRunner, modifySubspaceModels?: ModifySubspaceModelDto[], ): Promise { - let allocationUpdates: IUpdatedSpaceAllocations[] = []; - - const addDtos: ProcessTagDto[] = dtos - .filter((dto) => dto.action === ModifyAction.ADD) - .map((dto) => ({ - name: dto.name, - productUuid: dto.productUuid, - uuid: dto.newTagUuid, - })); - - if (addDtos.length > 0) { - allocationUpdates = await this.createProductAllocations( - projectUuid, - spaceModel, - addDtos, - queryRunner, - modifySubspaceModels, - ); - } + const allocationUpdates: IUpdatedSpaceAllocations[] = []; return allocationUpdates; + // const addDtos: ProcessTagDto[] = dtos + // .filter((dto) => dto.action === ModifyAction.ADD) + // .map((dto) => ({ + // name: dto.name, + // productUuid: dto.productUuid, + // uuid: dto.newTagUuid, + // })); + + // if (addDtos.length > 0) { + // allocationUpdates = await this.createProductAllocations( + // projectUuid, + // spaceModel, + // addDtos, + // queryRunner, + // modifySubspaceModels, + // ); + // } + // return allocationUpdates; } private createNewAllocation( spaceModel: SpaceModelEntity, tag: NewTagEntity, queryRunner?: QueryRunner, - ): SpaceModelProductAllocationEntity { - return queryRunner - ? queryRunner.manager.create(SpaceModelProductAllocationEntity, { - spaceModel, - product: tag.product, - tags: [tag], - }) - : this.spaceModelProductAllocationRepository.create({ - spaceModel, - product: tag.product, - tags: [tag], - }); + ) { + // : SpaceModelProductAllocationEntity + // return queryRunner + // ? queryRunner.manager.create(SpaceModelProductAllocationEntity, { + // spaceModel, + // product: tag.product, + // tags: [tag], + // }) + // : this.spaceModelProductAllocationRepository.create({ + // spaceModel, + // product: tag.product, + // tags: [tag], + // }); } private async getAllocationByProduct( @@ -309,7 +303,7 @@ export class SpaceModelProductAllocationService { } private async processDeleteActions( - dtos: ModifyTagModelDto[], + dtos: ModifyTagDto[], queryRunner: QueryRunner, spaceModel: SpaceModelEntity, ): Promise { @@ -317,10 +311,10 @@ export class SpaceModelProductAllocationService { if (!dtos || dtos.length === 0) { return; } - let allocationUpdateToPropagate: IUpdatedSpaceAllocations[] = []; + const allocationUpdateToPropagate: IUpdatedSpaceAllocations[] = []; const tagUuidsToDelete = dtos - .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) + // .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) .map((dto) => dto.tagUuid); if (tagUuidsToDelete.length === 0) return []; @@ -329,7 +323,7 @@ export class SpaceModelProductAllocationService { SpaceModelProductAllocationEntity, { where: { - tags: { uuid: In(tagUuidsToDelete) }, + tag: In(tagUuidsToDelete), spaceModel: { uuid: spaceModel.uuid, }, @@ -348,31 +342,28 @@ export class SpaceModelProductAllocationService { const allocationUpdates: SpaceModelProductAllocationEntity[] = []; for (const allocation of allocationsToUpdate) { - const updatedTags = allocation.tags.filter( - (tag) => !tagUuidsToDelete.includes(tag.uuid), - ); - - const deletedTags = allocation.tags.filter((tag) => - tagUuidsToDelete.includes(tag.uuid), - ); - - if (updatedTags.length === allocation.tags.length) { - continue; - } - - if (updatedTags.length === 0) { - deletedAllocations.push(allocation); - allocationUpdateToPropagate.push({ - deletedAllocation: allocation, - }); - } else { - allocation.tags = updatedTags; - allocationUpdates.push(allocation); - allocationUpdateToPropagate.push({ - allocation: allocation, - tagsRemoved: deletedTags, - }); - } + // const updatedTags = allocation.tags.filter( + // (tag) => !tagUuidsToDelete.includes(tag.uuid), + // ); + // const deletedTags = allocation.tags.filter((tag) => + // tagUuidsToDelete.includes(tag.uuid), + // ); + // if (updatedTags.length === allocation.tags.length) { + // continue; + // } + // if (updatedTags.length === 0) { + // deletedAllocations.push(allocation); + // allocationUpdateToPropagate.push({ + // deletedAllocation: allocation, + // }); + // } else { + // allocation.tags = updatedTags; + // allocationUpdates.push(allocation); + // allocationUpdateToPropagate.push({ + // allocation: allocation, + // tagsRemoved: deletedTags, + // }); + // } } if (allocationUpdates.length > 0) { @@ -415,33 +406,34 @@ export class SpaceModelProductAllocationService { tag: NewTagEntity, spaceModel: SpaceModelEntity, ): Promise { - const existingAllocationsForProduct = await queryRunner.manager.find( - SpaceModelProductAllocationEntity, - { - where: { - spaceModel: { - uuid: spaceModel.uuid, - }, - product: { - uuid: tag.product.uuid, - }, - }, - relations: ['tags'], - }, - ); + return true; + // const existingAllocationsForProduct = await queryRunner.manager.find( + // SpaceModelProductAllocationEntity, + // { + // where: { + // spaceModel: { + // uuid: spaceModel.uuid, + // }, + // product: { + // uuid: tag.product.uuid, + // }, + // }, + // relations: ['tags'], + // }, + // ); - const existingTagsForProduct = existingAllocationsForProduct.flatMap( - (allocation) => allocation.tags, - ); + // const existingTagsForProduct = existingAllocationsForProduct.flatMap( + // (allocation) => allocation.tags, + // ); - const isDuplicateTag = existingTagsForProduct.some( - (existingTag) => existingTag.uuid === tag.uuid, - ); + // const isDuplicateTag = existingTagsForProduct.some( + // (existingTag) => existingTag.uuid === tag.uuid, + // ); - if (isDuplicateTag) { - return true; - } - return false; + // if (isDuplicateTag) { + // return true; + // } + // return false; } async clearAllAllocations(spaceModelUuid: string, queryRunner: QueryRunner) { diff --git a/src/space-model/services/space-model.service.ts b/src/space-model/services/space-model.service.ts index 22b7aef..47d3c82 100644 --- a/src/space-model/services/space-model.service.ts +++ b/src/space-model/services/space-model.service.ts @@ -1,58 +1,47 @@ -import { - SpaceModelEntity, - SpaceModelProductAllocationEntity, - SpaceModelRepository, - SubspaceModelProductAllocationEntity, -} from '@app/common/modules/space-model'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { - CreateSpaceModelDto, - LinkSpacesToModelDto, - UpdateSpaceModelDto, -} from '../dtos'; -import { ProjectParam } from 'src/community/dtos'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { SubSpaceModelService } from './subspace/subspace-model.service'; -import { DataSource, In, QueryRunner, SelectQueryBuilder } from 'typeorm'; import { TypeORMCustomModel, TypeORMCustomModelFindAllQuery, } from '@app/common/models/typeOrmCustom.model'; -import { SpaceModelParam } from '../dtos/space-model-param'; -import { ProjectService } from 'src/project/services'; import { ProjectEntity } from '@app/common/modules/project/entities'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { + SpaceProductAllocationRepository, + SpaceRepository, +} from '@app/common/modules/space'; +import { + SpaceModelEntity, + SpaceModelRepository, +} from '@app/common/modules/space-model'; +import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; +import { ProjectParam } from 'src/community/dtos'; +import { DeviceService } from 'src/device/services'; +import { ProjectService } from 'src/project/services'; import { ProcessTagDto } from 'src/tags/dtos'; -import { SpaceModelProductAllocationService } from './space-model-product-allocation.service'; +import { DataSource, In, QueryRunner, SelectQueryBuilder } from 'typeorm'; import { PropogateDeleteSpaceModelCommand, PropogateUpdateSpaceModelCommand, PropogateUpdateSpaceModelProductAllocationCommand, } from '../commands'; import { - SpaceProductAllocationRepository, - SpaceRepository, -} from '@app/common/modules/space'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { - SubspaceProductAllocationRepository, - SubspaceRepository, -} from '@app/common/modules/space/repositories/subspace.repository'; -import { - ORPHAN_COMMUNITY_NAME, - ORPHAN_SPACE_NAME, -} from '@app/common/constants/orphan-constant'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; -import { DeviceEntity } from '@app/common/modules/device/entities'; -import { - ISingleSubspaceModel, - ISubspaceModelUpdates, - IUpdatedSpaceAllocations, -} from '../interfaces'; + CreateSpaceModelDto, + LinkSpacesToModelDto, + UpdateSpaceModelDto, +} from '../dtos'; +import { SpaceModelParam } from '../dtos/space-model-param'; +import { ISubspaceModelUpdates, IUpdatedSpaceAllocations } from '../interfaces'; +import { SpaceModelProductAllocationService } from './space-model-product-allocation.service'; +import { SubSpaceModelService } from './subspace'; @Injectable() export class SpaceModelService { @@ -60,6 +49,7 @@ export class SpaceModelService { private readonly dataSource: DataSource, private readonly spaceModelRepository: SpaceModelRepository, private readonly projectService: ProjectService, + private readonly deviceService: DeviceService, private readonly subSpaceModelService: SubSpaceModelService, private commandBus: CommandBus, private readonly spaceModelProductAllocationService: SpaceModelProductAllocationService, @@ -67,7 +57,6 @@ export class SpaceModelService { private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository, private readonly subspaceRepository: SubspaceRepository, private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository, - private readonly deviceRepository: DeviceRepository, ) {} async createSpaceModel( @@ -161,7 +150,7 @@ export class SpaceModelService { disabled: false, }; pageable.include = - 'subspaceModels.productAllocations,subspaceModelProductAllocations.tags,subspaceModels, productAllocations, productAllocations.tags'; + 'subspaceModels.productAllocations,subspaceModelProductAllocations.tag,subspaceModelProductAllocations.product,subspaceModels, productAllocations, productAllocations.tag,productAllocations.product'; const queryBuilder = this.buildSpaceModelQuery(param.projectUuid); @@ -231,7 +220,7 @@ export class SpaceModelService { spaceModel, queryRunner, param.projectUuid, - dto.tags, + // dto.tags, ); } @@ -386,10 +375,10 @@ export class SpaceModelService { 'productAllocations', 'subspaceModels', 'productAllocations.product', - 'productAllocations.tags', + 'productAllocations.tag', 'subspaceModels.productAllocations', 'subspaceModels.productAllocations.product', - 'subspaceModels.productAllocations.tags', + 'subspaceModels.productAllocations.tag', ], }); @@ -408,10 +397,10 @@ export class SpaceModelService { 'subspaces', 'productAllocations', 'productAllocations.product', - 'productAllocations.tags', + 'productAllocations.tag', 'subspaces.productAllocations', 'subspaces.productAllocations.product', - 'subspaces.productAllocations.product.tags', + 'subspaces.productAllocations.product.tag', 'community', ], }); @@ -433,7 +422,7 @@ export class SpaceModelService { if (!hasDependencies && !space.spaceModel) { await this.linkToSpace(space, spaceModel); } else if (dto.overwrite) { - await this.overwriteSpace(space, project); + await this.removeSpaceOldSubspacesAndAllocations(space, project); await this.linkToSpace(space, spaceModel); } }), @@ -470,10 +459,9 @@ export class SpaceModelService { space, inheritedFromModel: modelAllocation, product: modelAllocation.product, - tags: modelAllocation.tags, + tag: modelAllocation.tag, }), ); - if (queryRunner) { await queryRunner.manager.save( SpaceProductAllocationEntity, @@ -491,23 +479,20 @@ export class SpaceModelService { subSpaceModel: subspaceModel, space: space, }); - if (queryRunner) { await queryRunner.manager.save(SubspaceEntity, subspace); } else { await this.subspaceRepository.save(subspace); } - const subspaceAllocations = subspaceModel.productAllocations.map( (modelAllocation) => this.subspaceProductAllocationRepository.create({ subspace, inheritedFromModel: modelAllocation, product: modelAllocation.product, - tags: modelAllocation.tags, + tag: modelAllocation.tag, }), ); - if (subspaceAllocations.length) { if (queryRunner) { await queryRunner.manager.save( @@ -530,7 +515,7 @@ export class SpaceModelService { } } - async overwriteSpace( + async removeSpaceOldSubspacesAndAllocations( space: SpaceEntity, project: ProjectEntity, queryRunner?: QueryRunner, @@ -548,14 +533,6 @@ export class SpaceModelService { ? queryRunner.manager.getRepository(SubspaceProductAllocationEntity) : this.subspaceProductAllocationRepository; - const spaceRepository = queryRunner - ? queryRunner.manager.getRepository(SpaceEntity) - : this.spaceRepository; - - const deviceRepository = queryRunner - ? queryRunner.manager.getRepository(DeviceEntity) - : this.deviceRepository; - if (space.productAllocations.length) { await spaceProductAllocationRepository.delete({ uuid: In( @@ -584,26 +561,7 @@ export class SpaceModelService { ); if (space.devices.length > 0) { - const orphanSpace = await spaceRepository.findOne({ - where: { - community: { - name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, - }, - spaceName: ORPHAN_SPACE_NAME, - }, - }); - - if (!orphanSpace) { - throw new HttpException( - `Orphan space not found in community ${project.name}`, - HttpStatus.NOT_FOUND, - ); - } - - await deviceRepository.update( - { uuid: In(space.devices.map((device) => device.uuid)) }, - { spaceDevice: orphanSpace }, - ); + this.deviceService.addDevicesToOrphanSpace(space, project, queryRunner); } } catch (error) { throw new Error( @@ -612,19 +570,6 @@ export class SpaceModelService { } } - async validateName(modelName: string, projectUuid: string): Promise { - const isModelExist = await this.spaceModelRepository.findOne({ - where: { modelName, project: { uuid: projectUuid }, disabled: false }, - }); - - if (isModelExist) { - throw new HttpException( - `Model name ${modelName} already exists in the project with UUID ${projectUuid}.`, - HttpStatus.CONFLICT, - ); - } - } - async validateNameUsingQueryRunner( modelName: string, projectUuid: string, @@ -726,14 +671,20 @@ export class SpaceModelService { 'subspaceModelProductAllocations', ) .leftJoinAndSelect( - 'subspaceModelProductAllocations.tags', - 'subspaceModelTags', + 'subspaceModelProductAllocations.tag', + 'subspaceModelTag', + ) + .leftJoinAndSelect( + 'subspaceModelProductAllocations.product', + 'subspaceModelProduct', ) - .leftJoinAndSelect('subspaceModelTags.product', 'subspaceModelTagProduct') .leftJoinAndSelect('spaceModel.productAllocations', 'productAllocations') .leftJoinAndSelect('productAllocations.product', 'allocatedProduct') - .leftJoinAndSelect('productAllocations.tags', 'productTags') - .leftJoinAndSelect('productTags.product', 'productTagProduct') + .leftJoinAndSelect('productAllocations.tag', 'spaceModelTag') + .leftJoinAndSelect( + 'productAllocations.product', + 'productAllocationsProduct', + ) .where('spaceModel.disabled = false') .andWhere('spaceModel.project = :projectUuid', { projectUuid }); } @@ -753,42 +704,12 @@ export class SpaceModelService { updatedAt: subspace.updatedAt, subspaceName: subspace.subspaceName, disabled: subspace.disabled, - tags: this.extractTags(subspace.productAllocations), + productAllocations: subspace.productAllocations, })), - tags: this.extractTags(spaceModel.productAllocations), + productAllocations: spaceModel.productAllocations, })); } - private extractTags( - productAllocations: - | SpaceModelProductAllocationEntity[] - | SubspaceModelProductAllocationEntity[] - | undefined, - ): any[] { - if (!productAllocations) return []; - - return productAllocations - .flatMap((allocation) => allocation.tags ?? []) - .map((tag) => ({ - uuid: tag.uuid, - createdAt: tag.createdAt, - updatedAt: tag.updatedAt, - name: tag.name, - disabled: tag.disabled, - product: tag.product - ? { - uuid: tag.product.uuid, - createdAt: tag.product.createdAt, - updatedAt: tag.product.updatedAt, - catName: tag.product.catName, - prodId: tag.product.prodId, - name: tag.product.name, - prodType: tag.product.prodType, - } - : null, - })); - } - private formatSpaceModelResponse(spaceModel: SpaceModelEntity): any { return { uuid: spaceModel.uuid, @@ -803,9 +724,9 @@ export class SpaceModelService { updatedAt: subspace.updatedAt, subspaceName: subspace.subspaceName, disabled: subspace.disabled, - tags: this.extractTags(subspace.productAllocations), + productAllocations: subspace.productAllocations, })) ?? [], - tags: this.extractTags(spaceModel.productAllocations), + productAllocations: spaceModel.productAllocations, }; } } diff --git a/src/space-model/services/subspace/subspace-model-product-allocation.service.ts b/src/space-model/services/subspace/subspace-model-product-allocation.service.ts index 280a7d8..4347ec9 100644 --- a/src/space-model/services/subspace/subspace-model-product-allocation.service.ts +++ b/src/space-model/services/subspace/subspace-model-product-allocation.service.ts @@ -9,10 +9,9 @@ import { } from '@app/common/modules/space-model'; import { NewTagEntity } from '@app/common/modules/tag'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { ModifyTagModelDto } from 'src/space-model/dtos'; import { ISingleSubspaceModel } from 'src/space-model/interfaces'; import { IUpdatedAllocations } from 'src/space-model/interfaces/subspace-product-allocation-update-result.interface'; -import { ProcessTagDto } from 'src/tags/dtos'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; import { TagService as NewTagService } from 'src/tags/services'; import { In, QueryRunner } from 'typeorm'; @@ -31,172 +30,159 @@ export class SubspaceModelProductAllocationService { queryRunner?: QueryRunner, spaceAllocationsToExclude?: SpaceModelProductAllocationEntity[], ): Promise { - try { - const updatedAllocations: IUpdatedAllocations[] = []; - - const allocations: SubspaceModelProductAllocationEntity[] = []; - - for (const tag of tags) { - // Step 1: Check if this specific tag is already allocated at the space level - const existingTagInSpaceModel = await (queryRunner - ? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, { - where: { - spaceModel: { - uuid: spaceModel.uuid, - }, // Check at the space level - tags: { uuid: tag.uuid }, // Check for the specific tag - }, - relations: ['tags'], - }) - : this.spaceModelAllocationRepository.findOne({ - where: { - spaceModel: { - uuid: spaceModel.uuid, - }, - tags: { uuid: tag.uuid }, - }, - relations: ['tags', 'product'], - })); - - const isExcluded = spaceAllocationsToExclude?.some( - (excludedAllocation) => - excludedAllocation.product.uuid === tag.product.uuid && - excludedAllocation.tags.some((t) => t.uuid === tag.uuid), - ); - - // If tag is found at the space level, prevent allocation at the subspace level - if (!isExcluded && existingTagInSpaceModel) { - throw new HttpException( - `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspaceModel.spaceModel.uuid}). Cannot allocate the same tag in a subspace.`, - HttpStatus.BAD_REQUEST, - ); - } - - // Check if this specific tag is already allocated within another subspace of the same space - const existingTagInSameSpace = await (queryRunner - ? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, { - where: { - product: { uuid: tag.product.uuid }, - subspaceModel: { spaceModel: subspaceModel.spaceModel }, - tags: { uuid: tag.uuid }, // Ensure the exact tag is checked - }, - relations: ['subspaceModel', 'tags'], - }) - : this.subspaceModelProductAllocationRepository.findOne({ - where: { - product: { uuid: tag.product.uuid }, - subspaceModel: { spaceModel: subspaceModel.spaceModel }, - tags: { uuid: tag.uuid }, - }, - relations: ['subspaceModel', 'tags'], - })); - - // Prevent duplicate allocation if tag exists in another subspace of the same space - if ( - existingTagInSameSpace && - existingTagInSameSpace.subspaceModel.uuid !== subspaceModel.uuid - ) { - throw new HttpException( - `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspaceModel.uuid}) within the same space (${subspaceModel.spaceModel.uuid}).`, - HttpStatus.BAD_REQUEST, - ); - } - - //Check if there are existing allocations for this product in the subspace - const existingAllocationsForProduct = await (queryRunner - ? queryRunner.manager.find(SubspaceModelProductAllocationEntity, { - where: { - subspaceModel: { uuid: subspaceModel.uuid }, - product: { uuid: tag.product.uuid }, - }, - relations: ['tags'], - }) - : this.subspaceModelProductAllocationRepository.find({ - where: { - subspaceModel: { uuid: subspaceModel.uuid }, - product: { uuid: tag.product.uuid }, - }, - relations: ['tags'], - })); - - //Flatten all existing tags for this product in the subspace - const existingTagsForProduct = existingAllocationsForProduct.flatMap( - (allocation) => allocation.tags, - ); - - // Check if the tag is already assigned to the same product in this subspace - const isDuplicateTag = existingTagsForProduct.some( - (existingTag) => existingTag.uuid === tag.uuid, - ); - - if (isDuplicateTag) { - throw new HttpException( - `Tag ${tag.uuid} is already allocated to product ${tag.product.uuid} within this subspace (${subspaceModel.uuid}).`, - HttpStatus.BAD_REQUEST, - ); - } - - // If no existing allocation, create a new one - if (existingAllocationsForProduct.length === 0) { - const allocation = queryRunner - ? queryRunner.manager.create(SubspaceModelProductAllocationEntity, { - subspaceModel, - product: tag.product, - tags: [tag], - }) - : this.subspaceModelProductAllocationRepository.create({ - subspaceModel, - product: tag.product, - tags: [tag], - }); - allocations.push(allocation); - } else { - //If allocation exists, add the tag to it - existingAllocationsForProduct[0].tags.push(tag); - if (queryRunner) { - await queryRunner.manager.save( - SubspaceModelProductAllocationEntity, - existingAllocationsForProduct[0], - ); - } else { - await this.subspaceModelProductAllocationRepository.save( - existingAllocationsForProduct[0], - ); - } - updatedAllocations.push({ - allocation: existingAllocationsForProduct[0], - tagsAdded: [tag], - }); - } - } - - // Save newly created allocations - if (allocations.length > 0) { - if (queryRunner) { - await queryRunner.manager.save( - SubspaceModelProductAllocationEntity, - allocations, - ); - } else { - await this.subspaceModelProductAllocationRepository.save(allocations); - } - - allocations.forEach((allocation) => { - updatedAllocations.push({ newAllocation: allocation }); - }); - } - - return updatedAllocations; - } catch (error) { - throw new HttpException( - `An unexpected error occurred while creating subspace product allocations ${error}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } + // try { + const updatedAllocations: IUpdatedAllocations[] = []; + return updatedAllocations; + // const allocations: SubspaceModelProductAllocationEntity[] = []; + // for (const tag of tags) { + // // Step 1: Check if this specific tag is already allocated at the space level + // const existingTagInSpaceModel = await (queryRunner + // ? queryRunner.manager.findOne(SpaceModelProductAllocationEntity, { + // where: { + // spaceModel: { + // uuid: spaceModel.uuid, + // }, // Check at the space level + // tags: { uuid: tag.uuid }, // Check for the specific tag + // }, + // relations: ['tags'], + // }) + // : this.spaceModelAllocationRepository.findOne({ + // where: { + // spaceModel: { + // uuid: spaceModel.uuid, + // }, + // tags: { uuid: tag.uuid }, + // }, + // relations: ['tags', 'product'], + // })); + // const isExcluded = spaceAllocationsToExclude?.some( + // (excludedAllocation) => + // excludedAllocation.product.uuid === tag.product.uuid && + // excludedAllocation.tags.some((t) => t.uuid === tag.uuid), + // ); + // // If tag is found at the space level, prevent allocation at the subspace level + // if (!isExcluded && existingTagInSpaceModel) { + // throw new HttpException( + // `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspaceModel.spaceModel.uuid}). Cannot allocate the same tag in a subspace.`, + // HttpStatus.BAD_REQUEST, + // ); + // } + // // Check if this specific tag is already allocated within another subspace of the same space + // const existingTagInSameSpace = await (queryRunner + // ? queryRunner.manager.findOne(SubspaceModelProductAllocationEntity, { + // where: { + // product: { uuid: tag.product.uuid }, + // subspaceModel: { spaceModel: subspaceModel.spaceModel }, + // tags: { uuid: tag.uuid }, // Ensure the exact tag is checked + // }, + // relations: ['subspaceModel', 'tags'], + // }) + // : this.subspaceModelProductAllocationRepository.findOne({ + // where: { + // product: { uuid: tag.product.uuid }, + // subspaceModel: { spaceModel: subspaceModel.spaceModel }, + // tags: { uuid: tag.uuid }, + // }, + // relations: ['subspaceModel', 'tags'], + // })); + // // Prevent duplicate allocation if tag exists in another subspace of the same space + // if ( + // existingTagInSameSpace && + // existingTagInSameSpace.subspaceModel.uuid !== subspaceModel.uuid + // ) { + // throw new HttpException( + // `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspaceModel.uuid}) within the same space (${subspaceModel.spaceModel.uuid}).`, + // HttpStatus.BAD_REQUEST, + // ); + // } + // //Check if there are existing allocations for this product in the subspace + // const existingAllocationsForProduct = await (queryRunner + // ? queryRunner.manager.find(SubspaceModelProductAllocationEntity, { + // where: { + // subspaceModel: { uuid: subspaceModel.uuid }, + // product: { uuid: tag.product.uuid }, + // }, + // relations: ['tags'], + // }) + // : this.subspaceModelProductAllocationRepository.find({ + // where: { + // subspaceModel: { uuid: subspaceModel.uuid }, + // product: { uuid: tag.product.uuid }, + // }, + // relations: ['tags'], + // })); + // //Flatten all existing tags for this product in the subspace + // const existingTagsForProduct = existingAllocationsForProduct.flatMap( + // (allocation) => allocation.tags, + // ); + // // Check if the tag is already assigned to the same product in this subspace + // const isDuplicateTag = existingTagsForProduct.some( + // (existingTag) => existingTag.uuid === tag.uuid, + // ); + // if (isDuplicateTag) { + // throw new HttpException( + // `Tag ${tag.uuid} is already allocated to product ${tag.product.uuid} within this subspace (${subspaceModel.uuid}).`, + // HttpStatus.BAD_REQUEST, + // ); + // } + // // If no existing allocation, create a new one + // if (existingAllocationsForProduct.length === 0) { + // const allocation = queryRunner + // ? queryRunner.manager.create(SubspaceModelProductAllocationEntity, { + // subspaceModel, + // product: tag.product, + // tags: [tag], + // }) + // : this.subspaceModelProductAllocationRepository.create({ + // subspaceModel, + // product: tag.product, + // tags: [tag], + // }); + // allocations.push(allocation); + // } else { + // //If allocation exists, add the tag to it + // existingAllocationsForProduct[0].tags.push(tag); + // if (queryRunner) { + // await queryRunner.manager.save( + // SubspaceModelProductAllocationEntity, + // existingAllocationsForProduct[0], + // ); + // } else { + // await this.subspaceModelProductAllocationRepository.save( + // existingAllocationsForProduct[0], + // ); + // } + // updatedAllocations.push({ + // allocation: existingAllocationsForProduct[0], + // tagsAdded: [tag], + // }); + // } + // } + // // Save newly created allocations + // if (allocations.length > 0) { + // if (queryRunner) { + // await queryRunner.manager.save( + // SubspaceModelProductAllocationEntity, + // allocations, + // ); + // } else { + // await this.subspaceModelProductAllocationRepository.save(allocations); + // } + // allocations.forEach((allocation) => { + // updatedAllocations.push({ newAllocation: allocation }); + // }); + // } + // return updatedAllocations; + // } catch (error) { + // throw new HttpException( + // `An unexpected error occurred while creating subspace product allocations ${error}`, + // HttpStatus.INTERNAL_SERVER_ERROR, + // ); + // } } async processDeleteActions( - dtos: ModifyTagModelDto[], + dtos: ModifyTagDto[], queryRunner: QueryRunner, ): Promise { try { @@ -205,7 +191,7 @@ export class SubspaceModelProductAllocationService { } const tagUuidsToDelete = dtos - .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) + // .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) .map((dto) => dto.tagUuid); if (tagUuidsToDelete.length === 0) return []; @@ -213,8 +199,7 @@ export class SubspaceModelProductAllocationService { const allocationsToUpdate = await queryRunner.manager.find( SubspaceModelProductAllocationEntity, { - where: { tags: { uuid: In(tagUuidsToDelete) } }, - relations: ['tags'], + where: { tag: In(tagUuidsToDelete) }, }, ); @@ -224,20 +209,18 @@ export class SubspaceModelProductAllocationService { const allocationUpdates: SubspaceModelProductAllocationEntity[] = []; for (const allocation of allocationsToUpdate) { - const updatedTags = allocation.tags.filter( - (tag) => !tagUuidsToDelete.includes(tag.uuid), - ); - - if (updatedTags.length === allocation.tags.length) { - continue; - } - - if (updatedTags.length === 0) { - deletedAllocations.push(allocation); - } else { - allocation.tags = updatedTags; - allocationUpdates.push(allocation); - } + // const updatedTags = allocation.tags.filter( + // (tag) => !tagUuidsToDelete.includes(tag.uuid), + // ); + // if (updatedTags.length === allocation.tags.length) { + // continue; + // } + // if (updatedTags.length === 0) { + // deletedAllocations.push(allocation); + // } else { + // allocation.tags = updatedTags; + // allocationUpdates.push(allocation); + // } } if (allocationUpdates.length > 0) { @@ -280,254 +263,255 @@ export class SubspaceModelProductAllocationService { projectUuid: string, queryRunner: QueryRunner, spaceModel: SpaceModelEntity, - spaceTagUpdateDtos?: ModifyTagModelDto[], - ): Promise { + spaceTagUpdateDtos?: ModifyTagDto[], + ) { + // : Promise const spaceAllocationToExclude: SpaceModelProductAllocationEntity[] = []; const updatedAllocations: IUpdatedAllocations[] = []; + return updatedAllocations; + // for (const subspaceModel of subspaceModels) { + // const tagDtos = subspaceModel.tags; + // if (tagDtos.length > 0) { + // const tagsToAddDto: ProcessTagDto[] = tagDtos + // .filter((dto) => dto.action === ModifyAction.ADD) + // .map((dto) => ({ + // name: dto.name, + // productUuid: dto.productUuid, + // uuid: dto.newTagUuid, + // })); - for (const subspaceModel of subspaceModels) { - const tagDtos = subspaceModel.tags; - if (tagDtos.length > 0) { - const tagsToAddDto: ProcessTagDto[] = tagDtos - .filter((dto) => dto.action === ModifyAction.ADD) - .map((dto) => ({ - name: dto.name, - productUuid: dto.productUuid, - uuid: dto.newTagUuid, - })); + // const tagsToDeleteDto = tagDtos.filter( + // (dto) => dto.action === ModifyAction.DELETE, + // ); - const tagsToDeleteDto = tagDtos.filter( - (dto) => dto.action === ModifyAction.DELETE, - ); + // if (tagsToAddDto.length > 0) { + // let processedTags = await this.tagService.processTags( + // tagsToAddDto, + // projectUuid, + // queryRunner, + // ); - if (tagsToAddDto.length > 0) { - let processedTags = await this.tagService.processTags( - tagsToAddDto, - projectUuid, - queryRunner, - ); + // for (const subspaceDto of subspaceModels) { + // if ( + // subspaceDto !== subspaceModel && + // subspaceDto.action === ModifyAction.UPDATE && + // subspaceDto.tags + // ) { + // // Tag is deleted from one subspace and added in another subspace + // const deletedTags = subspaceDto.tags.filter( + // (tagDto) => + // tagDto.action === ModifyAction.DELETE && + // processedTags.some((tag) => tag.uuid === tagDto.tagUuid), + // ); - for (const subspaceDto of subspaceModels) { - if ( - subspaceDto !== subspaceModel && - subspaceDto.action === ModifyAction.UPDATE && - subspaceDto.tags - ) { - // Tag is deleted from one subspace and added in another subspace - const deletedTags = subspaceDto.tags.filter( - (tagDto) => - tagDto.action === ModifyAction.DELETE && - processedTags.some((tag) => tag.uuid === tagDto.tagUuid), - ); + // for (const deletedTag of deletedTags) { + // const allocation = await queryRunner.manager.findOne( + // SubspaceModelProductAllocationEntity, + // { + // where: { + // tags: { + // uuid: deletedTag.tagUuid, + // }, + // subspaceModel: { + // uuid: subspaceDto.subspaceModel.uuid, + // }, + // }, + // relations: ['tags', 'product', 'subspaceModel'], + // }, + // ); + // if (allocation) { + // const isCommonTag = allocation.tags.some( + // (tag) => tag.uuid === deletedTag.tagUuid, + // ); - for (const deletedTag of deletedTags) { - const allocation = await queryRunner.manager.findOne( - SubspaceModelProductAllocationEntity, - { - where: { - tags: { - uuid: deletedTag.tagUuid, - }, - subspaceModel: { - uuid: subspaceDto.subspaceModel.uuid, - }, - }, - relations: ['tags', 'product', 'subspaceModel'], - }, - ); - if (allocation) { - const isCommonTag = allocation.tags.some( - (tag) => tag.uuid === deletedTag.tagUuid, - ); + // if (allocation && isCommonTag) { + // const tagEntity = allocation.tags.find( + // (tag) => tag.uuid === deletedTag.tagUuid, + // ); - if (allocation && isCommonTag) { - const tagEntity = allocation.tags.find( - (tag) => tag.uuid === deletedTag.tagUuid, - ); + // allocation.tags = allocation.tags.filter( + // (tag) => tag.uuid !== deletedTag.tagUuid, + // ); - allocation.tags = allocation.tags.filter( - (tag) => tag.uuid !== deletedTag.tagUuid, - ); + // updatedAllocations.push({ + // allocation, + // tagsRemoved: [tagEntity], + // }); - updatedAllocations.push({ - allocation, - tagsRemoved: [tagEntity], - }); + // await queryRunner.manager.save(allocation); - await queryRunner.manager.save(allocation); + // const productAllocationExistInSubspace = + // await queryRunner.manager.findOne( + // SubspaceModelProductAllocationEntity, + // { + // where: { + // subspaceModel: { + // uuid: subspaceDto.subspaceModel.uuid, + // }, + // product: { uuid: allocation.product.uuid }, + // }, + // relations: ['tags'], + // }, + // ); - const productAllocationExistInSubspace = - await queryRunner.manager.findOne( - SubspaceModelProductAllocationEntity, - { - where: { - subspaceModel: { - uuid: subspaceDto.subspaceModel.uuid, - }, - product: { uuid: allocation.product.uuid }, - }, - relations: ['tags'], - }, - ); + // if (productAllocationExistInSubspace) { + // productAllocationExistInSubspace.tags.push(tagEntity); - if (productAllocationExistInSubspace) { - productAllocationExistInSubspace.tags.push(tagEntity); + // updatedAllocations.push({ + // allocation: productAllocationExistInSubspace, + // tagsAdded: [tagEntity], + // }); - updatedAllocations.push({ - allocation: productAllocationExistInSubspace, - tagsAdded: [tagEntity], - }); + // await queryRunner.manager.save( + // productAllocationExistInSubspace, + // ); + // } else { + // const newProductAllocation = queryRunner.manager.create( + // SubspaceModelProductAllocationEntity, + // { + // subspaceModel: subspaceModel.subspaceModel, + // product: allocation.product, + // tags: [tagEntity], + // }, + // ); - await queryRunner.manager.save( - productAllocationExistInSubspace, - ); - } else { - const newProductAllocation = queryRunner.manager.create( - SubspaceModelProductAllocationEntity, - { - subspaceModel: subspaceModel.subspaceModel, - product: allocation.product, - tags: [tagEntity], - }, - ); + // updatedAllocations.push({ + // allocation: newProductAllocation, + // }); - updatedAllocations.push({ - allocation: newProductAllocation, - }); + // await queryRunner.manager.save(newProductAllocation); + // } - await queryRunner.manager.save(newProductAllocation); - } + // // Remove the tag from processedTags to prevent duplication + // processedTags = processedTags.filter( + // (tag) => tag.uuid !== deletedTag.tagUuid, + // ); - // Remove the tag from processedTags to prevent duplication - processedTags = processedTags.filter( - (tag) => tag.uuid !== deletedTag.tagUuid, - ); + // // Remove the tag from subspaceDto.tags to ensure it's not processed again while processing other dtos. + // subspaceDto.tags = subspaceDto.tags.filter( + // (tagDto) => tagDto.tagUuid !== deletedTag.tagUuid, + // ); + // } + // } + // } + // } + // if ( + // subspaceDto !== subspaceModel && + // subspaceDto.action === ModifyAction.DELETE + // ) { + // const allocation = await queryRunner.manager.findOne( + // SubspaceModelProductAllocationEntity, + // { + // where: { + // subspaceModel: { uuid: subspaceDto.subspaceModel.uuid }, + // }, + // relations: ['tags'], + // }, + // ); - // Remove the tag from subspaceDto.tags to ensure it's not processed again while processing other dtos. - subspaceDto.tags = subspaceDto.tags.filter( - (tagDto) => tagDto.tagUuid !== deletedTag.tagUuid, - ); - } - } - } - } - if ( - subspaceDto !== subspaceModel && - subspaceDto.action === ModifyAction.DELETE - ) { - const allocation = await queryRunner.manager.findOne( - SubspaceModelProductAllocationEntity, - { - where: { - subspaceModel: { uuid: subspaceDto.subspaceModel.uuid }, - }, - relations: ['tags'], - }, - ); + // const repeatedTags = allocation?.tags.filter((tag) => + // processedTags.some( + // (processedTag) => processedTag.uuid === tag.uuid, + // ), + // ); + // if (repeatedTags.length > 0) { + // allocation.tags = allocation.tags.filter( + // (tag) => + // !repeatedTags.some( + // (repeatedTag) => repeatedTag.uuid === tag.uuid, + // ), + // ); - const repeatedTags = allocation?.tags.filter((tag) => - processedTags.some( - (processedTag) => processedTag.uuid === tag.uuid, - ), - ); - if (repeatedTags.length > 0) { - allocation.tags = allocation.tags.filter( - (tag) => - !repeatedTags.some( - (repeatedTag) => repeatedTag.uuid === tag.uuid, - ), - ); + // updatedAllocations.push({ + // allocation: allocation, + // tagsRemoved: repeatedTags, + // }); - updatedAllocations.push({ - allocation: allocation, - tagsRemoved: repeatedTags, - }); + // await queryRunner.manager.save(allocation); - await queryRunner.manager.save(allocation); + // const productAllocationExistInSubspace = + // await queryRunner.manager.findOne( + // SubspaceModelProductAllocationEntity, + // { + // where: { + // subspaceModel: { uuid: subspaceDto.subspaceModel.uuid }, + // product: { uuid: allocation.product.uuid }, + // }, + // relations: ['tags'], + // }, + // ); - const productAllocationExistInSubspace = - await queryRunner.manager.findOne( - SubspaceModelProductAllocationEntity, - { - where: { - subspaceModel: { uuid: subspaceDto.subspaceModel.uuid }, - product: { uuid: allocation.product.uuid }, - }, - relations: ['tags'], - }, - ); + // if (productAllocationExistInSubspace) { + // updatedAllocations.push({ + // allocation: productAllocationExistInSubspace, + // tagsAdded: repeatedTags, + // }); - if (productAllocationExistInSubspace) { - updatedAllocations.push({ - allocation: productAllocationExistInSubspace, - tagsAdded: repeatedTags, - }); + // productAllocationExistInSubspace.tags.push(...repeatedTags); + // await queryRunner.manager.save( + // productAllocationExistInSubspace, + // ); + // } else { + // const newProductAllocation = queryRunner.manager.create( + // SubspaceModelProductAllocationEntity, + // { + // subspaceModel: subspaceModel.subspaceModel, + // product: allocation.product, + // tags: repeatedTags, + // }, + // ); - productAllocationExistInSubspace.tags.push(...repeatedTags); - await queryRunner.manager.save( - productAllocationExistInSubspace, - ); - } else { - const newProductAllocation = queryRunner.manager.create( - SubspaceModelProductAllocationEntity, - { - subspaceModel: subspaceModel.subspaceModel, - product: allocation.product, - tags: repeatedTags, - }, - ); + // updatedAllocations.push({ + // newAllocation: newProductAllocation, + // }); - updatedAllocations.push({ - newAllocation: newProductAllocation, - }); + // await queryRunner.manager.save(newProductAllocation); + // } + // } + // } + // } + // if (spaceTagUpdateDtos) { + // const deletedSpaceTags = spaceTagUpdateDtos.filter( + // (tagDto) => + // tagDto.action === ModifyAction.DELETE && + // processedTags.some((tag) => tag.uuid === tagDto.tagUuid), + // ); + // for (const deletedTag of deletedSpaceTags) { + // const allocation = await queryRunner.manager.findOne( + // SpaceModelProductAllocationEntity, + // { + // where: { + // spaceModel: { uuid: spaceModel.uuid }, + // tags: { uuid: deletedTag.tagUuid }, + // }, + // relations: ['tags', 'product'], + // }, + // ); - await queryRunner.manager.save(newProductAllocation); - } - } - } - } - if (spaceTagUpdateDtos) { - const deletedSpaceTags = spaceTagUpdateDtos.filter( - (tagDto) => - tagDto.action === ModifyAction.DELETE && - processedTags.some((tag) => tag.uuid === tagDto.tagUuid), - ); - for (const deletedTag of deletedSpaceTags) { - const allocation = await queryRunner.manager.findOne( - SpaceModelProductAllocationEntity, - { - where: { - spaceModel: { uuid: spaceModel.uuid }, - tags: { uuid: deletedTag.tagUuid }, - }, - relations: ['tags', 'product'], - }, - ); + // if ( + // allocation && + // allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid) + // ) { + // spaceAllocationToExclude.push(allocation); + // } + // } + // } - if ( - allocation && - allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid) - ) { - spaceAllocationToExclude.push(allocation); - } - } - } - - // Create new product allocations - const newAllocations = await this.createProductAllocations( - subspaceModel.subspaceModel, - spaceModel, - processedTags, - queryRunner, - spaceAllocationToExclude, - ); - return [...updatedAllocations, ...newAllocations]; - } - if (tagsToDeleteDto.length > 0) { - await this.processDeleteActions(tagsToDeleteDto, queryRunner); - } - } - } + // // Create new product allocations + // const newAllocations = await this.createProductAllocations( + // subspaceModel.subspaceModel, + // spaceModel, + // processedTags, + // queryRunner, + // spaceAllocationToExclude, + // ); + // return [...updatedAllocations, ...newAllocations]; + // } + // if (tagsToDeleteDto.length > 0) { + // await this.processDeleteActions(tagsToDeleteDto, queryRunner); + // } + // } + // } } async clearAllAllocations(subspaceIds: string[], queryRunner: QueryRunner) { diff --git a/src/space-model/services/subspace/subspace-model.service.ts b/src/space-model/services/subspace/subspace-model.service.ts index fcf1175..4001040 100644 --- a/src/space-model/services/subspace/subspace-model.service.ts +++ b/src/space-model/services/subspace/subspace-model.service.ts @@ -1,3 +1,4 @@ +import { ModifyAction } from '@app/common/constants/modify-action.enum'; import { SpaceModelEntity, SpaceModelProductAllocationEntity, @@ -5,21 +6,21 @@ import { SubspaceModelProductAllocationEntity, SubspaceModelRepository, } from '@app/common/modules/space-model'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateSubspaceModelDto, ModifyTagModelDto } from '../../dtos'; -import { In, Not, QueryFailedError, QueryRunner } from 'typeorm'; import { ModifySubspaceModelDto } from 'src/space-model/dtos/subspaces-model-dtos'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { TagService } from 'src/tags/services'; -import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service'; import { IRelocatedAllocation, ISingleSubspaceModel, ISubspaceModelUpdates, } from 'src/space-model/interfaces'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; import { SubSpaceService } from 'src/space/services/subspace'; +import { ProcessTagDto } from 'src/tags/dtos'; +import { TagService } from 'src/tags/services'; +import { In, Not, QueryFailedError, QueryRunner } from 'typeorm'; +import { CreateSubspaceModelDto } from '../../dtos'; +import { SubspaceModelProductAllocationService } from './subspace-model-product-allocation.service'; @Injectable() export class SubSpaceModelService { @@ -117,7 +118,7 @@ export class SubSpaceModelService { spaceModel: SpaceModelEntity, queryRunner: QueryRunner, projectUuid: string, - spaceTagUpdateDtos?: ModifyTagModelDto[], + spaceTagUpdateDtos?: ModifyTagDto[], spaces?: SpaceEntity[], ): Promise { try { @@ -152,7 +153,7 @@ export class SubSpaceModelService { projectUuid, queryRunner, spaceModel, - spaceTagUpdateDtos, + // spaceTagUpdateDtos, ); const deletedSubspaces = await this.deleteSubspaceModels( @@ -188,7 +189,7 @@ export class SubSpaceModelService { deleteDtos: ModifySubspaceModelDto[], queryRunner: QueryRunner, spaceModel: SpaceModelEntity, - spaceTagUpdateDtos?: ModifyTagModelDto[], + spaceTagUpdateDtos?: ModifyTagDto[], ): Promise { try { if (!deleteDtos || deleteDtos.length === 0) { @@ -230,92 +231,82 @@ export class SubSpaceModelService { >(); for (const allocation of allocationsToRemove) { - const product = allocation.product; - const tags = allocation.tags; - const subspaceUuid = allocation.subspaceModel.uuid; - const spaceAllocationKey = `${spaceModel.uuid}-${product.uuid}`; - - if (!spaceAllocationsMap.has(spaceAllocationKey)) { - const spaceAllocation = await queryRunner.manager.findOne( - SpaceModelProductAllocationEntity, - { - where: { - spaceModel: { uuid: spaceModel.uuid }, - product: { uuid: product.uuid }, - }, - relations: ['tags'], - }, - ); - if (spaceAllocation) { - spaceAllocationsMap.set(spaceAllocationKey, spaceAllocation); - } - } - - const movedToAlreadyExistingSpaceAllocations: IRelocatedAllocation[] = - []; - const spaceAllocation = spaceAllocationsMap.get(spaceAllocationKey); - - if (spaceAllocation) { - const existingTagUuids = new Set( - spaceAllocation.tags.map((tag) => tag.uuid), - ); - const newTags = tags.filter( - (tag) => !existingTagUuids.has(tag.uuid), - ); - - if (newTags.length > 0) { - movedToAlreadyExistingSpaceAllocations.push({ - tags: newTags, - allocation: spaceAllocation, - }); - spaceAllocation.tags.push(...newTags); - - await queryRunner.manager.save(spaceAllocation); - } - } else { - let tagsToAdd = [...tags]; - - if (spaceTagUpdateDtos && spaceTagUpdateDtos.length > 0) { - const spaceTagDtosToAdd = spaceTagUpdateDtos.filter( - (dto) => dto.action === ModifyAction.ADD, - ); - - tagsToAdd = tagsToAdd.filter( - (tag) => - !spaceTagDtosToAdd.some( - (addDto) => - (addDto.name && addDto.name === tag.name) || - (addDto.newTagUuid && addDto.newTagUuid === tag.uuid), - ), - ); - } - - if (tagsToAdd.length > 0) { - const newSpaceAllocation = queryRunner.manager.create( - SpaceModelProductAllocationEntity, - { - spaceModel: spaceModel, - product: product, - tags: tags, - }, - ); - - movedToAlreadyExistingSpaceAllocations.push({ - allocation: newSpaceAllocation, - tags: tags, - }); - await queryRunner.manager.save(newSpaceAllocation); - } - } - - if (movedToAlreadyExistingSpaceAllocations.length > 0) { - if (!relocatedAllocationsMap.has(subspaceUuid)) { - relocatedAllocationsMap.set(subspaceUuid, []); - } - relocatedAllocationsMap - .get(subspaceUuid) - .push(...movedToAlreadyExistingSpaceAllocations); - } + // const product = allocation.product; + // const tags = allocation.tags; + // const subspaceUuid = allocation.subspaceModel.uuid; + // const spaceAllocationKey = `${spaceModel.uuid}-${product.uuid}`; + // if (!spaceAllocationsMap.has(spaceAllocationKey)) { + // const spaceAllocation = await queryRunner.manager.findOne( + // SpaceModelProductAllocationEntity, + // { + // where: { + // spaceModel: { uuid: spaceModel.uuid }, + // product: { uuid: product.uuid }, + // }, + // relations: ['tags'], + // }, + // ); + // if (spaceAllocation) { + // spaceAllocationsMap.set(spaceAllocationKey, spaceAllocation); + // } + // } + // const movedToAlreadyExistingSpaceAllocations: IRelocatedAllocation[] = + // []; + // const spaceAllocation = spaceAllocationsMap.get(spaceAllocationKey); + // if (spaceAllocation) { + // const existingTagUuids = new Set( + // spaceAllocation.tags.map((tag) => tag.uuid), + // ); + // const newTags = tags.filter( + // (tag) => !existingTagUuids.has(tag.uuid), + // ); + // if (newTags.length > 0) { + // movedToAlreadyExistingSpaceAllocations.push({ + // tags: newTags, + // allocation: spaceAllocation, + // }); + // spaceAllocation.tags.push(...newTags); + // await queryRunner.manager.save(spaceAllocation); + // } + // } else { + // let tagsToAdd = [...tags]; + // if (spaceTagUpdateDtos && spaceTagUpdateDtos.length > 0) { + // const spaceTagDtosToAdd = spaceTagUpdateDtos.filter( + // (dto) => dto.action === ModifyAction.ADD, + // ); + // tagsToAdd = tagsToAdd.filter( + // (tag) => + // !spaceTagDtosToAdd.some( + // (addDto) => + // (addDto.name && addDto.name === tag.name) || + // (addDto.newTagUuid && addDto.newTagUuid === tag.uuid), + // ), + // ); + // } + // if (tagsToAdd.length > 0) { + // const newSpaceAllocation = queryRunner.manager.create( + // SpaceModelProductAllocationEntity, + // { + // spaceModel: spaceModel, + // product: product, + // tags: tags, + // }, + // ); + // movedToAlreadyExistingSpaceAllocations.push({ + // allocation: newSpaceAllocation, + // tags: tags, + // }); + // await queryRunner.manager.save(newSpaceAllocation); + // } + // } + // if (movedToAlreadyExistingSpaceAllocations.length > 0) { + // if (!relocatedAllocationsMap.has(subspaceUuid)) { + // relocatedAllocationsMap.set(subspaceUuid, []); + // } + // relocatedAllocationsMap + // .get(subspaceUuid) + // .push(...movedToAlreadyExistingSpaceAllocations); + // } } await queryRunner.manager.remove( @@ -404,7 +395,7 @@ export class SubSpaceModelService { const updatedSubspaces: { subspaceModel: SubspaceModelEntity; - tags: ModifyTagModelDto[]; + tags: ModifyTagDto[]; action: ModifyAction.UPDATE; }[] = []; @@ -508,12 +499,6 @@ export class SubSpaceModelService { return subspaceModels.flatMap((subspace) => subspace.tags || []); } - extractTagsFromModifiedSubspaceModels( - subspaceModels: ModifySubspaceModelDto[], - ): ModifyTagModelDto[] { - return subspaceModels.flatMap((subspace) => subspace.tags || []); - } - private async getSubspacesByUuids( queryRunner: QueryRunner, subspaceUuids: string[], diff --git a/src/space-model/space-model.module.ts b/src/space-model/space-model.module.ts index cdb713c..3d868c6 100644 --- a/src/space-model/space-model.module.ts +++ b/src/space-model/space-model.module.ts @@ -1,34 +1,50 @@ -import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { SpaceModelController } from './controllers'; -import { SpaceModelService, SubSpaceModelService } from './services'; +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; import { - SpaceModelProductAllocationRepoitory, - SpaceModelRepository, - SubspaceModelProductAllocationRepoitory, - SubspaceModelRepository, - TagModelRepository, -} from '@app/common/modules/space-model'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; + PowerClampDailyRepository, + PowerClampHourlyRepository, + PowerClampMonthlyRepository, +} from '@app/common/modules/power-clamp/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; import { - PropogateDeleteSpaceModelHandler, - PropogateUpdateSpaceModelHandler, - PropogateUpdateSpaceModelProductAllocationHandler, -} from './handlers'; -import { CqrsModule } from '@nestjs/cqrs'; + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; import { InviteSpaceRepository, SpaceLinkRepository, SpaceProductAllocationRepository, SpaceRepository, - TagRepository, } from '@app/common/modules/space'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; import { SubspaceProductAllocationRepository, SubspaceRepository, } from '@app/common/modules/space/repositories/subspace.repository'; +import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { CommunityModule } from 'src/community/community.module'; +import { CommunityService } from 'src/community/services'; +import { DeviceService } from 'src/device/services'; +import { SceneService } from 'src/scene/services'; import { SpaceLinkService, SpaceService, @@ -36,37 +52,18 @@ import { SubSpaceService, ValidationService, } from 'src/space/services'; -import { TagService } from 'src/space/services/tag'; -import { CommunityService } from 'src/community/services'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { TagService as NewTagService } from 'src/tags/services/tags.service'; -import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; -import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service'; -import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service'; import { SpaceProductAllocationService } from 'src/space/services/space-product-allocation.service'; import { SubspaceProductAllocationService } from 'src/space/services/subspace/subspace-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 { TagService as NewTagService } from 'src/tags/services/tags.service'; +import { SpaceModelController } from './controllers'; import { - SceneIconRepository, - SceneRepository, -} from '@app/common/modules/scene/repositories'; -import { AutomationRepository } from '@app/common/modules/automation/repositories'; -import { CommunityModule } from 'src/community/community.module'; -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'; -import { OccupancyService } from '@app/common/helper/services/occupancy.service'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; + PropogateDeleteSpaceModelHandler, + PropogateUpdateSpaceModelHandler, + PropogateUpdateSpaceModelProductAllocationHandler, +} from './handlers'; +import { SpaceModelService, SubSpaceModelService } from './services'; +import { SpaceModelProductAllocationService } from './services/space-model-product-allocation.service'; +import { SubspaceModelProductAllocationService } from './services/subspace/subspace-model-product-allocation.service'; const CommandHandlers = [ PropogateUpdateSpaceModelHandler, @@ -88,13 +85,10 @@ const CommandHandlers = [ SubspaceModelRepository, ProductRepository, SubspaceRepository, - TagModelRepository, SubSpaceService, ValidationService, - TagService, SubspaceDeviceService, CommunityService, - TagRepository, DeviceRepository, TuyaService, CommunityRepository, diff --git a/src/space/dtos/add.space.dto.ts b/src/space/dtos/add.space.dto.ts index 0ffeac1..c40eef7 100644 --- a/src/space/dtos/add.space.dto.ts +++ b/src/space/dtos/add.space.dto.ts @@ -1,16 +1,19 @@ +import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { + ArrayUnique, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, + NotEquals, ValidateNested, } from 'class-validator'; -import { AddSubspaceDto } from './subspace'; import { ProcessTagDto } from 'src/tags/dtos'; +import { AddSubspaceDto } from './subspace'; export class AddSpaceDto { @ApiProperty({ @@ -19,6 +22,11 @@ export class AddSpaceDto { }) @IsString() @IsNotEmpty() + @NotEquals(ORPHAN_SPACE_NAME, { + message() { + return `Space name cannot be "${ORPHAN_SPACE_NAME}". Please choose a different name.`; + }, + }) spaceName: string; @ApiProperty({ @@ -74,6 +82,20 @@ export class AddSpaceDto { }) @IsOptional() @ValidateNested({ each: true }) + @ArrayUnique((subspace) => subspace.subspaceName, { + message(validationArguments) { + const subspaces = validationArguments.value; + const nameCounts = subspaces.reduce((acc, curr) => { + acc[curr.subspaceName] = (acc[curr.subspaceName] || 0) + 1; + return acc; + }, {}); + // Find duplicates + const duplicates = Object.keys(nameCounts).filter( + (name) => nameCounts[name] > 1, + ); + return `Duplicate subspace names found: ${duplicates.join(', ')}`; + }, + }) @Type(() => AddSubspaceDto) subspaces?: AddSubspaceDto[]; diff --git a/src/space/dtos/create-allocations.dto.ts b/src/space/dtos/create-allocations.dto.ts new file mode 100644 index 0000000..4fb7a2a --- /dev/null +++ b/src/space/dtos/create-allocations.dto.ts @@ -0,0 +1,29 @@ +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; +import { ProcessTagDto } from 'src/tags/dtos'; +import { QueryRunner } from 'typeorm'; + +export enum AllocationsOwnerType { + SPACE = 'space', + SUBSPACE = 'subspace', +} +export class BaseCreateAllocationsDto { + tags: ProcessTagDto[]; + projectUuid: string; + queryRunner: QueryRunner; + type: AllocationsOwnerType; +} + +export class CreateSpaceAllocationsDto extends BaseCreateAllocationsDto { + space: SpaceEntity; + type: AllocationsOwnerType.SPACE; +} + +export class CreateSubspaceAllocationsDto extends BaseCreateAllocationsDto { + subspace: SubspaceEntity; + type: AllocationsOwnerType.SUBSPACE; +} + +export type CreateAllocationsDto = + | CreateSpaceAllocationsDto + | CreateSubspaceAllocationsDto; diff --git a/src/space/dtos/subspace/modify.subspace.dto.ts b/src/space/dtos/subspace/modify.subspace.dto.ts index 39a40e6..7f32aca 100644 --- a/src/space/dtos/subspace/modify.subspace.dto.ts +++ b/src/space/dtos/subspace/modify.subspace.dto.ts @@ -1,47 +1,14 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - IsEnum, - IsOptional, - IsString, - IsArray, - ValidateNested, -} from 'class-validator'; -import { ModifyTagDto } from '../tag/modify-tag.dto'; - -export class ModifySubspaceDto { - @ApiProperty({ - description: 'Action to perform: add, update, or delete', - example: ModifyAction.ADD, - }) - @IsEnum(ModifyAction) - action: ModifyAction; +import { ApiPropertyOptional, PartialType } from '@nestjs/swagger'; +import { IsOptional, IsUUID } from 'class-validator'; +import { AddSubspaceDto } from './add.subspace.dto'; +export class ModifySubspaceDto extends PartialType(AddSubspaceDto) { @ApiPropertyOptional({ - description: 'UUID of the subspace (required for update/delete)', + description: + 'UUID of the subspace (will present if updating an existing subspace)', example: '123e4567-e89b-12d3-a456-426614174000', }) @IsOptional() - @IsString() + @IsUUID() uuid?: string; - - @ApiPropertyOptional({ - description: 'Name of the subspace (required for add/update)', - example: 'Living Room', - }) - @IsOptional() - @IsString() - subspaceName?: string; - - @ApiPropertyOptional({ - description: - 'List of tag modifications (add/update/delete) for the subspace', - type: [ModifyTagDto], - }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => ModifyTagDto) - tags?: ModifyTagDto[]; } diff --git a/src/space/dtos/tag/modify-tag.dto.ts b/src/space/dtos/tag/modify-tag.dto.ts index a8902c0..c9eff6e 100644 --- a/src/space/dtos/tag/modify-tag.dto.ts +++ b/src/space/dtos/tag/modify-tag.dto.ts @@ -1,26 +1,9 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, IsUUID } from 'class-validator'; export class ModifyTagDto { - @ApiProperty({ - description: 'Action to perform: add, update, or delete', - example: ModifyAction.ADD, - }) - @IsEnum(ModifyAction) - action: ModifyAction; - @ApiPropertyOptional({ - description: 'UUID of the new tag', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsOptional() - @IsUUID() - newTagUuid: string; - - @ApiPropertyOptional({ - description: - 'UUID of an existing tag (required for update/delete, optional for add)', + description: 'UUID of an existing tag', example: 'a1b2c3d4-5678-90ef-abcd-1234567890ef', }) @IsOptional() @@ -28,7 +11,7 @@ export class ModifyTagDto { tagUuid?: string; @ApiPropertyOptional({ - description: 'Name of the tag (required for add/update)', + description: 'Name of the tag', example: 'Temperature Sensor', }) @IsOptional() @@ -36,8 +19,7 @@ export class ModifyTagDto { name?: string; @ApiPropertyOptional({ - description: - 'UUID of the product associated with the tag (required for add)', + description: 'UUID of the product associated with the tag', example: 'c789a91e-549a-4753-9006-02f89e8170e0', }) @IsOptional() diff --git a/src/space/dtos/update.space.dto.ts b/src/space/dtos/update.space.dto.ts index 31ff54f..310f695 100644 --- a/src/space/dtos/update.space.dto.ts +++ b/src/space/dtos/update.space.dto.ts @@ -1,13 +1,15 @@ +import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { IsArray, IsNumber, IsOptional, IsString, + NotEquals, ValidateNested, } from 'class-validator'; import { ModifySubspaceDto } from './subspace'; -import { Type } from 'class-transformer'; import { ModifyTagDto } from './tag/modify-tag.dto'; export class UpdateSpaceDto { @@ -17,6 +19,11 @@ export class UpdateSpaceDto { }) @IsOptional() @IsString() + @NotEquals(ORPHAN_SPACE_NAME, { + message() { + return `Space name cannot be "${ORPHAN_SPACE_NAME}". Please choose a different name.`; + }, + }) spaceName?: string; @ApiProperty({ @@ -46,7 +53,7 @@ export class UpdateSpaceDto { @IsArray() @ValidateNested({ each: true }) @Type(() => ModifySubspaceDto) - subspace?: ModifySubspaceDto[]; + subspaces?: ModifySubspaceDto[]; @ApiPropertyOptional({ description: @@ -58,6 +65,7 @@ export class UpdateSpaceDto { @ValidateNested({ each: true }) @Type(() => ModifyTagDto) tags?: ModifyTagDto[]; + @ApiProperty({ description: 'UUID of the Space', example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', diff --git a/src/space/handlers/disable-space.handler.ts b/src/space/handlers/disable-space.handler.ts index 10f18d9..089ab76 100644 --- a/src/space/handlers/disable-space.handler.ts +++ b/src/space/handlers/disable-space.handler.ts @@ -1,3 +1,4 @@ +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { DeviceService } from 'src/device/services'; @@ -5,12 +6,10 @@ import { UserSpaceService } from 'src/users/services'; import { DataSource } from 'typeorm'; import { DisableSpaceCommand } from '../commands'; import { - SubSpaceService, SpaceLinkService, SpaceSceneService, + SubSpaceService, } from '../services'; -import { TagService } from '../services/tag'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; @CommandHandler(DisableSpaceCommand) export class DisableSpaceHandler @@ -19,7 +18,6 @@ export class DisableSpaceHandler constructor( private readonly subSpaceService: SubSpaceService, private readonly userService: UserSpaceService, - private readonly tagService: TagService, private readonly deviceService: DeviceService, private readonly spaceLinkService: SpaceLinkService, private readonly sceneService: SpaceSceneService, @@ -46,6 +44,9 @@ export class DisableSpaceHandler 'scenes', 'children', 'userSpaces', + 'productAllocations', + 'productAllocations.tag', + 'productAllocations.product', ], }); @@ -64,15 +65,15 @@ export class DisableSpaceHandler } } - const tagUuids = space.productAllocations?.map((tag) => tag.uuid) || []; + const tagUuids = + space.productAllocations?.map(({ tag }) => tag.uuid) || []; /* const subspaceDtos = space.subspaces?.map((subspace) => ({ subspaceUuid: subspace.uuid, })) || []; */ const deletionTasks = [ - // this.subSpaceService.deleteSubspaces(subspaceDtos, queryRunner), this.userService.deleteUserSpace(space.uuid), - this.tagService.deleteTags(tagUuids, queryRunner), + this.deviceService.deleteDevice( space.devices, orphanSpace, diff --git a/src/space/interfaces/single-subspace.interface.ts b/src/space/interfaces/single-subspace.interface.ts deleted file mode 100644 index 013193c..0000000 --- a/src/space/interfaces/single-subspace.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { ModifyTagDto } from '../dtos/tag/modify-tag.dto'; - -export interface ISingleSubspace { - subspace: SubspaceEntity; - action: ModifyAction; - tags: ModifyTagDto[]; -} diff --git a/src/space/interfaces/update-subspace-allocation.dto.ts b/src/space/interfaces/update-subspace-allocation.dto.ts new file mode 100644 index 0000000..f8a2b2d --- /dev/null +++ b/src/space/interfaces/update-subspace-allocation.dto.ts @@ -0,0 +1,6 @@ +import { ProcessTagDto } from 'src/tags/dtos'; + +export interface UpdateSpaceAllocationDto { + uuid: string; + tags: ProcessTagDto[]; +} diff --git a/src/space/services/product-allocation/product-allocation.service.ts b/src/space/services/product-allocation/product-allocation.service.ts new file mode 100644 index 0000000..7950f9f --- /dev/null +++ b/src/space/services/product-allocation/product-allocation.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; +import { TagService } from 'src/tags/services'; +import { + AllocationsOwnerType, + CreateAllocationsDto, +} from '../../dtos/create-allocations.dto'; +import { SpaceProductAllocationService } from '../space-product-allocation.service'; +import { SubspaceProductAllocationService } from '../subspace/subspace-product-allocation.service'; + +@Injectable() +export class ProductAllocationService { + constructor( + private readonly tagService: TagService, + private readonly spaceProductAllocationService: SpaceProductAllocationService, + private readonly subSpaceProductAllocationService: SubspaceProductAllocationService, + ) {} + + async createAllocations(dto: CreateAllocationsDto): Promise { + const { projectUuid, queryRunner, tags, type } = dto; + + const allocationsData = await this.tagService.processTags( + tags, + projectUuid, + queryRunner, + ); + + // Create a mapping of created tags by UUID and name for quick lookup + const createdTagsByUUID = new Map(allocationsData.map((t) => [t.uuid, t])); + const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); + + // Create the product-tag mapping based on the processed tags + const productTagMapping = tags.map(({ uuid, name, productUuid }) => { + const inputTag = uuid + ? createdTagsByUUID.get(uuid) + : createdTagsByName.get(name); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }); + + switch (type) { + case AllocationsOwnerType.SPACE: { + // todo: take to consideration original method implementation + await this.spaceProductAllocationService.createProductAllocations( + dto.space, + productTagMapping, + queryRunner, + ); + break; + } + case AllocationsOwnerType.SUBSPACE: { + await this.subSpaceProductAllocationService.createProductAllocations( + dto.subspace, + productTagMapping, + queryRunner, + ); + break; + } + } + } +} diff --git a/src/space/services/space-product-allocation.service.ts b/src/space/services/space-product-allocation.service.ts index 2d54875..7b0712c 100644 --- a/src/space/services/space-product-allocation.service.ts +++ b/src/space/services/space-product-allocation.service.ts @@ -1,113 +1,37 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ProductEntity } from '@app/common/modules/product/entities'; import { SpaceProductAllocationRepository } from '@app/common/modules/space'; import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { NewTagEntity } from '@app/common/modules/tag'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { In, QueryRunner } from 'typeorm'; -import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; -import { ModifyTagDto } from '../dtos/tag/modify-tag.dto'; -import { ModifySubspaceDto } from '../dtos'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { TagService as NewTagService } from 'src/tags/services'; -import { SpaceModelProductAllocationEntity } from '@app/common/modules/space-model'; -import { DeviceEntity } from '@app/common/modules/device/entities'; -import { ProjectEntity } from '@app/common/modules/project/entities'; -import { ValidationService } from './space-validation.service'; @Injectable() export class SpaceProductAllocationService { constructor( - private readonly tagService: NewTagService, private readonly spaceProductAllocationRepository: SpaceProductAllocationRepository, - private readonly spaceService: ValidationService, ) {} - async createSpaceProductAllocations( + async createProductAllocations( space: SpaceEntity, - processedTags: NewTagEntity[], + allocationsData: { product: string; tag: string }[], queryRunner: QueryRunner, - modifySubspaces?: ModifySubspaceDto[], ): Promise { try { - if (!processedTags.length) return; + if (!allocationsData.length) return; const productAllocations: SpaceProductAllocationEntity[] = []; - const existingAllocations = new Map< - string, - SpaceProductAllocationEntity - >(); - for (const tag of processedTags) { - let isTagNeeded = true; + for (const allocationData of allocationsData) { + if (await this.isAllocationExist(queryRunner, allocationData, space)) + continue; - if (modifySubspaces) { - const relatedSubspaces = await queryRunner.manager.find( - SubspaceProductAllocationEntity, - { - where: { - product: tag.product, - subspace: { space: { uuid: space.uuid } }, - tags: { uuid: tag.uuid }, - }, - relations: ['subspace', 'tags'], - }, - ); - - for (const subspaceWithTag of relatedSubspaces) { - const modifyingSubspace = modifySubspaces.find( - (subspace) => - subspace.action === ModifyAction.UPDATE && - subspace.uuid === subspaceWithTag.subspace.uuid, - ); - - if ( - modifyingSubspace && - modifyingSubspace.tags && - modifyingSubspace.tags.some( - (subspaceTag) => - subspaceTag.action === ModifyAction.DELETE && - subspaceTag.tagUuid === tag.uuid, - ) - ) { - isTagNeeded = true; - break; - } - } - } - - if (isTagNeeded) { - const isDuplicated = await this.validateTagWithinSpace( - queryRunner, - tag, - space, - ); - if (isDuplicated) continue; - - let allocation = existingAllocations.get(tag.product.uuid); - if (!allocation) { - allocation = await this.getAllocationByProduct( - tag.product, - space, - queryRunner, - ); - if (allocation) { - existingAllocations.set(tag.product.uuid, allocation); - } - } - - if (!allocation) { - allocation = this.createNewAllocation(space, tag, queryRunner); - productAllocations.push(allocation); - } else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) { - allocation.tags.push(tag); - await this.saveAllocation(allocation, queryRunner); - } - } + const allocation = this.createNewAllocation( + space, + allocationData, + queryRunner, + ); + productAllocations.push(allocation); } - if (productAllocations.length > 0) { await this.saveAllocations(productAllocations, queryRunner); } @@ -118,97 +42,6 @@ export class SpaceProductAllocationService { ); } } - - async createAllocationFromModel( - modelAllocations: SpaceModelProductAllocationEntity[], - queryRunner: QueryRunner, - spaces?: SpaceEntity[], - ) { - if (!spaces || spaces.length === 0 || !modelAllocations.length) return; - - const allocations: SpaceProductAllocationEntity[] = []; - - for (const space of spaces) { - for (const modelAllocation of modelAllocations) { - const allocation = queryRunner.manager.create( - SpaceProductAllocationEntity, - { - space, - product: modelAllocation.product, - tags: modelAllocation.tags, - inheritedFromModel: modelAllocation, - }, - ); - allocations.push(allocation); - } - } - - if (allocations.length > 0) { - await queryRunner.manager.save(SpaceProductAllocationEntity, allocations); - } - } - - async addTagToAllocationFromModel( - modelAllocation: SpaceModelProductAllocationEntity, - queryRunner: QueryRunner, - tag: NewTagEntity, - spaces?: SpaceEntity[], - ) { - try { - if (!spaces || spaces.length === 0 || !modelAllocation) return; - - const spaceAllocations = await queryRunner.manager.find( - SpaceProductAllocationEntity, - { - where: { inheritedFromModel: { uuid: modelAllocation.uuid } }, - relations: ['tags'], - }, - ); - - if (spaceAllocations.length === 0) return; - - for (const allocation of spaceAllocations) { - allocation.tags.push(tag); - } - - await queryRunner.manager.save( - SpaceProductAllocationEntity, - spaceAllocations, - ); - } catch (error) { - throw new HttpException( - 'Failed to add tag to allocation from model', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async updateSpaceProductAllocations( - dtos: ModifyTagDto[], - projectUuid: string, - space: SpaceEntity, - queryRunner: QueryRunner, - modifySubspace?: ModifySubspaceDto[], - ): Promise { - if (!dtos || dtos.length === 0) return; - - try { - await Promise.all([ - this.processAddActions( - dtos, - projectUuid, - space, - queryRunner, - modifySubspace, - ), - - this.processDeleteActions(dtos, queryRunner, space), - ]); - } catch (error) { - throw this.handleError(error, 'Error while updating product allocations'); - } - } - async unlinkModels(space: SpaceEntity, queryRunner: QueryRunner) { try { if (!space.productAllocations || space.productAllocations.length === 0) @@ -231,307 +64,30 @@ export class SpaceProductAllocationService { } } - async propagateDeleteToInheritedAllocations( - queryRunner: QueryRunner, - allocationsToUpdate: SpaceModelProductAllocationEntity[], - tagUuidsToDelete: string[], - project: ProjectEntity, - spaces?: SpaceEntity[], - ): Promise { - try { - const inheritedAllocationUpdates: SpaceProductAllocationEntity[] = []; - const inheritedAllocationsToDelete: SpaceProductAllocationEntity[] = []; - - for (const allocation of allocationsToUpdate) { - for (const inheritedAllocation of allocation.inheritedSpaceAllocations) { - const updatedInheritedTags = inheritedAllocation.tags.filter( - (tag) => !tagUuidsToDelete.includes(tag.uuid), - ); - - if (updatedInheritedTags.length === inheritedAllocation.tags.length) { - continue; - } - - if (updatedInheritedTags.length === 0) { - inheritedAllocationsToDelete.push(inheritedAllocation); - } else { - inheritedAllocation.tags = updatedInheritedTags; - inheritedAllocationUpdates.push(inheritedAllocation); - } - } - } - - if (inheritedAllocationUpdates.length > 0) { - await queryRunner.manager.save( - SpaceProductAllocationEntity, - inheritedAllocationUpdates, - ); - } - - if (inheritedAllocationsToDelete.length > 0) { - await queryRunner.manager.remove( - SpaceProductAllocationEntity, - inheritedAllocationsToDelete, - ); - } - - if (spaces && spaces.length > 0) { - await this.moveDevicesToOrphanSpace( - queryRunner, - spaces, - tagUuidsToDelete, - project, - ); - } - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from('space_product_tags') - .where( - 'space_product_allocation_uuid NOT IN (' + - queryRunner.manager - .createQueryBuilder() - .select('allocation.uuid') - .from(SpaceProductAllocationEntity, 'allocation') - .getQuery() + - ')', - ) - .execute(); - } catch (error) { - throw this.handleError( - error, - `Failed to propagate tag deletion to inherited allocations`, - ); - } - } - - async moveDevicesToOrphanSpace( - queryRunner: QueryRunner, - spaces: SpaceEntity[], - tagUuidsToDelete: string[], - project: ProjectEntity, - ): Promise { - try { - const orphanSpace = await this.spaceService.getOrphanSpace(project); - - const devicesToMove = await queryRunner.manager - .createQueryBuilder(DeviceEntity, 'device') - .leftJoinAndSelect('device.tag', 'tag') - .where('device.spaceDevice IN (:...spaceUuids)', { - spaceUuids: spaces.map((space) => space.uuid), - }) - .andWhere('tag.uuid IN (:...tagUuidsToDelete)', { tagUuidsToDelete }) - .getMany(); - - if (devicesToMove.length === 0) return; - - await queryRunner.manager - .createQueryBuilder() - .update(DeviceEntity) - .set({ spaceDevice: orphanSpace }) - .where('uuid IN (:...deviceUuids)', { - deviceUuids: devicesToMove.map((device) => device.uuid), - }) - .execute(); - } catch (error) { - throw this.handleError(error, `Failed to move devices to orphan space`); - } - } - - private async processDeleteActions( - dtos: ModifyTagDto[], + private async isAllocationExist( queryRunner: QueryRunner, + allocation: { product: string; tag: string }, space: SpaceEntity, - ): Promise { - try { - if (!dtos || dtos.length === 0) return; - const tagUuidsToDelete = dtos - .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) - .map((dto) => dto.tagUuid); - - if (tagUuidsToDelete.length === 0) return []; - - const allocationsToUpdate = await queryRunner.manager.find( + ): Promise { + const existingAllocations = + await queryRunner.manager.findOne( SpaceProductAllocationEntity, { where: { - tags: { uuid: In(tagUuidsToDelete) }, - space: { uuid: space.uuid }, - }, - relations: ['tags'], - }, - ); - - if (!allocationsToUpdate || allocationsToUpdate.length === 0) return []; - - const deletedAllocations: SpaceProductAllocationEntity[] = []; - const allocationUpdates: SpaceProductAllocationEntity[] = []; - - for (const allocation of allocationsToUpdate) { - const updatedTags = allocation.tags.filter( - (tag) => !tagUuidsToDelete.includes(tag.uuid), - ); - - if (updatedTags.length === allocation.tags.length) { - continue; - } - - if (updatedTags.length === 0) { - deletedAllocations.push(allocation); - } else { - allocation.tags = updatedTags; - allocationUpdates.push(allocation); - } - } - - if (allocationUpdates.length > 0) { - await queryRunner.manager.save( - SpaceProductAllocationEntity, - allocationUpdates, - ); - } - - if (deletedAllocations.length > 0) { - await queryRunner.manager.remove( - SpaceProductAllocationEntity, - deletedAllocations, - ); - } - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from('space_product_tags') - .where( - 'space_product_allocation_uuid NOT IN (' + - queryRunner.manager - .createQueryBuilder() - .select('allocation.uuid') - .from(SpaceProductAllocationEntity, 'allocation') - .getQuery() + - ')', - ) - .execute(); - - return deletedAllocations; - } catch (error) { - throw this.handleError(error, `Failed to delete tags in space`); - } - } - private async processAddActions( - dtos: ModifyTagDto[], - projectUuid: string, - space: SpaceEntity, - queryRunner: QueryRunner, - modifySubspace?: ModifySubspaceDto[], - ): Promise { - const addDtos: ProcessTagDto[] = dtos - .filter((dto) => dto.action === ModifyAction.ADD) - .map((dto) => ({ - name: dto.name, - productUuid: dto.productUuid, - uuid: dto.newTagUuid, - })); - - if (addDtos.length > 0) { - const processedTags = await this.tagService.processTags( - addDtos, - projectUuid, - queryRunner, - ); - - await this.createSpaceProductAllocations( - space, - processedTags, - queryRunner, - modifySubspace, - ); - } - } - private async validateTagWithinSpace( - queryRunner: QueryRunner, - tag: NewTagEntity, - space: SpaceEntity, - ): Promise { - const existingAllocationsForProduct = await queryRunner.manager.find( - SpaceProductAllocationEntity, - { - where: { - space: { - uuid: space.uuid, - }, - product: { - uuid: tag.product.uuid, + space: { + uuid: space.uuid, + }, + tag: { + uuid: allocation.tag, + }, + product: { + uuid: allocation.product, + }, }, }, - relations: ['tags'], - }, - ); + ); - if ( - !existingAllocationsForProduct || - existingAllocationsForProduct.length === 0 - ) { - return false; - } - - const existingTagsForProduct = existingAllocationsForProduct.flatMap( - (allocation) => allocation.tags || [], - ); - - return existingTagsForProduct.some( - (existingTag) => existingTag.uuid === tag.uuid, - ); - } - - private async getAllocationByProduct( - product: ProductEntity, - space: SpaceEntity, - queryRunner?: QueryRunner, - ): Promise { - return queryRunner - ? queryRunner.manager.findOne(SpaceProductAllocationEntity, { - where: { - space: { uuid: space.uuid }, - product: { uuid: product.uuid }, - }, - relations: ['tags'], - }) - : this.spaceProductAllocationRepository.findOne({ - where: { - space: { uuid: space.uuid }, - product: { uuid: product.uuid }, - }, - relations: ['tags'], - }); - } - - createNewAllocation( - space: SpaceEntity, - tag: NewTagEntity, - queryRunner?: QueryRunner, - ): SpaceProductAllocationEntity { - return queryRunner - ? queryRunner.manager.create(SpaceProductAllocationEntity, { - space, - product: tag.product, - tags: [tag], - }) - : this.spaceProductAllocationRepository.create({ - space, - product: tag.product, - tags: [tag], - }); - } - - private async saveAllocation( - allocation: SpaceProductAllocationEntity, - queryRunner?: QueryRunner, - ) { - queryRunner - ? await queryRunner.manager.save(SpaceProductAllocationEntity, allocation) - : await this.spaceProductAllocationRepository.save(allocation); + return existingAllocations ? true : false; } async saveAllocations( @@ -546,6 +102,34 @@ export class SpaceProductAllocationService { : await this.spaceProductAllocationRepository.save(allocations); } + async clearAllAllocations(spaceUuid: string, queryRunner: QueryRunner) { + try { + await queryRunner.manager.delete(SpaceProductAllocationEntity, { + space: { uuid: spaceUuid }, + }); + } catch (error) { + throw this.handleError(error, 'Failed to clear all allocations'); + } + } + + private createNewAllocation( + space: SpaceEntity, + allocationData: { product: string; tag: string }, + queryRunner?: QueryRunner, + ): SpaceProductAllocationEntity { + return queryRunner + ? queryRunner.manager.create(SpaceProductAllocationEntity, { + space, + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, + }) + : this.spaceProductAllocationRepository.create({ + space, + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, + }); + } + private handleError(error: any, message: string): HttpException { return new HttpException( error instanceof HttpException ? error.message : message, @@ -554,36 +138,4 @@ export class SpaceProductAllocationService { : HttpStatus.INTERNAL_SERVER_ERROR, ); } - async clearAllAllocations(spaceUuid: string, queryRunner: QueryRunner) { - try { - const allocationUuids = await queryRunner.manager - .createQueryBuilder(SpaceProductAllocationEntity, 'allocation') - .select('allocation.uuid') - .where('allocation.space_uuid = :spaceUuid', { spaceUuid }) - .getRawMany() - .then((results) => results.map((r) => r.allocation_uuid)); - - if (allocationUuids.length === 0) { - return; - } - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from('space_product_tags') - .where('space_product_allocation_uuid IN (:...allocationUuids)', { - allocationUuids, - }) - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from(SpaceProductAllocationEntity) - .where('space_uuid = :spaceUuid', { spaceUuid }) - .execute(); - } catch (error) { - throw this.handleError(error, 'Failed to clear all allocations'); - } - } } diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index 3bbf6b2..0eac0d2 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -1,3 +1,12 @@ +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { + SpaceModelEntity, + SpaceModelRepository, +} from '@app/common/modules/space-model'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { BadRequestException, @@ -7,25 +16,11 @@ import { Inject, Injectable, } from '@nestjs/common'; +import { In } from 'typeorm'; import { CommunityService } from '../../community/services'; import { ProjectService } from '../../project/services'; -import { - SpaceModelEntity, - SpaceModelRepository, -} from '@app/common/modules/space-model'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { DeviceRepository } from '@app/common/modules/device/repositories'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { ValidateSpacesDto } from '../dtos/validation.space.dto'; import { ProjectParam } from '../dtos'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { In } from 'typeorm'; -import { - ORPHAN_COMMUNITY_NAME, - ORPHAN_SPACE_NAME, -} from '@app/common/constants/orphan-constant'; -import { ProjectEntity } from '@app/common/modules/project/entities'; +import { ValidateSpacesDto } from '../dtos/validation.space.dto'; @Injectable() export class ValidationService { @@ -130,9 +125,7 @@ export class ValidationService { 'subspaces', 'productAllocations', 'productAllocations.product', - 'productAllocations.tags', 'subspaces.productAllocations', - 'subspaces.productAllocations.tags', 'subspaces.productAllocations.product', 'subspaces.devices', 'spaceModel', @@ -146,13 +139,13 @@ export class ValidationService { ); } - const devices = await this.deviceRepository.find({ - where: { spaceDevice: { uuid: spaceUuid } }, - select: ['uuid', 'deviceTuyaUuid', 'isActive', 'createdAt', 'updatedAt'], - relations: ['productDevice', 'subspace'], - }); + // const devices = await this.deviceRepository.find({ + // where: { spaceDevice: { uuid: spaceUuid } }, + // select: ['uuid', 'deviceTuyaUuid', 'isActive', 'createdAt', 'updatedAt'], + // relations: ['productDevice', 'subspace'], + // }); - space.devices = devices; + // space.devices = devices; return space; } @@ -191,8 +184,8 @@ export class ValidationService { 'subspaceProductAllocations', ) .leftJoinAndSelect( - 'subspaceProductAllocations.tags', - 'subspaceAllocationTags', + 'subspaceProductAllocations.tag', + 'subspaceAllocationTag', ) .leftJoinAndSelect( 'subspaceProductAllocations.product', @@ -203,7 +196,7 @@ export class ValidationService { 'productAllocations.product', 'productAllocationProduct', ) - .leftJoinAndSelect('productAllocations.tags', 'productAllocationTags') + .leftJoinAndSelect('productAllocations.tag', 'productAllocationTag') .andWhere('spaceModel.disabled = :disabled', { disabled: false }) .where('spaceModel.uuid = :uuid', { uuid: spaceModelUuid }); @@ -219,39 +212,6 @@ export class ValidationService { return spaceModel; } - async getFullSpaceHierarchy( - space: SpaceEntity, - ): Promise<{ uuid: string; spaceName: string }[]> { - try { - // Fetch only the relevant spaces, starting with the target space - const targetSpace = await this.spaceRepository.findOne({ - where: { uuid: space.uuid }, - relations: ['parent', 'children'], - }); - - // Fetch only the ancestors of the target space - const ancestors = await this.fetchAncestors(targetSpace); - - // Optionally, fetch descendants if required - const descendants = await this.fetchDescendants(targetSpace); - - const fullHierarchy = [...ancestors, targetSpace, ...descendants].map( - (space) => ({ - uuid: space.uuid, - spaceName: space.spaceName, - }), - ); - - return fullHierarchy; - } catch (error) { - console.error('Error fetching space hierarchy:', error.message); - throw new HttpException( - 'Error fetching space hierarchy', - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - private async fetchAncestors(space: SpaceEntity): Promise { const ancestors: SpaceEntity[] = []; @@ -275,27 +235,6 @@ export class ValidationService { return ancestors.reverse(); } - private async fetchDescendants(space: SpaceEntity): Promise { - const descendants: SpaceEntity[] = []; - - // Fetch the immediate children of the current space - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['children'], // To continue fetching downwards - }); - - for (const child of children) { - // Add the child to the descendants list - descendants.push(child); - - // Recursively fetch the child's descendants - const childDescendants = await this.fetchDescendants(child); - descendants.push(...childDescendants); - } - - return descendants; - } - async getParentHierarchy( space: SpaceEntity, ): Promise<{ uuid: string; spaceName: string }[]> { @@ -323,24 +262,4 @@ export class ValidationService { ); } } - - async getOrphanSpace(project: ProjectEntity): Promise { - const orphanSpace = await this.spaceRepository.findOne({ - where: { - community: { - name: `${ORPHAN_COMMUNITY_NAME}-${project.name}`, - }, - spaceName: ORPHAN_SPACE_NAME, - }, - }); - - if (!orphanSpace) { - throw new HttpException( - `Orphan space not found in community ${project.name}`, - HttpStatus.NOT_FOUND, - ); - } - - return orphanSpace; - } } diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index c001394..1312030 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -1,3 +1,14 @@ +import { + ORPHAN_COMMUNITY_NAME, + ORPHAN_SPACE_NAME, +} from '@app/common/constants/orphan-constant'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { generateRandomString } from '@app/common/helper/randomString'; +import { removeCircularReferences } from '@app/common/helper/removeCircularReferences'; +import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; import { InviteSpaceRepository, SpaceRepository, @@ -8,36 +19,25 @@ import { HttpStatus, Injectable, } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { DeviceService } from 'src/device/services'; +import { SpaceModelService } from 'src/space-model/services'; +import { ProcessTagDto } from 'src/tags/dtos'; +import { TagService } from 'src/tags/services/tags.service'; +import { DataSource, In, Not, QueryRunner } from 'typeorm'; +import { DisableSpaceCommand } from '../commands'; import { AddSpaceDto, - AddSubspaceDto, CommunitySpaceParam, GetSpaceParam, UpdateSpaceDto, } from '../dtos'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { generateRandomString } from '@app/common/helper/randomString'; -import { SpaceLinkService } from './space-link'; -import { SubSpaceService } from './subspace'; -import { DataSource, QueryRunner } from 'typeorm'; -import { ValidationService } from './space-validation.service'; -import { - ORPHAN_COMMUNITY_NAME, - ORPHAN_SPACE_NAME, -} from '@app/common/constants/orphan-constant'; -import { CommandBus } from '@nestjs/cqrs'; -import { TagService as NewTagService } from 'src/tags/services/tags.service'; -import { SpaceModelService } from 'src/space-model/services'; -import { DisableSpaceCommand } from '../commands'; import { GetSpaceDto } from '../dtos/get.space.dto'; -import { removeCircularReferences } from '@app/common/helper/removeCircularReferences'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { SpaceProductAllocationService } from './space-product-allocation.service'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { DeviceService } from 'src/device/services'; import { SpaceWithParentsDto } from '../dtos/space.parents.dto'; +import { SpaceLinkService } from './space-link'; +import { SpaceProductAllocationService } from './space-product-allocation.service'; +import { ValidationService } from './space-validation.service'; +import { SubSpaceService } from './subspace'; @Injectable() export class SpaceService { constructor( @@ -47,7 +47,7 @@ export class SpaceService { private readonly spaceLinkService: SpaceLinkService, private readonly subSpaceService: SubSpaceService, private readonly validationService: ValidationService, - private readonly newTagService: NewTagService, + private readonly tagService: TagService, private readonly spaceModelService: SpaceModelService, private readonly deviceService: DeviceService, private commandBus: CommandBus, @@ -62,13 +62,6 @@ export class SpaceService { addSpaceDto; const { communityUuid, projectUuid } = params; - if (addSpaceDto.spaceName === ORPHAN_SPACE_NAME) { - throw new HttpException( - `Name ${ORPHAN_SPACE_NAME} cannot be used`, - HttpStatus.BAD_REQUEST, - ); - } - const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); @@ -80,7 +73,7 @@ export class SpaceService { projectUuid, ); - this.validateSpaceCreation(addSpaceDto, spaceModelUuid); + this.validateSpaceCreationCriteria({ spaceModelUuid, subspaces, tags }); const parent = parentUuid ? await this.validationService.validateSpace(parentUuid) @@ -99,27 +92,24 @@ export class SpaceService { }); const newSpace = await queryRunner.manager.save(space); + const subspaceTags = - this.subSpaceService.extractTagsFromSubspace(subspaces); - const allTags = [...tags, ...subspaceTags]; - this.validateUniqueTags(allTags); + subspaces?.flatMap((subspace) => subspace.tags || []) || []; + + this.checkDuplicateTags([...tags, ...subspaceTags]); + if (spaceModelUuid) { - const hasDependencies = subspaces?.length > 0 || tags?.length > 0; - if (!hasDependencies) { - await this.spaceModelService.linkToSpace( - newSpace, - spaceModel, - queryRunner, - ); - } else if (hasDependencies) { - throw new HttpException( - `Space cannot be linked to a model because it has existing dependencies (subspaces or tags).`, - HttpStatus.BAD_REQUEST, - ); - } + // no need to check for existing dependencies here as validateSpaceCreationCriteria + // ensures no tags or subspaces are present along with spaceModelUuid + await this.spaceModelService.linkToSpace( + newSpace, + spaceModel, + queryRunner, + ); } await Promise.all([ + // todo: remove this logic as we are not using space links anymore direction && parent ? this.spaceLinkService.saveSpaceLink( parent.uuid, @@ -129,16 +119,15 @@ export class SpaceService { ) : Promise.resolve(), subspaces?.length - ? this.createSubspaces( + ? this.subSpaceService.createSubspacesFromDto( subspaces, - newSpace, + space, queryRunner, - null, projectUuid, ) : Promise.resolve(), tags?.length - ? this.createTags(tags, projectUuid, queryRunner, newSpace) + ? this.createAllocations(tags, projectUuid, queryRunner, newSpace) : Promise.resolve(), ]); @@ -160,7 +149,7 @@ export class SpaceService { await queryRunner.release(); } } - private validateUniqueTags(allTags: ProcessTagDto[]) { + private checkDuplicateTags(allTags: ProcessTagDto[]) { const tagUuidSet = new Set(); const tagNameProductSet = new Set(); @@ -213,8 +202,8 @@ export class SpaceService { { incomingConnectionDisabled: false }, ) .leftJoinAndSelect('space.productAllocations', 'productAllocations') - // .leftJoinAndSelect('productAllocations.tags', 'tags') - // .leftJoinAndSelect('productAllocations.product', 'product') + .leftJoinAndSelect('productAllocations.tag', 'tag') + .leftJoinAndSelect('productAllocations.product', 'product') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -225,11 +214,11 @@ export class SpaceService { 'subspaces.productAllocations', 'subspaceProductAllocations', ) - // .leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTag') - // .leftJoinAndSelect( - // 'subspaceProductAllocations.product', - // 'subspaceProduct', - // ) + .leftJoinAndSelect('subspaceProductAllocations.tag', 'subspaceTag') + .leftJoinAndSelect( + 'subspaceProductAllocations.product', + 'subspaceProduct', + ) .leftJoinAndSelect('space.spaceModel', 'spaceModel') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { @@ -282,28 +271,7 @@ export class SpaceService { } } - private transformSpace(space) { - const { productAllocations, subspaces, ...restSpace } = space; - - const tags = productAllocations.flatMap((pa) => pa.tags); - - const transformedSubspaces = subspaces.map((subspace) => { - const { - productAllocations: subspaceProductAllocations, - ...restSubspace - } = subspace; - const subspaceTags = subspaceProductAllocations.flatMap((pa) => pa.tags); - return { - ...restSubspace, - tags: subspaceTags, - }; - }); - return { - ...restSpace, - tags, - subspaces: transformedSubspaces, - }; - } + // todo refactor this method to eliminate wrong use of tags async findOne(params: GetSpaceParam): Promise { const { communityUuid, spaceUuid, projectUuid } = params; try { @@ -327,13 +295,9 @@ export class SpaceService { 'incomingConnections.disabled = :incomingConnectionDisabled', { incomingConnectionDisabled: false }, ) - // .leftJoinAndSelect( - // 'space.tags', - // 'tags', - // 'tags.disabled = :tagDisabled', - // { tagDisabled: false }, - // ) - // .leftJoinAndSelect('tags.product', 'tagProduct') + .leftJoinAndSelect('space.productAllocations', 'productAllocations') + .leftJoinAndSelect('productAllocations.tag', 'spaceTag') + .leftJoinAndSelect('productAllocations.product', 'spaceProduct') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -341,12 +305,14 @@ export class SpaceService { { subspaceDisabled: false }, ) .leftJoinAndSelect( - 'subspaces.tags', - 'subspaceTags', - 'subspaceTags.disabled = :subspaceTagsDisabled', - { subspaceTagsDisabled: false }, + 'subspaces.productAllocations', + 'subspaceProductAllocations', + ) + .leftJoinAndSelect('subspaceProductAllocations.tag', 'subspaceTag') + .leftJoinAndSelect( + 'subspaceProductAllocations.product', + 'subspaceProduct', ) - // .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { orphanSpaceName: ORPHAN_SPACE_NAME, @@ -456,7 +422,7 @@ export class SpaceService { const { communityUuid, spaceUuid, projectUuid } = params; const queryRunner = this.dataSource.createQueryRunner(); - const hasSubspace = updateSpaceDto.subspace?.length > 0; + const hasSubspace = updateSpaceDto.subspaces?.length > 0; const hasTags = updateSpaceDto.tags?.length > 0; try { @@ -473,13 +439,6 @@ export class SpaceService { spaceUuid, ); - if (space.spaceName === ORPHAN_SPACE_NAME) { - throw new HttpException( - `Space "${ORPHAN_SPACE_NAME}" cannot be updated`, - HttpStatus.BAD_REQUEST, - ); - } - if (space.spaceModel && !updateSpaceDto.spaceModelUuid) { await queryRunner.manager.update(SpaceEntity, space.uuid, { spaceModel: null, @@ -512,7 +471,11 @@ export class SpaceService { queryRunner, ); } else if (hasDependencies) { - await this.spaceModelService.overwriteSpace( + // check for uuids that didn't change, + // get their device ids and check if they has a tag in device entity, + // if so move them ot the orphan space + + await this.spaceModelService.removeSpaceOldSubspacesAndAllocations( space, project, queryRunner, @@ -536,23 +499,45 @@ export class SpaceService { ); } - if (hasSubspace) { - await this.subSpaceService.modifySubSpace( - updateSpaceDto.subspace, + if (updateSpaceDto.subspaces) { + await this.subSpaceService.updateSubspaceInSpace( + updateSpaceDto.subspaces, queryRunner, space, projectUuid, - updateSpaceDto.tags, ); } if (updateSpaceDto.tags) { - await this.spaceProductAllocationService.updateSpaceProductAllocations( - updateSpaceDto.tags, + await queryRunner.manager.delete(SpaceProductAllocationEntity, { + space: { uuid: space.uuid }, + tag: { + uuid: Not( + In( + updateSpaceDto.tags + .filter((tag) => tag.tagUuid) + .map((tag) => tag.tagUuid), + ), + ), + }, + }); + await this.createAllocations( + updateSpaceDto.tags.map((tag) => ({ + name: tag.name, + uuid: tag.tagUuid, + productUuid: tag.productUuid, + })), projectUuid, - space, queryRunner, - updateSpaceDto.subspace, + space, + ); + } + + if (space.devices?.length) { + await this.deviceService.addDevicesToOrphanSpace( + space, + project, + queryRunner, ); } @@ -636,7 +621,7 @@ export class SpaceService { // Get all spaces that are children of the provided space, including the parent-child relations const spaces = await this.spaceRepository.find({ where: { parent: { uuid: spaceUuid }, disabled: false }, - relations: ['parent', 'children'], // Include parent and children relations + relations: ['parent', 'children'], }); // Organize spaces into a hierarchical structure @@ -715,13 +700,13 @@ export class SpaceService { return rootSpaces; } - private validateSpaceCreation( - addSpaceDto: AddSpaceDto, - spaceModelUuid?: string, - ) { + private validateSpaceCreationCriteria({ + spaceModelUuid, + tags, + subspaces, + }: Pick): void { const hasTagsOrSubspaces = - (addSpaceDto.tags && addSpaceDto.tags.length > 0) || - (addSpaceDto.subspaces && addSpaceDto.subspaces.length > 0); + (tags && tags.length > 0) || (subspaces && subspaces.length > 0); if (spaceModelUuid && hasTagsOrSubspaces) { throw new HttpException( @@ -731,36 +716,36 @@ export class SpaceService { } } - private async createSubspaces( - subspaces: AddSubspaceDto[], - space: SpaceEntity, - queryRunner: QueryRunner, - tags: ProcessTagDto[], - projectUuid: string, - ): Promise { - space.subspaces = await this.subSpaceService.createSubspacesFromDto( - subspaces, - space, - queryRunner, - null, - projectUuid, - ); - } - - private async createTags( + private async createAllocations( tags: ProcessTagDto[], projectUuid: string, queryRunner: QueryRunner, space: SpaceEntity, ): Promise { - const processedTags = await this.newTagService.processTags( + const allocationsData = await this.tagService.processTags( tags, projectUuid, queryRunner, ); - await this.spaceProductAllocationService.createSpaceProductAllocations( + + // Create a mapping of created tags by UUID and name for quick lookup + const createdTagsByUUID = new Map(allocationsData.map((t) => [t.uuid, t])); + const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); + + // Create the product-tag mapping based on the processed tags + const productTagMapping = tags.map(({ uuid, name, productUuid }) => { + const inputTag = uuid + ? createdTagsByUUID.get(uuid) + : createdTagsByName.get(name); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }); + + await this.spaceProductAllocationService.createProductAllocations( space, - processedTags, + productTagMapping, queryRunner, ); } diff --git a/src/space/services/subspace/subspace-device.service.ts b/src/space/services/subspace/subspace-device.service.ts index 5a572e5..4c8d66b 100644 --- a/src/space/services/subspace/subspace-device.service.ts +++ b/src/space/services/subspace/subspace-device.service.ts @@ -1,14 +1,13 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { DeviceEntity } from '@app/common/modules/device/entities'; import { DeviceRepository } from '@app/common/modules/device/repositories'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeviceService } from 'src/device/services'; +import { In, QueryRunner } from 'typeorm'; import { DeviceSubSpaceParam, GetSubSpaceParam } from '../../dtos'; import { ValidationService } from '../space-validation.service'; -import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; -import { In, QueryRunner } from 'typeorm'; -import { DeviceEntity } from '@app/common/modules/device/entities'; -import { TagRepository } from '@app/common/modules/space'; -import { DeviceService } from 'src/device/services'; @Injectable() export class SubspaceDeviceService { @@ -17,7 +16,6 @@ export class SubspaceDeviceService { private readonly deviceRepository: DeviceRepository, private readonly deviceService: DeviceService, private readonly validationService: ValidationService, - private readonly tagRepository: TagRepository, ) {} async listDevicesInSubspace( @@ -202,21 +200,6 @@ export class SubspaceDeviceService { ); } - async findNextTag(): Promise { - const tags = await this.tagRepository.find({ select: ['tag'] }); - - const tagNumbers = tags - .map((t) => t.tag.match(/^Tag (\d+)$/)) - .filter((match) => match) - .map((match) => parseInt(match[1])) - .sort((a, b) => a - b); - - const nextTagNumber = tagNumbers.length - ? tagNumbers[tagNumbers.length - 1] + 1 - : 1; - return nextTagNumber; - } - private async findDeviceWithSubspaceAndTag(deviceUuid: string) { return await this.deviceRepository.findOne({ where: { uuid: deviceUuid }, diff --git a/src/space/services/subspace/subspace-product-allocation.service.ts b/src/space/services/subspace/subspace-product-allocation.service.ts index 8f7a664..b976ce6 100644 --- a/src/space/services/subspace/subspace-product-allocation.service.ts +++ b/src/space/services/subspace/subspace-product-allocation.service.ts @@ -1,18 +1,11 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { ProductEntity } from '@app/common/modules/product/entities'; import { SpaceProductAllocationRepository } from '@app/common/modules/space'; -import { SpaceProductAllocationEntity } from '@app/common/modules/space/entities/space-product-allocation.entity'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; import { SubspaceProductAllocationRepository } from '@app/common/modules/space/repositories/subspace.repository'; -import { NewTagEntity } from '@app/common/modules/tag'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; -import { ISingleSubspace } from 'src/space/interfaces/single-subspace.interface'; -import { ProcessTagDto } from 'src/tags/dtos'; +import { UpdateSpaceAllocationDto } from 'src/space/interfaces/update-subspace-allocation.dto'; import { TagService as NewTagService } from 'src/tags/services'; -import { In, QueryRunner } from 'typeorm'; +import { In, Not, QueryRunner } from 'typeorm'; @Injectable() export class SubspaceProductAllocationService { @@ -23,44 +16,38 @@ export class SubspaceProductAllocationService { private readonly subspaceProductAllocationRepository: SubspaceProductAllocationRepository, ) {} - async createSubspaceProductAllocations( + async createProductAllocations( subspace: SubspaceEntity, - processedTags: NewTagEntity[], + allocationsData: { product: string; tag: string }[], queryRunner?: QueryRunner, - spaceAllocationsToExclude?: SpaceProductAllocationEntity[], + // spaceAllocationsToExclude?: SpaceProductAllocationEntity[], ): Promise { try { - if (!processedTags.length) return; + if (!allocationsData.length) return; const allocations: SubspaceProductAllocationEntity[] = []; - for (const tag of processedTags) { - await this.validateTagWithinSubspace( - queryRunner, - tag, - subspace, - spaceAllocationsToExclude, - ); + for (const allocationData of allocationsData) { + // await this.validateTagWithinSubspace( + // queryRunner, + // allocationData.tag, + // subspace, + // spaceAllocationsToExclude, + // ); - let allocation = await this.getAllocationByProduct( - tag.product, - subspace, - queryRunner, - ); - - if (!allocation) { - allocation = this.createNewSubspaceAllocation( - subspace, - tag, - queryRunner, - ); - allocations.push(allocation); - } else if (!allocation.tags.some((t) => t.uuid === tag.uuid)) { - allocation.tags.push(tag); - await this.saveAllocation(allocation, queryRunner); + if ( + await this.isAllocationExist(allocationData, subspace, queryRunner) + ) { + continue; } - } + const allocation = this.createNewSubspaceAllocation( + subspace, + allocationData, + queryRunner, + ); + allocations.push(allocation); + } if (allocations.length > 0) { await this.saveAllocations(allocations, queryRunner); } @@ -71,307 +58,130 @@ export class SubspaceProductAllocationService { ); } } - async updateSubspaceProductAllocations( - subspaces: ISingleSubspace[], + + async updateSubspaceProductAllocationsV2( + subSpaces: UpdateSpaceAllocationDto[], projectUuid: string, queryRunner: QueryRunner, - space: SpaceEntity, - spaceTagUpdateDtos?: ModifyTagDto[], ) { - const spaceAllocationToExclude: SpaceProductAllocationEntity[] = []; - for (const subspace of subspaces) { - if (!subspace.tags || subspace.tags.length === 0) continue; - const tagDtos = subspace.tags; - const tagsToAddDto: ProcessTagDto[] = tagDtos - .filter((dto) => dto.action === ModifyAction.ADD) - .map((dto) => ({ - name: dto.name, - productUuid: dto.productUuid, - uuid: dto.newTagUuid, - })); + await Promise.all( + subSpaces.map(async (subspace) => { + await queryRunner.manager.delete(SubspaceProductAllocationEntity, { + subspace: { uuid: subspace.uuid }, + tag: { + uuid: Not( + In( + subspace.tags.filter((tag) => tag.uuid).map((tag) => tag.uuid), + ), + ), + }, + }); + const subspaceEntity = await queryRunner.manager.findOne( + SubspaceEntity, + { + where: { uuid: subspace.uuid }, + }, + ); - const tagsToDeleteDto = tagDtos.filter( - (dto) => dto.action === ModifyAction.DELETE, - ); - - if (tagsToAddDto.length > 0) { - let processedTags = await this.tagService.processTags( - tagsToAddDto, + const processedTags = await this.tagService.processTags( + subspace.tags, projectUuid, queryRunner, ); - for (const subspaceDto of subspaces) { - if ( - subspaceDto !== subspace && - subspaceDto.action === ModifyAction.UPDATE && - subspaceDto.tags - ) { - const deletedTags = subspaceDto.tags.filter( - (tagDto) => - tagDto.action === ModifyAction.DELETE && - processedTags.some((tag) => tag.uuid === tagDto.tagUuid), - ); + const createdTagsByUUID = new Map( + processedTags.map((t) => [t.uuid, t]), + ); + const createdTagsByName = new Map( + processedTags.map((t) => [t.name, t]), + ); - for (const deletedTag of deletedTags) { - const allocation = await queryRunner.manager.findOne( - SubspaceProductAllocationEntity, - { - where: { - subspace: { uuid: subspaceDto.subspace.uuid }, - }, - relations: ['tags', 'product', 'subspace'], - }, - ); + // Create the product-tag mapping based on the processed tags + const productTagMapping = subspace.tags.map( + ({ uuid, name, productUuid }) => { + const inputTag = uuid + ? createdTagsByUUID.get(uuid) + : createdTagsByName.get(name); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }, + ); - const isCommonTag = allocation.tags.some( - (tag) => tag.uuid === deletedTag.tagUuid, - ); - - if (allocation && isCommonTag) { - const tagEntity = allocation.tags.find( - (tag) => tag.uuid === deletedTag.tagUuid, - ); - - allocation.tags = allocation.tags.filter( - (tag) => tag.uuid !== deletedTag.tagUuid, - ); - - await queryRunner.manager.save(allocation); - - const productAllocationExistInSubspace = - await queryRunner.manager.findOne( - SubspaceProductAllocationEntity, - { - where: { - subspace: { - uuid: subspaceDto.subspace.uuid, - }, - product: { uuid: allocation.product.uuid }, - }, - relations: ['tags'], - }, - ); - - if (productAllocationExistInSubspace) { - productAllocationExistInSubspace.tags.push(tagEntity); - await queryRunner.manager.save( - productAllocationExistInSubspace, - ); - } else { - const newProductAllocation = queryRunner.manager.create( - SubspaceProductAllocationEntity, - { - subspace: subspace.subspace, - product: allocation.product, - tags: [tagEntity], - }, - ); - - await queryRunner.manager.save(newProductAllocation); - } - - processedTags = processedTags.filter( - (tag) => tag.uuid !== deletedTag.tagUuid, - ); - - subspaceDto.tags = subspaceDto.tags.filter( - (tagDto) => tagDto.tagUuid !== deletedTag.tagUuid, - ); - } - } - } - if ( - subspaceDto !== subspace && - subspaceDto.action === ModifyAction.DELETE - ) { - const allocation = await queryRunner.manager.findOne( - SubspaceProductAllocationEntity, - { - where: { - subspace: { uuid: subspaceDto.subspace.uuid }, - }, - relations: ['tags'], - }, - ); - - const repeatedTags = allocation?.tags.filter((tag) => - processedTags.some( - (processedTag) => processedTag.uuid === tag.uuid, - ), - ); - if (repeatedTags.length > 0) { - allocation.tags = allocation.tags.filter( - (tag) => - !repeatedTags.some( - (repeatedTag) => repeatedTag.uuid === tag.uuid, - ), - ); - - await queryRunner.manager.save(allocation); - - const productAllocationExistInSubspace = - await queryRunner.manager.findOne( - SubspaceProductAllocationEntity, - { - where: { - subspace: { uuid: subspaceDto.subspace.uuid }, - product: { uuid: allocation.product.uuid }, - }, - relations: ['tags'], - }, - ); - - if (productAllocationExistInSubspace) { - productAllocationExistInSubspace.tags.push(...repeatedTags); - await queryRunner.manager.save( - productAllocationExistInSubspace, - ); - } else { - const newProductAllocation = queryRunner.manager.create( - SubspaceProductAllocationEntity, - { - subspace: subspace.subspace, - product: allocation.product, - tags: repeatedTags, - }, - ); - - await queryRunner.manager.save(newProductAllocation); - } - - const newAllocation = queryRunner.manager.create( - SubspaceProductAllocationEntity, - { - subspace: subspace.subspace, - product: allocation.product, - tags: repeatedTags, - }, - ); - - await queryRunner.manager.save(newAllocation); - } - } - } - if (spaceTagUpdateDtos) { - const deletedSpaceTags = spaceTagUpdateDtos.filter( - (tagDto) => - tagDto.action === ModifyAction.DELETE && - processedTags.some((tag) => tag.uuid === tagDto.tagUuid), - ); - for (const deletedTag of deletedSpaceTags) { - const allocation = await queryRunner.manager.findOne( - SpaceProductAllocationEntity, - { - where: { - space: { uuid: space.uuid }, - tags: { uuid: deletedTag.tagUuid }, - }, - relations: ['tags', 'subspace'], - }, - ); - - if ( - allocation && - allocation.tags.some((tag) => tag.uuid === deletedTag.tagUuid) - ) { - spaceAllocationToExclude.push(allocation); - } - } - } - - await this.createSubspaceProductAllocations( - subspace.subspace, - processedTags, + await this.createProductAllocations( + subspaceEntity, + productTagMapping, queryRunner, - spaceAllocationToExclude, ); - } - if (tagsToDeleteDto.length > 0) { - await this.processDeleteActions(tagsToDeleteDto, queryRunner); - } - } + }), + ); } - async processDeleteActions( - dtos: ModifyTagDto[], - queryRunner: QueryRunner, - ): Promise { - try { - if (!dtos || dtos.length === 0) { - throw new Error('No DTOs provided for deletion.'); - } - - const tagUuidsToDelete = dtos - .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) - .map((dto) => dto.tagUuid); - - if (tagUuidsToDelete.length === 0) return []; - - const allocationsToUpdate = await queryRunner.manager.find( - SubspaceProductAllocationEntity, - { - where: { tags: { uuid: In(tagUuidsToDelete) } }, - relations: ['tags'], - }, - ); - - if (!allocationsToUpdate || allocationsToUpdate.length === 0) return []; - - const deletedAllocations: SubspaceProductAllocationEntity[] = []; - const allocationUpdates: SubspaceProductAllocationEntity[] = []; - - for (const allocation of allocationsToUpdate) { - const updatedTags = allocation.tags.filter( - (tag) => !tagUuidsToDelete.includes(tag.uuid), - ); - - if (updatedTags.length === allocation.tags.length) { - continue; - } - - if (updatedTags.length === 0) { - deletedAllocations.push(allocation); - } else { - allocation.tags = updatedTags; - allocationUpdates.push(allocation); - } - } - - if (allocationUpdates.length > 0) { - await queryRunner.manager.save( - SubspaceProductAllocationEntity, - allocationUpdates, - ); - } - - if (deletedAllocations.length > 0) { - await queryRunner.manager.remove( - SubspaceProductAllocationEntity, - deletedAllocations, - ); - } - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from('subspace_product_tags') - .where( - 'subspace_product_allocation_uuid NOT IN ' + - queryRunner.manager - .createQueryBuilder() - .select('allocation.uuid') - .from(SubspaceProductAllocationEntity, 'allocation') - .getQuery() + - ')', - ) - .execute(); - - return deletedAllocations; - } catch (error) { - throw this.handleError(error, `Failed to delete tags in subspace`); - } - } + // async processDeleteActions(dtos: ModifyTagDto[], queryRunner: QueryRunner) { + // // : Promise + // try { + // // if (!dtos || dtos.length === 0) { + // // throw new Error('No DTOs provided for deletion.'); + // // } + // // const tagUuidsToDelete = dtos + // // .filter((dto) => dto.action === ModifyAction.DELETE && dto.tagUuid) + // // .map((dto) => dto.tagUuid); + // // if (tagUuidsToDelete.length === 0) return []; + // // const allocationsToUpdate = await queryRunner.manager.find( + // // SubspaceProductAllocationEntity, + // // { + // // where: { tag: In(tagUuidsToDelete) }, + // // }, + // // ); + // // if (!allocationsToUpdate || allocationsToUpdate.length === 0) return []; + // // const deletedAllocations: SubspaceProductAllocationEntity[] = []; + // // const allocationUpdates: SubspaceProductAllocationEntity[] = []; + // // for (const allocation of allocationsToUpdate) { + // // const updatedTags = allocation.tags.filter( + // // (tag) => !tagUuidsToDelete.includes(tag.uuid), + // // ); + // // if (updatedTags.length === allocation.tags.length) { + // // continue; + // // } + // // if (updatedTags.length === 0) { + // // deletedAllocations.push(allocation); + // // } else { + // // allocation.tags = updatedTags; + // // allocationUpdates.push(allocation); + // // } + // // } + // // if (allocationUpdates.length > 0) { + // // await queryRunner.manager.save( + // // SubspaceProductAllocationEntity, + // // allocationUpdates, + // // ); + // // } + // // if (deletedAllocations.length > 0) { + // // await queryRunner.manager.remove( + // // SubspaceProductAllocationEntity, + // // deletedAllocations, + // // ); + // // } + // // await queryRunner.manager + // // .createQueryBuilder() + // // .delete() + // // .from('subspace_product_tags') + // // .where( + // // 'subspace_product_allocation_uuid NOT IN ' + + // // queryRunner.manager + // // .createQueryBuilder() + // // .select('allocation.uuid') + // // .from(SubspaceProductAllocationEntity, 'allocation') + // // .getQuery() + + // // ')', + // // ) + // // .execute(); + // // return deletedAllocations; + // } catch (error) { + // throw this.handleError(error, `Failed to delete tags in subspace`); + // } + // } async unlinkModels( allocations: SubspaceProductAllocationEntity[], @@ -395,108 +205,106 @@ export class SubspaceProductAllocationService { } } - private async validateTagWithinSubspace( - queryRunner: QueryRunner | undefined, - tag: NewTagEntity, - subspace: SubspaceEntity, - spaceAllocationsToExclude?: SpaceProductAllocationEntity[], - ): Promise { - const existingTagInSpace = await (queryRunner - ? queryRunner.manager.findOne(SpaceProductAllocationEntity, { - where: { - product: { uuid: tag.product.uuid }, - space: { uuid: subspace.space.uuid }, - tags: { uuid: tag.uuid }, - }, - relations: ['tags'], - }) - : this.spaceProductAllocationRepository.findOne({ - where: { - product: { uuid: tag.product.uuid }, - space: { uuid: subspace.space.uuid }, - tags: { uuid: tag.uuid }, - }, - relations: ['tags'], - })); + // private async validateTagWithinSubspace( + // queryRunner: QueryRunner | undefined, + // tag: NewTagEntity & { product: string }, + // subspace: SubspaceEntity, + // spaceAllocationsToExclude?: SpaceProductAllocationEntity[], + // ): Promise { + // // const existingTagInSpace = await (queryRunner + // // ? queryRunner.manager.findOne(SpaceProductAllocationEntity, { + // // where: { + // // product: { uuid: tag.product }, + // // space: { uuid: subspace.space.uuid }, + // // tag: { uuid: tag.uuid }, + // // }, + // // }) + // // : this.spaceProductAllocationRepository.findOne({ + // // where: { + // // product: { uuid: tag.product }, + // // space: { uuid: subspace.space.uuid }, + // // tag: { uuid: tag.uuid }, + // // }, + // // })); + // // const isExcluded = spaceAllocationsToExclude?.some( + // // (excludedAllocation) => + // // excludedAllocation.product.uuid === tag.product && + // // excludedAllocation.tags.some((t) => t.uuid === tag.uuid), + // // ); + // // if (!isExcluded && existingTagInSpace) { + // // throw new HttpException( + // // `Tag ${tag.uuid} (Product: ${tag.product}) is already allocated at the space level (${subspace.space.uuid}). Cannot allocate the same tag in a subspace.`, + // // HttpStatus.BAD_REQUEST, + // // ); + // // } + // // // ?: Check if the tag is already allocated in another "subspace" within the same space + // // const existingTagInSameSpace = await (queryRunner + // // ? queryRunner.manager.findOne(SubspaceProductAllocationEntity, { + // // where: { + // // product: { uuid: tag.product }, + // // subspace: { space: subspace.space }, + // // tag: { uuid: tag.uuid }, + // // }, + // // relations: ['subspace'], + // // }) + // // : this.subspaceProductAllocationRepository.findOne({ + // // where: { + // // product: { uuid: tag.product }, + // // subspace: { space: subspace.space }, + // // tag: { uuid: tag.uuid }, + // // }, + // // relations: ['subspace'], + // // })); + // // if ( + // // existingTagInSameSpace && + // // existingTagInSameSpace.subspace.uuid !== subspace.uuid + // // ) { + // // throw new HttpException( + // // `Tag ${tag.uuid} (Product: ${tag.product}) is already allocated in another subspace (${existingTagInSameSpace.subspace.uuid}) within the same space (${subspace.space.uuid}).`, + // // HttpStatus.BAD_REQUEST, + // // ); + // // } + // } - const isExcluded = spaceAllocationsToExclude?.some( - (excludedAllocation) => - excludedAllocation.product.uuid === tag.product.uuid && - excludedAllocation.tags.some((t) => t.uuid === tag.uuid), - ); - - if (!isExcluded && existingTagInSpace) { - throw new HttpException( - `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated at the space level (${subspace.space.uuid}). Cannot allocate the same tag in a subspace.`, - HttpStatus.BAD_REQUEST, - ); - } - - const existingTagInSameSpace = await (queryRunner - ? queryRunner.manager.findOne(SubspaceProductAllocationEntity, { - where: { - product: { uuid: tag.product.uuid }, - subspace: { space: subspace.space }, - tags: { uuid: tag.uuid }, - }, - relations: ['subspace', 'tags'], - }) - : this.subspaceProductAllocationRepository.findOne({ - where: { - product: { uuid: tag.product.uuid }, - subspace: { space: subspace.space }, - tags: { uuid: tag.uuid }, - }, - relations: ['subspace', 'tags'], - })); - - if ( - existingTagInSameSpace && - existingTagInSameSpace.subspace.uuid !== subspace.uuid - ) { - throw new HttpException( - `Tag ${tag.uuid} (Product: ${tag.product.uuid}) is already allocated in another subspace (${existingTagInSameSpace.subspace.uuid}) within the same space (${subspace.space.uuid}).`, - HttpStatus.BAD_REQUEST, - ); - } - } private createNewSubspaceAllocation( subspace: SubspaceEntity, - tag: NewTagEntity, + allocationData: { product: string; tag: string }, queryRunner?: QueryRunner, ): SubspaceProductAllocationEntity { return queryRunner ? queryRunner.manager.create(SubspaceProductAllocationEntity, { subspace, - product: tag.product, - tags: [tag], + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, }) : this.subspaceProductAllocationRepository.create({ subspace, - product: tag.product, - tags: [tag], + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, }); } - private async getAllocationByProduct( - product: ProductEntity, + + private async isAllocationExist( + allocationData: { product: string; tag: string }, subspace: SubspaceEntity, queryRunner?: QueryRunner, - ): Promise { - return queryRunner - ? queryRunner.manager.findOne(SubspaceProductAllocationEntity, { + ): Promise { + const allocation = queryRunner + ? await queryRunner.manager.findOne(SubspaceProductAllocationEntity, { where: { subspace: { uuid: subspace.uuid }, - product: { uuid: product.uuid }, + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, }, - relations: ['tags'], }) - : this.subspaceProductAllocationRepository.findOne({ + : await this.subspaceProductAllocationRepository.findOne({ where: { subspace: { uuid: subspace.uuid }, - product: { uuid: product.uuid }, + product: { uuid: allocationData.product }, + tag: { uuid: allocationData.tag }, }, - relations: ['tags'], }); + return !!allocation; } private async saveAllocation( allocation: SubspaceProductAllocationEntity, @@ -535,34 +343,9 @@ export class SubspaceProductAllocationService { } async clearAllAllocations(subspaceUuids: string[], queryRunner: QueryRunner) { try { - const allocationUuids = await queryRunner.manager - .createQueryBuilder(SubspaceProductAllocationEntity, 'allocation') - .select('allocation.uuid') - .where('allocation.subspace_uuid IN (:...subspaceUuids)', { - subspaceUuids, - }) - .getRawMany() - .then((results) => results.map((r) => r.allocation_uuid)); - - if (allocationUuids.length === 0) { - return; - } - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from('subspace_product_tags') - .where('subspace_product_allocation_uuid IN (:...allocationUuids)', { - allocationUuids, - }) - .execute(); - - await queryRunner.manager - .createQueryBuilder() - .delete() - .from(SubspaceProductAllocationEntity) - .where('subspace_uuid IN (:...subspaceUuids)', { subspaceUuids }) - .execute(); + await queryRunner.manager.delete(SubspaceProductAllocationEntity, { + subspace: { uuid: In(subspaceUuids) }, + }); } catch (error) { throw new HttpException( error instanceof HttpException diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts index d74d355..d9bcd3d 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -1,32 +1,33 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { SubspaceDto } from '@app/common/modules/space/dtos'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { In, Not, QueryRunner } from 'typeorm'; import { AddSubspaceDto, GetSpaceParam, GetSubSpaceParam, ModifySubspaceDto, } from '../../dtos'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { - TypeORMCustomModel, - TypeORMCustomModelFindAllQuery, -} from '@app/common/models/typeOrmCustom.model'; -import { PageResponse } from '@app/common/dto/pagination.response.dto'; -import { SubspaceDto } from '@app/common/modules/space/dtos'; -import { In, QueryRunner } from 'typeorm'; import { SubspaceModelEntity } from '@app/common/modules/space-model'; -import { ValidationService } from '../space-validation.service'; -import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { SubspaceDeviceService } from './subspace-device.service'; -import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { TagService as NewTagService } from 'src/tags/services/tags.service'; -import { SubspaceProductAllocationService } from './subspace-product-allocation.service'; import { SubspaceProductAllocationEntity } from '@app/common/modules/space/entities/subspace/subspace-product-allocation.entity'; +import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; +import { SubspaceRepository } from '@app/common/modules/space/repositories/subspace.repository'; +import { + AllocationsOwnerType, + CreateAllocationsDto, +} from 'src/space/dtos/create-allocations.dto'; +import { TagService as NewTagService } from 'src/tags/services/tags.service'; +import { ValidationService } from '../space-validation.service'; +import { SubspaceDeviceService } from './subspace-device.service'; +import { SubspaceProductAllocationService } from './subspace-product-allocation.service'; @Injectable() export class SubSpaceService { @@ -55,9 +56,9 @@ export class SubSpaceService { ); const subspaces = subspaceData.map((data) => - queryRunner.manager.create(this.subspaceRepository.target, data), + queryRunner.manager.create(SubspaceEntity, data), ); - return await queryRunner.manager.save(subspaces); + return queryRunner.manager.save(subspaces); } catch (error) { throw new HttpException( `An unexpected error occurred while creating subspaces. ${error}`, @@ -91,42 +92,30 @@ export class SubSpaceService { addSubspaceDtos: AddSubspaceDto[], space: SpaceEntity, queryRunner: QueryRunner, - otherTags?: ProcessTagDto[], projectUuid?: string, ): Promise { try { - this.checkForDuplicateNames( - addSubspaceDtos.map(({ subspaceName }) => subspaceName), + const createdSubspaces = await this.createSubspaces( + addSubspaceDtos.map((dto) => ({ + subspaceName: dto.subspaceName, + space, + })), + queryRunner, ); - - const subspaceData = addSubspaceDtos.map((dto) => ({ - subspaceName: dto.subspaceName, - space, - })); - - const subspaces = await this.createSubspaces(subspaceData, queryRunner); await Promise.all( - addSubspaceDtos.map(async (dto, index) => { - const subspace = subspaces[index]; - - const allTags = [...(dto.tags || []), ...(otherTags || [])]; - - if (allTags.length) { - const processedTags = await this.newTagService.processTags( - allTags, - projectUuid, - queryRunner, - ); - - await this.subspaceProductAllocationService.createSubspaceProductAllocations( - subspace, - processedTags, - queryRunner, - ); - } + addSubspaceDtos.map(async ({ tags }, index) => { + // map the dto to the corresponding subspace + const subspace = createdSubspaces[index]; + await this.createAllocations({ + projectUuid, + queryRunner, + tags, + type: AllocationsOwnerType.SUBSPACE, + subspace, + }); }), ); - return subspaces; + return createdSubspaces; } catch (error) { if (error instanceof HttpException) { throw error; @@ -312,65 +301,62 @@ export class SubSpaceService { deleteResults.push({ uuid: dto.subspaceUuid }); } - return deleteResults; + return deleteResults; } */ - async modifySubSpace( + async updateSubspaceInSpace( subspaceDtos: ModifySubspaceDto[], queryRunner: QueryRunner, - space?: SpaceEntity, - projectUuid?: string, - spaceTagUpdateDtos?: ModifyTagDto[], + space: SpaceEntity, + projectUuid: string, ) { - if (!subspaceDtos || subspaceDtos.length === 0) { - return; - } try { - const addedSubspaces = []; - const updatedSubspaces = []; - - for (const subspace of subspaceDtos) { - switch (subspace.action) { - case ModifyAction.ADD: - const addedSubspace = await this.handleAddAction( - subspace, - space, - queryRunner, - ); - if (addedSubspace) addedSubspaces.push(addedSubspace); - - break; - case ModifyAction.UPDATE: - const updatedSubspace = await this.handleUpdateAction( - subspace, - queryRunner, - ); - if (updatedSubspace) { - updatedSubspaces.push(updatedSubspace); - } - break; - case ModifyAction.DELETE: - await this.handleDeleteAction(subspace, queryRunner); - break; - default: - throw new HttpException( - `Invalid action "${subspace.action}".`, - HttpStatus.BAD_REQUEST, - ); - } - } - - const combinedSubspaces = [...addedSubspaces, ...updatedSubspaces].filter( - (subspace) => subspace !== undefined, + // disable subspaces that are not in the provided list & delte their allocations + await queryRunner.manager.update( + SubspaceEntity, + { + uuid: Not( + In(subspaceDtos.filter(({ uuid }) => uuid).map(({ uuid }) => uuid)), + ), + space: { uuid: space.uuid }, + }, + { + disabled: true, + }, ); - if (combinedSubspaces.length > 0) { - await this.subspaceProductAllocationService.updateSubspaceProductAllocations( - combinedSubspaces, + await queryRunner.manager.delete(SubspaceProductAllocationEntity, { + subspace: { uuid: Not(In(subspaceDtos.map((dto) => dto.uuid))) }, + }); + + // create or update subspaces provided in the list + const newSubspaces = this.subspaceRepository.create( + subspaceDtos.filter((dto) => !dto.uuid), + ); + + const updatedSubspaces: SubspaceEntity[] = await queryRunner.manager.save( + SubspaceEntity, + [...newSubspaces, ...subspaceDtos.filter((dto) => dto.uuid)].map( + (subspace) => ({ ...subspace, space }), + ), + ); + + // create or update allocations for the subspaces + if (updatedSubspaces.length > 0) { + await this.subspaceProductAllocationService.updateSubspaceProductAllocationsV2( + subspaceDtos.map((dto) => { + if (!dto.uuid) { + dto.uuid = updatedSubspaces.find( + (subspace) => subspace.subspaceName === dto.subspaceName, + )?.uuid; + } + return { + tags: dto.tags || [], + uuid: dto.uuid, + }; + }), projectUuid, queryRunner, - space, - spaceTagUpdateDtos, ); } } catch (error) { @@ -427,70 +413,10 @@ export class SubSpaceService { }); } - private async handleAddAction( - subspace: ModifySubspaceDto, - space: SpaceEntity, - queryRunner: QueryRunner, - ): Promise { - const createTagDtos: ProcessTagDto[] = - subspace.tags?.map((tag) => ({ - name: tag.name as string, - uuid: tag.tagUuid, - productUuid: tag.productUuid as string, - })) || []; - const subSpace = await this.createSubspacesFromDto( - [{ subspaceName: subspace.subspaceName, tags: createTagDtos }], - space, - queryRunner, - ); - return subSpace[0]; - } - - private async handleUpdateAction( - modifyDto: ModifySubspaceDto, - queryRunner: QueryRunner, - ): Promise { - const subspace = await this.findOne(modifyDto.uuid); - const updatedSubspace = await this.update( - queryRunner, - subspace, - modifyDto.subspaceName, - ); - return updatedSubspace; - } - - async update( - queryRunner: QueryRunner, - subspace: SubspaceEntity, - subspaceName?: string, - ) { - return await this.updateSubspaceName(queryRunner, subspace, subspaceName); - } - - async handleDeleteAction( - modifyDto: ModifySubspaceDto, - queryRunner: QueryRunner, - ): Promise { - const subspace = await this.findOne(modifyDto.uuid); - - await queryRunner.manager.update( - this.subspaceRepository.target, - { uuid: subspace.uuid }, - { disabled: true }, - ); - - if (subspace.devices.length > 0) { - await this.deviceService.deleteSubspaceDevices( - subspace.devices, - queryRunner, - ); - } - } - private async findOne(subspaceUuid: string): Promise { const subspace = await this.subspaceRepository.findOne({ where: { uuid: subspaceUuid, disabled: false }, - relations: ['tags', 'space', 'devices', 'tags.product', 'tags.device'], + relations: ['space', 'devices'], }); if (!subspace) { throw new HttpException( @@ -501,36 +427,6 @@ export class SubSpaceService { return subspace; } - async updateSubspaceName( - queryRunner: QueryRunner, - subSpace: SubspaceEntity, - subspaceName?: string, - ): Promise { - if (subspaceName) { - subSpace.subspaceName = subspaceName; - return await queryRunner.manager.save(subSpace); - } - return subSpace; - } - - private async checkForDuplicateNames(names: string[]): Promise { - const seenNames = new Set(); - const duplicateNames = new Set(); - - for (const name of names) { - if (!seenNames.add(name)) { - duplicateNames.add(name); - } - } - - if (duplicateNames.size > 0) { - throw new HttpException( - `Duplicate subspace names found: ${[...duplicateNames].join(', ')}`, - HttpStatus.CONFLICT, - ); - } - } - private async checkExistingNamesInSpace( names: string[], space: SpaceEntity, @@ -557,16 +453,6 @@ export class SubSpaceService { } } - private async validateName( - names: string[], - space: SpaceEntity, - ): Promise { - await this.checkForDuplicateNames(names); - await this.checkExistingNamesInSpace(names, space); - } - extractTagsFromSubspace(addSubspaceDto: AddSubspaceDto[]): ProcessTagDto[] { - return addSubspaceDto.flatMap((subspace) => subspace.tags || []); - } async clearSubspaces(subspaceUuids: string[], queryRunner: QueryRunner) { try { await queryRunner.manager.update( @@ -590,4 +476,40 @@ export class SubSpaceService { ); } } + + async createAllocations(dto: CreateAllocationsDto): Promise { + const { projectUuid, queryRunner, tags, type } = dto; + + const allocationsData = await this.newTagService.processTags( + tags, + projectUuid, + queryRunner, + ); + + // Create a mapping of created tags by UUID and name for quick lookup + const createdTagsByUUID = new Map(allocationsData.map((t) => [t.uuid, t])); + const createdTagsByName = new Map(allocationsData.map((t) => [t.name, t])); + + // Create the product-tag mapping based on the processed tags + const productTagMapping = tags.map(({ uuid, name, productUuid }) => { + const inputTag = uuid + ? createdTagsByUUID.get(uuid) + : createdTagsByName.get(name); + return { + tag: inputTag?.uuid, + product: productUuid, + }; + }); + + switch (type) { + case AllocationsOwnerType.SUBSPACE: { + await this.subspaceProductAllocationService.createProductAllocations( + dto.subspace, + productTagMapping, + queryRunner, + ); + break; + } + } + } } diff --git a/src/space/services/tag/tag.service.ts b/src/space/services/tag/tag.service.ts index 1f3e977..9de3d0a 100644 --- a/src/space/services/tag/tag.service.ts +++ b/src/space/services/tag/tag.service.ts @@ -1,597 +1,8 @@ -import { ModifyAction } from '@app/common/constants/modify-action.enum'; -import { TagRepository } from '@app/common/modules/space'; -import { TagModel } from '@app/common/modules/space-model'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; -import { SubspaceEntity } from '@app/common/modules/space/entities/subspace/subspace.entity'; -import { TagEntity } from '@app/common/modules/space/entities/tag.entity'; -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { ProductService } from 'src/product/services'; -import { ModifySubspaceDto } from 'src/space/dtos'; -import { ModifyTagDto } from 'src/space/dtos/tag/modify-tag.dto'; -import { ProcessTagDto } from 'src/tags/dtos'; -import { QueryRunner } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +// todo: find out why we need to import this +// in community module in order for the whole system to work @Injectable() export class TagService { - constructor( - private readonly tagRepository: TagRepository, - private readonly productService: ProductService, - ) {} - - async createTags( - tags: ProcessTagDto[], - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - additionalTags?: ProcessTagDto[], - tagsToDelete?: ModifyTagDto[], - ): Promise { - this.validateTagsInput(tags); - - const combinedTags = this.combineTags(tags, additionalTags); - this.ensureNoDuplicateTags(combinedTags); - - const tagEntitiesToCreate = tags.filter((tagDto) => !tagDto.uuid); - const tagEntitiesToUpdate = tags.filter((tagDto) => !!tagDto.uuid); - - try { - const createdTags = await this.bulkSaveTags( - tagEntitiesToCreate, - queryRunner, - space, - subspace, - tagsToDelete, - ); - const updatedTags = await this.moveTags( - tagEntitiesToUpdate, - queryRunner, - space, - subspace, - ); - - return [...createdTags, ...updatedTags]; - } catch (error) { - throw this.handleUnexpectedError('Failed to save tags', error); - } - } - - async bulkSaveTags( - tags: ProcessTagDto[], - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - tagsToDelete?: ModifyTagDto[], - ): Promise { - if (!tags.length) { - return []; - } - - const tagEntities = await Promise.all( - tags.map((tagDto) => - this.prepareTagEntity( - tagDto, - queryRunner, - space, - subspace, - tagsToDelete, - ), - ), - ); - - try { - return await queryRunner.manager.save(tagEntities); - } catch (error) { - if (error instanceof HttpException) { - throw error; - } - - throw new HttpException( - `Failed to save tag models due to an unexpected error: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async moveTags( - tags: ProcessTagDto[], - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - ): Promise { - if (!tags.length) { - return []; - } - - try { - return await Promise.all( - tags.map(async (tagDto) => { - try { - const tag = await this.getTagByUuid(tagDto.uuid); - if (!tag) { - throw new HttpException( - `Tag with UUID ${tagDto.uuid} not found.`, - HttpStatus.NOT_FOUND, - ); - } - - if (subspace && subspace.space) { - await queryRunner.manager.update( - this.tagRepository.target, - { uuid: tag.uuid }, - { subspace }, - ); - tag.subspace = subspace; - } - - if (!subspace && space) { - await queryRunner.manager.update( - this.tagRepository.target, - { uuid: tag.uuid }, - { subspace: null }, - ); - tag.subspace = null; - } - - return tag; - } catch (error) { - throw error; - } - }), - ); - } catch (error) { - throw new HttpException( - `Failed to move tags due to an unexpected error: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async createTagsFromModel( - queryRunner: QueryRunner, - tagModels: TagModel[], - space?: SpaceEntity, - subspace?: SubspaceEntity, - ): Promise { - if (!tagModels?.length) return; - - const tags = tagModels.map((model) => - queryRunner.manager.create(this.tagRepository.target, { - tag: model.tag, - space: space || undefined, - subspace: subspace || undefined, - product: model.product, - }), - ); - - await queryRunner.manager.save(tags); - } - - async updateTag( - tag: ModifyTagDto, - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - ): Promise { - try { - const existingTag = await this.getTagByUuid(tag.tagUuid); - - const contextSpace = space ?? subspace?.space; - - if (contextSpace && tag.name !== existingTag.tag) { - await this.checkTagReuse( - tag.name, - existingTag.product.uuid, - contextSpace, - ); - } - - return await queryRunner.manager.save( - Object.assign(existingTag, { tag: tag.name }), - ); - } catch (error) { - throw this.handleUnexpectedError('Failed to update tags', error); - } - } - - async updateTagsFromModel( - model: TagModel, - queryRunner: QueryRunner, - ): Promise { - try { - const tags = await this.tagRepository.find({ - where: { - model: { - uuid: model.uuid, - }, - }, - }); - - if (!tags.length) return; - - await queryRunner.manager.update( - this.tagRepository.target, - { model: { uuid: model.uuid } }, - { tag: model.tag }, - ); - } catch (error) { - if (error instanceof HttpException) { - throw error; - } - - throw new HttpException( - `Failed to update tags for model with UUID: ${model.uuid}. Reason: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async deleteTags(tagUuids: string[], queryRunner: QueryRunner) { - if (!tagUuids?.length) return; - - try { - await Promise.all( - tagUuids.map((id) => - queryRunner.manager.update( - this.tagRepository.target, - { uuid: id }, - { disabled: true, device: null }, - ), - ), - ); - return { message: 'Tags deleted successfully', tagUuids }; - } catch (error) { - throw this.handleUnexpectedError('Failed to delete tags', error); - } - } - - async deleteTagFromModel(modelUuid: string, queryRunner: QueryRunner) { - try { - const tags = await this.tagRepository.find({ - where: { - model: { - uuid: modelUuid, - }, - }, - }); - - if (!tags.length) return; - - await queryRunner.manager.update( - this.tagRepository.target, - { model: { uuid: modelUuid } }, - { disabled: true, device: null }, - ); - } catch (error) { - if (error instanceof HttpException) { - throw error; - } - - throw new HttpException( - `Failed to update tags for model with UUID: ${modelUuid}. Reason: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - async modifyTags( - tags: ModifyTagDto[], - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - ): Promise { - if (!tags?.length) return; - - try { - const tagsToDelete = tags.filter( - (tag) => tag.action === ModifyAction.DELETE, - ); - - await Promise.all( - tags.map(async (tag) => { - switch (tag.action) { - case ModifyAction.ADD: - await this.createTags( - [ - { - name: tag.name, - productUuid: tag.productUuid, - uuid: tag.tagUuid, - }, - ], - queryRunner, - space, - subspace, - null, - tagsToDelete, - ); - break; - case ModifyAction.UPDATE: - await this.updateTag(tag, queryRunner, space, subspace); - break; - case ModifyAction.DELETE: - await this.deleteTags([tag.tagUuid], queryRunner); - break; - default: - throw new HttpException( - `Invalid action "${tag.action}" provided.`, - HttpStatus.BAD_REQUEST, - ); - } - }), - ); - } catch (error) { - throw this.handleUnexpectedError('Failed to modify tags', error); - } - } - - async unlinkModels(tags: TagEntity[], queryRunner: QueryRunner) { - if (!tags?.length) return; - - try { - tags.forEach((tag) => { - tag.model = null; - }); - - await queryRunner.manager.save(tags); - } catch (error) { - throw this.handleUnexpectedError('Failed to unlink tag models', error); - } - } - - private findDuplicateTags(tags: ProcessTagDto[]): string[] { - const seen = new Map(); - const duplicates: string[] = []; - - tags.forEach((tagDto) => { - const key = `${tagDto.productUuid}-${tagDto.name}`; - if (seen.has(key)) { - duplicates.push(`${tagDto.name} for Product: ${tagDto.productUuid}`); - } else { - seen.set(key, true); - } - }); - - return duplicates; - } - - private async checkTagReuse( - tag: string, - productUuid: string, - space: SpaceEntity, - tagsToDelete?: ModifyTagDto[], - ): Promise { - const { uuid: spaceUuid } = space; - - const tagExists = await this.tagRepository.find({ - where: [ - { - tag, - product: { uuid: productUuid }, - disabled: false, - }, - { - tag, - subspace: { space: { uuid: spaceUuid } }, - product: { uuid: productUuid }, - disabled: false, - }, - ], - }); - - const filteredTagExists = tagExists.filter( - (existingTag) => - !tagsToDelete?.some( - (deleteTag) => deleteTag.tagUuid === existingTag.uuid, - ), - ); - - if (filteredTagExists.length > 0) { - throw new HttpException(`Tag can't be reused`, HttpStatus.CONFLICT); - } - } - - private async prepareTagEntity( - tagDto: ProcessTagDto, - queryRunner: QueryRunner, - space?: SpaceEntity, - subspace?: SubspaceEntity, - tagsToDelete?: ModifyTagDto[], - ): Promise { - try { - const product = await this.productService.findOne(tagDto.productUuid); - - if (!product) { - throw new HttpException( - `Product with UUID ${tagDto.productUuid} not found.`, - HttpStatus.NOT_FOUND, - ); - } - - if (space) { - await this.checkTagReuse( - tagDto.name, - tagDto.productUuid, - space, - tagsToDelete, - ); - } else if (subspace && subspace.space) { - await this.checkTagReuse( - tagDto.name, - tagDto.productUuid, - subspace.space, - ); - } else { - throw new HttpException( - `Invalid subspace or space provided.`, - HttpStatus.BAD_REQUEST, - ); - } - - return queryRunner.manager.create(TagEntity, { - tag: tagDto.name, - product: product.data, - space: space, - subspace: subspace, - }); - } catch (error) { - if (error instanceof HttpException) { - throw error; - } - throw new HttpException( - `An error occurred while preparing the tag entity: ${error.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - private async getTagByUuid(uuid: string): Promise { - const tag = await this.tagRepository.findOne({ - where: { uuid }, - relations: ['product'], - }); - - if (!tag) { - throw new HttpException( - `Tag with ID ${uuid} not found.`, - HttpStatus.NOT_FOUND, - ); - } - return tag; - } - - private handleUnexpectedError( - message: string, - error: unknown, - ): HttpException { - if (error instanceof HttpException) throw error; - return new HttpException( - `${message}: ${(error as Error)?.message || 'Unknown error'}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - private combineTags( - primaryTags: ProcessTagDto[], - additionalTags?: ProcessTagDto[], - ): ProcessTagDto[] { - return additionalTags ? [...primaryTags, ...additionalTags] : primaryTags; - } - - private ensureNoDuplicateTags(tags: ProcessTagDto[]): void { - const duplicates = this.findDuplicateTags(tags); - - if (duplicates.length > 0) { - throw new HttpException( - `Duplicate tags found: ${duplicates.join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - } - - private validateTagsInput(tags: ProcessTagDto[]): void { - if (!tags?.length) { - return; - } - } - - getSubspaceTagsToBeAdded( - spaceTags?: ModifyTagDto[], - subspaceModels?: ModifySubspaceDto[], - ): ModifyTagDto[] { - if (!subspaceModels || subspaceModels.length === 0) { - return spaceTags; - } - - const spaceTagsToDelete = spaceTags?.filter( - (tag) => tag.action === 'delete', - ); - - const tagsToAdd = subspaceModels.flatMap( - (subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [], - ); - - const commonTagUuids = new Set( - tagsToAdd - .filter((tagToAdd) => - spaceTagsToDelete.some( - (tagToDelete) => tagToAdd.tagUuid === tagToDelete.tagUuid, - ), - ) - .map((tag) => tag.tagUuid), - ); - - const remainingTags = spaceTags.filter( - (tag) => !commonTagUuids.has(tag.tagUuid), // Exclude tags in commonTagUuids - ); - - return remainingTags; - } - - getModifiedSubspaces( - spaceTags?: ModifyTagDto[], - subspaceModels?: ModifySubspaceDto[], - ): ModifySubspaceDto[] { - if (!subspaceModels || subspaceModels.length === 0) { - return []; - } - - // Extract tags marked for addition in spaceTags - const spaceTagsToAdd = spaceTags?.filter((tag) => tag.action === 'add'); - - const subspaceTagsToAdd = subspaceModels.flatMap( - (subspace) => subspace.tags?.filter((tag) => tag.action === 'add') || [], - ); - - const subspaceTagsToDelete = subspaceModels.flatMap( - (subspace) => - subspace.tags?.filter((tag) => tag.action === 'delete') || [], - ); - - const subspaceTagsToDeleteUuids = new Set( - subspaceTagsToDelete.map((tag) => tag.tagUuid), - ); - - const commonTagsInSubspaces = subspaceTagsToAdd.filter((tag) => - subspaceTagsToDeleteUuids.has(tag.tagUuid), - ); - - // Find UUIDs of tags that are common between spaceTagsToAdd and subspace tags marked for deletion - const commonTagUuids = new Set( - spaceTagsToAdd - .flatMap((tagToAdd) => - subspaceModels.flatMap( - (subspace) => - subspace.tags?.filter( - (tagToDelete) => - tagToDelete.action === 'delete' && - tagToAdd.tagUuid === tagToDelete.tagUuid, - ) || [], - ), - ) - .map((tag) => tag.tagUuid), - ); - - // Modify subspaceModels by removing tags with UUIDs present in commonTagUuids - let modifiedSubspaces = subspaceModels.map((subspace) => ({ - ...subspace, - tags: - subspace.tags?.filter((tag) => !commonTagUuids.has(tag.tagUuid)) || [], - })); - - modifiedSubspaces = modifiedSubspaces.map((subspace) => ({ - ...subspace, - tags: - subspace.tags?.filter( - (tag) => - !( - tag.action === 'delete' && - commonTagsInSubspaces.some( - (commonTag) => commonTag.tagUuid === tag.tagUuid, - ) - ), - ) || [], - })); - - return modifiedSubspaces; - } + constructor() {} } diff --git a/src/space/space.module.ts b/src/space/space.module.ts index 0b33696..58f54a5 100644 --- a/src/space/space.module.ts +++ b/src/space/space.module.ts @@ -1,14 +1,82 @@ +import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; +import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; +import { OccupancyService } from '@app/common/helper/services/occupancy.service'; +import { PowerClampService } from '@app/common/helper/services/power.clamp.service'; +import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { + InviteUserRepository, + InviteUserSpaceRepository, +} from '@app/common/modules/Invite-user/repositiories'; +import { AutomationRepository } from '@app/common/modules/automation/repositories'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; +import { + DeviceRepository, + DeviceUserPermissionRepository, +} from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { + PowerClampDailyRepository, + PowerClampHourlyRepository, + PowerClampMonthlyRepository, +} from '@app/common/modules/power-clamp/repositories'; +import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { RegionRepository } from '@app/common/modules/region/repositories'; +import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; +import { + SceneIconRepository, + SceneRepository, +} from '@app/common/modules/scene/repositories'; +import { + SpaceModelProductAllocationRepoitory, + SpaceModelRepository, + SubspaceModelProductAllocationRepoitory, + SubspaceModelRepository, +} from '@app/common/modules/space-model'; +import { + InviteSpaceRepository, + SpaceLinkRepository, + SpaceProductAllocationRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; +import { + SubspaceProductAllocationRepository, + SubspaceRepository, +} from '@app/common/modules/space/repositories/subspace.repository'; import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module'; +import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; +import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { CommunityModule } from 'src/community/community.module'; +import { DeviceService } from 'src/device/services'; +import { + SpaceModelService, + SubSpaceModelService, +} from 'src/space-model/services'; +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 { TagService as NewTagService } from 'src/tags/services/tags.service'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { UserService, UserSpaceService } from 'src/users/services'; +import { SceneService } from '../scene/services'; import { SpaceController, SpaceDeviceController, + SpaceSceneController, SpaceUserController, SubSpaceController, SubSpaceDeviceController, - SpaceSceneController, } from './controllers'; +import { SpaceValidationController } from './controllers/space-validation.controller'; +import { DisableSpaceHandler } from './handlers'; import { SpaceDeviceService, SpaceLinkService, @@ -17,81 +85,10 @@ import { SpaceUserService, SubspaceDeviceService, SubSpaceService, + ValidationService, } from './services'; -import { - SpaceRepository, - SpaceLinkRepository, - TagRepository, - InviteSpaceRepository, - SpaceProductAllocationRepository, -} from '@app/common/modules/space/repositories'; -import { CommunityRepository } from '@app/common/modules/community/repositories'; -import { - UserRepository, - UserSpaceRepository, -} from '@app/common/modules/user/repositories'; -import { - DeviceRepository, - DeviceUserPermissionRepository, -} from '@app/common/modules/device/repositories'; -import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; -import { ProductRepository } from '@app/common/modules/product/repositories'; -import { SceneService } from '../scene/services'; -import { - SceneIconRepository, - SceneRepository, -} from '@app/common/modules/scene/repositories'; -import { DeviceService } from 'src/device/services'; -import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; -import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; -import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories'; -import { ProjectRepository } from '@app/common/modules/project/repositiories'; -import { - SpaceModelProductAllocationRepoitory, - SpaceModelRepository, - SubspaceModelProductAllocationRepoitory, - SubspaceModelRepository, - TagModelRepository, -} from '@app/common/modules/space-model'; -import { CommunityModule } from 'src/community/community.module'; -import { ValidationService } from './services'; -import { - SubspaceProductAllocationRepository, - SubspaceRepository, -} from '@app/common/modules/space/repositories/subspace.repository'; -import { TagService } from './services/tag'; -import { - SpaceModelService, - SubSpaceModelService, -} from 'src/space-model/services'; -import { UserService, UserSpaceService } from 'src/users/services'; -import { UserDevicePermissionService } from 'src/user-device-permission/services'; -import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; -import { CqrsModule } from '@nestjs/cqrs'; -import { DisableSpaceHandler } from './handlers'; -import { RegionRepository } from '@app/common/modules/region/repositories'; -import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; -import { - InviteUserRepository, - InviteUserSpaceRepository, -} from '@app/common/modules/Invite-user/repositiories'; -import { AutomationRepository } from '@app/common/modules/automation/repositories'; -import { TagService as NewTagService } from 'src/tags/services/tags.service'; -import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; -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 { SpaceProductAllocationService } from './services/space-product-allocation.service'; import { SubspaceProductAllocationService } from './services/subspace/subspace-product-allocation.service'; -import { SpaceValidationController } from './controllers/space-validation.controller'; -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'; -import { OccupancyService } from '@app/common/helper/services/occupancy.service'; -import { AqiDataService } from '@app/common/helper/services/aqi.data.service'; export const CommandHandlers = [DisableSpaceHandler]; @@ -108,11 +105,8 @@ export const CommandHandlers = [DisableSpaceHandler]; ], providers: [ ValidationService, - TagModelRepository, - TagRepository, SpaceService, TuyaService, - TagService, ProductRepository, SubSpaceService, SpaceDeviceService, @@ -152,7 +146,6 @@ export const CommandHandlers = [DisableSpaceHandler]; InviteUserRepository, InviteUserSpaceRepository, AutomationRepository, - TagService, NewTagService, SpaceModelProductAllocationRepoitory, SubspaceModelProductAllocationRepoitory, diff --git a/src/tags/services/tags.service.ts b/src/tags/services/tags.service.ts index c8a4a7a..b82ebc2 100644 --- a/src/tags/services/tags.service.ts +++ b/src/tags/services/tags.service.ts @@ -1,22 +1,22 @@ +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { ProjectParam } from '@app/common/dto/project-param.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { ProductEntity } from '@app/common/modules/product/entities'; import { ProductRepository } from '@app/common/modules/product/repositories'; +import { ProjectEntity } from '@app/common/modules/project/entities'; import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { NewTagEntity } from '@app/common/modules/tag'; import { NewTagRepository } from '@app/common/modules/tag/repositories/tag-repository'; import { - Injectable, ConflictException, - NotFoundException, HttpException, HttpStatus, + Injectable, + NotFoundException, } from '@nestjs/common'; -import { CreateTagDto } from '../dtos/tags.dto'; -import { ProductEntity } from '@app/common/modules/product/entities'; -import { ProjectEntity } from '@app/common/modules/project/entities'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { NewTagEntity } from '@app/common/modules/tag'; -import { BulkCreateTagsDto, ProcessTagDto } from '../dtos'; import { In, QueryRunner } from 'typeorm'; -import { ProjectParam } from '@app/common/dto/project-param.dto'; +import { ProcessTagDto } from '../dtos'; +import { CreateTagDto } from '../dtos/tags.dto'; @Injectable() export class TagService { @@ -32,7 +32,7 @@ export class TagService { const tags = await this.tagRepository.find({ where: { project: { uuid: projectUuid } }, - relations: ['product', 'project'], + relations: ['project'], }); return new SuccessResponseDto({ @@ -66,74 +66,65 @@ export class TagService { }); } + /** + * Processes an array of tag DTOs, creating or updating tags in the database. + * @param tagDtos - The array of tag DTOs to process. + * @param projectUuid - The UUID of the project to associate the tags with. + * @param queryRunner - Optional TypeORM query runner for transaction management. + * @returns An array of the processed tag entities. + */ async processTags( tagDtos: ProcessTagDto[], projectUuid: string, queryRunner?: QueryRunner, ): Promise { try { + const dbManager = queryRunner + ? queryRunner.manager + : this.tagRepository.manager; if (!tagDtos || tagDtos.length === 0) { return []; } - const newTagDtos: CreateTagDto[] = []; - const existingTagUuids: string[] = []; - let fetchedExistingTags: NewTagEntity[] = []; - const directlyFetchedTags: NewTagEntity[] = []; + const [tagsWithUuid, tagsWithoutUuid]: [ + Pick[], + Omit[], + ] = this.splitTagsByUuid(tagDtos); - // Separate existing tag UUIDs and new tag DTOs - for (const tagDto of tagDtos) { - if (tagDto.uuid) { - existingTagUuids.push(tagDto.uuid); - } else { - if (!tagDto.name || !tagDto.productUuid) { - throw new HttpException( - `Tag name or product UUID is missing`, - HttpStatus.BAD_REQUEST, - ); - } + // create a set of unique existing tag names for the project + const upsertedTagsByNameResult = await dbManager.upsert( + NewTagEntity, + Array.from( + new Set(tagsWithoutUuid.map((tag) => tag.name)).values(), + ).map((name) => ({ + name, + project: { uuid: projectUuid }, + })), + ['name', 'project'], + ); - const existingTag = await queryRunner.manager.findOne(NewTagEntity, { - where: { - name: tagDto.name, - product: { uuid: tagDto.productUuid }, - project: { uuid: projectUuid }, - }, - relations: ['product'], - }); - - if (!existingTag) { - newTagDtos.push(tagDto); - } else { - directlyFetchedTags.push(existingTag); - } - } - } + const createdTagsByName = await dbManager.find(NewTagEntity, { + where: { + uuid: In(upsertedTagsByNameResult.identifiers.map((id) => id.uuid)), + }, + }); + let foundByUuidTags: NewTagEntity[] = []; // Fetch existing tags using UUIDs - if (existingTagUuids.length > 0) { - fetchedExistingTags = await (queryRunner - ? queryRunner.manager.find(NewTagEntity, { - where: { - uuid: In(existingTagUuids), - project: { uuid: projectUuid }, - }, - relations: ['product'], - }) - : this.tagRepository.find({ - where: { - uuid: In(existingTagUuids), - project: { uuid: projectUuid }, - }, - relations: ['product'], - })); + if (tagsWithUuid.length) { + foundByUuidTags = await dbManager.find(NewTagEntity, { + where: { + uuid: In([...tagsWithUuid.map((tag) => tag.uuid)]), + project: { uuid: projectUuid }, + }, + }); } // Ensure all provided UUIDs exist in the database - if (fetchedExistingTags.length !== existingTagUuids.length) { - const foundUuids = new Set(fetchedExistingTags.map((tag) => tag.uuid)); - const missingUuids = existingTagUuids.filter( - (uuid) => !foundUuids.has(uuid), + if (foundByUuidTags.length !== tagsWithUuid.length) { + const foundUuids = new Set(foundByUuidTags.map((tag) => tag.uuid)); + const missingUuids = tagsWithUuid.filter( + ({ uuid }) => !foundUuids.has(uuid), ); throw new HttpException( @@ -142,23 +133,7 @@ export class TagService { ); } - let newlyCreatedTags: NewTagEntity[] = []; - - if (newTagDtos.length > 0) { - newlyCreatedTags = await this.bulkCreateTags( - { projectUuid, tags: newTagDtos }, - queryRunner, - ); - } - - // Combine all found and created tags - const allTags = [ - ...fetchedExistingTags, - ...newlyCreatedTags, - ...directlyFetchedTags, - ]; - - return allTags; + return [...foundByUuidTags, ...createdTagsByName]; } catch (error) { console.error(error); throw new HttpException( @@ -172,79 +147,6 @@ export class TagService { } } - async bulkCreateTags( - dto: BulkCreateTagsDto, - queryRunner?: QueryRunner, - ): Promise { - try { - const { projectUuid, tags } = dto; - const newTags: NewTagEntity[] = []; - - const project = await this.getProjectByUuid(projectUuid); - - // Extract unique product UUIDs - const productUuids = Array.from( - new Set(tags.map((tag) => tag.productUuid)), - ); - - const productMap = await this.getProductMap(productUuids); - - // Fetch existing tag names for this project - const existingTags = await this.tagRepository.find({ - where: { project: { uuid: projectUuid } }, - relations: ['product'], - }); - - // Convert existing tags into a Map for quick lookup - const existingTagMap = new Map(); - existingTags.forEach((tag) => { - existingTagMap.set(tag.name, tag.product?.uuid || null); - }); - - for (const tag of tags) { - const existingProductUuid = existingTagMap.get(tag.name); - - if (existingProductUuid) { - if (existingProductUuid !== tag.productUuid) { - throw new HttpException( - `Tag "${tag.name}" already exists but is associated with a different product.`, - HttpStatus.CONFLICT, - ); - } - } else { - newTags.push( - this.tagRepository.create({ - name: tag.name, - product: productMap.get(tag.productUuid), - project, - }), - ); - } - } - - if (newTags.length > 0) { - if (queryRunner) { - await queryRunner.manager.save(NewTagEntity, newTags); - } else { - await this.tagRepository.save(newTags); - } - } else { - } - - return newTags; - } catch (error) { - console.error(error); - throw new HttpException( - error instanceof HttpException - ? error.message - : 'An unexpected error occurred while creating tags', - error instanceof HttpException - ? error.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - private async getProductByUuid(uuid: string): Promise { const product = await this.productRepository.findOne({ where: { uuid } }); if (!product) { @@ -276,30 +178,25 @@ export class TagService { } } - private async getProductMap( - uuids: string[], - ): Promise> { - const products = await this.productRepository.find({ - where: { uuid: In(uuids) }, - }); - - if (products.length !== uuids.length) { - const foundUuids = new Set(products.map((p) => p.uuid)); - const missingUuids = uuids.filter((id) => !foundUuids.has(id)); - throw new NotFoundException( - `Products not found for UUIDs: ${missingUuids.join(', ')}`, - ); - } - - return new Map(products.map((product) => [product.uuid, product])); - } - - private async getExistingTagNames(projectUuid: string): Promise> { - const tags = await this.tagRepository.find({ - where: { project: { uuid: projectUuid } }, - select: ['name'], - }); - - return new Set(tags.map((tag) => tag.name)); + private splitTagsByUuid( + tagDtos: ProcessTagDto[], + ): [ProcessTagDto[], ProcessTagDto[]] { + return tagDtos.reduce<[ProcessTagDto[], ProcessTagDto[]]>( + ([withUuid, withoutUuid], tag) => { + if (tag.uuid) { + withUuid.push(tag); + } else { + if (!tag.name || !tag.productUuid) { + throw new HttpException( + `Tag name or product UUID is missing`, + HttpStatus.BAD_REQUEST, + ); + } + withoutUuid.push(tag); + } + return [withUuid, withoutUuid]; + }, + [[], []], + ); } } From ea9a65178dbc12cdb9a9fcd8651e434a95e9122c Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Wed, 11 Jun 2025 16:28:33 +0300 Subject: [PATCH 54/56] fix: add space filter to "join" operation instead of "and" operation (#405) --- src/community/services/community.service.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 081c0ac..fc33011 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -105,18 +105,19 @@ export class CommunityService { */ pageable.where = {}; let qb: undefined | SelectQueryBuilder = undefined; + + qb = this.communityRepository + .createQueryBuilder('c') + .leftJoin('c.spaces', 's', 's.disabled = false') + .where('c.project = :projectUuid', { projectUuid }) + .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) + .distinct(true); if (pageable.search) { - qb = this.communityRepository - .createQueryBuilder('c') - .leftJoin('c.spaces', 's') - .where('c.project = :projectUuid', { projectUuid }) - .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) - .andWhere('s.disabled = false') - .andWhere( - `c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`, - ) - .distinct(true); + qb.andWhere( + `c.name ILIKE '%${pageable.search}%' OR s.space_name ILIKE '%${pageable.search}%'`, + ); } + const customModel = TypeORMCustomModel(this.communityRepository); const { baseResponseDto, paginationResponseDto } = From f2ed04f20671ae18175d073cb1a13b1b4a5e07da Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 12 Jun 2025 02:49:59 -0600 Subject: [PATCH 55/56] feat: add SpaceDailyOccupancyDuration entity, DTO, and repository for occupancy tracking --- libs/common/src/database/database.module.ts | 2 ++ .../src/modules/occupancy/dtos/index.ts | 1 + .../modules/occupancy/dtos/occupancy.dto.ts | 23 +++++++++++++ .../src/modules/occupancy/entities/index.ts | 1 + .../occupancy/entities/occupancy.entity.ts | 32 +++++++++++++++++++ .../occupancy/occupancy.repository.module.ts | 11 +++++++ .../modules/occupancy/repositories/index.ts | 1 + .../repositories/occupancy.repository.ts | 10 ++++++ .../modules/space/entities/space.entity.ts | 7 ++++ 9 files changed, 88 insertions(+) create mode 100644 libs/common/src/modules/occupancy/dtos/index.ts create mode 100644 libs/common/src/modules/occupancy/dtos/occupancy.dto.ts create mode 100644 libs/common/src/modules/occupancy/entities/index.ts create mode 100644 libs/common/src/modules/occupancy/entities/occupancy.entity.ts create mode 100644 libs/common/src/modules/occupancy/occupancy.repository.module.ts create mode 100644 libs/common/src/modules/occupancy/repositories/index.ts create mode 100644 libs/common/src/modules/occupancy/repositories/occupancy.repository.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 2187c72..2196901 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -58,6 +58,7 @@ import { UserSpaceEntity, } from '../modules/user/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; +import { SpaceDailyOccupancyDurationEntity } from '../modules/occupancy/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -117,6 +118,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; PresenceSensorDailyDeviceEntity, PresenceSensorDailySpaceEntity, AqiSpaceDailyPollutantStatsEntity, + SpaceDailyOccupancyDurationEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/occupancy/dtos/index.ts b/libs/common/src/modules/occupancy/dtos/index.ts new file mode 100644 index 0000000..956af8a --- /dev/null +++ b/libs/common/src/modules/occupancy/dtos/index.ts @@ -0,0 +1 @@ +export * from './occupancy.dto'; diff --git a/libs/common/src/modules/occupancy/dtos/occupancy.dto.ts b/libs/common/src/modules/occupancy/dtos/occupancy.dto.ts new file mode 100644 index 0000000..bb455c9 --- /dev/null +++ b/libs/common/src/modules/occupancy/dtos/occupancy.dto.ts @@ -0,0 +1,23 @@ +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class SpaceDailyOccupancyDurationDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public spaceUuid: string; + + @IsString() + @IsNotEmpty() + public eventDate: string; + + @IsNumber() + @IsNotEmpty() + public occupancyPercentage: number; + + @IsNumber() + @IsNotEmpty() + public occupiedSeconds: number; +} diff --git a/libs/common/src/modules/occupancy/entities/index.ts b/libs/common/src/modules/occupancy/entities/index.ts new file mode 100644 index 0000000..d272e17 --- /dev/null +++ b/libs/common/src/modules/occupancy/entities/index.ts @@ -0,0 +1 @@ +export * from './occupancy.entity'; diff --git a/libs/common/src/modules/occupancy/entities/occupancy.entity.ts b/libs/common/src/modules/occupancy/entities/occupancy.entity.ts new file mode 100644 index 0000000..7d77ffc --- /dev/null +++ b/libs/common/src/modules/occupancy/entities/occupancy.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { SpaceEntity } from '../../space/entities/space.entity'; +import { SpaceDailyOccupancyDurationDto } from '../dtos'; + +@Entity({ name: 'space-daily-occupancy-duration' }) +@Unique(['spaceUuid', 'eventDate']) +export class SpaceDailyOccupancyDurationEntity extends AbstractEntity { + @Column({ nullable: false }) + public spaceUuid: string; + + @Column({ nullable: false, type: 'date' }) + public eventDate: string; + + public CountTotalPresenceDetected: number; + + @ManyToOne(() => SpaceEntity, (space) => space.presenceSensorDaily) + space: SpaceEntity; + + @Column({ type: 'int' }) + occupancyPercentage: number; + + @Column({ type: 'int', nullable: true }) + occupiedSeconds?: number; + + @Column({ type: 'int', nullable: true }) + deviceCount?: number; + constructor(partial: Partial) { + super(); + Object.assign(this, partial); + } +} diff --git a/libs/common/src/modules/occupancy/occupancy.repository.module.ts b/libs/common/src/modules/occupancy/occupancy.repository.module.ts new file mode 100644 index 0000000..027cb9f --- /dev/null +++ b/libs/common/src/modules/occupancy/occupancy.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SpaceDailyOccupancyDurationEntity } from './entities/occupancy.entity'; + +@Module({ + providers: [], + exports: [], + controllers: [], + imports: [TypeOrmModule.forFeature([SpaceDailyOccupancyDurationEntity])], +}) +export class SpaceDailyOccupancyDurationRepositoryModule {} diff --git a/libs/common/src/modules/occupancy/repositories/index.ts b/libs/common/src/modules/occupancy/repositories/index.ts new file mode 100644 index 0000000..85821db --- /dev/null +++ b/libs/common/src/modules/occupancy/repositories/index.ts @@ -0,0 +1 @@ +export * from './occupancy.repository'; diff --git a/libs/common/src/modules/occupancy/repositories/occupancy.repository.ts b/libs/common/src/modules/occupancy/repositories/occupancy.repository.ts new file mode 100644 index 0000000..45a1b17 --- /dev/null +++ b/libs/common/src/modules/occupancy/repositories/occupancy.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { SpaceDailyOccupancyDurationEntity } from '../entities/occupancy.entity'; + +@Injectable() +export class SpaceDailyOccupancyDurationEntityRepository extends Repository { + constructor(private dataSource: DataSource) { + super(SpaceDailyOccupancyDurationEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index e5e3b12..56b5d4f 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -12,6 +12,7 @@ import { SpaceProductAllocationEntity } from './space-product-allocation.entity' import { SubspaceEntity } from './subspace/subspace.entity'; import { PresenceSensorDailySpaceEntity } from '../../presence-sensor/entities'; import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities'; +import { SpaceDailyOccupancyDurationEntity } from '../../occupancy/entities'; @Entity({ name: 'space' }) export class SpaceEntity extends AbstractEntity { @@ -119,6 +120,12 @@ export class SpaceEntity extends AbstractEntity { @OneToMany(() => AqiSpaceDailyPollutantStatsEntity, (aqi) => aqi.space) aqiSensorDaily: AqiSpaceDailyPollutantStatsEntity[]; + @OneToMany( + () => SpaceDailyOccupancyDurationEntity, + (occupancy) => occupancy.space, + ) + occupancyDaily: SpaceDailyOccupancyDurationEntity[]; + constructor(partial: Partial) { super(); Object.assign(this, partial); From a91d0f22a4fd96f4fc0bca30177d861bb93cd035 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Fri, 13 Jun 2025 09:46:41 +0300 Subject: [PATCH 56/56] fix: send correct enable status to email sender function (#407) --- libs/common/src/util/email.service.ts | 19 ++++-- .../services/invite-user.service.ts | 68 ++++++++++--------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts index aee78fe..0a9b1f2 100644 --- a/libs/common/src/util/email.service.ts +++ b/libs/common/src/util/email.service.ts @@ -1,7 +1,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import * as nodemailer from 'nodemailer'; import axios from 'axios'; +import * as nodemailer from 'nodemailer'; import { SEND_EMAIL_API_URL_DEV, SEND_EMAIL_API_URL_PROD, @@ -83,12 +83,17 @@ export class EmailService { ); } } - async sendEmailWithTemplate( - email: string, - name: string, - isEnable: boolean, - isDelete: boolean, - ): Promise { + async sendEmailWithTemplate({ + email, + name, + isEnable, + isDelete, + }: { + email: string; + name: string; + isEnable: boolean; + isDelete: boolean; + }): Promise { const isProduction = process.env.NODE_ENV === 'production'; const API_TOKEN = this.configService.get( 'email-config.MAILTRAP_API_TOKEN', diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index 3288912..0cf8e42 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -1,36 +1,42 @@ -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; -import { AddUserInvitationDto } from '../dtos'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { RoleType } from '@app/common/constants/role.type.enum'; import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { generateRandomString } from '@app/common/helper/randomString'; -import { EntityManager, In, IsNull, Not, QueryRunner } from 'typeorm'; -import { DataSource } from 'typeorm'; -import { UserEntity } from '@app/common/modules/user/entities'; -import { RoleType } from '@app/common/constants/role.type.enum'; +import { InviteUserEntity } from '@app/common/modules/Invite-user/entities'; import { InviteUserRepository, InviteUserSpaceRepository, } from '@app/common/modules/Invite-user/repositiories'; -import { CheckEmailDto } from '../dtos/check-email.dto'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; +import { SpaceRepository } from '@app/common/modules/space'; +import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; +import { UserEntity } from '@app/common/modules/user/entities'; import { UserRepository } from '@app/common/modules/user/repositories'; import { EmailService } from '@app/common/util/email.service'; -import { SpaceRepository } from '@app/common/modules/space'; -import { ActivateCodeDto } from '../dtos/active-code.dto'; -import { UserSpaceService } from 'src/users/services'; +import { + BadRequestException, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; import { SpaceUserService } from 'src/space/services'; +import { UserSpaceService } from 'src/users/services'; +import { + DataSource, + EntityManager, + In, + IsNull, + Not, + QueryRunner, +} from 'typeorm'; +import { AddUserInvitationDto } from '../dtos'; +import { ActivateCodeDto } from '../dtos/active-code.dto'; +import { CheckEmailDto } from '../dtos/check-email.dto'; import { DisableUserInvitationDto, UpdateUserInvitationDto, } from '../dtos/update.invite-user.dto'; -import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; -import { InviteUserEntity } from '@app/common/modules/Invite-user/entities'; -import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; @Injectable() export class InviteUserService { @@ -658,12 +664,12 @@ export class InviteUserService { HttpStatus.BAD_REQUEST, ); } - await this.emailService.sendEmailWithTemplate( - userData.email, - userData.firstName, - disable, - false, - ); + await this.emailService.sendEmailWithTemplate({ + email: userData.email, + name: userData.firstName, + isEnable: !disable, + isDelete: false, + }); await queryRunner.commitTransaction(); return new SuccessResponseDto({ @@ -797,12 +803,12 @@ export class InviteUserService { { isActive: false }, ); } - await this.emailService.sendEmailWithTemplate( - userData.email, - userData.firstName, - false, - true, - ); + await this.emailService.sendEmailWithTemplate({ + email: userData.email, + name: userData.firstName, + isEnable: false, + isDelete: true, + }); await queryRunner.commitTransaction(); return new SuccessResponseDto({