@codeclimbers/server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/src/app.module.d.ts +4 -0
- package/dist/src/app.module.js +49 -0
- package/dist/src/app.module.js.map +1 -0
- package/dist/src/assets/startup.plist.d.ts +1 -0
- package/dist/src/assets/startup.plist.js +41 -0
- package/dist/src/assets/startup.plist.js.map +1 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.d.ts +5 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.js +29 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.js.map +1 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.d.ts +5 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js +27 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js.map +1 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.js +44 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/sentry.d.ts +1 -0
- package/dist/src/sentry.js +13 -0
- package/dist/src/sentry.js.map +1 -0
- package/dist/src/v1/activities/activities.service.d.ts +21 -0
- package/dist/src/v1/activities/activities.service.js +174 -0
- package/dist/src/v1/activities/activities.service.js.map +1 -0
- package/dist/src/v1/activities/pulse.controller.d.ts +19 -0
- package/dist/src/v1/activities/pulse.controller.js +92 -0
- package/dist/src/v1/activities/pulse.controller.js.map +1 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.d.ts +13 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.js +64 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.js.map +1 -0
- package/dist/src/v1/database/__tests__/knex.test.d.ts +1 -0
- package/dist/src/v1/database/__tests__/knex.test.js +18 -0
- package/dist/src/v1/database/__tests__/knex.test.js.map +1 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.d.ts +1 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.js +141 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.js.map +1 -0
- package/dist/src/v1/database/knex.d.ts +5 -0
- package/dist/src/v1/database/knex.js +87 -0
- package/dist/src/v1/database/knex.js.map +1 -0
- package/dist/src/v1/database/migrations.d.ts +1 -0
- package/dist/src/v1/database/migrations.js +16 -0
- package/dist/src/v1/database/migrations.js.map +1 -0
- package/dist/src/v1/database/pulse.repo.d.ts +17 -0
- package/dist/src/v1/database/pulse.repo.js +115 -0
- package/dist/src/v1/database/pulse.repo.js.map +1 -0
- package/dist/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
- package/dist/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
- package/dist/src/v1/database/queries/getStatusBarDetails.sql +42 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.d.ts +19 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.js +97 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.js.map +1 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.d.ts +4 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js +25 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js.map +1 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.d.ts +3 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.js +21 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.js.map +1 -0
- package/dist/src/v1/startup/darwinStartup.service.d.ts +8 -0
- package/dist/src/v1/startup/darwinStartup.service.js +114 -0
- package/dist/src/v1/startup/darwinStartup.service.js.map +1 -0
- package/dist/src/v1/startup/linuxStartup.service.d.ts +8 -0
- package/dist/src/v1/startup/linuxStartup.service.js +104 -0
- package/dist/src/v1/startup/linuxStartup.service.js.map +1 -0
- package/dist/src/v1/startup/startup.controller.d.ts +8 -0
- package/dist/src/v1/startup/startup.controller.js +46 -0
- package/dist/src/v1/startup/startup.controller.js.map +1 -0
- package/dist/src/v1/startup/startup.util.d.ts +5 -0
- package/dist/src/v1/startup/startup.util.js +19 -0
- package/dist/src/v1/startup/startup.util.js.map +1 -0
- package/dist/src/v1/startup/startupService.factory.d.ts +9 -0
- package/dist/src/v1/startup/startupService.factory.js +41 -0
- package/dist/src/v1/startup/startupService.factory.js.map +1 -0
- package/dist/src/v1/startup/unsupportedStartup.service.d.ts +6 -0
- package/dist/src/v1/startup/unsupportedStartup.service.js +29 -0
- package/dist/src/v1/startup/unsupportedStartup.service.js.map +1 -0
- package/dist/src/v1/v1.module.d.ts +2 -0
- package/dist/src/v1/v1.module.js +41 -0
- package/dist/src/v1/v1.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils/__tests__/activites.util.test.d.ts +1 -0
- package/dist/utils/__tests__/activites.util.test.js +18 -0
- package/dist/utils/__tests__/activites.util.test.js.map +1 -0
- package/dist/utils/__tests__/helpers.util.test.d.ts +1 -0
- package/dist/utils/__tests__/helpers.util.test.js +120 -0
- package/dist/utils/__tests__/helpers.util.test.js.map +1 -0
- package/dist/utils/__tests__/wakatime.util.test.d.ts +1 -0
- package/dist/utils/__tests__/wakatime.util.test.js +210 -0
- package/dist/utils/__tests__/wakatime.util.test.js.map +1 -0
- package/dist/utils/activities.util.d.ts +15 -0
- package/dist/utils/activities.util.js +147 -0
- package/dist/utils/activities.util.js.map +1 -0
- package/dist/utils/allExceptions.filter.d.ts +7 -0
- package/dist/utils/allExceptions.filter.js +39 -0
- package/dist/utils/allExceptions.filter.js.map +1 -0
- package/dist/utils/codeClimberErrors.d.ts +16 -0
- package/dist/utils/codeClimberErrors.js +27 -0
- package/dist/utils/codeClimberErrors.js.map +1 -0
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/environment.util.d.ts +1 -0
- package/dist/utils/environment.util.js +6 -0
- package/dist/utils/environment.util.js.map +1 -0
- package/dist/utils/helpers.util.d.ts +7 -0
- package/dist/utils/helpers.util.js +116 -0
- package/dist/utils/helpers.util.js.map +1 -0
- package/dist/utils/node.util.d.ts +7 -0
- package/dist/utils/node.util.js +26 -0
- package/dist/utils/node.util.js.map +1 -0
- package/dist/utils/sql.util.d.ts +1 -0
- package/dist/utils/sql.util.js +11 -0
- package/dist/utils/sql.util.js.map +1 -0
- package/dist/utils/sqlReader.util.d.ts +5 -0
- package/dist/utils/sqlReader.util.js +34 -0
- package/dist/utils/sqlReader.util.js.map +1 -0
- package/dist/utils/wakatime.util.d.ts +6 -0
- package/dist/utils/wakatime.util.js +63 -0
- package/dist/utils/wakatime.util.js.map +1 -0
- package/index.ts +5 -0
- package/jest.config.js +8 -0
- package/knexfile.js +14 -0
- package/nest-cli.json +15 -0
- package/package.json +77 -0
- package/src/app.module.ts +37 -0
- package/src/assets/startup.plist.ts +36 -0
- package/src/common/infrastructure/http/controllers/health.controller.ts +9 -0
- package/src/common/infrastructure/http/middleware/requestlogger.middleware.ts +22 -0
- package/src/main.ts +51 -0
- package/src/sentry.ts +14 -0
- package/src/types/activities.api.d.ts +18 -0
- package/src/types/time.api.d.ts +23 -0
- package/src/types/utils.d.ts +8 -0
- package/src/types/wakatimeProxy.api.d.ts +55 -0
- package/src/v1/activities/activities.service.ts +213 -0
- package/src/v1/activities/pulse.controller.ts +68 -0
- package/src/v1/activities/wakatimeProxy.controller.ts +33 -0
- package/src/v1/database/__tests__/knex.test.ts +18 -0
- package/src/v1/database/__tests__/pulse.repo.test.ts +209 -0
- package/src/v1/database/knex.ts +107 -0
- package/src/v1/database/migrations.ts +13 -0
- package/src/v1/database/models/pulse.d.ts +24 -0
- package/src/v1/database/pulse.repo.ts +132 -0
- package/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
- package/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
- package/src/v1/database/queries/getStatusBarDetails.sql +42 -0
- package/src/v1/dtos/createWakatimePulse.dto.ts +66 -0
- package/src/v1/dtos/getCategoryTimeOverview.dto.ts +9 -0
- package/src/v1/dtos/getWeekOverview.dto.ts +6 -0
- package/src/v1/startup/darwinStartup.service.ts +114 -0
- package/src/v1/startup/linuxStartup.service.ts +101 -0
- package/src/v1/startup/startup.controller.ts +23 -0
- package/src/v1/startup/startup.util.ts +21 -0
- package/src/v1/startup/startupService.factory.ts +28 -0
- package/src/v1/startup/unsupportedStartup.service.ts +21 -0
- package/src/v1/v1.module.ts +28 -0
- package/test/app.e2e-spec.ts +24 -0
- package/test/jest-e2e.json +9 -0
- package/test/jest.globalSetup.js +15 -0
- package/test/jest.globalTeardown.js +8 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +22 -0
- package/utils/__tests__/activites.util.test.ts +18 -0
- package/utils/__tests__/helpers.util.test.ts +155 -0
- package/utils/__tests__/wakatime.util.test.ts +222 -0
- package/utils/activities.util.ts +193 -0
- package/utils/allExceptions.filter.ts +43 -0
- package/utils/codeClimberErrors.ts +32 -0
- package/utils/constants.ts +1 -0
- package/utils/environment.util.ts +1 -0
- package/utils/helpers.util.ts +131 -0
- package/utils/node.util.ts +29 -0
- package/utils/sql.util.ts +13 -0
- package/utils/sqlReader.util.ts +49 -0
- package/utils/wakatime.util.ts +78 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare namespace CodeClimbers {
|
|
2
|
+
export interface WakatimePulseStatusDao {
|
|
3
|
+
project: string
|
|
4
|
+
language: string
|
|
5
|
+
editor: string
|
|
6
|
+
operatingSystem: string
|
|
7
|
+
machine: string
|
|
8
|
+
branch: string
|
|
9
|
+
seconds: number | string
|
|
10
|
+
maxHeartbeatTime: string
|
|
11
|
+
minHeartbeatTime: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Source {
|
|
15
|
+
name: string
|
|
16
|
+
lastActive: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare namespace CodeClimbers {
|
|
2
|
+
export interface WeekOverview {
|
|
3
|
+
longestDayMinutes: number
|
|
4
|
+
yesterdayMinutes: number
|
|
5
|
+
todayMinutes: number
|
|
6
|
+
weekMinutes: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WeekOverviewDao {
|
|
10
|
+
message: string
|
|
11
|
+
data: WeekOverview
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TimeOverview {
|
|
15
|
+
category: string
|
|
16
|
+
minutes: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TimeOverviewDao {
|
|
20
|
+
message: string
|
|
21
|
+
data: TimeOverview[]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
declare namespace CodeClimbers {
|
|
2
|
+
interface ActivitiesDetail {
|
|
3
|
+
digital: string
|
|
4
|
+
hours: number
|
|
5
|
+
minutes: number
|
|
6
|
+
name?: string
|
|
7
|
+
percent?: number
|
|
8
|
+
seconds?: number
|
|
9
|
+
text: string
|
|
10
|
+
total_seconds: number
|
|
11
|
+
}
|
|
12
|
+
export interface ActivitiesStatusBarData {
|
|
13
|
+
categories?: ActivitiesDetail[]
|
|
14
|
+
dependencies?: ActivitiesDetail[]
|
|
15
|
+
editors?: ActivitiesDetail[]
|
|
16
|
+
languages?: ActivitiesDetail[]
|
|
17
|
+
machines?: ActivitiesDetail[]
|
|
18
|
+
operating_systems?: ActivitiesDetail[]
|
|
19
|
+
projects?: ActivitiesDetail[]
|
|
20
|
+
branches?: ActivitiesDetail[] | null
|
|
21
|
+
entities?: ActivitiesDetail[] | null
|
|
22
|
+
grand_total?: ActivitiesDetail
|
|
23
|
+
range?: {
|
|
24
|
+
date: string
|
|
25
|
+
end: string
|
|
26
|
+
start: string
|
|
27
|
+
text: string
|
|
28
|
+
timezone: string
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export interface ActivitiesStatusBar {
|
|
32
|
+
cached_at: string
|
|
33
|
+
data: ActivitiesStatusBarData
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateWakatimePulseDto {
|
|
37
|
+
userId?: string
|
|
38
|
+
entity: string
|
|
39
|
+
type: string
|
|
40
|
+
category?: string
|
|
41
|
+
project: string
|
|
42
|
+
branch: string
|
|
43
|
+
language?: string
|
|
44
|
+
is_write?: boolean
|
|
45
|
+
editor?: string
|
|
46
|
+
operating_system?: string
|
|
47
|
+
machine?: string
|
|
48
|
+
user_agent?: string
|
|
49
|
+
time: number | string
|
|
50
|
+
hash?: string
|
|
51
|
+
origin?: string
|
|
52
|
+
origin_id?: string
|
|
53
|
+
description?: string
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import { CreateWakatimePulseDto } from '../dtos/createWakatimePulse.dto'
|
|
3
|
+
import activitiesUtil from '../../../utils/activities.util'
|
|
4
|
+
import { PulseRepo } from '../database/pulse.repo'
|
|
5
|
+
import * as os from 'node:os'
|
|
6
|
+
import * as dayjs from 'dayjs'
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class ActivitiesService {
|
|
10
|
+
constructor(private readonly pulseRepo: PulseRepo) {
|
|
11
|
+
this.pulseRepo = pulseRepo
|
|
12
|
+
}
|
|
13
|
+
async getActivityStatusBar(): Promise<CodeClimbers.ActivitiesStatusBar> {
|
|
14
|
+
const statusBarRaw = await this.pulseRepo.getStatusBarDetails()
|
|
15
|
+
return activitiesUtil.mapStatusBarRawToDto(statusBarRaw)
|
|
16
|
+
}
|
|
17
|
+
// process the pulse
|
|
18
|
+
async createPulse(pulseDto: CreateWakatimePulseDto) {
|
|
19
|
+
const latestProject = await this.pulseRepo.getLatestProject()
|
|
20
|
+
const pulse: CodeClimbers.Pulse = this.mapDtoToPulse(
|
|
21
|
+
pulseDto,
|
|
22
|
+
latestProject,
|
|
23
|
+
)
|
|
24
|
+
await this.pulseRepo.createPulse(pulse)
|
|
25
|
+
return activitiesUtil.pulseSuccessResponse(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async createPulses(pulsesDto: CreateWakatimePulseDto[]) {
|
|
29
|
+
const latestProject = await this.pulseRepo.getLatestProject()
|
|
30
|
+
const pulses: CodeClimbers.Pulse[] = pulsesDto.map((dto) =>
|
|
31
|
+
this.mapDtoToPulse(dto, latestProject),
|
|
32
|
+
)
|
|
33
|
+
const uniquePulses = activitiesUtil.filterUniqueByHash(pulses)
|
|
34
|
+
|
|
35
|
+
await this.pulseRepo.createPulses(uniquePulses)
|
|
36
|
+
return activitiesUtil.pulseSuccessResponse(pulsesDto.length)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getLatestPulses(): Promise<CodeClimbers.Pulse[] | undefined> {
|
|
40
|
+
return this.pulseRepo.getLatestPulses()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getCategoryTimeOverview(
|
|
44
|
+
startDate: string,
|
|
45
|
+
endDate: string,
|
|
46
|
+
): Promise<CodeClimbers.TimeOverview[]> {
|
|
47
|
+
return await this.pulseRepo.getCategoryTimeOverview(startDate, endDate)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getWeekOverview(date: string): Promise<CodeClimbers.WeekOverview> {
|
|
51
|
+
const currentDate = new Date(date)
|
|
52
|
+
const weekStart = new Date(
|
|
53
|
+
new Date(currentDate).setDate(currentDate.getDate() - 7),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const tomorrow = new Date(new Date(date).setDate(currentDate.getDate() + 1))
|
|
57
|
+
|
|
58
|
+
const yesterday = new Date(
|
|
59
|
+
new Date(date).setDate(currentDate.getDate() - 1),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const longestDayMinutes = await this.pulseRepo.getLongestDayInRangeMinutes(
|
|
63
|
+
weekStart,
|
|
64
|
+
currentDate,
|
|
65
|
+
)
|
|
66
|
+
const yesterdayMinutes = await this.pulseRepo.getRangeMinutes(
|
|
67
|
+
yesterday,
|
|
68
|
+
currentDate,
|
|
69
|
+
)
|
|
70
|
+
const todayMinutes = await this.pulseRepo.getRangeMinutes(
|
|
71
|
+
currentDate,
|
|
72
|
+
tomorrow,
|
|
73
|
+
)
|
|
74
|
+
const weekMinutes = await this.pulseRepo.getRangeMinutes(
|
|
75
|
+
weekStart,
|
|
76
|
+
currentDate,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
longestDayMinutes,
|
|
81
|
+
yesterdayMinutes,
|
|
82
|
+
todayMinutes,
|
|
83
|
+
weekMinutes,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getSources(): Promise<CodeClimbers.Source[]> {
|
|
88
|
+
const userAgentsAndLastActive =
|
|
89
|
+
await this.pulseRepo.getUniqueUserAgentsAndLastActive()
|
|
90
|
+
const sources = new Set<string>()
|
|
91
|
+
|
|
92
|
+
userAgentsAndLastActive.forEach((userAgent) => {
|
|
93
|
+
const source = activitiesUtil.getSourceFromUserAgent(userAgent.userAgent)
|
|
94
|
+
if (source) {
|
|
95
|
+
sources.add(source)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return Array.from(sources).map((source) => {
|
|
100
|
+
const maxLastActive = userAgentsAndLastActive
|
|
101
|
+
.filter((userAgent) => {
|
|
102
|
+
return (
|
|
103
|
+
source ===
|
|
104
|
+
activitiesUtil.getSourceFromUserAgent(userAgent.userAgent)
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
.reduce((max, userAgent) => {
|
|
108
|
+
return new Date(userAgent.lastActive) > new Date(max)
|
|
109
|
+
? userAgent.lastActive
|
|
110
|
+
: max
|
|
111
|
+
}, new Date(0).toISOString())
|
|
112
|
+
|
|
113
|
+
return { name: source, lastActive: maxLastActive }
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async generatePulsesCSV(): Promise<Buffer> {
|
|
118
|
+
const pulses = await this.pulseRepo.getAllPulses()
|
|
119
|
+
const csvString = this.convertPulsesToCSV(pulses)
|
|
120
|
+
return Buffer.from(csvString, 'utf-8')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private userAgent() {
|
|
124
|
+
const platform = os.platform()
|
|
125
|
+
const release = os.release()
|
|
126
|
+
const arch = os.arch()
|
|
127
|
+
const nodeVersion = process.version
|
|
128
|
+
|
|
129
|
+
return `Node/${nodeVersion.slice(1)} (${platform}; ${arch}) OS/${release}`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private mapDtoToPulse(
|
|
133
|
+
dto: CreateWakatimePulseDto,
|
|
134
|
+
latestProject?: string,
|
|
135
|
+
): CodeClimbers.Pulse {
|
|
136
|
+
const project =
|
|
137
|
+
(dto.project === '<<PROJECT>>' || !dto.project) && latestProject
|
|
138
|
+
? latestProject
|
|
139
|
+
: dto.project
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
userId: 'local',
|
|
143
|
+
project,
|
|
144
|
+
branch: dto.branch,
|
|
145
|
+
entity: dto.entity,
|
|
146
|
+
type: dto.type,
|
|
147
|
+
isWrite: dto.is_write || false,
|
|
148
|
+
editor: dto.editor || '',
|
|
149
|
+
language: dto.language || Intl.DateTimeFormat().resolvedOptions().locale,
|
|
150
|
+
operatingSystem: dto.operating_system || os.platform(),
|
|
151
|
+
machine: dto.machine || os.hostname(),
|
|
152
|
+
userAgent: dto.user_agent || this.userAgent(),
|
|
153
|
+
time: dayjs((dto.time as number) * 1000).toISOString(),
|
|
154
|
+
hash: `${activitiesUtil.calculatePulseHash(dto)}`,
|
|
155
|
+
origin: dto.origin || '',
|
|
156
|
+
originId: dto.origin_id || '',
|
|
157
|
+
category: dto.category || '',
|
|
158
|
+
createdAt: dayjs().toISOString(),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private convertPulsesToCSV(pulses: CodeClimbers.Pulse[]): string {
|
|
163
|
+
const header = [
|
|
164
|
+
'ID',
|
|
165
|
+
'User ID',
|
|
166
|
+
'Entity',
|
|
167
|
+
'Type',
|
|
168
|
+
'Category',
|
|
169
|
+
'Project',
|
|
170
|
+
'Branch',
|
|
171
|
+
'Language',
|
|
172
|
+
'Is Write',
|
|
173
|
+
'Editor',
|
|
174
|
+
'Operating System',
|
|
175
|
+
'Machine',
|
|
176
|
+
'User Agent',
|
|
177
|
+
'Time',
|
|
178
|
+
'Hash',
|
|
179
|
+
'Origin',
|
|
180
|
+
'Origin ID',
|
|
181
|
+
'Created At',
|
|
182
|
+
'Description',
|
|
183
|
+
].join(',')
|
|
184
|
+
|
|
185
|
+
const rows = pulses.map((row) =>
|
|
186
|
+
[
|
|
187
|
+
row.id,
|
|
188
|
+
row.userId,
|
|
189
|
+
row.entity,
|
|
190
|
+
row.type,
|
|
191
|
+
row.category,
|
|
192
|
+
row.project,
|
|
193
|
+
row.branch,
|
|
194
|
+
row.language,
|
|
195
|
+
row.isWrite,
|
|
196
|
+
row.editor,
|
|
197
|
+
row.operatingSystem,
|
|
198
|
+
row.machine,
|
|
199
|
+
row.userAgent,
|
|
200
|
+
row.time,
|
|
201
|
+
row.hash,
|
|
202
|
+
row.origin,
|
|
203
|
+
row.originId,
|
|
204
|
+
row.createdAt,
|
|
205
|
+
row.description,
|
|
206
|
+
]
|
|
207
|
+
.map((value) => (value === null ? '' : value.toString()))
|
|
208
|
+
.join(','),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return [header, ...rows].join('\n')
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Controller, Get, Query, Res } from '@nestjs/common'
|
|
2
|
+
import { ActivitiesService } from './activities.service'
|
|
3
|
+
import { Response } from 'express'
|
|
4
|
+
import { GetCategoryTimeOverviewDto } from '../dtos/getCategoryTimeOverview.dto'
|
|
5
|
+
import { GetWeekOverviewDto } from '../dtos/getWeekOverview.dto'
|
|
6
|
+
|
|
7
|
+
@Controller('pulses')
|
|
8
|
+
export class PulseController {
|
|
9
|
+
constructor(private readonly activitiesService: ActivitiesService) {
|
|
10
|
+
this.activitiesService = activitiesService
|
|
11
|
+
}
|
|
12
|
+
@Get('latest')
|
|
13
|
+
async latestPulses(): Promise<{
|
|
14
|
+
message: string
|
|
15
|
+
data: CodeClimbers.Pulse[] | undefined
|
|
16
|
+
}> {
|
|
17
|
+
const pulse = await this.activitiesService.getLatestPulses()
|
|
18
|
+
return { message: 'success', data: pulse }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Get('weekOverview')
|
|
22
|
+
async getWeekOverview(
|
|
23
|
+
@Query() dto: GetWeekOverviewDto,
|
|
24
|
+
): Promise<CodeClimbers.WeekOverviewDao> {
|
|
25
|
+
const result: CodeClimbers.WeekOverview =
|
|
26
|
+
await this.activitiesService.getWeekOverview(dto.date)
|
|
27
|
+
return { message: 'success', data: result }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Get('categoryTimeOverview')
|
|
31
|
+
async getCategoryTimeOverview(
|
|
32
|
+
@Query() times: GetCategoryTimeOverviewDto,
|
|
33
|
+
): Promise<CodeClimbers.TimeOverviewDao> {
|
|
34
|
+
const result: CodeClimbers.TimeOverview[] =
|
|
35
|
+
await this.activitiesService.getCategoryTimeOverview(
|
|
36
|
+
times.startDate,
|
|
37
|
+
times.endDate,
|
|
38
|
+
)
|
|
39
|
+
return { message: 'success', data: result }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Get('sources')
|
|
43
|
+
async getSources(): Promise<
|
|
44
|
+
Promise<{
|
|
45
|
+
message: string
|
|
46
|
+
data: CodeClimbers.Source[] | []
|
|
47
|
+
}>
|
|
48
|
+
> {
|
|
49
|
+
const sources = await this.activitiesService.getSources()
|
|
50
|
+
return { message: 'success', data: sources }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Get('export')
|
|
54
|
+
async exportPulses(@Res() response: Response): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const csvBuffer = await this.activitiesService.generatePulsesCSV()
|
|
57
|
+
response.setHeader('Content-Type', 'text/csv')
|
|
58
|
+
response.setHeader(
|
|
59
|
+
'Content-Disposition',
|
|
60
|
+
'attachment; filename="pulses.csv"',
|
|
61
|
+
)
|
|
62
|
+
response.send(csvBuffer)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error exporting pulses CSV:', error)
|
|
65
|
+
response.status(500).send('Failed to export pulses')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Body, Controller, Get, Logger, Post } from '@nestjs/common'
|
|
2
|
+
import { ActivitiesService } from './activities.service'
|
|
3
|
+
import { CreateWakatimePulseDto } from '../dtos/createWakatimePulse.dto'
|
|
4
|
+
|
|
5
|
+
@Controller('wakatime')
|
|
6
|
+
export class WakatimeController {
|
|
7
|
+
constructor(private readonly activitiesService: ActivitiesService) {
|
|
8
|
+
this.activitiesService = activitiesService
|
|
9
|
+
}
|
|
10
|
+
@Get('users/current/statusbar/today')
|
|
11
|
+
async getStatusBar(): Promise<CodeClimbers.ActivitiesStatusBar> {
|
|
12
|
+
const result = await this.activitiesService.getActivityStatusBar()
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Post('users/current/heartbeats')
|
|
17
|
+
async createPulse(@Body() body: CreateWakatimePulseDto): Promise<{
|
|
18
|
+
Responses: number[][]
|
|
19
|
+
}> {
|
|
20
|
+
Logger.log(JSON.stringify(body), 'wakatimeProxy.controller')
|
|
21
|
+
const result = await this.activitiesService.createPulse(body)
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Post('users/current/heartbeats.bulk')
|
|
26
|
+
async createPulses(@Body() body: CreateWakatimePulseDto[]): Promise<{
|
|
27
|
+
Responses: number[][]
|
|
28
|
+
}> {
|
|
29
|
+
Logger.log(JSON.stringify(body), 'wakatimeProxy.controller')
|
|
30
|
+
const result = await this.activitiesService.createPulses(body)
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { knex, SQL_LITE_TEST_FILE } from '../knex'
|
|
2
|
+
|
|
3
|
+
describe('knex', () => {
|
|
4
|
+
it('Should connect successfully', async () => {
|
|
5
|
+
await knex.raw('SELECT 1').catch((e) => {
|
|
6
|
+
expect(e).toBeUndefined()
|
|
7
|
+
})
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('Should be using test path', () => {
|
|
11
|
+
expect(knex.client.config.connection.filename).toEqual(SQL_LITE_TEST_FILE)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterAll((done) => {
|
|
16
|
+
knex.destroy()
|
|
17
|
+
done()
|
|
18
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { knex } from '../knex'
|
|
2
|
+
import { PulseRepo } from '../pulse.repo'
|
|
3
|
+
|
|
4
|
+
const pulseRepo = new PulseRepo(knex)
|
|
5
|
+
|
|
6
|
+
describe('pulse.repo', () => {
|
|
7
|
+
const SECOND = 1_000
|
|
8
|
+
const MINUTE = SECOND * 60
|
|
9
|
+
const HOUR = MINUTE * 60
|
|
10
|
+
const DAY = HOUR * 24
|
|
11
|
+
const YEAR = DAY * 365
|
|
12
|
+
const NOW = new Date()
|
|
13
|
+
NOW.setUTCHours(0, 0, 0, 0)
|
|
14
|
+
|
|
15
|
+
const TIMESTAMP = NOW.getTime()
|
|
16
|
+
|
|
17
|
+
// SEED BASED TESTS:
|
|
18
|
+
it('Should get latest pulses', async () => {
|
|
19
|
+
const pulses = await pulseRepo.getLatestPulses()
|
|
20
|
+
expect(pulses.length).toEqual(10)
|
|
21
|
+
// Make sure it is the newest ones
|
|
22
|
+
pulses.slice(0, 3).forEach((pulse) => {
|
|
23
|
+
expect(pulse.entity).toEqual('NEW')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Should get all pulses', async () => {
|
|
28
|
+
const pulses = await pulseRepo.getAllPulses()
|
|
29
|
+
expect(pulses.length).toEqual(102)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('Should get minutes in range', async () => {
|
|
33
|
+
const endDate = new Date()
|
|
34
|
+
const startDate = new Date(endDate.getTime() - HOUR * 2)
|
|
35
|
+
const minutes = await pulseRepo.getRangeMinutes(startDate, endDate)
|
|
36
|
+
|
|
37
|
+
expect(minutes).toEqual(2)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Should get the longest day in minutes range', async () => {
|
|
41
|
+
const TEN_DAYS = DAY * 10
|
|
42
|
+
|
|
43
|
+
const endDate = new Date()
|
|
44
|
+
endDate.setUTCHours(0, 0, 0, 0)
|
|
45
|
+
const startDate = new Date(endDate.getTime() - TEN_DAYS)
|
|
46
|
+
startDate.setUTCHours(0, 0, 0, 0)
|
|
47
|
+
|
|
48
|
+
const longestDay = await pulseRepo.getLongestDayInRangeMinutes(
|
|
49
|
+
startDate,
|
|
50
|
+
endDate,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(longestDay).toEqual(2)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('Should get category time overview', async () => {
|
|
57
|
+
const TEN_DAYS = DAY * 10
|
|
58
|
+
|
|
59
|
+
const endDate = new Date()
|
|
60
|
+
const startDate = new Date(endDate.getTime() - TEN_DAYS)
|
|
61
|
+
|
|
62
|
+
const categoryTimeOverview = await pulseRepo.getCategoryTimeOverview(
|
|
63
|
+
startDate.toISOString(),
|
|
64
|
+
endDate.toISOString(),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(categoryTimeOverview.length).toEqual(2)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Should get the last project', async () => {
|
|
71
|
+
const lastProject = await pulseRepo.getLatestProject()
|
|
72
|
+
expect(lastProject).toEqual('Latest Project')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('Should get unique user agents and last active', async () => {
|
|
76
|
+
const uniqueUserAgents = await pulseRepo.getUniqueUserAgentsAndLastActive()
|
|
77
|
+
expect(uniqueUserAgents.length).toEqual(3)
|
|
78
|
+
|
|
79
|
+
const vscode = uniqueUserAgents.find((n) => n.userAgent === 'vscode')
|
|
80
|
+
const VSCODE_LAST_ACTIVE = new Date(TIMESTAMP - DAY)
|
|
81
|
+
expect(vscode).toBeTruthy()
|
|
82
|
+
expect(vscode.lastActive).toEqual(VSCODE_LAST_ACTIVE.toISOString())
|
|
83
|
+
|
|
84
|
+
const chrome = uniqueUserAgents.find(
|
|
85
|
+
(n) => n.userAgent === 'chrome-chrome_extension',
|
|
86
|
+
)
|
|
87
|
+
const CHROME_LAST_ACTIVE = new Date(TIMESTAMP - DAY * 3)
|
|
88
|
+
expect(chrome).toBeTruthy()
|
|
89
|
+
expect(chrome.lastActive).toEqual(CHROME_LAST_ACTIVE.toISOString())
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// --------- WRITES ------------
|
|
93
|
+
const dummyPulse = (time?: string): CodeClimbers.Pulse => ({
|
|
94
|
+
userId: '1',
|
|
95
|
+
entity: 'NEW',
|
|
96
|
+
category: 'NEW',
|
|
97
|
+
time: time ?? new Date().toISOString(),
|
|
98
|
+
createdAt: time ?? new Date().toISOString(),
|
|
99
|
+
project: 'NEW',
|
|
100
|
+
type: '',
|
|
101
|
+
isWrite: false,
|
|
102
|
+
editor: '',
|
|
103
|
+
operatingSystem: '',
|
|
104
|
+
machine: '',
|
|
105
|
+
userAgent: '',
|
|
106
|
+
hash: String(Math.floor(new Date().getTime() / Math.random())),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('Should create a pulse', async () => {
|
|
110
|
+
const createdPulse = await pulseRepo.createPulse(dummyPulse())
|
|
111
|
+
|
|
112
|
+
expect(createdPulse).toBeTruthy()
|
|
113
|
+
expect(createdPulse).toHaveLength(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('Should create pulses', async () => {
|
|
117
|
+
const createdPulses = await pulseRepo.createPulses([
|
|
118
|
+
dummyPulse(),
|
|
119
|
+
dummyPulse(),
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
expect(createdPulses).toBeTruthy()
|
|
123
|
+
expect(createdPulses).toHaveLength(2)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// LOGIC BASED TESTS. Go back a year to prevent issues with seeder data.
|
|
127
|
+
it('Should group many heartbeats within a 2 minute period correctly', async () => {
|
|
128
|
+
const interval = SECOND * 10
|
|
129
|
+
const timePeriod = 12
|
|
130
|
+
|
|
131
|
+
const startDate = new Date(TIMESTAMP - YEAR)
|
|
132
|
+
const endDate = new Date(TIMESTAMP - YEAR + interval * timePeriod)
|
|
133
|
+
|
|
134
|
+
const pulses = []
|
|
135
|
+
for (let i = 0; i < timePeriod; i++) {
|
|
136
|
+
pulses.push(
|
|
137
|
+
dummyPulse(new Date(startDate.getTime() + i * interval).toISOString()),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await pulseRepo.createPulses(pulses)
|
|
142
|
+
|
|
143
|
+
const minutes = await pulseRepo.getRangeMinutes(startDate, endDate)
|
|
144
|
+
expect(minutes).toEqual(2)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('Should group many heart beats at a different intervals correctly', async () => {
|
|
148
|
+
const timeAgo = TIMESTAMP - YEAR - DAY * 2
|
|
149
|
+
|
|
150
|
+
const startDate = new Date(timeAgo)
|
|
151
|
+
const endDate = new Date(timeAgo + MINUTE * 45)
|
|
152
|
+
|
|
153
|
+
// Start, 3 at 10 minutes, 2 at 20 minutes, 30 minutes.
|
|
154
|
+
// 7 total, but should only count as 4.
|
|
155
|
+
await pulseRepo.createPulses([
|
|
156
|
+
dummyPulse(new Date(startDate.getTime()).toISOString()),
|
|
157
|
+
dummyPulse(new Date(startDate.getTime() + MINUTE * 10).toISOString()),
|
|
158
|
+
dummyPulse(
|
|
159
|
+
new Date(startDate.getTime() + MINUTE * 10 + SECOND * 9).toISOString(),
|
|
160
|
+
),
|
|
161
|
+
dummyPulse(
|
|
162
|
+
new Date(startDate.getTime() + MINUTE * 10 + SECOND * 5).toISOString(),
|
|
163
|
+
),
|
|
164
|
+
dummyPulse(new Date(startDate.getTime() + MINUTE * 20).toISOString()),
|
|
165
|
+
dummyPulse(
|
|
166
|
+
new Date(startDate.getTime() + MINUTE * 20 + SECOND * 5).toISOString(),
|
|
167
|
+
),
|
|
168
|
+
dummyPulse(new Date(startDate.getTime() + MINUTE * 30).toISOString()),
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
const minutes = await pulseRepo.getRangeMinutes(startDate, endDate)
|
|
172
|
+
expect(minutes).toEqual(8)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Edge case: Does the query handle days. If I have a heartbeat at 11:59pm or 12:00am, does that heartbeat count towards the correct day?
|
|
176
|
+
it('Should group heartbeats at the end of the day correctly', async () => {
|
|
177
|
+
const timeAgo = TIMESTAMP - YEAR - DAY * 14
|
|
178
|
+
|
|
179
|
+
const startDate = new Date(timeAgo)
|
|
180
|
+
const endDate = new Date(timeAgo + DAY * 7)
|
|
181
|
+
|
|
182
|
+
// Start, 11:59pm, 12:00am, 12:01am.
|
|
183
|
+
// 5 total, but should only count as 2
|
|
184
|
+
await pulseRepo.createPulses([
|
|
185
|
+
dummyPulse(new Date(startDate.getTime()).toISOString()),
|
|
186
|
+
dummyPulse(new Date(startDate.getTime() - MINUTE).toISOString()),
|
|
187
|
+
dummyPulse(new Date(startDate.getTime() + DAY - MINUTE).toISOString()),
|
|
188
|
+
dummyPulse(new Date(startDate.getTime() + DAY).toISOString()),
|
|
189
|
+
dummyPulse(new Date(startDate.getTime() + DAY + MINUTE).toISOString()),
|
|
190
|
+
dummyPulse(
|
|
191
|
+
new Date(startDate.getTime() + DAY + MINUTE * 2).toISOString(),
|
|
192
|
+
),
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
const minutes = await pulseRepo.getLongestDayInRangeMinutes(
|
|
196
|
+
startDate,
|
|
197
|
+
endDate,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
expect(minutes).toEqual(3)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// TODO: Write test for representing categories correctly, as they are not all equal.
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
afterAll((done) => {
|
|
207
|
+
knex.destroy()
|
|
208
|
+
done()
|
|
209
|
+
})
|