@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.
- package/README.md +73 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/src/app.module.d.ts +4 -0
- package/dist/src/app.module.js +49 -0
- package/dist/src/app.module.js.map +1 -0
- package/dist/src/assets/startup.plist.d.ts +1 -0
- package/dist/src/assets/startup.plist.js +41 -0
- package/dist/src/assets/startup.plist.js.map +1 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.d.ts +5 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.js +29 -0
- package/dist/src/common/infrastructure/http/controllers/health.controller.js.map +1 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.d.ts +5 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js +27 -0
- package/dist/src/common/infrastructure/http/middleware/requestlogger.middleware.js.map +1 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.js +44 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/sentry.d.ts +1 -0
- package/dist/src/sentry.js +13 -0
- package/dist/src/sentry.js.map +1 -0
- package/dist/src/v1/activities/activities.service.d.ts +21 -0
- package/dist/src/v1/activities/activities.service.js +174 -0
- package/dist/src/v1/activities/activities.service.js.map +1 -0
- package/dist/src/v1/activities/pulse.controller.d.ts +19 -0
- package/dist/src/v1/activities/pulse.controller.js +92 -0
- package/dist/src/v1/activities/pulse.controller.js.map +1 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.d.ts +13 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.js +64 -0
- package/dist/src/v1/activities/wakatimeProxy.controller.js.map +1 -0
- package/dist/src/v1/database/__tests__/knex.test.d.ts +1 -0
- package/dist/src/v1/database/__tests__/knex.test.js +18 -0
- package/dist/src/v1/database/__tests__/knex.test.js.map +1 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.d.ts +1 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.js +141 -0
- package/dist/src/v1/database/__tests__/pulse.repo.test.js.map +1 -0
- package/dist/src/v1/database/knex.d.ts +5 -0
- package/dist/src/v1/database/knex.js +87 -0
- package/dist/src/v1/database/knex.js.map +1 -0
- package/dist/src/v1/database/migrations.d.ts +1 -0
- package/dist/src/v1/database/migrations.js +16 -0
- package/dist/src/v1/database/migrations.js.map +1 -0
- package/dist/src/v1/database/pulse.repo.d.ts +17 -0
- package/dist/src/v1/database/pulse.repo.js +115 -0
- package/dist/src/v1/database/pulse.repo.js.map +1 -0
- package/dist/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
- package/dist/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
- package/dist/src/v1/database/queries/getStatusBarDetails.sql +42 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.d.ts +19 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.js +97 -0
- package/dist/src/v1/dtos/createWakatimePulse.dto.js.map +1 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.d.ts +4 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js +25 -0
- package/dist/src/v1/dtos/getCategoryTimeOverview.dto.js.map +1 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.d.ts +3 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.js +21 -0
- package/dist/src/v1/dtos/getWeekOverview.dto.js.map +1 -0
- package/dist/src/v1/startup/darwinStartup.service.d.ts +8 -0
- package/dist/src/v1/startup/darwinStartup.service.js +114 -0
- package/dist/src/v1/startup/darwinStartup.service.js.map +1 -0
- package/dist/src/v1/startup/linuxStartup.service.d.ts +8 -0
- package/dist/src/v1/startup/linuxStartup.service.js +104 -0
- package/dist/src/v1/startup/linuxStartup.service.js.map +1 -0
- package/dist/src/v1/startup/startup.controller.d.ts +8 -0
- package/dist/src/v1/startup/startup.controller.js +46 -0
- package/dist/src/v1/startup/startup.controller.js.map +1 -0
- package/dist/src/v1/startup/startup.util.d.ts +5 -0
- package/dist/src/v1/startup/startup.util.js +19 -0
- package/dist/src/v1/startup/startup.util.js.map +1 -0
- package/dist/src/v1/startup/startupService.factory.d.ts +9 -0
- package/dist/src/v1/startup/startupService.factory.js +41 -0
- package/dist/src/v1/startup/startupService.factory.js.map +1 -0
- package/dist/src/v1/startup/unsupportedStartup.service.d.ts +6 -0
- package/dist/src/v1/startup/unsupportedStartup.service.js +29 -0
- package/dist/src/v1/startup/unsupportedStartup.service.js.map +1 -0
- package/dist/src/v1/v1.module.d.ts +2 -0
- package/dist/src/v1/v1.module.js +41 -0
- package/dist/src/v1/v1.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils/__tests__/activites.util.test.d.ts +1 -0
- package/dist/utils/__tests__/activites.util.test.js +18 -0
- package/dist/utils/__tests__/activites.util.test.js.map +1 -0
- package/dist/utils/__tests__/helpers.util.test.d.ts +1 -0
- package/dist/utils/__tests__/helpers.util.test.js +120 -0
- package/dist/utils/__tests__/helpers.util.test.js.map +1 -0
- package/dist/utils/__tests__/wakatime.util.test.d.ts +1 -0
- package/dist/utils/__tests__/wakatime.util.test.js +210 -0
- package/dist/utils/__tests__/wakatime.util.test.js.map +1 -0
- package/dist/utils/activities.util.d.ts +15 -0
- package/dist/utils/activities.util.js +147 -0
- package/dist/utils/activities.util.js.map +1 -0
- package/dist/utils/allExceptions.filter.d.ts +7 -0
- package/dist/utils/allExceptions.filter.js +39 -0
- package/dist/utils/allExceptions.filter.js.map +1 -0
- package/dist/utils/codeClimberErrors.d.ts +16 -0
- package/dist/utils/codeClimberErrors.js +27 -0
- package/dist/utils/codeClimberErrors.js.map +1 -0
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/environment.util.d.ts +1 -0
- package/dist/utils/environment.util.js +6 -0
- package/dist/utils/environment.util.js.map +1 -0
- package/dist/utils/helpers.util.d.ts +7 -0
- package/dist/utils/helpers.util.js +116 -0
- package/dist/utils/helpers.util.js.map +1 -0
- package/dist/utils/node.util.d.ts +7 -0
- package/dist/utils/node.util.js +26 -0
- package/dist/utils/node.util.js.map +1 -0
- package/dist/utils/sql.util.d.ts +1 -0
- package/dist/utils/sql.util.js +11 -0
- package/dist/utils/sql.util.js.map +1 -0
- package/dist/utils/sqlReader.util.d.ts +5 -0
- package/dist/utils/sqlReader.util.js +34 -0
- package/dist/utils/sqlReader.util.js.map +1 -0
- package/dist/utils/wakatime.util.d.ts +6 -0
- package/dist/utils/wakatime.util.js +63 -0
- package/dist/utils/wakatime.util.js.map +1 -0
- package/index.ts +5 -0
- package/jest.config.js +8 -0
- package/knexfile.js +14 -0
- package/nest-cli.json +15 -0
- package/package.json +77 -0
- package/src/app.module.ts +37 -0
- package/src/assets/startup.plist.ts +36 -0
- package/src/common/infrastructure/http/controllers/health.controller.ts +9 -0
- package/src/common/infrastructure/http/middleware/requestlogger.middleware.ts +22 -0
- package/src/main.ts +51 -0
- package/src/sentry.ts +14 -0
- package/src/types/activities.api.d.ts +18 -0
- package/src/types/time.api.d.ts +23 -0
- package/src/types/utils.d.ts +8 -0
- package/src/types/wakatimeProxy.api.d.ts +55 -0
- package/src/v1/activities/activities.service.ts +213 -0
- package/src/v1/activities/pulse.controller.ts +68 -0
- package/src/v1/activities/wakatimeProxy.controller.ts +33 -0
- package/src/v1/database/__tests__/knex.test.ts +18 -0
- package/src/v1/database/__tests__/pulse.repo.test.ts +209 -0
- package/src/v1/database/knex.ts +107 -0
- package/src/v1/database/migrations.ts +13 -0
- package/src/v1/database/models/pulse.d.ts +24 -0
- package/src/v1/database/pulse.repo.ts +132 -0
- package/src/v1/database/queries/getCategoryTimeOverview.sql +6 -0
- package/src/v1/database/queries/getLongestDayInRangeMinutes.sql +11 -0
- package/src/v1/database/queries/getStatusBarDetails.sql +42 -0
- package/src/v1/dtos/createWakatimePulse.dto.ts +66 -0
- package/src/v1/dtos/getCategoryTimeOverview.dto.ts +9 -0
- package/src/v1/dtos/getWeekOverview.dto.ts +6 -0
- package/src/v1/startup/darwinStartup.service.ts +114 -0
- package/src/v1/startup/linuxStartup.service.ts +101 -0
- package/src/v1/startup/startup.controller.ts +23 -0
- package/src/v1/startup/startup.util.ts +21 -0
- package/src/v1/startup/startupService.factory.ts +28 -0
- package/src/v1/startup/unsupportedStartup.service.ts +21 -0
- package/src/v1/v1.module.ts +28 -0
- package/test/app.e2e-spec.ts +24 -0
- package/test/jest-e2e.json +9 -0
- package/test/jest.globalSetup.js +15 -0
- package/test/jest.globalTeardown.js +8 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +22 -0
- package/utils/__tests__/activites.util.test.ts +18 -0
- package/utils/__tests__/helpers.util.test.ts +155 -0
- package/utils/__tests__/wakatime.util.test.ts +222 -0
- package/utils/activities.util.ts +193 -0
- package/utils/allExceptions.filter.ts +43 -0
- package/utils/codeClimberErrors.ts +32 -0
- package/utils/constants.ts +1 -0
- package/utils/environment.util.ts +1 -0
- package/utils/helpers.util.ts +131 -0
- package/utils/node.util.ts +29 -0
- package/utils/sql.util.ts +13 -0
- package/utils/sqlReader.util.ts +49 -0
- package/utils/wakatime.util.ts +78 -0
|
@@ -0,0 +1,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
|
+
}
|