@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.
Files changed (175) hide show
  1. package/README.md +73 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +22 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/app.module.d.ts +4 -0
  6. package/dist/src/app.module.js +49 -0
  7. package/dist/src/app.module.js.map +1 -0
  8. package/dist/src/assets/startup.plist.d.ts +1 -0
  9. package/dist/src/assets/startup.plist.js +41 -0
  10. package/dist/src/assets/startup.plist.js.map +1 -0
  11. package/dist/src/common/infrastructure/http/controllers/health.controller.d.ts +5 -0
  12. package/dist/src/common/infrastructure/http/controllers/health.controller.js +29 -0
  13. package/dist/src/common/infrastructure/http/controllers/health.controller.js.map +1 -0
  14. package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.d.ts +5 -0
  15. package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js +27 -0
  16. package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js.map +1 -0
  17. package/dist/src/main.d.ts +2 -0
  18. package/dist/src/main.js +44 -0
  19. package/dist/src/main.js.map +1 -0
  20. package/dist/src/sentry.d.ts +1 -0
  21. package/dist/src/sentry.js +13 -0
  22. package/dist/src/sentry.js.map +1 -0
  23. package/dist/src/v1/activities/activities.service.d.ts +21 -0
  24. package/dist/src/v1/activities/activities.service.js +174 -0
  25. package/dist/src/v1/activities/activities.service.js.map +1 -0
  26. package/dist/src/v1/activities/pulse.controller.d.ts +19 -0
  27. package/dist/src/v1/activities/pulse.controller.js +92 -0
  28. package/dist/src/v1/activities/pulse.controller.js.map +1 -0
  29. package/dist/src/v1/activities/wakatimeProxy.controller.d.ts +13 -0
  30. package/dist/src/v1/activities/wakatimeProxy.controller.js +64 -0
  31. package/dist/src/v1/activities/wakatimeProxy.controller.js.map +1 -0
  32. package/dist/src/v1/database/__tests__/knex.test.d.ts +1 -0
  33. package/dist/src/v1/database/__tests__/knex.test.js +18 -0
  34. package/dist/src/v1/database/__tests__/knex.test.js.map +1 -0
  35. package/dist/src/v1/database/__tests__/pulse.repo.test.d.ts +1 -0
  36. package/dist/src/v1/database/__tests__/pulse.repo.test.js +141 -0
  37. package/dist/src/v1/database/__tests__/pulse.repo.test.js.map +1 -0
  38. package/dist/src/v1/database/knex.d.ts +5 -0
  39. package/dist/src/v1/database/knex.js +87 -0
  40. package/dist/src/v1/database/knex.js.map +1 -0
  41. package/dist/src/v1/database/migrations.d.ts +1 -0
  42. package/dist/src/v1/database/migrations.js +16 -0
  43. package/dist/src/v1/database/migrations.js.map +1 -0
  44. package/dist/src/v1/database/pulse.repo.d.ts +17 -0
  45. package/dist/src/v1/database/pulse.repo.js +115 -0
  46. package/dist/src/v1/database/pulse.repo.js.map +1 -0
  47. package/dist/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
  48. package/dist/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
  49. package/dist/src/v1/database/queries/getStatusBarDetails.sql +42 -0
  50. package/dist/src/v1/dtos/createWakatimePulse.dto.d.ts +19 -0
  51. package/dist/src/v1/dtos/createWakatimePulse.dto.js +97 -0
  52. package/dist/src/v1/dtos/createWakatimePulse.dto.js.map +1 -0
  53. package/dist/src/v1/dtos/getCategoryTimeOverview.dto.d.ts +4 -0
  54. package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js +25 -0
  55. package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js.map +1 -0
  56. package/dist/src/v1/dtos/getWeekOverview.dto.d.ts +3 -0
  57. package/dist/src/v1/dtos/getWeekOverview.dto.js +21 -0
  58. package/dist/src/v1/dtos/getWeekOverview.dto.js.map +1 -0
  59. package/dist/src/v1/startup/darwinStartup.service.d.ts +8 -0
  60. package/dist/src/v1/startup/darwinStartup.service.js +114 -0
  61. package/dist/src/v1/startup/darwinStartup.service.js.map +1 -0
  62. package/dist/src/v1/startup/linuxStartup.service.d.ts +8 -0
  63. package/dist/src/v1/startup/linuxStartup.service.js +104 -0
  64. package/dist/src/v1/startup/linuxStartup.service.js.map +1 -0
  65. package/dist/src/v1/startup/startup.controller.d.ts +8 -0
  66. package/dist/src/v1/startup/startup.controller.js +46 -0
  67. package/dist/src/v1/startup/startup.controller.js.map +1 -0
  68. package/dist/src/v1/startup/startup.util.d.ts +5 -0
  69. package/dist/src/v1/startup/startup.util.js +19 -0
  70. package/dist/src/v1/startup/startup.util.js.map +1 -0
  71. package/dist/src/v1/startup/startupService.factory.d.ts +9 -0
  72. package/dist/src/v1/startup/startupService.factory.js +41 -0
  73. package/dist/src/v1/startup/startupService.factory.js.map +1 -0
  74. package/dist/src/v1/startup/unsupportedStartup.service.d.ts +6 -0
  75. package/dist/src/v1/startup/unsupportedStartup.service.js +29 -0
  76. package/dist/src/v1/startup/unsupportedStartup.service.js.map +1 -0
  77. package/dist/src/v1/v1.module.d.ts +2 -0
  78. package/dist/src/v1/v1.module.js +41 -0
  79. package/dist/src/v1/v1.module.js.map +1 -0
  80. package/dist/tsconfig.build.tsbuildinfo +1 -0
  81. package/dist/utils/__tests__/activites.util.test.d.ts +1 -0
  82. package/dist/utils/__tests__/activites.util.test.js +18 -0
  83. package/dist/utils/__tests__/activites.util.test.js.map +1 -0
  84. package/dist/utils/__tests__/helpers.util.test.d.ts +1 -0
  85. package/dist/utils/__tests__/helpers.util.test.js +120 -0
  86. package/dist/utils/__tests__/helpers.util.test.js.map +1 -0
  87. package/dist/utils/__tests__/wakatime.util.test.d.ts +1 -0
  88. package/dist/utils/__tests__/wakatime.util.test.js +210 -0
  89. package/dist/utils/__tests__/wakatime.util.test.js.map +1 -0
  90. package/dist/utils/activities.util.d.ts +15 -0
  91. package/dist/utils/activities.util.js +147 -0
  92. package/dist/utils/activities.util.js.map +1 -0
  93. package/dist/utils/allExceptions.filter.d.ts +7 -0
  94. package/dist/utils/allExceptions.filter.js +39 -0
  95. package/dist/utils/allExceptions.filter.js.map +1 -0
  96. package/dist/utils/codeClimberErrors.d.ts +16 -0
  97. package/dist/utils/codeClimberErrors.js +27 -0
  98. package/dist/utils/codeClimberErrors.js.map +1 -0
  99. package/dist/utils/constants.d.ts +1 -0
  100. package/dist/utils/constants.js +5 -0
  101. package/dist/utils/constants.js.map +1 -0
  102. package/dist/utils/environment.util.d.ts +1 -0
  103. package/dist/utils/environment.util.js +6 -0
  104. package/dist/utils/environment.util.js.map +1 -0
  105. package/dist/utils/helpers.util.d.ts +7 -0
  106. package/dist/utils/helpers.util.js +116 -0
  107. package/dist/utils/helpers.util.js.map +1 -0
  108. package/dist/utils/node.util.d.ts +7 -0
  109. package/dist/utils/node.util.js +26 -0
  110. package/dist/utils/node.util.js.map +1 -0
  111. package/dist/utils/sql.util.d.ts +1 -0
  112. package/dist/utils/sql.util.js +11 -0
  113. package/dist/utils/sql.util.js.map +1 -0
  114. package/dist/utils/sqlReader.util.d.ts +5 -0
  115. package/dist/utils/sqlReader.util.js +34 -0
  116. package/dist/utils/sqlReader.util.js.map +1 -0
  117. package/dist/utils/wakatime.util.d.ts +6 -0
  118. package/dist/utils/wakatime.util.js +63 -0
  119. package/dist/utils/wakatime.util.js.map +1 -0
  120. package/index.ts +5 -0
  121. package/jest.config.js +8 -0
  122. package/knexfile.js +14 -0
  123. package/nest-cli.json +15 -0
  124. package/package.json +77 -0
  125. package/src/app.module.ts +37 -0
  126. package/src/assets/startup.plist.ts +36 -0
  127. package/src/common/infrastructure/http/controllers/health.controller.ts +9 -0
  128. package/src/common/infrastructure/http/middleware/requestlogger.middleware.ts +22 -0
  129. package/src/main.ts +51 -0
  130. package/src/sentry.ts +14 -0
  131. package/src/types/activities.api.d.ts +18 -0
  132. package/src/types/time.api.d.ts +23 -0
  133. package/src/types/utils.d.ts +8 -0
  134. package/src/types/wakatimeProxy.api.d.ts +55 -0
  135. package/src/v1/activities/activities.service.ts +213 -0
  136. package/src/v1/activities/pulse.controller.ts +68 -0
  137. package/src/v1/activities/wakatimeProxy.controller.ts +33 -0
  138. package/src/v1/database/__tests__/knex.test.ts +18 -0
  139. package/src/v1/database/__tests__/pulse.repo.test.ts +209 -0
  140. package/src/v1/database/knex.ts +107 -0
  141. package/src/v1/database/migrations.ts +13 -0
  142. package/src/v1/database/models/pulse.d.ts +24 -0
  143. package/src/v1/database/pulse.repo.ts +132 -0
  144. package/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
  145. package/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
  146. package/src/v1/database/queries/getStatusBarDetails.sql +42 -0
  147. package/src/v1/dtos/createWakatimePulse.dto.ts +66 -0
  148. package/src/v1/dtos/getCategoryTimeOverview.dto.ts +9 -0
  149. package/src/v1/dtos/getWeekOverview.dto.ts +6 -0
  150. package/src/v1/startup/darwinStartup.service.ts +114 -0
  151. package/src/v1/startup/linuxStartup.service.ts +101 -0
  152. package/src/v1/startup/startup.controller.ts +23 -0
  153. package/src/v1/startup/startup.util.ts +21 -0
  154. package/src/v1/startup/startupService.factory.ts +28 -0
  155. package/src/v1/startup/unsupportedStartup.service.ts +21 -0
  156. package/src/v1/v1.module.ts +28 -0
  157. package/test/app.e2e-spec.ts +24 -0
  158. package/test/jest-e2e.json +9 -0
  159. package/test/jest.globalSetup.js +15 -0
  160. package/test/jest.globalTeardown.js +8 -0
  161. package/tsconfig.build.json +4 -0
  162. package/tsconfig.json +22 -0
  163. package/utils/__tests__/activites.util.test.ts +18 -0
  164. package/utils/__tests__/helpers.util.test.ts +155 -0
  165. package/utils/__tests__/wakatime.util.test.ts +222 -0
  166. package/utils/activities.util.ts +193 -0
  167. package/utils/allExceptions.filter.ts +43 -0
  168. package/utils/codeClimberErrors.ts +32 -0
  169. package/utils/constants.ts +1 -0
  170. package/utils/environment.util.ts +1 -0
  171. package/utils/helpers.util.ts +131 -0
  172. package/utils/node.util.ts +29 -0
  173. package/utils/sql.util.ts +13 -0
  174. package/utils/sqlReader.util.ts +49 -0
  175. 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,8 @@
1
+ declare namespace CodeClimbers {
2
+ export interface StartupService {
3
+ enableStartup: () => Promise<void>
4
+ disableStartup: () => Promise<void>
5
+ launchAndEnableStartup: () => Promise<void>
6
+ closeAndDisableStartup: () => Promise<void>
7
+ }
8
+ }
@@ -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
+ })