@oneuptime/common 10.0.67 → 10.0.69

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 (88) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +370 -0
  2. package/Models/DatabaseModels/Alert.ts +2 -0
  3. package/Models/DatabaseModels/ApiKey.ts +2 -0
  4. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +3 -0
  5. package/Models/DatabaseModels/Incident.ts +2 -0
  6. package/Models/DatabaseModels/KubernetesResource.ts +19 -0
  7. package/Models/DatabaseModels/Label.ts +2 -0
  8. package/Models/DatabaseModels/Monitor.ts +2 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicy.ts +2 -0
  10. package/Models/DatabaseModels/Project.ts +80 -0
  11. package/Models/DatabaseModels/ScheduledMaintenance.ts +2 -0
  12. package/Models/DatabaseModels/StatusPage.ts +2 -0
  13. package/Models/DatabaseModels/Team.ts +2 -0
  14. package/Models/DatabaseModels/UserTelegram.ts +1 -1
  15. package/Server/API/KubernetesResourceAPI.ts +2 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.ts +35 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +14 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  19. package/Server/Services/AuditLogService.ts +574 -0
  20. package/Server/Services/DatabaseService.ts +103 -0
  21. package/Server/Services/Index.ts +2 -0
  22. package/Server/Services/KubernetesResourceService.ts +300 -8
  23. package/Server/Utils/VM/VMRunner.ts +39 -22
  24. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  25. package/Types/AuditLog/AuditLogAction.ts +7 -0
  26. package/Types/BaseDatabase/EnableAuditLogOn.ts +5 -0
  27. package/Types/Database/EnableAuditLog.ts +18 -0
  28. package/Types/IsolatedVM/ReturnResult.ts +6 -0
  29. package/Types/Kubernetes/KubernetesInventoryExtractor.ts +15 -1
  30. package/Types/Permission.ts +13 -0
  31. package/build/dist/Models/AnalyticsModels/AuditLog.js +337 -0
  32. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -0
  33. package/build/dist/Models/DatabaseModels/Alert.js +2 -0
  34. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  35. package/build/dist/Models/DatabaseModels/ApiKey.js +2 -0
  36. package/build/dist/Models/DatabaseModels/ApiKey.js.map +1 -1
  37. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  38. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  39. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  40. package/build/dist/Models/DatabaseModels/KubernetesResource.js +20 -0
  41. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  42. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  43. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  44. package/build/dist/Models/DatabaseModels/Monitor.js +2 -0
  45. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js +2 -0
  47. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js.map +1 -1
  48. package/build/dist/Models/DatabaseModels/Project.js +82 -0
  49. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  50. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +2 -0
  51. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/StatusPage.js +2 -0
  53. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/Team.js +2 -0
  55. package/build/dist/Models/DatabaseModels/Team.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/UserTelegram.js +1 -1
  57. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  58. package/build/dist/Server/API/KubernetesResourceAPI.js +2 -0
  59. package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -1
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js +18 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js.map +1 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +12 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  65. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  66. package/build/dist/Server/Services/AuditLogService.js +402 -0
  67. package/build/dist/Server/Services/AuditLogService.js.map +1 -0
  68. package/build/dist/Server/Services/DatabaseService.js +79 -8
  69. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  70. package/build/dist/Server/Services/Index.js +2 -0
  71. package/build/dist/Server/Services/Index.js.map +1 -1
  72. package/build/dist/Server/Services/KubernetesResourceService.js +202 -8
  73. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  74. package/build/dist/Server/Utils/VM/VMRunner.js +33 -19
  75. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  76. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  77. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  78. package/build/dist/Types/AuditLog/AuditLogAction.js +8 -0
  79. package/build/dist/Types/AuditLog/AuditLogAction.js.map +1 -0
  80. package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js +2 -0
  81. package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js.map +1 -0
  82. package/build/dist/Types/Database/EnableAuditLog.js +15 -0
  83. package/build/dist/Types/Database/EnableAuditLog.js.map +1 -0
  84. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +7 -1
  85. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -1
  86. package/build/dist/Types/Permission.js +11 -0
  87. package/build/dist/Types/Permission.js.map +1 -1
  88. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1776801030808 implements MigrationInterface {
4
+ public name: string = "MigrationName1776801030808";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "Project" ADD "enableAuditLogs" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "Project" ADD "auditLogsRetentionInDays" integer NOT NULL DEFAULT '7'`,
12
+ );
13
+ await queryRunner.query(
14
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
15
+ );
16
+ await queryRunner.query(
17
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
18
+ );
19
+ }
20
+
21
+ public async down(queryRunner: QueryRunner): Promise<void> {
22
+ await queryRunner.query(
23
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
27
+ );
28
+ await queryRunner.query(
29
+ `ALTER TABLE "Project" DROP COLUMN "auditLogsRetentionInDays"`,
30
+ );
31
+ await queryRunner.query(
32
+ `ALTER TABLE "Project" DROP COLUMN "enableAuditLogs"`,
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,14 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1776865086264 implements MigrationInterface {
4
+ name = 'MigrationName1776865086264'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "KubernetesResource" ADD "containerCount" integer`);
8
+ }
9
+
10
+ public async down(queryRunner: QueryRunner): Promise<void> {
11
+ await queryRunner.query(`ALTER TABLE "KubernetesResource" DROP COLUMN "containerCount"`);
12
+ }
13
+
14
+ }
@@ -286,6 +286,8 @@ import { MigrationName1776509413763 } from "./1776509413763-MigrationName";
286
286
  import { MigrationName1776541018853 } from "./1776541018853-MigrationName";
287
287
  import { MigrationName1776544084793 } from "./1776544084793-MigrationName";
288
288
  import { MigrationName1776761171349 } from "./1776761171349-MigrationName";
289
+ import { MigrationName1776801030808 } from "./1776801030808-MigrationName";
290
+ import { MigrationName1776865086264 } from "./1776865086264-MigrationName";
289
291
  export default [
290
292
  InitialMigration,
291
293
  MigrationName1717678334852,
@@ -575,4 +577,6 @@ export default [
575
577
  MigrationName1776541018853,
576
578
  MigrationName1776544084793,
577
579
  MigrationName1776761171349,
580
+ MigrationName1776801030808,
581
+ MigrationName1776865086264,
578
582
  ];
@@ -0,0 +1,574 @@
1
+ import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
2
+ import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
3
+ import ProjectService from "./ProjectService";
4
+ import { IsBillingEnabled, IsEnterpriseEdition } from "../EnvironmentConfig";
5
+ import logger from "../Utils/Logger";
6
+ import AuditLog from "../../Models/AnalyticsModels/AuditLog";
7
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
8
+ import Project from "../../Models/DatabaseModels/Project";
9
+ import User from "../../Models/DatabaseModels/User";
10
+ import AuditLogAction from "../../Types/AuditLog/AuditLogAction";
11
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
12
+ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
13
+ import { getColumnAccessControlForAllColumns } from "../../Types/Database/AccessControl/ColumnAccessControl";
14
+ import { JSONArray, JSONObject } from "../../Types/JSON";
15
+ import ObjectID from "../../Types/ObjectID";
16
+ import UserType from "../../Types/UserType";
17
+ import UserService from "./UserService";
18
+ import OneUptimeDate from "../../Types/Date";
19
+
20
+ const PROJECT_SETTINGS_CACHE_TTL_MS: number = 60 * 1000;
21
+ const USER_CACHE_TTL_MS: number = 5 * 60 * 1000;
22
+
23
+ const SKIPPED_FIELDS: ReadonlySet<string> = new Set<string>([
24
+ "_id",
25
+ "id",
26
+ "createdAt",
27
+ "updatedAt",
28
+ "deletedAt",
29
+ "version",
30
+ "slug",
31
+ ]);
32
+
33
+ const NAME_CANDIDATE_FIELDS: ReadonlyArray<string> = [
34
+ "name",
35
+ "title",
36
+ "displayName",
37
+ ];
38
+
39
+ interface CachedProjectSettings {
40
+ enableAuditLogs: boolean;
41
+ retentionInDays: number;
42
+ planName: PlanType | undefined;
43
+ expiresAt: number;
44
+ }
45
+
46
+ interface CachedUser {
47
+ name: string | null;
48
+ email: string | null;
49
+ expiresAt: number;
50
+ }
51
+
52
+ export class AuditLogService extends AnalyticsDatabaseService<AuditLog> {
53
+ private projectSettingsCache: Map<string, CachedProjectSettings> = new Map();
54
+ private userCache: Map<string, CachedUser> = new Map();
55
+
56
+ public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
57
+ super({ modelType: AuditLog, database: clickhouseDatabase });
58
+ }
59
+
60
+ public invalidateProjectSettings(projectId: ObjectID): void {
61
+ this.projectSettingsCache.delete(projectId.toString());
62
+ }
63
+
64
+ public async recordCreate<TModel extends BaseModel>(data: {
65
+ model: TModel;
66
+ createdItem: TModel;
67
+ props: DatabaseCommonInteractionProps;
68
+ }): Promise<void> {
69
+ try {
70
+ const projectId: ObjectID | undefined = this.resolveProjectId(
71
+ data.model,
72
+ data.createdItem,
73
+ data.props,
74
+ );
75
+
76
+ if (!projectId) {
77
+ return;
78
+ }
79
+
80
+ const settings: CachedProjectSettings | null =
81
+ await this.getProjectSettings(projectId);
82
+
83
+ if (!this.isEligible(settings, data.props)) {
84
+ return;
85
+ }
86
+
87
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
88
+ const changes: JSONArray = this.buildSnapshotChanges({
89
+ model: data.createdItem,
90
+ redactedFields,
91
+ valueKey: "newValue",
92
+ });
93
+
94
+ await this.insert({
95
+ projectId,
96
+ resourceType: this.getResourceType(data.model),
97
+ resourceId: data.createdItem.id ?? null,
98
+ resourceName: this.getResourceName(data.createdItem),
99
+ action: AuditLogAction.Create,
100
+ changes,
101
+ props: data.props,
102
+ retentionInDays: settings!.retentionInDays,
103
+ });
104
+ } catch (err) {
105
+ logger.warn("AuditLog: failed to record create event");
106
+ logger.warn(err);
107
+ }
108
+ }
109
+
110
+ public async recordUpdate<TModel extends BaseModel>(data: {
111
+ model: TModel;
112
+ before: TModel;
113
+ updatedFields: JSONObject;
114
+ itemId: ObjectID;
115
+ props: DatabaseCommonInteractionProps;
116
+ }): Promise<void> {
117
+ try {
118
+ const projectId: ObjectID | undefined = this.resolveProjectId(
119
+ data.model,
120
+ data.before,
121
+ data.props,
122
+ );
123
+
124
+ if (!projectId) {
125
+ return;
126
+ }
127
+
128
+ const settings: CachedProjectSettings | null =
129
+ await this.getProjectSettings(projectId);
130
+
131
+ if (!this.isEligible(settings, data.props)) {
132
+ return;
133
+ }
134
+
135
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
136
+ const changes: JSONArray = this.buildUpdateDiff({
137
+ before: data.before,
138
+ updatedFields: data.updatedFields,
139
+ redactedFields,
140
+ });
141
+
142
+ if (changes.length === 0) {
143
+ return;
144
+ }
145
+
146
+ await this.insert({
147
+ projectId,
148
+ resourceType: this.getResourceType(data.model),
149
+ resourceId: data.itemId,
150
+ resourceName: this.getResourceName(data.before),
151
+ action: AuditLogAction.Update,
152
+ changes,
153
+ props: data.props,
154
+ retentionInDays: settings!.retentionInDays,
155
+ });
156
+ } catch (err) {
157
+ logger.warn("AuditLog: failed to record update event");
158
+ logger.warn(err);
159
+ }
160
+ }
161
+
162
+ public async recordDelete<TModel extends BaseModel>(data: {
163
+ model: TModel;
164
+ deletedItem: TModel;
165
+ itemId: ObjectID;
166
+ props: DatabaseCommonInteractionProps;
167
+ }): Promise<void> {
168
+ try {
169
+ const projectId: ObjectID | undefined = this.resolveProjectId(
170
+ data.model,
171
+ data.deletedItem,
172
+ data.props,
173
+ );
174
+
175
+ if (!projectId) {
176
+ return;
177
+ }
178
+
179
+ const settings: CachedProjectSettings | null =
180
+ await this.getProjectSettings(projectId);
181
+
182
+ if (!this.isEligible(settings, data.props)) {
183
+ return;
184
+ }
185
+
186
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
187
+ const changes: JSONArray = this.buildSnapshotChanges({
188
+ model: data.deletedItem,
189
+ redactedFields,
190
+ valueKey: "oldValue",
191
+ });
192
+
193
+ await this.insert({
194
+ projectId,
195
+ resourceType: this.getResourceType(data.model),
196
+ resourceId: data.itemId,
197
+ resourceName: this.getResourceName(data.deletedItem),
198
+ action: AuditLogAction.Delete,
199
+ changes,
200
+ props: data.props,
201
+ retentionInDays: settings!.retentionInDays,
202
+ });
203
+ } catch (err) {
204
+ logger.warn("AuditLog: failed to record delete event");
205
+ logger.warn(err);
206
+ }
207
+ }
208
+
209
+ private async insert(params: {
210
+ projectId: ObjectID;
211
+ resourceType: string;
212
+ resourceId: ObjectID | null;
213
+ resourceName: string | null;
214
+ action: AuditLogAction;
215
+ changes: JSONArray;
216
+ props: DatabaseCommonInteractionProps;
217
+ retentionInDays: number;
218
+ }): Promise<void> {
219
+ const auditLog: AuditLog = new AuditLog();
220
+ auditLog.projectId = params.projectId;
221
+ auditLog.resourceType = params.resourceType;
222
+ if (params.resourceId) {
223
+ auditLog.resourceId = params.resourceId;
224
+ }
225
+ if (params.resourceName) {
226
+ auditLog.resourceName = params.resourceName;
227
+ }
228
+ auditLog.action = params.action;
229
+
230
+ const actor: {
231
+ userId: ObjectID | null;
232
+ userName: string | null;
233
+ userEmail: string | null;
234
+ userType: string | null;
235
+ } = await this.resolveActor(params.props);
236
+
237
+ if (actor.userId) {
238
+ auditLog.userId = actor.userId;
239
+ }
240
+ if (actor.userName) {
241
+ auditLog.userName = actor.userName;
242
+ }
243
+ if (actor.userEmail) {
244
+ auditLog.userEmail = actor.userEmail;
245
+ }
246
+ if (actor.userType) {
247
+ auditLog.userType = actor.userType;
248
+ }
249
+
250
+ auditLog.changes = params.changes;
251
+ auditLog.retentionDate = this.computeRetentionDate(params.retentionInDays);
252
+
253
+ await this.create({
254
+ data: auditLog,
255
+ props: { isRoot: true },
256
+ });
257
+ }
258
+
259
+ private computeRetentionDate(retentionInDays: number): Date {
260
+ const days: number = Math.max(1, Math.min(retentionInDays || 7, 180));
261
+ return OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), days);
262
+ }
263
+
264
+ private isEligible(
265
+ settings: CachedProjectSettings | null,
266
+ _props: DatabaseCommonInteractionProps,
267
+ ): boolean {
268
+ if (!settings) {
269
+ return false;
270
+ }
271
+
272
+ if (!settings.enableAuditLogs) {
273
+ return false;
274
+ }
275
+
276
+ if (IsEnterpriseEdition) {
277
+ return true;
278
+ }
279
+
280
+ if (IsBillingEnabled) {
281
+ return settings.planName === PlanType.Enterprise;
282
+ }
283
+
284
+ /*
285
+ * Neither enterprise edition nor billing is enabled — audit logs are not
286
+ * available on the free self-hosted build.
287
+ */
288
+ return false;
289
+ }
290
+
291
+ private async getProjectSettings(
292
+ projectId: ObjectID,
293
+ ): Promise<CachedProjectSettings | null> {
294
+ const key: string = projectId.toString();
295
+ const now: number = Date.now();
296
+ const cached: CachedProjectSettings | undefined =
297
+ this.projectSettingsCache.get(key);
298
+
299
+ if (cached && cached.expiresAt > now) {
300
+ return cached;
301
+ }
302
+
303
+ const project: Project | null = await ProjectService.findOneById({
304
+ id: projectId,
305
+ select: {
306
+ _id: true,
307
+ enableAuditLogs: true,
308
+ auditLogsRetentionInDays: true,
309
+ planName: true,
310
+ },
311
+ props: { isRoot: true },
312
+ });
313
+
314
+ if (!project) {
315
+ return null;
316
+ }
317
+
318
+ const settings: CachedProjectSettings = {
319
+ enableAuditLogs: Boolean(project.enableAuditLogs),
320
+ retentionInDays: project.auditLogsRetentionInDays ?? 7,
321
+ planName: project.planName,
322
+ expiresAt: now + PROJECT_SETTINGS_CACHE_TTL_MS,
323
+ };
324
+
325
+ this.projectSettingsCache.set(key, settings);
326
+ return settings;
327
+ }
328
+
329
+ private async resolveActor(props: DatabaseCommonInteractionProps): Promise<{
330
+ userId: ObjectID | null;
331
+ userName: string | null;
332
+ userEmail: string | null;
333
+ userType: string | null;
334
+ }> {
335
+ const userType: string | null = props.userType
336
+ ? String(props.userType)
337
+ : props.isRoot
338
+ ? "System"
339
+ : null;
340
+
341
+ if (!props.userId) {
342
+ return {
343
+ userId: null,
344
+ userName: null,
345
+ userEmail: null,
346
+ userType,
347
+ };
348
+ }
349
+
350
+ // API-key actions don't have a user record to look up.
351
+ if (props.userType === UserType.API) {
352
+ return {
353
+ userId: props.userId,
354
+ userName: null,
355
+ userEmail: null,
356
+ userType,
357
+ };
358
+ }
359
+
360
+ const cached: { name: string | null; email: string | null } | null =
361
+ await this.getUserInfo(props.userId);
362
+
363
+ return {
364
+ userId: props.userId,
365
+ userName: cached?.name ?? null,
366
+ userEmail: cached?.email ?? null,
367
+ userType,
368
+ };
369
+ }
370
+
371
+ private async getUserInfo(
372
+ userId: ObjectID,
373
+ ): Promise<{ name: string | null; email: string | null } | null> {
374
+ const key: string = userId.toString();
375
+ const now: number = Date.now();
376
+ const cached: CachedUser | undefined = this.userCache.get(key);
377
+
378
+ if (cached && cached.expiresAt > now) {
379
+ return { name: cached.name, email: cached.email };
380
+ }
381
+
382
+ const user: User | null = await UserService.findOneById({
383
+ id: userId,
384
+ select: { _id: true, name: true, email: true },
385
+ props: { isRoot: true },
386
+ });
387
+
388
+ const name: string | null = user?.name?.toString() ?? null;
389
+ const email: string | null = user?.email?.toString() ?? null;
390
+
391
+ this.userCache.set(key, {
392
+ name,
393
+ email,
394
+ expiresAt: now + USER_CACHE_TTL_MS,
395
+ });
396
+
397
+ return { name, email };
398
+ }
399
+
400
+ private resolveProjectId<TModel extends BaseModel>(
401
+ model: TModel,
402
+ item: TModel,
403
+ props: DatabaseCommonInteractionProps,
404
+ ): ObjectID | undefined {
405
+ if (props.tenantId) {
406
+ return props.tenantId;
407
+ }
408
+
409
+ const tenantColumn: string | null = model.getTenantColumn();
410
+ if (!tenantColumn) {
411
+ return undefined;
412
+ }
413
+
414
+ const value: ObjectID | undefined = item.getValue<ObjectID>(tenantColumn);
415
+ return value ?? undefined;
416
+ }
417
+
418
+ private getRedactedFields<TModel extends BaseModel>(
419
+ model: TModel,
420
+ ): Set<string> {
421
+ const redacted: Set<string> = new Set<string>();
422
+ const allAccessControl: { [key: string]: { read?: unknown[] } } =
423
+ getColumnAccessControlForAllColumns(model) as {
424
+ [key: string]: { read?: unknown[] };
425
+ };
426
+
427
+ for (const key of Object.keys(allAccessControl)) {
428
+ const ac: { read?: unknown[] } | undefined = allAccessControl[key];
429
+ if (ac && Array.isArray(ac.read) && ac.read.length === 0) {
430
+ redacted.add(key);
431
+ }
432
+ }
433
+
434
+ return redacted;
435
+ }
436
+
437
+ private buildSnapshotChanges<TModel extends BaseModel>(data: {
438
+ model: TModel;
439
+ redactedFields: Set<string>;
440
+ valueKey: "oldValue" | "newValue";
441
+ }): JSONArray {
442
+ const changes: JSONArray = [];
443
+ const columns: Array<string> = data.model.getTableColumns().columns;
444
+
445
+ for (const column of columns) {
446
+ if (SKIPPED_FIELDS.has(column)) {
447
+ continue;
448
+ }
449
+
450
+ if (data.redactedFields.has(column)) {
451
+ continue;
452
+ }
453
+
454
+ const value: unknown = (data.model as unknown as Record<string, unknown>)[
455
+ column
456
+ ];
457
+
458
+ if (value === undefined) {
459
+ continue;
460
+ }
461
+
462
+ changes.push({
463
+ field: column,
464
+ [data.valueKey]: this.serializeValue(value),
465
+ } as JSONObject);
466
+ }
467
+
468
+ return changes;
469
+ }
470
+
471
+ private buildUpdateDiff<TModel extends BaseModel>(data: {
472
+ before: TModel;
473
+ updatedFields: JSONObject;
474
+ redactedFields: Set<string>;
475
+ }): JSONArray {
476
+ const changes: JSONArray = [];
477
+
478
+ for (const field of Object.keys(data.updatedFields)) {
479
+ if (SKIPPED_FIELDS.has(field)) {
480
+ continue;
481
+ }
482
+
483
+ if (data.redactedFields.has(field)) {
484
+ continue;
485
+ }
486
+
487
+ const newValue: unknown = data.updatedFields[field];
488
+ const oldValue: unknown = (
489
+ data.before as unknown as Record<string, unknown>
490
+ )[field];
491
+
492
+ if (this.areValuesEqual(oldValue, newValue)) {
493
+ continue;
494
+ }
495
+
496
+ changes.push({
497
+ field,
498
+ oldValue: this.serializeValue(oldValue),
499
+ newValue: this.serializeValue(newValue),
500
+ } as JSONObject);
501
+ }
502
+
503
+ return changes;
504
+ }
505
+
506
+ private areValuesEqual(a: unknown, b: unknown): boolean {
507
+ if (a === b) {
508
+ return true;
509
+ }
510
+ if (a === null || a === undefined) {
511
+ return b === null || b === undefined;
512
+ }
513
+ if (b === null || b === undefined) {
514
+ return false;
515
+ }
516
+ try {
517
+ return JSON.stringify(a) === JSON.stringify(b);
518
+ } catch {
519
+ return false;
520
+ }
521
+ }
522
+
523
+ private serializeValue(value: unknown): unknown {
524
+ if (value === null || value === undefined) {
525
+ return null;
526
+ }
527
+ if (value instanceof ObjectID) {
528
+ return value.toString();
529
+ }
530
+ if (value instanceof Date) {
531
+ return value.toISOString();
532
+ }
533
+ if (
534
+ typeof value === "string" ||
535
+ typeof value === "number" ||
536
+ typeof value === "boolean"
537
+ ) {
538
+ return value;
539
+ }
540
+ try {
541
+ return JSON.parse(JSON.stringify(value));
542
+ } catch {
543
+ return String(value);
544
+ }
545
+ }
546
+
547
+ private getResourceType<TModel extends BaseModel>(model: TModel): string {
548
+ if (model.singularName) {
549
+ return model.singularName;
550
+ }
551
+ return (model as unknown as { constructor: { name: string } }).constructor
552
+ .name;
553
+ }
554
+
555
+ private getResourceName<TModel extends BaseModel>(
556
+ item: TModel,
557
+ ): string | null {
558
+ for (const field of NAME_CANDIDATE_FIELDS) {
559
+ const columns: Array<string> = item.getTableColumns().columns;
560
+ if (!columns.includes(field)) {
561
+ continue;
562
+ }
563
+ const value: unknown = (item as unknown as Record<string, unknown>)[
564
+ field
565
+ ];
566
+ if (typeof value === "string" && value.length > 0) {
567
+ return value;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+ }
573
+
574
+ export default new AuditLogService();