@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,193 @@
1
+ import * as dayjs from 'dayjs'
2
+ import { groupBy, maxBy, minBy } from './helpers.util'
3
+ const cyrb53 = (str: string, seed = 0) => {
4
+ let h1 = 0xdeadbeef ^ seed,
5
+ h2 = 0x41c6ce57 ^ seed
6
+ for (let i = 0, ch; i < str.length; i++) {
7
+ ch = str.charCodeAt(i)
8
+ h1 = Math.imul(h1 ^ ch, 2654435761)
9
+ h2 = Math.imul(h2 ^ ch, 1597334677)
10
+ }
11
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
12
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
13
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
14
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
15
+
16
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0)
17
+ }
18
+
19
+ const calculatePulseHash = (
20
+ pulse: CodeClimbers.CreateWakatimePulseDto,
21
+ ): number => {
22
+ /* eslint-disable @typescript-eslint/no-unused-vars */
23
+ const {
24
+ editor, // do not include these fields in hash
25
+ user_agent,
26
+ origin,
27
+ origin_id,
28
+ operating_system,
29
+ machine,
30
+ ...pulseHash
31
+ } = { ...pulse }
32
+
33
+ const hash = cyrb53(JSON.stringify(pulseHash))
34
+ return hash
35
+ }
36
+
37
+ function filterUniqueByHash(arr: CodeClimbers.Pulse[]) {
38
+ const uniqueMap = new Map()
39
+
40
+ return arr.filter((obj) => {
41
+ if (!uniqueMap.has(obj.hash)) {
42
+ uniqueMap.set(obj.hash, true)
43
+ return true
44
+ }
45
+ return false
46
+ })
47
+ }
48
+
49
+ function pulseSuccessResponse(n: number) {
50
+ const responses = [...Array(n).keys()].map((i) => [null, 201])
51
+
52
+ return {
53
+ Responses: responses,
54
+ }
55
+ }
56
+
57
+ function defaultStatusBar(): CodeClimbers.ActivitiesStatusBar {
58
+ const now = dayjs()
59
+
60
+ return {
61
+ data: {
62
+ categories: [],
63
+ dependencies: [],
64
+ editors: [],
65
+ languages: [],
66
+ machines: [],
67
+ operating_systems: [],
68
+ projects: [],
69
+ branches: null,
70
+ entities: null,
71
+ grand_total: {
72
+ digital: '0:0',
73
+ hours: 0,
74
+ minutes: 0,
75
+ text: '0 hrs 0 mins',
76
+ total_seconds: 0,
77
+ },
78
+ range: {
79
+ date: now.toISOString(),
80
+ end: now.toISOString(),
81
+ start: now.startOf('day').toISOString(),
82
+ text: '',
83
+ timezone: 'UTC',
84
+ },
85
+ },
86
+ cached_at: now.toISOString(),
87
+ }
88
+ }
89
+ function getStatusByKey(
90
+ data: CodeClimbers.WakatimePulseStatusDao[],
91
+ dataKey: string,
92
+ ): CodeClimbers.ActivitiesDetail[] {
93
+ const keyWithoutS = dataKey.replace(/s$/, '')
94
+ const groupedData = groupBy(data, keyWithoutS)
95
+ return Object.keys(groupedData).map((key: string) => {
96
+ const group = groupedData[key]
97
+ const totalSeconds = group.reduce(
98
+ (acc, x) => acc + parseInt(x.seconds as string),
99
+ 0,
100
+ )
101
+ const hours = Math.floor(totalSeconds / 3600)
102
+ const minutes = Math.floor((totalSeconds % 3600) / 60)
103
+ const seconds = Math.floor(totalSeconds % 60)
104
+ return {
105
+ digital: `${hours}:${minutes}:${seconds}`,
106
+ hours,
107
+ minutes,
108
+ name: key || 'unknown',
109
+ percent: 100,
110
+ seconds,
111
+ text: `${hours} hrs ${seconds >= 30 ? minutes + 1 : minutes} mins`,
112
+ total_seconds: data.reduce(
113
+ (acc, x) => acc + parseInt(x.seconds as string),
114
+ 0,
115
+ ),
116
+ }
117
+ })
118
+ }
119
+
120
+ function mapStatusBarRawToDto(
121
+ statusBarRaw: CodeClimbers.WakatimePulseStatusDao[],
122
+ ): CodeClimbers.ActivitiesStatusBar {
123
+ if (statusBarRaw.length <= 0) return defaultStatusBar()
124
+ const now = new Date()
125
+
126
+ const statusbar: CodeClimbers.ActivitiesStatusBar = {
127
+ cached_at: '',
128
+ data: {
129
+ branches: null,
130
+ entities: null,
131
+ },
132
+ }
133
+
134
+ statusbar.data.editors = getStatusByKey(statusBarRaw, 'editors')
135
+ statusbar.data.languages = getStatusByKey(statusBarRaw, 'languages')
136
+ statusbar.data.machines = getStatusByKey(statusBarRaw, 'machines')
137
+ statusbar.data.operating_systems = getStatusByKey(
138
+ statusBarRaw,
139
+ 'operating_systems',
140
+ )
141
+ statusbar.data.projects = getStatusByKey(statusBarRaw, 'projects')
142
+
143
+ // const grandTotalSeconds = sumBy(statusBarRaw, (x) => parseInt(x.seconds))
144
+ const grandTotalSeconds = statusBarRaw.reduce(
145
+ (acc, x) => acc + parseInt(x.seconds as string),
146
+ 0,
147
+ )
148
+ const hours = Math.floor(grandTotalSeconds / 3600)
149
+ const minutes = Math.floor((grandTotalSeconds % 3600) / 60)
150
+ const seconds = Math.floor(grandTotalSeconds % 60)
151
+ statusbar.data.grand_total = {
152
+ digital: `${hours}:${minutes}`,
153
+ hours,
154
+ minutes,
155
+ text: `${hours} hrs ${seconds >= 30 ? minutes + 1 : minutes} mins`,
156
+ total_seconds: grandTotalSeconds,
157
+ }
158
+
159
+ const dates = statusBarRaw.map((x) => new Date(x.maxHeartbeatTime))
160
+ statusbar.data.range = {
161
+ date: new Date().toISOString(),
162
+ end:
163
+ maxBy(statusBarRaw, (item) => new Date(item.maxHeartbeatTime))
164
+ ?.maxHeartbeatTime || '',
165
+ start:
166
+ minBy(statusBarRaw, (item) => new Date(item.minHeartbeatTime))
167
+ ?.minHeartbeatTime || '',
168
+ text: '',
169
+ timezone: 'UTC',
170
+ }
171
+ statusbar.cached_at = new Date().toISOString()
172
+
173
+ return statusbar
174
+ }
175
+
176
+ function getSourceFromUserAgent(userAgent: string): string | undefined {
177
+ const sourceRegex = /.*?\/.*?\s([^0-9]*)\//
178
+ const match = userAgent.match(sourceRegex)
179
+ if (match) {
180
+ return match[1]
181
+ }
182
+
183
+ return undefined
184
+ }
185
+
186
+ export default {
187
+ mapStatusBarRawToDto,
188
+ calculatePulseHash,
189
+ filterUniqueByHash,
190
+ pulseSuccessResponse,
191
+ cyrb53,
192
+ getSourceFromUserAgent,
193
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ ExceptionFilter,
3
+ Catch,
4
+ ArgumentsHost,
5
+ HttpException,
6
+ HttpStatus,
7
+ Logger,
8
+ } from '@nestjs/common'
9
+
10
+ export type BEErrorReport = {
11
+ customMessage?: string
12
+ } & Error
13
+
14
+ @Catch()
15
+ export class AllExceptionsFilter implements ExceptionFilter {
16
+ async catch(exception: unknown, host: ArgumentsHost) {
17
+ const ctx = host.switchToHttp()
18
+ const response = ctx.getResponse()
19
+
20
+ const status =
21
+ exception instanceof HttpException
22
+ ? exception.getStatus()
23
+ : HttpStatus.INTERNAL_SERVER_ERROR
24
+ const message =
25
+ exception instanceof HttpException
26
+ ? exception.getResponse()
27
+ : 'Internal server error'
28
+
29
+ const errorReport: BEErrorReport = {
30
+ message: typeof message === 'object' ? JSON.stringify(message) : message,
31
+ name: exception instanceof Error ? exception.name : 'Error',
32
+ stack: exception instanceof Error ? exception.stack : undefined,
33
+ }
34
+ if (status >= 500) {
35
+ Logger.error(errorReport)
36
+ // Sentry.captureException(exception)
37
+ } else {
38
+ Logger.debug(errorReport)
39
+ }
40
+
41
+ response.status(status).json(message)
42
+ }
43
+ }
@@ -0,0 +1,32 @@
1
+ import { ValidationError } from 'class-validator'
2
+
3
+ /**
4
+ * Custom Errors are used when a developer wants the error to be displayed to the end user
5
+ * All other errors are reported internally
6
+ */
7
+ export class CodeClimberError extends Error {
8
+ public message!: string
9
+ public status: number
10
+
11
+ public constructor(message: string, status = 500) {
12
+ super(message)
13
+ this.name = this.constructor.name
14
+ Error.captureStackTrace(this, this.constructor)
15
+ this.status = status
16
+ }
17
+ }
18
+
19
+ export namespace CodeClimberError {
20
+ class ValidationErr extends CodeClimberError {
21
+ validationErrors?: ValidationError[]
22
+ public constructor(message: string, validationErrors?: ValidationError[]) {
23
+ super(message, 422)
24
+ this.validationErrors = validationErrors
25
+ }
26
+ }
27
+ export class InvalidBody extends ValidationErr {
28
+ constructor(validationErrors: ValidationError[], message?: string) {
29
+ super(message || 'Expected request body was invalid', validationErrors)
30
+ }
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export const PROCESS_NAME = 'codeclimbers-server'
@@ -0,0 +1 @@
1
+ export const isCli = () => process.env.CODECLIMBERS_SERVER_APP_CONTEXT === 'cli'
@@ -0,0 +1,131 @@
1
+ export function forOwn<T>(
2
+ obj: Record<string, T>,
3
+ iteratee: (value: T, key: string, obj: Record<string, T>) => void,
4
+ ): void {
5
+ if (obj === null || obj === undefined) {
6
+ return
7
+ }
8
+
9
+ const keys = Object.keys(obj)
10
+ for (let i = 0; i < keys.length; i++) {
11
+ const key = keys[i]
12
+ iteratee(obj[key], key, obj)
13
+ }
14
+ }
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ export function isPlainObject(value: any): boolean {
18
+ if (typeof value !== 'object' || value === null) {
19
+ return false
20
+ }
21
+
22
+ const prototype = Object.getPrototypeOf(value)
23
+ if (prototype === null) {
24
+ return true
25
+ }
26
+
27
+ const constructor = prototype.constructor
28
+ if (typeof constructor !== 'function') {
29
+ return false
30
+ }
31
+
32
+ const constructorString = Function.prototype.toString.call(constructor)
33
+ return (
34
+ constructorString.indexOf('[native code]') !== -1 && constructor === Object
35
+ )
36
+ }
37
+
38
+ export function snakeCase(str: string): string {
39
+ if (typeof str !== 'string') {
40
+ return ''
41
+ }
42
+
43
+ const result: string[] = []
44
+ let currentWord = ''
45
+
46
+ for (let i = 0; i < str.length; i++) {
47
+ const char = str[i]
48
+ const nextChar = str[i + 1]
49
+
50
+ if (char === ' ' || char === '-') {
51
+ if (currentWord) {
52
+ result.push(currentWord.toLowerCase())
53
+ currentWord = ''
54
+ }
55
+ } else if (char >= 'A' && char <= 'Z') {
56
+ if (currentWord && nextChar && !(nextChar >= 'A' && nextChar <= 'Z')) {
57
+ result.push(currentWord.toLowerCase())
58
+ currentWord = ''
59
+ }
60
+ currentWord += char.toLowerCase()
61
+ } else {
62
+ currentWord += char
63
+ }
64
+ }
65
+
66
+ if (currentWord) {
67
+ result.push(currentWord.toLowerCase())
68
+ }
69
+
70
+ return result.join('_')
71
+ }
72
+
73
+ export function camelCase(str: string): string {
74
+ if (typeof str !== 'string') {
75
+ return ''
76
+ }
77
+
78
+ const words = str.split(/[\s-_]+/)
79
+ const result: string[] = []
80
+
81
+ for (let i = 0; i < words.length; i++) {
82
+ const word = words[i]
83
+ if (word.length === 0) {
84
+ continue
85
+ }
86
+
87
+ if (i === 0) {
88
+ result.push(word.toLowerCase())
89
+ } else {
90
+ result.push(word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
91
+ }
92
+ }
93
+
94
+ return result.join('')
95
+ }
96
+
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ export function maxBy<T>(arr: T[], iteratee: (item: T) => any): T | undefined {
99
+ if (!arr || arr.length === 0) return undefined
100
+ return arr.reduce((acc, item) => {
101
+ const value = iteratee(item)
102
+ if (value > iteratee(acc)) return item
103
+ return acc
104
+ }, arr[0])
105
+ }
106
+
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ export function minBy<T>(arr: T[], iteratee: (item: T) => any): T | undefined {
109
+ if (!arr || arr.length === 0) return undefined
110
+ return arr.reduce((acc, item) => {
111
+ const value = iteratee(item)
112
+ if (value < iteratee(acc)) return item
113
+ return acc
114
+ }, arr[0])
115
+ }
116
+
117
+ // groupBy function that takes an array and groups it by a key
118
+ export function groupBy<T>(arr: T[], key: string): Record<string, T[]> {
119
+ return arr.reduce(
120
+ (acc, item) => {
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ const keyValue = (item as Record<string, any>)[key]
123
+ if (!acc[keyValue]) {
124
+ acc[keyValue] = []
125
+ }
126
+ acc[keyValue].push(item)
127
+ return acc
128
+ },
129
+ {} as Record<string, T[]>,
130
+ )
131
+ }
@@ -0,0 +1,29 @@
1
+ import * as path from 'node:path'
2
+ import * as os from 'node:os'
3
+ import * as fs from 'node:fs'
4
+ import { execSync } from 'node:child_process'
5
+
6
+ export const BIN_PATH = path.join(__dirname, '..', '..', '..', '..', 'bin')
7
+ export const HOME_DIR = os.homedir()
8
+ export const CODE_CLIMBER_META_DIR = `${HOME_DIR}/.codeclimbers`
9
+ export const DB_PATH = path.join(CODE_CLIMBER_META_DIR, 'codeclimber.sqlite')
10
+ export const APP_DIST_PATH = path.join(
11
+ __dirname,
12
+ '..',
13
+ '..',
14
+ '..',
15
+ 'app',
16
+ 'dist',
17
+ )
18
+ export const NODE_PATH = function () {
19
+ const result = execSync('which node').toString().trim()
20
+ const dir = result.slice(0, -5) // result is /usr/local/bin/node, we need to remove node
21
+ return dir
22
+ }
23
+
24
+ export const initDBDir = () => {
25
+ if (!fs.existsSync(CODE_CLIMBER_META_DIR)) {
26
+ fs.mkdirSync(CODE_CLIMBER_META_DIR, { recursive: true })
27
+ }
28
+ fs.chmodSync(CODE_CLIMBER_META_DIR, '755')
29
+ }
@@ -0,0 +1,13 @@
1
+ export const toSQL = (query) => {
2
+ const { sql, bindings } = query.toSQL()
3
+ const fullQuery = bindings.reduce(
4
+ (acc, binding) =>
5
+ acc.replace(
6
+ '?',
7
+ binding instanceof Date ? `'${binding.toISOString()}'` : `'${binding}'`,
8
+ ),
9
+ sql,
10
+ )
11
+ console.log(fullQuery)
12
+ return fullQuery
13
+ }
@@ -0,0 +1,49 @@
1
+ import { readFile } from 'fs/promises'
2
+ import { dirname, join } from 'path'
3
+
4
+ // Initialize a cache object
5
+ const cache: Record<string, string> = {}
6
+
7
+ // Utility function to read file content and return it as a string
8
+ async function getFileContentAsString(
9
+ fileName: string,
10
+ additionalPath = 'queries',
11
+ ) {
12
+ try {
13
+ // Dynamically determine the directory of the caller
14
+ // Create a new Error and use its stack trace
15
+ const err = new Error()
16
+ const stack = err.stack || ''
17
+ // Find the second entry in the stack trace, which should correspond to the caller
18
+ const caller = stack.split('\n')[2] || ''
19
+ // Extract the file path from the caller string
20
+ const match = caller.match(/\((?:file:\/\/)?(.*?):\d+:\d+\)$/)
21
+ if (!match) {
22
+ throw new Error('Could not determine caller file path')
23
+ }
24
+ const callerPath = match[1]
25
+ const callerDir = dirname(callerPath)
26
+
27
+ // Generate a unique cache key based on the caller directory and file name
28
+ const cacheKey = `${callerDir}:${additionalPath}:${fileName}`
29
+
30
+ // Check if the result is already in the cache
31
+ if (cache[cacheKey]) {
32
+ return cache[cacheKey] // Return the cached result
33
+ }
34
+
35
+ // Resolve the file path relative to the caller file's directory
36
+ const resolvedPath = join(callerDir, additionalPath, fileName)
37
+ // Read and return the file content
38
+ const content = await readFile(resolvedPath, 'utf8')
39
+ cache[cacheKey] = content // Store the result in the cache
40
+ return content
41
+ } catch (error) {
42
+ console.error('Error reading file:', error)
43
+ throw error // Re-throw the error to handle it in the calling function
44
+ }
45
+ }
46
+
47
+ export default {
48
+ getFileContentAsString,
49
+ }
@@ -0,0 +1,78 @@
1
+ import * as fs from 'fs/promises'
2
+ import * as path from 'path'
3
+ import { HOME_DIR } from './node.util'
4
+ import { Logger } from '@nestjs/common'
5
+
6
+ // Define the file path (adjust as needed)
7
+ const filePath: string = path.join(HOME_DIR, '.wakatime.cfg')
8
+
9
+ // Define types
10
+ type IniSection = Record<string, string>
11
+ export type IniConfig = Record<string, IniSection>
12
+
13
+ // Function to parse INI content
14
+ export function parseIni(content: string): IniConfig {
15
+ const result: IniConfig = {}
16
+ const lines: string[] = content.split('\n')
17
+ let currentSection = ''
18
+
19
+ for (const line of lines) {
20
+ const trimmedLine: string = line.trim()
21
+ if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
22
+ currentSection = trimmedLine.slice(1, -1)
23
+ result[currentSection] = {}
24
+ } else if (trimmedLine && currentSection) {
25
+ const [key, value] = trimmedLine.split('=').map((part) => part.trim())
26
+ if (key && value) {
27
+ result[currentSection][key] = value
28
+ }
29
+ }
30
+ }
31
+
32
+ return result
33
+ }
34
+
35
+ // Function to stringify INI content
36
+ export function stringifyIni(data: IniConfig): string {
37
+ const entries = Object.entries(data)
38
+ .map(([section, entries]) => {
39
+ const sectionContent: string = Object.entries(entries)
40
+ .map(([key, value]) => `${key} = ${value}`)
41
+ .join('\n')
42
+ return `[${section}]\n${sectionContent}`
43
+ })
44
+ .join('\n\n')
45
+ return `${entries}\n`
46
+ }
47
+
48
+ export async function updateSettings(
49
+ newSettings: Record<string, string>,
50
+ ): Promise<void> {
51
+ try {
52
+ let config: IniConfig
53
+
54
+ try {
55
+ // Try to read the existing file
56
+ const data: string = await fs.readFile(filePath, 'utf8')
57
+ config = parseIni(data)
58
+ } catch (error) {
59
+ // File doesn't exist, create a new configuration
60
+ config = { settings: {} }
61
+ }
62
+
63
+ // Ensure the 'settings' section exists
64
+ if (!config.settings) {
65
+ config.settings = {}
66
+ }
67
+
68
+ // Update the settings
69
+ Object.assign(config.settings, newSettings)
70
+
71
+ // Write the updated content back to the file
72
+ const updatedContent: string = stringifyIni(config)
73
+ await fs.writeFile(filePath, updatedContent, 'utf8')
74
+ Logger.log('File updated successfully', 'WakatimeUtil')
75
+ } catch (error) {
76
+ Logger.error('Error updating file:', error)
77
+ }
78
+ }