@oneuptime/common 11.0.9 → 11.0.12

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 (125) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +134 -0
  2. package/Models/DatabaseModels/CloudResource.ts +134 -0
  3. package/Models/DatabaseModels/DockerHost.ts +134 -0
  4. package/Models/DatabaseModels/DockerSwarmCluster.ts +134 -0
  5. package/Models/DatabaseModels/Host.ts +134 -0
  6. package/Models/DatabaseModels/KubernetesCluster.ts +134 -0
  7. package/Models/DatabaseModels/PodmanHost.ts +134 -0
  8. package/Models/DatabaseModels/ProxmoxCluster.ts +134 -0
  9. package/Models/DatabaseModels/RumApplication.ts +134 -0
  10. package/Models/DatabaseModels/ServerlessFunction.ts +134 -0
  11. package/Models/DatabaseModels/Service.ts +134 -0
  12. package/README.md +1 -1
  13. package/Scripts/generate-service-worker.js +46 -36
  14. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +4 -2
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.ts +331 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Readme.md +3 -3
  18. package/Server/Services/AIAgentService.ts +9 -4
  19. package/Server/Services/AnalyticsDatabaseService.ts +89 -56
  20. package/Server/Services/CephClusterService.ts +6 -4
  21. package/Server/Services/CloudResourceService.ts +6 -4
  22. package/Server/Services/DatabaseService.ts +126 -0
  23. package/Server/Services/DockerHostService.ts +6 -4
  24. package/Server/Services/DockerSwarmClusterService.ts +6 -4
  25. package/Server/Services/HostService.ts +6 -4
  26. package/Server/Services/KubernetesClusterService.ts +6 -4
  27. package/Server/Services/PodmanHostService.ts +6 -4
  28. package/Server/Services/ProbeService.ts +10 -4
  29. package/Server/Services/ProxmoxClusterService.ts +6 -4
  30. package/Server/Services/RumApplicationService.ts +6 -4
  31. package/Server/Services/ServerlessFunctionService.ts +6 -4
  32. package/Server/Services/ServiceService.ts +17 -4
  33. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  34. package/Server/Utils/Monitor/MonitorResource.ts +24 -10
  35. package/Server/Utils/Telemetry/EntityRegistry.ts +10 -5
  36. package/Server/Utils/Workspace/Slack/app-manifest.json +86 -92
  37. package/Tests/Server/Services/UpdateColumnsByIdWithoutHooks.test.ts +154 -0
  38. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +12 -9
  39. package/Tests/Types/DiskSize.test.ts +49 -0
  40. package/Types/Dashboard/DashboardComponents/DashboardTableComponent.ts +11 -0
  41. package/Types/Icon/IconProp.ts +1 -0
  42. package/Types/Monitor/MonitorType.ts +15 -31
  43. package/Types/Monitor/schema.json +32 -35
  44. package/UI/Components/BulkUpdate/BulkArchiveActions.tsx +150 -0
  45. package/UI/Components/Icon/Icon.tsx +42 -126
  46. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -5
  47. package/UI/esbuild-config.js +136 -110
  48. package/Utils/ClientAPI/README.md +1 -1
  49. package/build/dist/Models/DatabaseModels/CephCluster.js +136 -0
  50. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/CloudResource.js +136 -0
  52. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  53. package/build/dist/Models/DatabaseModels/DockerHost.js +136 -0
  54. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +136 -0
  56. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/Host.js +136 -0
  58. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  59. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +136 -0
  60. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/PodmanHost.js +136 -0
  62. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +136 -0
  64. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/RumApplication.js +136 -0
  66. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/ServerlessFunction.js +136 -0
  68. package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
  69. package/build/dist/Models/DatabaseModels/Service.js +136 -0
  70. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  71. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +4 -2
  72. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js +120 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  77. package/build/dist/Server/Services/AIAgentService.js +9 -4
  78. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  79. package/build/dist/Server/Services/AnalyticsDatabaseService.js +52 -17
  80. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  81. package/build/dist/Server/Services/CephClusterService.js +6 -4
  82. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  83. package/build/dist/Server/Services/CloudResourceService.js +6 -4
  84. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  85. package/build/dist/Server/Services/DatabaseService.js +101 -0
  86. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  87. package/build/dist/Server/Services/DockerHostService.js +6 -4
  88. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  89. package/build/dist/Server/Services/DockerSwarmClusterService.js +6 -4
  90. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  91. package/build/dist/Server/Services/HostService.js +6 -4
  92. package/build/dist/Server/Services/HostService.js.map +1 -1
  93. package/build/dist/Server/Services/KubernetesClusterService.js +6 -4
  94. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  95. package/build/dist/Server/Services/PodmanHostService.js +6 -4
  96. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  97. package/build/dist/Server/Services/ProbeService.js +10 -4
  98. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  99. package/build/dist/Server/Services/ProxmoxClusterService.js +6 -4
  100. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  101. package/build/dist/Server/Services/RumApplicationService.js +6 -4
  102. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  103. package/build/dist/Server/Services/ServerlessFunctionService.js +6 -4
  104. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  105. package/build/dist/Server/Services/ServiceService.js +17 -4
  106. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  107. package/build/dist/Server/Utils/Monitor/MonitorResource.js +24 -10
  108. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  109. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +10 -5
  110. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  111. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +3 -9
  112. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTableComponent.js.map +1 -1
  113. package/build/dist/Types/Icon/IconProp.js +1 -0
  114. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  115. package/build/dist/Types/Monitor/MonitorType.js +15 -31
  116. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  117. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js +96 -0
  118. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js.map +1 -0
  119. package/build/dist/UI/Components/Icon/Icon.js +17 -41
  120. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  121. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -3
  122. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  123. package/jest.config.json +47 -66
  124. package/package.json +1 -1
  125. package/tsconfig.json +109 -113
@@ -193,12 +193,14 @@ export class Service extends DatabaseService<Model> {
193
193
  data.agentVersion = extra.agentVersion;
194
194
  }
195
195
 
196
- await this.updateOneById({
196
+ /*
197
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
198
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
199
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
200
+ */
201
+ await this.updateColumnsByIdWithoutHooks({
197
202
  id: clusterId,
198
203
  data: data,
199
- props: {
200
- isRoot: true,
201
- },
202
204
  });
203
205
  }
204
206
 
@@ -236,12 +236,14 @@ export class Service extends DatabaseService<Model> {
236
236
  data.agentVersion = extra.agentVersion;
237
237
  }
238
238
 
239
- await this.updateOneById({
239
+ /*
240
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
241
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
242
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
243
+ */
244
+ await this.updateColumnsByIdWithoutHooks({
240
245
  id: hostId,
241
246
  data: data,
242
- props: {
243
- isRoot: true,
244
- },
245
247
  });
246
248
  }
247
249
 
@@ -120,14 +120,20 @@ export class Service extends DatabaseService<Model> {
120
120
 
121
121
  const now: Date = OneUptimeDate.getCurrentDate();
122
122
 
123
- await this.updateOneById({
123
+ /*
124
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
125
+ * `version` bump. This fires from the probe-auth middleware on every
126
+ * authenticated probe request, so it must not head the hot-row Postgres
127
+ * lock convoy the full updateOneById pipeline causes. A lastAlive-only
128
+ * write never sets connectionStatus, so the connection-status-change
129
+ * hooks (onBeforeUpdate/onUpdateSuccess) are inert here. See
130
+ * ServiceService.updateLastSeen.
131
+ */
132
+ await this.updateColumnsByIdWithoutHooks({
124
133
  id: probeId,
125
134
  data: {
126
135
  lastAlive: now,
127
136
  },
128
- props: {
129
- isRoot: true,
130
- },
131
137
  });
132
138
  }
133
139
 
@@ -250,12 +250,14 @@ export class Service extends DatabaseService<Model> {
250
250
  data.guestsWithoutBackupCount = extra.guestsWithoutBackupCount;
251
251
  }
252
252
 
253
- await this.updateOneById({
253
+ /*
254
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
255
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
256
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
257
+ */
258
+ await this.updateColumnsByIdWithoutHooks({
254
259
  id: clusterId,
255
260
  data: data,
256
- props: {
257
- isRoot: true,
258
- },
259
261
  });
260
262
  }
261
263
 
@@ -185,12 +185,14 @@ export class Service extends DatabaseService<Model> {
185
185
  data.sdkLanguage = extra.sdkLanguage;
186
186
  }
187
187
 
188
- await this.updateOneById({
188
+ /*
189
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
190
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
191
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
192
+ */
193
+ await this.updateColumnsByIdWithoutHooks({
189
194
  id: rumApplicationId,
190
195
  data: data,
191
- props: {
192
- isRoot: true,
193
- },
194
196
  });
195
197
  }
196
198
 
@@ -229,12 +229,14 @@ export class Service extends DatabaseService<Model> {
229
229
  data.runtimeVersion = extra.runtimeVersion;
230
230
  }
231
231
 
232
- await this.updateOneById({
232
+ /*
233
+ * Heartbeat write: a single-statement UPDATE with no hooks and no
234
+ * `version` bump, avoiding the hot-row Postgres lock convoy that the
235
+ * full updateOneById pipeline causes. See ServiceService.updateLastSeen.
236
+ */
237
+ await this.updateColumnsByIdWithoutHooks({
233
238
  id: serverlessFunctionId,
234
239
  data: data,
235
- props: {
236
- isRoot: true,
237
- },
238
240
  });
239
241
  }
240
242
 
@@ -198,12 +198,25 @@ export class Service extends DatabaseService<Model> {
198
198
  data.cloudAccountId = extra.cloudAccountId;
199
199
  }
200
200
 
201
- await this.updateOneById({
201
+ /*
202
+ * Heartbeat write. This is the single hottest UPDATE in the system —
203
+ * every telemetry batch for a service re-resolves the same serviceId —
204
+ * so it must NOT go through the full updateOneById pipeline (permission
205
+ * SELECT -> _findBy SELECT -> save() with `version = version + 1` ->
206
+ * workflow/realtime/audit hooks). That pipeline holds the Service row's
207
+ * write lock across several round trips and, when an event loop stalls
208
+ * mid-`save()` transaction, leaves it open as the head of a lock convoy
209
+ * that starves the Postgres connection pool. lastSeenAt is an internal
210
+ * liveness timestamp with no onBeforeUpdate/onUpdateSuccess hooks on this
211
+ * model (connected / disconnected status is derived from it at read
212
+ * time), so a bare single-statement UPDATE is both correct and far
213
+ * cheaper. This also intentionally drops the per-update workflow trigger
214
+ * Service's @EnableWorkflow({ update: true }) fired on every heartbeat —
215
+ * a liveness ping should not trigger user workflows.
216
+ */
217
+ await this.updateColumnsByIdWithoutHooks({
202
218
  id: serviceId,
203
219
  data: data,
204
- props: {
205
- isRoot: true,
206
- },
207
220
  });
208
221
  }
209
222
 
@@ -87,7 +87,7 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
87
87
  .append(setStatement)
88
88
  .append(
89
89
  SQL`
90
- WHERE TRUE `
90
+ WHERE TRUE `,
91
91
  )
92
92
  .append(whereStatement);
93
93
  /* eslint-enable prettier/prettier */
@@ -316,17 +316,27 @@ export default class MonitorResourceUtil {
316
316
  * the monitor to flap between Online and Offline every minute.
317
317
  */
318
318
  if (!serverMonitorResponse.onlyCheckRequestReceivedAt) {
319
- await MonitorService.updateOneById({
319
+ /*
320
+ * Heartbeat write: single-statement UPDATE, no hooks and no
321
+ * `version` bump. This DELIBERATELY drops the per-update workflow
322
+ * trigger and audit-log entry that Monitor's @EnableWorkflow /
323
+ * @EnableAuditLog fire on every changed update — the old
324
+ * `ignoreHooks: true` did NOT suppress those (they are gated on
325
+ * the model flag, not on ignoreHooks), so a heartbeat used to
326
+ * spam an on-update workflow + an audit row every ingest. A
327
+ * liveness ping should do neither. onUpdateSuccess is inert here
328
+ * regardless (gated on status/interval/steps/name/etc., none of
329
+ * which are written). Also skips the pre-SELECT that would reload
330
+ * the large serverMonitorResponse jsonb row. See
331
+ * ServiceService.updateLastSeen.
332
+ */
333
+ await MonitorService.updateColumnsByIdWithoutHooks({
320
334
  id: monitor.id!,
321
335
  data: {
322
336
  serverMonitorRequestReceivedAt:
323
337
  serverMonitorResponse.requestReceivedAt!,
324
338
  serverMonitorResponse,
325
339
  },
326
- props: {
327
- isRoot: true,
328
- ignoreHooks: true,
329
- },
330
340
  });
331
341
 
332
342
  logger.debug(
@@ -344,7 +354,15 @@ export default class MonitorResourceUtil {
344
354
  `${dataToProcess.monitorId.toString()} - Incoming request received at ${incomingMonitorRequest.incomingRequestReceivedAt}`,
345
355
  );
346
356
 
347
- await MonitorService.updateOneById({
357
+ /*
358
+ * Heartbeat write: single-statement UPDATE, no hooks and no
359
+ * `version` bump. As with the server-monitor branch above, this
360
+ * deliberately drops the per-update workflow trigger + audit-log
361
+ * entry Monitor would otherwise fire on every heartbeat, and skips
362
+ * the pre-SELECT of the large incomingMonitorRequest jsonb row. See
363
+ * ServiceService.updateLastSeen.
364
+ */
365
+ await MonitorService.updateColumnsByIdWithoutHooks({
348
366
  id: monitor.id!,
349
367
  data: {
350
368
  incomingRequestMonitorHeartbeatCheckedAt:
@@ -353,10 +371,6 @@ export default class MonitorResourceUtil {
353
371
  JSON.stringify(incomingMonitorRequest),
354
372
  ) as IncomingMonitorRequest,
355
373
  } as any,
356
- props: {
357
- isRoot: true,
358
- ignoreHooks: true,
359
- },
360
374
  });
361
375
 
362
376
  logger.debug(
@@ -271,11 +271,17 @@ export async function reconcileByNaturalKey<
271
271
  });
272
272
 
273
273
  if (existing) {
274
- // Throttled bump of lastSeenAt (+ any caller-supplied merge fields).
275
- await data.service.updateOneById({
274
+ /*
275
+ * Throttled bump of lastSeenAt (+ any caller-supplied merge fields).
276
+ * Heartbeat write: single-statement UPDATE, no hooks and no `version`
277
+ * bump (TelemetryEntity/Relationship enable no update workflow/realtime/
278
+ * audit). buildBump returns only plain values — lastSeenAt plus, at most,
279
+ * the descriptiveAttributes / labels JSON columns — which the primitive
280
+ * persists via the driver transformer path. See ServiceService.updateLastSeen.
281
+ */
282
+ await data.service.updateColumnsByIdWithoutHooks({
276
283
  id: existing.id!,
277
284
  data: buildBump(existing),
278
- props: { isRoot: true },
279
285
  });
280
286
  return;
281
287
  }
@@ -306,10 +312,9 @@ export async function reconcileByNaturalKey<
306
312
  logger.debug(
307
313
  `EntityRegistry: create raced for ${data.describe} (concurrent insert); bumping lastSeenAt on the winning row.`,
308
314
  );
309
- await data.service.updateOneById({
315
+ await data.service.updateColumnsByIdWithoutHooks({
310
316
  id: winner.id!,
311
317
  data: buildBump(winner),
312
- props: { isRoot: true },
313
318
  });
314
319
  return;
315
320
  }
@@ -1,97 +1,91 @@
1
1
  {
2
- "display_information": {
3
- "name": "OneUptime",
4
- "description": "The Complete Open-Source Observability Platform",
5
- "background_color": "#000000",
6
- "long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
7
- },
2
+ "display_information": {
3
+ "name": "OneUptime",
4
+ "description": "The Complete Open-Source Observability Platform",
5
+ "background_color": "#000000",
6
+ "long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
7
+ },
8
8
 
9
- "features": {
10
- "app_home": {
11
- "home_tab_enabled": false,
12
- "messages_tab_enabled": true,
13
- "messages_tab_read_only_enabled": false
14
- },
15
- "bot_user": {
16
- "display_name": "OneUptime",
17
- "always_online": true
18
- },
19
- "shortcuts": [
20
- {
21
- "name": "Create Scheduled Event",
22
- "type": "global",
23
- "callback_id": "/maintenance",
24
- "description": "Create a new scheduled event in OneUptime"
25
- },
26
- {
27
- "name": "Create New Incident",
28
- "type": "global",
29
- "callback_id": "/incident",
30
- "description": "Creates a new incident in OneUptime"
31
- }
32
- ],
33
- "slash_commands": [
34
- {
35
- "command": "/incident",
36
- "url": "{{SERVER_URL}}/api/slack/interactive",
37
- "description": "Create a new Incident in OneUptime",
38
- "usage_hint": "[incident title]",
39
- "should_escape": false
40
- },
41
- {
42
- "command": "/maintenance",
43
- "url": "{{SERVER_URL}}/api/slack/interactive",
44
- "description": "Create a new Scheduled Maintenance in OneUptime",
45
- "usage_hint": "[maintenance title]",
46
- "should_escape": false
47
- }
48
- ]
9
+ "features": {
10
+ "app_home": {
11
+ "home_tab_enabled": false,
12
+ "messages_tab_enabled": true,
13
+ "messages_tab_read_only_enabled": false
14
+ },
15
+ "bot_user": {
16
+ "display_name": "OneUptime",
17
+ "always_online": true
49
18
  },
50
- "oauth_config": {
51
- "redirect_urls": [
52
- "{{SERVER_URL}}/api/slack/auth"
53
- ],
54
- "scopes": {
55
- "user": [
56
- "users:read"
57
- ],
58
- "bot": [
59
- "commands",
60
- "channels:history",
61
- "channels:join",
62
- "channels:manage",
63
- "channels:read",
64
- "channels:write.invites",
65
- "channels:write.topic",
66
- "chat:write",
67
- "users:read",
68
- "groups:read",
69
- "groups:write",
70
- "groups:history",
71
- "im:read",
72
- "im:write",
73
- "im:history",
74
- "mpim:read",
75
- "mpim:write",
76
- "mpim:history",
77
- "reactions:read"
78
- ]
79
- }
19
+ "shortcuts": [
20
+ {
21
+ "name": "Create Scheduled Event",
22
+ "type": "global",
23
+ "callback_id": "/maintenance",
24
+ "description": "Create a new scheduled event in OneUptime"
25
+ },
26
+ {
27
+ "name": "Create New Incident",
28
+ "type": "global",
29
+ "callback_id": "/incident",
30
+ "description": "Creates a new incident in OneUptime"
31
+ }
32
+ ],
33
+ "slash_commands": [
34
+ {
35
+ "command": "/incident",
36
+ "url": "{{SERVER_URL}}/api/slack/interactive",
37
+ "description": "Create a new Incident in OneUptime",
38
+ "usage_hint": "[incident title]",
39
+ "should_escape": false
40
+ },
41
+ {
42
+ "command": "/maintenance",
43
+ "url": "{{SERVER_URL}}/api/slack/interactive",
44
+ "description": "Create a new Scheduled Maintenance in OneUptime",
45
+ "usage_hint": "[maintenance title]",
46
+ "should_escape": false
47
+ }
48
+ ]
49
+ },
50
+ "oauth_config": {
51
+ "redirect_urls": ["{{SERVER_URL}}/api/slack/auth"],
52
+ "scopes": {
53
+ "user": ["users:read"],
54
+ "bot": [
55
+ "commands",
56
+ "channels:history",
57
+ "channels:join",
58
+ "channels:manage",
59
+ "channels:read",
60
+ "channels:write.invites",
61
+ "channels:write.topic",
62
+ "chat:write",
63
+ "users:read",
64
+ "groups:read",
65
+ "groups:write",
66
+ "groups:history",
67
+ "im:read",
68
+ "im:write",
69
+ "im:history",
70
+ "mpim:read",
71
+ "mpim:write",
72
+ "mpim:history",
73
+ "reactions:read"
74
+ ]
75
+ }
76
+ },
77
+ "settings": {
78
+ "interactivity": {
79
+ "is_enabled": true,
80
+ "request_url": "{{SERVER_URL}}/api/slack/interactive",
81
+ "message_menu_options_url": "{{SERVER_URL}}/api/slack/options-load"
80
82
  },
81
- "settings": {
82
- "interactivity": {
83
- "is_enabled": true,
84
- "request_url": "{{SERVER_URL}}/api/slack/interactive",
85
- "message_menu_options_url": "{{SERVER_URL}}/api/slack/options-load"
86
- },
87
- "org_deploy_enabled": true,
88
- "socket_mode_enabled": false,
89
- "token_rotation_enabled": false,
90
- "event_subscriptions": {
91
- "request_url": "{{SERVER_URL}}/api/slack/events",
92
- "bot_events": [
93
- "reaction_added"
94
- ]
95
- }
83
+ "org_deploy_enabled": true,
84
+ "socket_mode_enabled": false,
85
+ "token_rotation_enabled": false,
86
+ "event_subscriptions": {
87
+ "request_url": "{{SERVER_URL}}/api/slack/events",
88
+ "bot_events": ["reaction_added"]
96
89
  }
97
- }
90
+ }
91
+ }
@@ -0,0 +1,154 @@
1
+ import ServiceService from "../../../Server/Services/ServiceService";
2
+ import ObjectID from "../../../Types/ObjectID";
3
+ import { describe, expect, test, afterEach } from "@jest/globals";
4
+
5
+ /*
6
+ * DatabaseService.updateColumnsByIdWithoutHooks is the fast heartbeat write
7
+ * path behind ServiceService.updateLastSeen. It exists to keep the single
8
+ * hottest UPDATE in the system (lastSeenAt, bumped on every telemetry batch)
9
+ * out of the full updateOneById pipeline, which holds the row's write lock
10
+ * across several round trips and was observed heading a Postgres lock convoy
11
+ * that starved the connection pool.
12
+ *
13
+ * These tests mock the TypeORM manager (no Postgres) and lock in the two
14
+ * invariants that make the fix work:
15
+ * - it emits exactly ONE parameterized UPDATE, and
16
+ * - it NEVER rewrites the optimistic-lock `version` column (a regression
17
+ * here would re-introduce the contention the fix removed).
18
+ */
19
+
20
+ type QueryCall = [string, Array<unknown>];
21
+
22
+ const SERVICE_COLUMNS: Record<string, string> = {
23
+ lastSeenAt: "lastSeenAt",
24
+ serviceVersion: "serviceVersion",
25
+ deploymentEnvironment: "deploymentEnvironment",
26
+ };
27
+
28
+ /*
29
+ * Stand-in for the TypeORM driver's persist path. The real method applies
30
+ * column transformers (ObjectID -> uuid string, JSON.stringify, etc.); we
31
+ * model that here by wrapping a string value in `prepared(...)` so a test
32
+ * can assert the column value was routed through it rather than bound raw.
33
+ */
34
+ function fakePreparePersistentValue(value: unknown): unknown {
35
+ return typeof value === "string" ? `prepared(${value})` : value;
36
+ }
37
+
38
+ function mockRepository(result: unknown = []): jest.Mock {
39
+ const query: jest.Mock = jest.fn().mockResolvedValue(result);
40
+ jest.spyOn(ServiceService, "getRepository").mockReturnValue({
41
+ manager: {
42
+ query,
43
+ connection: {
44
+ driver: { preparePersistentValue: fakePreparePersistentValue },
45
+ },
46
+ },
47
+ metadata: {
48
+ tableName: "Service",
49
+ primaryColumns: [{ databaseName: "_id" }],
50
+ updateDateColumn: { databaseName: "updatedAt" },
51
+ findColumnWithPropertyName: (propertyName: string) => {
52
+ return SERVICE_COLUMNS[propertyName]
53
+ ? { databaseName: SERVICE_COLUMNS[propertyName] }
54
+ : undefined;
55
+ },
56
+ },
57
+ } as any);
58
+ return query;
59
+ }
60
+
61
+ describe("DatabaseService.updateColumnsByIdWithoutHooks", () => {
62
+ afterEach(() => {
63
+ jest.restoreAllMocks();
64
+ });
65
+
66
+ test("emits a single UPDATE and never bumps the version column", async () => {
67
+ const query: jest.Mock = mockRepository();
68
+ const id: ObjectID = ObjectID.generate();
69
+ const lastSeenAt: Date = new Date();
70
+
71
+ await ServiceService.updateColumnsByIdWithoutHooks({
72
+ id,
73
+ data: { lastSeenAt } as never,
74
+ });
75
+
76
+ expect(query).toHaveBeenCalledTimes(1);
77
+ const [sql, params] = query.mock.calls[0] as QueryCall;
78
+
79
+ // The whole point of the fix: no optimistic-lock churn on the hot row.
80
+ expect(sql).not.toContain("version");
81
+ // updatedAt is still refreshed, exactly as the normal update path does.
82
+ expect(sql).toContain(`"updatedAt" = CURRENT_TIMESTAMP`);
83
+ expect(sql).toBe(
84
+ `UPDATE "Service" SET "lastSeenAt" = $1, "updatedAt" = CURRENT_TIMESTAMP WHERE "_id" = $2`,
85
+ );
86
+ expect(params).toEqual([lastSeenAt, id.toString()]);
87
+ });
88
+
89
+ test("maps multiple entity properties to columns in stable param order", async () => {
90
+ const query: jest.Mock = mockRepository();
91
+ const id: ObjectID = ObjectID.generate();
92
+ const lastSeenAt: Date = new Date();
93
+
94
+ await ServiceService.updateColumnsByIdWithoutHooks({
95
+ id,
96
+ data: {
97
+ lastSeenAt,
98
+ serviceVersion: "1.2.3",
99
+ deploymentEnvironment: "production",
100
+ } as never,
101
+ });
102
+
103
+ const [sql, params] = query.mock.calls[0] as QueryCall;
104
+ expect(sql).toBe(
105
+ `UPDATE "Service" SET "lastSeenAt" = $1, "serviceVersion" = $2, ` +
106
+ `"deploymentEnvironment" = $3, "updatedAt" = CURRENT_TIMESTAMP WHERE "_id" = $4`,
107
+ );
108
+ /*
109
+ * Column values are routed through the driver's persist path (here the
110
+ * `prepared(...)` stand-in), so transformers/JSON/coercion apply exactly
111
+ * as save() would. The `_id` in the WHERE clause is bound raw.
112
+ */
113
+ expect(params).toEqual([
114
+ lastSeenAt,
115
+ "prepared(1.2.3)",
116
+ "prepared(production)",
117
+ id.toString(),
118
+ ]);
119
+ });
120
+
121
+ test("throws on an unknown column instead of silently writing nothing", async () => {
122
+ mockRepository();
123
+ await expect(
124
+ ServiceService.updateColumnsByIdWithoutHooks({
125
+ id: ObjectID.generate(),
126
+ data: { notAColumn: "x" } as never,
127
+ }),
128
+ ).rejects.toThrow(/unknown column "notAColumn"/);
129
+ });
130
+
131
+ test("rejects SQL-expression (function) values it cannot bind", async () => {
132
+ const query: jest.Mock = mockRepository();
133
+ await expect(
134
+ ServiceService.updateColumnsByIdWithoutHooks({
135
+ id: ObjectID.generate(),
136
+ data: {
137
+ lastSeenAt: (() => {
138
+ return "NOW()";
139
+ }) as never,
140
+ } as never,
141
+ }),
142
+ ).rejects.toThrow(/SQL-expression values are not supported/);
143
+ expect(query).not.toHaveBeenCalled();
144
+ });
145
+
146
+ test("does not issue a query when there are no columns to update", async () => {
147
+ const query: jest.Mock = mockRepository();
148
+ await ServiceService.updateColumnsByIdWithoutHooks({
149
+ id: ObjectID.generate(),
150
+ data: {} as never,
151
+ });
152
+ expect(query).not.toHaveBeenCalled();
153
+ });
154
+ });
@@ -111,12 +111,15 @@ describe("StatementGenerator", () => {
111
111
  expect(jest.mocked(logger.debug)).toHaveBeenNthCalledWith(2, statement);
112
112
 
113
113
  /* eslint-disable prettier/prettier */
114
- expectStatement(statement, SQL`
115
- ALTER TABLE ${'oneuptime'}.${'<table-name>'}
114
+ expectStatement(
115
+ statement,
116
+ SQL`
117
+ ALTER TABLE ${"oneuptime"}.${"<table-name>"}
116
118
  UPDATE <set-statement>
117
119
  WHERE TRUE <where-statement>
118
- `);
119
- /* eslint-enable prettier/prettier */
120
+ `,
121
+ );
122
+ /* eslint-enable prettier/prettier */
120
123
  });
121
124
  });
122
125
 
@@ -790,18 +793,18 @@ describe("StatementGenerator", () => {
790
793
  expect(jest.mocked(logger.debug)).toHaveBeenNthCalledWith(2, statement);
791
794
 
792
795
  /* eslint-disable prettier/prettier */
793
- const expectedStatement: Statement = SQL`
794
- CREATE TABLE IF NOT EXISTS ${'oneuptime'}.${'<table-name>'}
796
+ const expectedStatement: Statement = SQL`
797
+ CREATE TABLE IF NOT EXISTS ${"oneuptime"}.${"<table-name>"}
795
798
  (
796
799
  <columns-create-statement>
797
800
  )
798
801
  ENGINE = MergeTree
799
802
  PARTITION BY (column_ObjectID)
800
803
 
801
- PRIMARY KEY (${'column_ObjectID'})
802
- ORDER BY (${'column_ObjectID'})
804
+ PRIMARY KEY (${"column_ObjectID"})
805
+ ORDER BY (${"column_ObjectID"})
803
806
  `;
804
- /* eslint-enable prettier/prettier */
807
+ /* eslint-enable prettier/prettier */
805
808
 
806
809
  // Normalize whitespace for comparison to avoid formatting issues
807
810
  const normalizeWhitespace: (s: string) => string = (