@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,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,6 @@
1
+ SELECT category, count() AS minutes
2
+ FROM (SELECT category, count()
3
+ FROM activities_pulse
4
+ WHERE date(activities_pulse.time) BETWEEN :startDate AND :endDate
5
+ GROUP BY strftime('%s', time) / 60)
6
+ GROUP BY category
@@ -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,9 @@
1
+ import { IsDateString } from 'class-validator'
2
+
3
+ export class GetCategoryTimeOverviewDto {
4
+ @IsDateString()
5
+ startDate: string
6
+
7
+ @IsDateString()
8
+ endDate: string
9
+ }
@@ -0,0 +1,6 @@
1
+ import { IsDateString } from 'class-validator'
2
+
3
+ export class GetWeekOverviewDto {
4
+ @IsDateString()
5
+ date: string
6
+ }
@@ -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
+ }