@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,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
+ }