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