@codeclimbers/server 0.0.1

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