@codeclimbers/server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
})
|