Skip to content
Snippets Groups Projects
queryRunner.service.ts 12.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • Bastien DUMONT's avatar
    Bastien DUMONT committed
    import { Client, Q, QueryDefinition } from 'cozy-client'
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    import {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      EGL_DAY_DOCTYPE,
      EGL_MONTH_DOCTYPE,
      EGL_YEAR_DOCTYPE,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      ENEDIS_DAY_DOCTYPE,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      ENEDIS_MINUTE_DOCTYPE,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      ENEDIS_MONTH_DOCTYPE,
      ENEDIS_YEAR_DOCTYPE,
      GRDF_DAY_DOCTYPE,
      GRDF_MONTH_DOCTYPE,
      GRDF_YEAR_DOCTYPE,
    } from 'doctypes'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import { DataloadState } from 'enum/dataload.enum'
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    import { FluidType } from 'enum/fluid.enum'
    
    Yoan VALLET's avatar
    Yoan VALLET committed
    import { TimeStep } from 'enum/timeStep.enum'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import { DateTime, Interval } from 'luxon'
    
    Yoan VALLET's avatar
    Yoan VALLET committed
    import { Dataload, TimePeriod } from 'models'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import logApp from 'utils/logger'
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
    
    export default class QueryRunner {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      // TODO to be clean up
    
      /* eslint-disable camelcase */
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      private readonly _max_limit = 1000
      private readonly _default_months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
      private readonly _default_days = [
    
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
        22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      ]
    
      /* eslint-enable camelcase */
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    
      private readonly _client: Client
    
      constructor(_client: Client) {
        this._client = _client
      }
    
    
      private buildListQuery(
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        timeStep: TimeStep,
    
        timePeriod: TimePeriod,
        fluidType: FluidType,
        limit: number
      ) {
        const doctype = this.getRelevantDoctype(fluidType, timeStep)
        return Q(doctype)
          .where(this.getPredicate(timePeriod, timeStep))
          .limitBy(limit)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      }
    
    
      private buildMaxQuery(
    
    Romain CREY's avatar
    Romain CREY committed
        timeStep: TimeStep,
    
        maxTimePeriod: TimePeriod,
        fluidType: FluidType,
        limit: number
      ) {
        const doctype = this.getRelevantDoctype(fluidType, timeStep)
    
        if (timeStep === TimeStep.HALF_AN_HOUR) {
    
          return Q(doctype)
            .where(this.getPredicate(maxTimePeriod, TimeStep.HALF_AN_HOUR))
    
            .indexFields(['load'])
    
            .limitBy(1)
            .sortBy([{ load: 'desc' }])
    
    Romain CREY's avatar
    Romain CREY committed
        }
    
        return Q(doctype)
          .where(this.getPredicate(maxTimePeriod, timeStep))
    
          .indexFields(['load'])
    
          .limitBy(limit)
          .sortBy([{ load: 'desc' }])
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      private buildFirstDateQuery(
        fluidType: FluidType,
        limit: number,
        timeStep?: TimeStep
      ) {
        const doctype = this.getRelevantDoctype(fluidType, timeStep || TimeStep.DAY)
    
          .where({ year: { $ne: null }, month: { $ne: null }, day: { $ne: null } })
    
          .indexFields(['year', 'month', 'day'])
          .sortBy([{ year: 'asc' }, { month: 'asc' }, { day: 'asc' }])
          .limitBy(limit)
      }
    
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      private buildLastDateQuery(
        fluidType: FluidType,
        limit: number,
        timeStep?: TimeStep
      ) {
        const doctype = this.getRelevantDoctype(fluidType, timeStep || TimeStep.DAY)
    
        return Q(doctype)
    
          .where({ year: { $ne: null }, month: { $ne: null }, day: { $ne: null } })
    
          .indexFields(['year', 'month', 'day'])
          .sortBy([{ year: 'desc' }, { month: 'desc' }, { day: 'desc' }])
          .limitBy(limit)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      }
    
      private async fetchData(query: QueryDefinition) {
        let result = null
        try {
          result = await this._client.query(query)
        } catch (error) {
          // log stuff
    
          // throw new Error('Fetch data failed in query runner')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          logApp.error('QueryRunner error: ', error)
    
      private filterDataList(
        data: any,
        timePeriod: TimePeriod,
        timeStep: TimeStep
      ) {
    
        // increase timeperiod range because the last data for a day is actually stored the next day at 00:00
    
        if (timeStep === TimeStep.HALF_AN_HOUR) {
    
          timePeriod.startDate = timePeriod.startDate.plus({ minutes: 30 })
          timePeriod.endDate = timePeriod.endDate.plus({ minutes: 30 })
        }
    
        const filteredResult = data.data.filter((entry: any) =>
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          this.withinDateBoundaries(
            DateTime.local(
              entry.year,
    
              entry.month || 1,
              entry.day || 1,
              entry.hour,
              entry.minute
    
            ).setZone('utc', {
              keepLocalTime: true,
            }),
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
            timePeriod
          )
        )
        return filteredResult
      }
    
    
      private mapDataList(data: any, timeStep?: TimeStep): Dataload[] {
        // set back every half hour data at -30 minutes so it is displayed as 23:30 instead of 00:00 etc..
        const minusValue = timeStep === TimeStep.HALF_AN_HOUR ? 30 : 0
    
        const mappedResult = data.map((entry: any) => ({
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          date: DateTime.local(
            entry.year,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
            entry.hour,
            entry.minute
    
          )
            .setZone('utc', {
              keepLocalTime: true,
            })
            .minus({ minutes: minusValue }),
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          value: entry.load,
    
          state: DataloadState.VALID,
    
    Yoan VALLET's avatar
    Yoan VALLET committed
          valueDetail: null,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        }))
        return mappedResult
      }
    
    
      private withinDateBoundaries(dateTime: DateTime, timePeriod: TimePeriod) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        return dateTime <= timePeriod.endDate && dateTime >= timePeriod.startDate
      }
    
    
      private getInBetweenMonths(timePeriod: TimePeriod) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        const intervalCount = Interval.fromDateTimes(
          timePeriod.startDate,
          timePeriod.endDate
        ).count('month')
        if (intervalCount >= 12) return this._default_months
        const monthList = []
        let offset = 0
        if (timePeriod.startDate.year !== timePeriod.endDate.year) offset = 12
        for (
          let m = timePeriod.startDate.month;
          m <= timePeriod.endDate.month + offset;
          m++
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          monthList.push(m > offset ? m - offset : m)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        return monthList
      }
    
    
      private getInBetweenDays(timePeriod: TimePeriod) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        const intervalCount = Interval.fromDateTimes(
          timePeriod.startDate,
          timePeriod.endDate
        ).count('day')
        if (intervalCount >= 31) return this._default_days
        const dayList = []
        let offset = 0
        if (timePeriod.startDate.month !== timePeriod.endDate.month) offset = 31
        for (
          let d = timePeriod.startDate.day;
          d <= timePeriod.endDate.day + offset;
          d++
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          dayList.push(d > offset ? d - offset : d)
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        return dayList
      }
    
    
      private getPredicate(timePeriod: TimePeriod, timeStep: TimeStep) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        let predicate = {}
        switch (timeStep) {
          case TimeStep.HALF_AN_HOUR:
            predicate = {
              year: {
                $eq: timePeriod.startDate.year,
              },
              month: {
                $eq: timePeriod.startDate.month,
              },
              day: {
    
                $in: [timePeriod.startDate.day, timePeriod.startDate.day + 1],
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
              },
            }
            break
    
          case TimeStep.WEEK:
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
          case TimeStep.DAY:
            predicate = {
              year: {
                $lte: timePeriod.endDate.year,
                $gte: timePeriod.startDate.year,
              },
              month: {
                $in: this.getInBetweenMonths(timePeriod),
              },
              day: {
                $in: this.getInBetweenDays(timePeriod),
              },
            }
            break
          case TimeStep.MONTH:
            predicate = {
              year: {
                $lte: timePeriod.endDate.year,
                $gte: timePeriod.startDate.year,
              },
              month: {
                $in: this.getInBetweenMonths(timePeriod),
              },
            }
            break
          case TimeStep.YEAR:
            predicate = {
              year: {
                $lte: timePeriod.endDate.year,
                $gte: timePeriod.startDate.year,
              },
            }
            break
        }
        return predicate
      }
    
      private getRelevantDoctype(fluidType: FluidType, timeStep: TimeStep) {
        let doctype = ''
        switch (fluidType) {
          case FluidType.ELECTRICITY:
            {
              switch (timeStep) {
                case TimeStep.HALF_AN_HOUR:
                  doctype = ENEDIS_MINUTE_DOCTYPE
                  break
    
                case TimeStep.WEEK:
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
                case TimeStep.DAY:
                  doctype = ENEDIS_DAY_DOCTYPE
                  break
                case TimeStep.MONTH:
                  doctype = ENEDIS_MONTH_DOCTYPE
                  break
                case TimeStep.YEAR:
                  doctype = ENEDIS_YEAR_DOCTYPE
                  break
                default:
                  doctype = ENEDIS_DAY_DOCTYPE
              }
            }
            break
    
          case FluidType.WATER:
            {
              switch (timeStep) {
    
                case TimeStep.WEEK:
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
                case TimeStep.DAY:
                  doctype = EGL_DAY_DOCTYPE
                  break
                case TimeStep.MONTH:
                  doctype = EGL_MONTH_DOCTYPE
                  break
                case TimeStep.YEAR:
                  doctype = EGL_YEAR_DOCTYPE
                  break
                default:
                  doctype = EGL_DAY_DOCTYPE
              }
            }
            break
    
          case FluidType.GAS:
            {
              switch (timeStep) {
    
                case TimeStep.WEEK:
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
                case TimeStep.DAY:
                  doctype = GRDF_DAY_DOCTYPE
                  break
                case TimeStep.MONTH:
                  doctype = GRDF_MONTH_DOCTYPE
                  break
                case TimeStep.YEAR:
                  doctype = GRDF_YEAR_DOCTYPE
                  break
                default:
                  doctype = GRDF_DAY_DOCTYPE
              }
            }
            break
    
          default:
            break
        }
    
        return doctype
      }
    
    
      public async fetchFluidData(
        timePeriod: TimePeriod,
        timeStep: TimeStep,
        fluidType: FluidType
      ): Promise<Dataload[] | null> {
        const query: QueryDefinition = this.buildListQuery(
          timeStep,
          timePeriod,
          fluidType,
          this._max_limit
        )
        const result = await this.fetchData(query)
    
          const filteredResult = this.filterDataList(result, timePeriod, timeStep)
    
          const mappedResult: Dataload[] = this.mapDataList(
            filteredResult,
            timeStep
          )
    
          return mappedResult
        }
        return null
      }
    
    
      public async fetchFluidRawDoctype(
        timePeriod: TimePeriod,
        timeStep: TimeStep,
        fluidType: FluidType
      ): Promise<QueryResult> {
        const query: QueryDefinition = this.buildListQuery(
          timeStep,
          timePeriod,
          fluidType,
          this._max_limit
        )
        return this.fetchData(query)
      }
    
    
      public async fetchFluidMaxData(
        maxTimePeriod: TimePeriod,
        timeStep: TimeStep,
    
        fluidType: FluidType,
        withDate?: boolean
      ): Promise<number | Dataload | null> {
    
        const query: QueryDefinition = this.buildMaxQuery(
          timeStep,
          maxTimePeriod,
          fluidType,
          this._max_limit
        )
        const result = await this.fetchData(query)
        if (timeStep === TimeStep.HALF_AN_HOUR) {
          const lastDayOfPreviousMonth = {
            startDate: maxTimePeriod.startDate.plus({ day: -1 }),
    
            endDate: maxTimePeriod.startDate.plus({ day: -1 }).endOf('day'),
    
          }
          const lastDayOfPreviousMonthQuery: QueryDefinition = this.buildMaxQuery(
            timeStep,
            lastDayOfPreviousMonth,
            fluidType,
            this._max_limit
          )
          const lastDayOfPreviousMonthResult = await this.fetchData(
            lastDayOfPreviousMonthQuery
          )
          return Math.max(
            lastDayOfPreviousMonthResult && lastDayOfPreviousMonthResult.data[0]
              ? lastDayOfPreviousMonthResult.data[0].load
              : 0,
            result && result.data[0] ? result.data[0].load : 0
          )
        }
    
          const filteredResult = this.filterDataList(
            result,
            maxTimePeriod,
            timeStep
          )
    
          const mappedResult = this.mapDataList(filteredResult, timeStep)
    
          if (withDate) {
            return mappedResult && mappedResult[0] && mappedResult[0]
          }
    
          return mappedResult && mappedResult[0] && mappedResult[0].value
        }
        return null
      }
    
    
      public async getFirstDateData(
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        fluidType: FluidType,
        timeStep?: TimeStep
    
      ): Promise<DateTime | null> {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        const query: QueryDefinition = this.buildFirstDateQuery(
          fluidType,
          1,
          timeStep
        )
    
        const result = await this.fetchData(query)
        if (
    
          result?.data[0]?.year &&
          result?.data[0]?.month &&
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          result?.data[0]?.day >= 0
    
        ) {
          return DateTime.local(
            result.data[0].year,
            result.data[0].month,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            result.data[0].hour || 0,
            result.data[0].minute || 0
    
          ).setZone('utc', {
            keepLocalTime: true,
          })
        }
        return null
      }
    
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      public async getLastDateData(
        fluidType: FluidType,
        timeStep?: TimeStep
      ): Promise<DateTime | null> {
        const query: QueryDefinition = this.buildLastDateQuery(
          fluidType,
          1,
          timeStep
        )
    
        const result = await this.fetchData(query)
        if (
    
          result?.data[0]?.year &&
          result?.data[0]?.month &&
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          result?.data[0]?.day >= 0
    
        ) {
          return DateTime.local(
            result.data[0].year,
            result.data[0].month,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            result.data[0].hour || 0,
            result.data[0].minute || 0
    
          ).setZone('utc', {
            keepLocalTime: true,
          })
    
        }
        return null
      }
    
      public async getEntries(fluidType: FluidType, timeStep: TimeStep) {
        const doctype = this.getRelevantDoctype(fluidType, timeStep)
        try {
    
          const query = Q(doctype).where({}).limitBy(1)
    
          const result = await this._client.query(query)
          return result
        } catch (error) {
          return null
        }
      }
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
    }