@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,107 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import Knex, { Knex as KnexTypes } from 'knex'
|
|
3
|
+
|
|
4
|
+
import { Module } from '@nestjs/common'
|
|
5
|
+
import { KnexModule } from 'nestjs-knex'
|
|
6
|
+
import * as path from 'path'
|
|
7
|
+
import {
|
|
8
|
+
forOwn,
|
|
9
|
+
isPlainObject,
|
|
10
|
+
snakeCase,
|
|
11
|
+
camelCase,
|
|
12
|
+
} from '../../../utils/helpers.util'
|
|
13
|
+
import { initDBDir, DB_PATH, BIN_PATH } from '../../../utils/node.util'
|
|
14
|
+
|
|
15
|
+
const deepMapKeys = function (obj: any, fn: any) {
|
|
16
|
+
const x: { [key: string]: any } = {}
|
|
17
|
+
|
|
18
|
+
forOwn(obj, function (v, k) {
|
|
19
|
+
if (Array.isArray(v)) {
|
|
20
|
+
v = v.map(function (x) {
|
|
21
|
+
return isPlainObject(x) ? deepMapKeys(x, fn) : x
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
if (isPlainObject(v)) {
|
|
25
|
+
v = deepMapKeys(v, fn)
|
|
26
|
+
}
|
|
27
|
+
x[fn(v, k)] = v
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return x
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const postProcessResponse = (result: any) => {
|
|
34
|
+
if (!result) {
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
return camelCaseKeys(result)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const wrapIdentifier = (value: any, origImpl: any) => {
|
|
41
|
+
if (snakeCase(value)) {
|
|
42
|
+
return origImpl(snakeCase(value))
|
|
43
|
+
} else {
|
|
44
|
+
return origImpl(value)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const camelCaseKeys = (obj: any) => {
|
|
49
|
+
if (Array.isArray(obj)) {
|
|
50
|
+
return obj.map((row) => {
|
|
51
|
+
return deepMapKeys(row, (v: any, k: any) => camelCase(k))
|
|
52
|
+
})
|
|
53
|
+
} else {
|
|
54
|
+
return deepMapKeys(obj, (v: any, k: any) => camelCase(k))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const IS_TEST = process.env.NODE_ENV === 'test'
|
|
59
|
+
|
|
60
|
+
initDBDir()
|
|
61
|
+
|
|
62
|
+
// https://github.com/knex/knex/issues/1871#issuecomment-452342526
|
|
63
|
+
export const SQL_LITE_TEST_FILE = 'codeclimber.test.sqlite'
|
|
64
|
+
|
|
65
|
+
const knexConfig: KnexTypes.Config = {
|
|
66
|
+
client: 'sqlite3',
|
|
67
|
+
connection: {
|
|
68
|
+
filename: IS_TEST ? SQL_LITE_TEST_FILE : DB_PATH,
|
|
69
|
+
},
|
|
70
|
+
migrations: {
|
|
71
|
+
directory: path.join(BIN_PATH, 'migrations'),
|
|
72
|
+
tableName: 'knex_migrations',
|
|
73
|
+
},
|
|
74
|
+
seeds: {
|
|
75
|
+
directory: path.join(BIN_PATH, 'seeds'),
|
|
76
|
+
},
|
|
77
|
+
useNullAsDefault: true,
|
|
78
|
+
postProcessResponse, // Stuff coming back from the DB
|
|
79
|
+
wrapIdentifier, // Anything going into the DB
|
|
80
|
+
// debug: true,
|
|
81
|
+
// log: {
|
|
82
|
+
// warn(message) {
|
|
83
|
+
// console.log(message)
|
|
84
|
+
// },
|
|
85
|
+
// error(message) {
|
|
86
|
+
// console.log(message)
|
|
87
|
+
// },
|
|
88
|
+
// deprecate(message) {
|
|
89
|
+
// console.log(message)
|
|
90
|
+
// },
|
|
91
|
+
// debug(message) {
|
|
92
|
+
// console.log(message)
|
|
93
|
+
// },
|
|
94
|
+
// },
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const knex = Knex(knexConfig)
|
|
98
|
+
|
|
99
|
+
const knexModule = KnexModule.forRoot({
|
|
100
|
+
config: knexConfig,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
@Module({
|
|
104
|
+
imports: [knexModule],
|
|
105
|
+
exports: [knexModule],
|
|
106
|
+
})
|
|
107
|
+
export class DbModule {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common'
|
|
2
|
+
import { knex } from './knex'
|
|
3
|
+
|
|
4
|
+
export const startMigrations = async () => {
|
|
5
|
+
Logger.log('Running Migrations')
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Locations of migrations is relative to project root
|
|
9
|
+
await knex.migrate.latest()
|
|
10
|
+
} finally {
|
|
11
|
+
Logger.log('Migrations Complete')
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare namespace CodeClimbers {
|
|
2
|
+
export interface Pulse {
|
|
3
|
+
id?: number
|
|
4
|
+
userId: string // Used by wakatime to identify the user (email address). We store as "local"
|
|
5
|
+
entity: string // The activity that was done. The file worked on or the url of the site visited
|
|
6
|
+
type: string // What type of entity it is. For example: 'file' or 'domain'
|
|
7
|
+
category?: string // What category the activity falls under. For example: 'coding', 'browsing', 'designing'
|
|
8
|
+
project?: string // The project the activity was done under. Gathered from the directory name the work was done from. Only used for 'file' type activities
|
|
9
|
+
branch?: string // The branch the activity was done under. Gathered from the git branch the work was done from. Only used for 'file' type activities
|
|
10
|
+
language?: string // The language the activity was done in. Gathered from the file extension. Only used for 'file' type activities
|
|
11
|
+
isWrite: boolean // Whether the activity was a write operation. If false, it was a read operation
|
|
12
|
+
editor: string // if the activity was done in an editor, the name of the editor. Only used for 'file' type activities
|
|
13
|
+
operatingSystem: string // The operating system the activity was done on. Gathered from the user agent
|
|
14
|
+
application?: string // The application the activity was done in.
|
|
15
|
+
machine: string // The machine the activity was done on. Gathered from the user agent
|
|
16
|
+
userAgent: string // The user agent of the activity. Used to determine the operating system and application
|
|
17
|
+
time: string // The time the activity was done. In ISO format
|
|
18
|
+
hash: string // A hash of the activity. Used to determine if the activity is a duplicate. Browser extension sends a lot of duplicate activities
|
|
19
|
+
origin?: string // unused
|
|
20
|
+
originId?: string // unused
|
|
21
|
+
createdAt: string // The time the activity was created in the database. In ISO format
|
|
22
|
+
description?: string // A description of the activity generated by the cli based on the other fields
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
2
|
+
import { InjectKnex, Knex } from 'nestjs-knex'
|
|
3
|
+
import sqlReaderUtil from '../../../utils/sqlReader.util'
|
|
4
|
+
import * as dayjs from 'dayjs'
|
|
5
|
+
|
|
6
|
+
interface MinutesQuery {
|
|
7
|
+
minutes: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class PulseRepo {
|
|
12
|
+
constructor(@InjectKnex() private readonly knex: Knex) {}
|
|
13
|
+
|
|
14
|
+
tableName = 'activities_pulse'
|
|
15
|
+
|
|
16
|
+
async getStatusBarDetails(): Promise<CodeClimbers.WakatimePulseStatusDao[]> {
|
|
17
|
+
const startOfDay = dayjs().startOf('day').toISOString()
|
|
18
|
+
const endOfDay = dayjs().endOf('day').toISOString()
|
|
19
|
+
const getTimeQuery = await sqlReaderUtil.getFileContentAsString(
|
|
20
|
+
'getStatusBarDetails.sql',
|
|
21
|
+
)
|
|
22
|
+
return this.knex.raw(getTimeQuery, { startOfDay, endOfDay })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getLatestPulses(): Promise<CodeClimbers.Pulse[] | undefined> {
|
|
26
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName)
|
|
27
|
+
.orderBy('created_at', 'desc')
|
|
28
|
+
.limit(10)
|
|
29
|
+
return res
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getAllPulses(): Promise<CodeClimbers.Pulse[] | undefined> {
|
|
33
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName).orderBy(
|
|
34
|
+
'created_at',
|
|
35
|
+
'desc',
|
|
36
|
+
)
|
|
37
|
+
return res
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getMinutesInRangeQuery(startDate: Date, endDate: Date) {
|
|
41
|
+
return this.knex<MinutesQuery[]>(this.tableName)
|
|
42
|
+
.select(this.knex.raw('count(*) * 2 as minutes'))
|
|
43
|
+
.from(this.tableName)
|
|
44
|
+
.whereBetween('time', [startDate.toISOString(), endDate.toISOString()])
|
|
45
|
+
.groupBy(this.knex.raw("strftime('%s', time) / 120"))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getLongestDayInRangeMinutes(
|
|
49
|
+
startDate: Date,
|
|
50
|
+
endDate: Date,
|
|
51
|
+
): Promise<number> {
|
|
52
|
+
const getLongestDayMinutesQuery =
|
|
53
|
+
await sqlReaderUtil.getFileContentAsString(
|
|
54
|
+
'getLongestDayInRangeMinutes.sql',
|
|
55
|
+
)
|
|
56
|
+
const [result] = await this.knex.raw<MinutesQuery[]>(
|
|
57
|
+
getLongestDayMinutesQuery,
|
|
58
|
+
{
|
|
59
|
+
startDate: startDate.toISOString(),
|
|
60
|
+
endDate: endDate.toISOString(),
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return result.minutes
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getRangeMinutes(startDate: Date, endDate: Date): Promise<number> {
|
|
68
|
+
const result: MinutesQuery = await this.knex<MinutesQuery[]>(this.tableName)
|
|
69
|
+
.with('getMinutes', this.getMinutesInRangeQuery(startDate, endDate))
|
|
70
|
+
.select(this.knex.raw('count(*) * 2 as minutes'))
|
|
71
|
+
.first()
|
|
72
|
+
.from('getMinutes')
|
|
73
|
+
|
|
74
|
+
return result.minutes
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getCategoryTimeOverview(
|
|
78
|
+
startDate: string,
|
|
79
|
+
endDate: string,
|
|
80
|
+
): Promise<CodeClimbers.TimeOverview[]> {
|
|
81
|
+
const query = this.knex<MinutesQuery[]>(this.tableName)
|
|
82
|
+
.select(this.knex.raw('category, count(*) * 2'))
|
|
83
|
+
.from(this.tableName)
|
|
84
|
+
.whereBetween('time', [startDate, endDate])
|
|
85
|
+
.groupBy(this.knex.raw("strftime('%s', time) / 120"))
|
|
86
|
+
|
|
87
|
+
return await this.knex<CodeClimbers.TimeOverviewDao[]>(this.tableName)
|
|
88
|
+
.with('getMinutes', query)
|
|
89
|
+
.select(this.knex.raw('category, count() * 2 as minutes'))
|
|
90
|
+
.groupBy('category')
|
|
91
|
+
.from('getMinutes')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async createPulse(pulse: CodeClimbers.Pulse) {
|
|
95
|
+
Logger.log('Creating pulse', 'pulse.repo')
|
|
96
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName)
|
|
97
|
+
.insert(pulse)
|
|
98
|
+
.returning('*')
|
|
99
|
+
|
|
100
|
+
return res
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async createPulses(pulses: CodeClimbers.Pulse[]) {
|
|
104
|
+
Logger.log('Creating pulses', 'pulse.repo')
|
|
105
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName)
|
|
106
|
+
.insert(pulses)
|
|
107
|
+
.returning('*')
|
|
108
|
+
Logger.log(`created ${pulses.length} pulses`, 'pulse.repo')
|
|
109
|
+
return res
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
async getUniqueUserAgentsAndLastActive(): Promise<any[]> {
|
|
114
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName)
|
|
115
|
+
.select('user_agent', this.knex.raw('MAX(created_at) as last_active'))
|
|
116
|
+
.groupBy('user_agent')
|
|
117
|
+
.orderBy('last_active', 'desc')
|
|
118
|
+
return res
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getLatestProject(): Promise<string | undefined> {
|
|
122
|
+
const res = await this.knex<CodeClimbers.Pulse>(this.tableName)
|
|
123
|
+
.select('project')
|
|
124
|
+
.whereNotNull('project')
|
|
125
|
+
.whereNotIn('project', ['', '<<LAST_PROJECT>>'])
|
|
126
|
+
.andWhere('time', '<', "datetime('now', '-15 minutes')")
|
|
127
|
+
.orderBy('time', 'desc')
|
|
128
|
+
.first()
|
|
129
|
+
|
|
130
|
+
return await res?.project
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
WITH get_total_minutes (time, total_minutes) AS (
|
|
2
|
+
SELECT time, count() as total_minutes
|
|
3
|
+
FROM activities_pulse
|
|
4
|
+
WHERE date(activities_pulse.time) BETWEEN :startDate AND :endDate
|
|
5
|
+
GROUP BY strftime('%s', time) / 60),
|
|
6
|
+
get_day_minutes (time, day_minutes) AS (
|
|
7
|
+
SELECT time, count() as day_minutes
|
|
8
|
+
FROM get_total_minutes
|
|
9
|
+
GROUP BY strftime('%Y-%m-%d', time))
|
|
10
|
+
SELECT max(day_minutes) as minutes
|
|
11
|
+
FROM get_day_minutes;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
WITH heartbeats_with_diff AS (
|
|
2
|
+
SELECT
|
|
3
|
+
project,
|
|
4
|
+
language,
|
|
5
|
+
editor,
|
|
6
|
+
operating_system,
|
|
7
|
+
machine,
|
|
8
|
+
branch,
|
|
9
|
+
time,
|
|
10
|
+
MIN(
|
|
11
|
+
(JULIANDAY(time) - JULIANDAY(LAG(time) OVER w)) * 86400,
|
|
12
|
+
120
|
|
13
|
+
) AS diff
|
|
14
|
+
FROM
|
|
15
|
+
activities_pulse
|
|
16
|
+
WHERE
|
|
17
|
+
time >= :startOfDay
|
|
18
|
+
AND time < :endOfDay
|
|
19
|
+
WINDOW
|
|
20
|
+
w AS (ORDER BY time)
|
|
21
|
+
)
|
|
22
|
+
SELECT
|
|
23
|
+
project,
|
|
24
|
+
language,
|
|
25
|
+
editor,
|
|
26
|
+
operating_system,
|
|
27
|
+
machine,
|
|
28
|
+
branch,
|
|
29
|
+
ROUND(SUM(MAX(1, diff))) AS seconds,
|
|
30
|
+
MIN(time) AS min_heartbeat_time,
|
|
31
|
+
MAX(time) AS max_heartbeat_time
|
|
32
|
+
FROM
|
|
33
|
+
heartbeats_with_diff
|
|
34
|
+
WHERE
|
|
35
|
+
diff IS NOT NULL
|
|
36
|
+
GROUP BY
|
|
37
|
+
project,
|
|
38
|
+
language,
|
|
39
|
+
editor,
|
|
40
|
+
operating_system,
|
|
41
|
+
machine,
|
|
42
|
+
branch
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator'
|
|
2
|
+
|
|
3
|
+
export class CreateWakatimePulseDto {
|
|
4
|
+
@IsOptional()
|
|
5
|
+
@IsString()
|
|
6
|
+
userId?: string
|
|
7
|
+
|
|
8
|
+
@IsString()
|
|
9
|
+
entity: string
|
|
10
|
+
|
|
11
|
+
@IsString()
|
|
12
|
+
type: string
|
|
13
|
+
|
|
14
|
+
@IsOptional()
|
|
15
|
+
@IsString()
|
|
16
|
+
category?: string
|
|
17
|
+
|
|
18
|
+
@IsString()
|
|
19
|
+
project: string
|
|
20
|
+
|
|
21
|
+
@IsString()
|
|
22
|
+
branch: string
|
|
23
|
+
|
|
24
|
+
@IsOptional()
|
|
25
|
+
@IsString()
|
|
26
|
+
language?: string
|
|
27
|
+
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsBoolean()
|
|
30
|
+
is_write?: boolean
|
|
31
|
+
|
|
32
|
+
@IsOptional()
|
|
33
|
+
@IsString()
|
|
34
|
+
editor?: string
|
|
35
|
+
|
|
36
|
+
@IsOptional()
|
|
37
|
+
@IsString()
|
|
38
|
+
operating_system?: string
|
|
39
|
+
|
|
40
|
+
@IsOptional()
|
|
41
|
+
@IsString()
|
|
42
|
+
machine?: string
|
|
43
|
+
|
|
44
|
+
@IsOptional()
|
|
45
|
+
@IsString()
|
|
46
|
+
user_agent?: string
|
|
47
|
+
|
|
48
|
+
@IsNumber()
|
|
49
|
+
time: number | string
|
|
50
|
+
|
|
51
|
+
@IsOptional()
|
|
52
|
+
@IsString()
|
|
53
|
+
hash?: string
|
|
54
|
+
|
|
55
|
+
@IsOptional()
|
|
56
|
+
@IsString()
|
|
57
|
+
origin?: string
|
|
58
|
+
|
|
59
|
+
@IsOptional()
|
|
60
|
+
@IsString()
|
|
61
|
+
origin_id?: string
|
|
62
|
+
|
|
63
|
+
@IsOptional()
|
|
64
|
+
@IsString()
|
|
65
|
+
description?: string
|
|
66
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
BIN_PATH,
|
|
5
|
+
CODE_CLIMBER_META_DIR,
|
|
6
|
+
NODE_PATH,
|
|
7
|
+
} from '../../../utils/node.util'
|
|
8
|
+
import startupUtil from './startup.util'
|
|
9
|
+
const { Service } = startupUtil.getServiceLib()
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class DarwinStartupService implements CodeClimbers.StartupService {
|
|
13
|
+
private service: typeof Service
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.service = new Service({
|
|
17
|
+
name: 'CodeClimbers',
|
|
18
|
+
description: 'CodeClimbers service',
|
|
19
|
+
script: `${path.join(BIN_PATH, 'startup.js')}`,
|
|
20
|
+
logpath: CODE_CLIMBER_META_DIR,
|
|
21
|
+
env: [
|
|
22
|
+
{
|
|
23
|
+
name: 'NODE_ENV',
|
|
24
|
+
value: process.env.NODE_ENV || 'production',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'CODE_CLIMBER_BIN_PATH',
|
|
28
|
+
value: BIN_PATH,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'NODE_PATH',
|
|
32
|
+
value: NODE_PATH(),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'DEBUG',
|
|
36
|
+
value: process.env.DEBUG,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
logOnAsUser: true,
|
|
40
|
+
runAsAgent: true,
|
|
41
|
+
wait: 5,
|
|
42
|
+
grow: 0,
|
|
43
|
+
maxRestarts: 10,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (process.env.NODE_ENV === 'development' || process.env.DEBUG === '*') {
|
|
47
|
+
this.service.on('install', () => {
|
|
48
|
+
console.log(`${this.service.name} installed`)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
this.service.on('alreadyinstalled', () => {
|
|
52
|
+
console.log(`${this.service.name} already installed`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.service.on('uninstall', () => {
|
|
56
|
+
console.log(`${this.service.name} uninstalled`)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
this.service.on('start', () => {
|
|
60
|
+
console.log(`${this.service.name} started`)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
this.service.on('stop', () => {
|
|
64
|
+
console.log(`${this.service.name} stopped`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
this.service.on('error', (error) => {
|
|
68
|
+
console.error(`${this.service.name} error:`, error)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async enableStartup(): Promise<void> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
this.service.install()
|
|
76
|
+
this.service.on('install', () => {
|
|
77
|
+
resolve()
|
|
78
|
+
})
|
|
79
|
+
this.service.on('alreadyinstalled', () => {
|
|
80
|
+
resolve()
|
|
81
|
+
})
|
|
82
|
+
this.service.on('error', (error) => reject(error))
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async disableStartup(): Promise<void> {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
this.service.uninstall()
|
|
89
|
+
this.service.on('uninstall', () => resolve())
|
|
90
|
+
this.service.on('error', (error) => reject(error))
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async launchAndEnableStartup(): Promise<void> {
|
|
95
|
+
await this.enableStartup()
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
this.service.start()
|
|
98
|
+
this.service.on('start', () => resolve())
|
|
99
|
+
this.service.on('error', (error) => reject(error))
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async closeAndDisableStartup(): Promise<void> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
this.service.stop()
|
|
106
|
+
this.service.on('stop', () => {
|
|
107
|
+
this.disableStartup()
|
|
108
|
+
.then(() => resolve())
|
|
109
|
+
.catch((error) => reject(error))
|
|
110
|
+
})
|
|
111
|
+
this.service.on('error', (error) => reject(error))
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
2
|
+
// eslint-disable-next-line import/no-unresolved
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { BIN_PATH, CODE_CLIMBER_META_DIR } from '../../../utils/node.util'
|
|
5
|
+
import startupUtil from './startup.util'
|
|
6
|
+
const { Service } = startupUtil.getServiceLib()
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class DarwinStartupService implements CodeClimbers.StartupService {
|
|
10
|
+
private service: typeof Service
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.service = new Service({
|
|
14
|
+
name: 'CodeClimbers',
|
|
15
|
+
description: 'CodeClimbers service',
|
|
16
|
+
script: `${path.join(BIN_PATH, 'startup.js')}`,
|
|
17
|
+
logpath: CODE_CLIMBER_META_DIR,
|
|
18
|
+
env: [
|
|
19
|
+
{
|
|
20
|
+
name: 'NODE_ENV',
|
|
21
|
+
value: 'production',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'CODE_CLIMBER_BIN_PATH',
|
|
25
|
+
value: BIN_PATH,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
logOnAsUser: true,
|
|
29
|
+
runAsAgent: true,
|
|
30
|
+
wait: 5,
|
|
31
|
+
grow: 0,
|
|
32
|
+
maxRestarts: 10,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
this.service.on('install', () => {
|
|
36
|
+
Logger.log(`${this.service.name.get} installed`)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
this.service.on('alreadyinstalled', () => {
|
|
40
|
+
Logger.log(`${this.service.name} already installed`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.service.on('uninstall', () => {
|
|
44
|
+
Logger.log(`${this.service.name} uninstalled`)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
this.service.on('start', () => {
|
|
48
|
+
Logger.log(`${this.service.name} started`)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
this.service.on('stop', () => {
|
|
52
|
+
Logger.log(`${this.service.name} stopped`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.service.on('error', (error) => {
|
|
56
|
+
Logger.error(`${this.service.name} error:`, error)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async enableStartup(): Promise<void> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
this.service.install()
|
|
63
|
+
this.service.on('install', () => {
|
|
64
|
+
resolve()
|
|
65
|
+
})
|
|
66
|
+
this.service.on('alreadyinstalled', () => {
|
|
67
|
+
resolve()
|
|
68
|
+
})
|
|
69
|
+
this.service.on('error', (error) => reject(error))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async disableStartup(): Promise<void> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
this.service.uninstall()
|
|
76
|
+
this.service.on('uninstall', () => resolve())
|
|
77
|
+
this.service.on('error', (error) => reject(error))
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async launchAndEnableStartup(): Promise<void> {
|
|
82
|
+
await this.enableStartup()
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
this.service.start()
|
|
85
|
+
this.service.on('start', () => resolve())
|
|
86
|
+
this.service.on('error', (error) => reject(error))
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async closeAndDisableStartup(): Promise<void> {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
this.service.stop()
|
|
93
|
+
this.service.on('stop', () => {
|
|
94
|
+
this.disableStartup()
|
|
95
|
+
.then(() => resolve())
|
|
96
|
+
.catch((error) => reject(error))
|
|
97
|
+
})
|
|
98
|
+
this.service.on('error', (error) => reject(error))
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Controller, Post } from '@nestjs/common'
|
|
2
|
+
import { StartupServiceFactory } from './startupService.factory'
|
|
3
|
+
|
|
4
|
+
@Controller('/startup')
|
|
5
|
+
export class StartupController {
|
|
6
|
+
private startupService: CodeClimbers.StartupService
|
|
7
|
+
|
|
8
|
+
constructor(private readonly startupServiceFactory: StartupServiceFactory) {
|
|
9
|
+
this.startupService = this.startupServiceFactory.getStartupService()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Post('/enable')
|
|
13
|
+
async enableStartup(): Promise<string> {
|
|
14
|
+
await this.startupService.enableStartup()
|
|
15
|
+
return 'OK'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Post('/disable')
|
|
19
|
+
async disableStartup(): Promise<string> {
|
|
20
|
+
await this.startupService.disableStartup()
|
|
21
|
+
return 'OK'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* these modules are not available on all platforms, so we have to import them conditionally
|
|
3
|
+
* or we'll get build and runtime errors
|
|
4
|
+
*/
|
|
5
|
+
function getServiceLib() {
|
|
6
|
+
const os = process.platform
|
|
7
|
+
switch (os) {
|
|
8
|
+
case 'darwin':
|
|
9
|
+
return require('node-mac')
|
|
10
|
+
case 'linux':
|
|
11
|
+
return require('node-linux')
|
|
12
|
+
case 'win32':
|
|
13
|
+
return require('node-windows')
|
|
14
|
+
default:
|
|
15
|
+
throw new Error('Unsupported platform')
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
getServiceLib,
|
|
21
|
+
}
|