@oneuptime/common 12.0.29 → 12.0.30

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 (189) hide show
  1. package/Models/DatabaseModels/Index.ts +11 -0
  2. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  3. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  4. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  5. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  6. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  7. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  8. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  9. package/Server/EnvironmentConfig.ts +138 -0
  10. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  13. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  14. package/Server/Services/Index.ts +12 -0
  15. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  16. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  17. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  18. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  19. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  20. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  21. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  22. package/Server/Services/TeamMemberService.ts +489 -1
  23. package/Server/Services/TeamService.ts +50 -0
  24. package/Server/Services/UserNotificationSettingService.ts +44 -1
  25. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  26. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  27. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  28. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  29. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  30. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  31. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  32. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  33. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  34. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  35. package/Server/Utils/Response.ts +134 -0
  36. package/Server/Utils/StartServer.ts +10 -0
  37. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  38. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  39. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  40. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  41. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  42. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  43. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  44. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  45. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  46. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  47. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  48. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  49. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  50. package/Tests/Server/API/Helpers.ts +8 -0
  51. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  52. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  53. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  54. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  55. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  56. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  57. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  58. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  59. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  60. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  61. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  62. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  63. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  64. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  65. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  66. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  67. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  68. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  69. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  70. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  71. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  72. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  73. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  74. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  75. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  76. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  77. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  78. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  79. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  80. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  82. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  83. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  84. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  85. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  86. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  87. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  88. package/Types/Calendar/ICalendar.ts +442 -0
  89. package/Types/Email/EmailTemplateType.ts +2 -0
  90. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  91. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  92. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  93. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  94. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  95. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  96. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  97. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  100. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  102. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  103. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  104. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  105. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  106. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  107. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  108. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  109. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  110. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  111. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  112. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  113. package/build/dist/Server/EnvironmentConfig.js +102 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  116. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  122. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  123. package/build/dist/Server/Services/Index.js +12 -0
  124. package/build/dist/Server/Services/Index.js.map +1 -1
  125. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  126. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  127. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  128. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  129. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  130. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  131. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  132. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  133. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  134. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  135. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  136. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  137. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  138. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  139. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  140. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  141. package/build/dist/Server/Services/TeamService.js +43 -0
  142. package/build/dist/Server/Services/TeamService.js.map +1 -1
  143. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  144. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  145. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  146. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  147. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  148. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  149. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  150. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  151. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  152. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  153. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  154. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  155. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  156. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  157. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  158. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  159. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  160. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  161. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  162. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  163. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  164. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  165. package/build/dist/Server/Utils/Response.js +97 -0
  166. package/build/dist/Server/Utils/Response.js.map +1 -1
  167. package/build/dist/Server/Utils/StartServer.js +9 -1
  168. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  169. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  170. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  171. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  172. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  173. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  174. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  175. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  176. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  177. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  178. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  179. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  180. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  181. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  182. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  183. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  184. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  185. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  186. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  187. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  188. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,642 @@
1
+ import AllModelTypes from "../../Models/DatabaseModels/Index";
2
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
3
+ import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
4
+ import OnCallDutyPolicyScheduleCalendarFeed from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed";
5
+ import Project from "../../Models/DatabaseModels/Project";
6
+ import ProjectOnCallCalendarFeed from "../../Models/DatabaseModels/ProjectOnCallCalendarFeed";
7
+ import User from "../../Models/DatabaseModels/User";
8
+ import UserOnCallCalendarFeed from "../../Models/DatabaseModels/UserOnCallCalendarFeed";
9
+ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
10
+ import { ColumnAccessControl } from "../../Types/BaseDatabase/AccessControl";
11
+ import { TableColumnMetadata } from "../../Types/Database/TableColumn";
12
+ import TableColumnType from "../../Types/Database/TableColumnType";
13
+ import Permission from "../../Types/Permission";
14
+ import { describe, expect, test } from "@jest/globals";
15
+ import { getMetadataArgsStorage } from "typeorm";
16
+ import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
17
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
18
+ import type { RelationMetadataArgs } from "typeorm/metadata-args/RelationMetadataArgs";
19
+
20
+ /*
21
+ * The three calendar-feed models were written from one template, so this is
22
+ * a sweep rather than three hand-written files: a template applied three
23
+ * times fails in the same place three times.
24
+ *
25
+ * What is pinned is what is invisible until it is wrong in production:
26
+ *
27
+ * - registration in the model index, without which the table never exists
28
+ * - a UNIQUE CRUD route, because a duplicate silently shadows another model
29
+ * - tenant scoping and the Growth plan gate
30
+ * - the token columns: never readable, never creatable/updatable through
31
+ * the API, hidden from documentation, minted server-side (`computed`),
32
+ * the plaintext encrypted at rest, the hash UNIQUE at the database
33
+ * - the settings the owner may change and the bookkeeping they may not
34
+ * - the uniqueness rule that makes "mint or rotate" an upsert
35
+ */
36
+
37
+ type ModelType = { new (): BaseModel };
38
+
39
+ interface FeedSpec {
40
+ name: string;
41
+ modelType: ModelType;
42
+ crudApiPath: string;
43
+ /** The columns whose (non-token) uniqueness makes the feed an upsert. */
44
+ uniqueColumns: Array<string>;
45
+ /** Columns the API may update on an existing row. */
46
+ updatableSettings: Array<string>;
47
+ /** Extra columns this model has beyond the shared set. */
48
+ extraColumns: Array<string>;
49
+ /** Boolean/number defaults the model must carry. */
50
+ defaults: Record<string, boolean | number>;
51
+ }
52
+
53
+ const FEEDS: Array<FeedSpec> = [
54
+ {
55
+ name: "UserOnCallCalendarFeed",
56
+ modelType: UserOnCallCalendarFeed,
57
+ crudApiPath: "/user-on-call-calendar-feed",
58
+ uniqueColumns: ["projectId", "userId"],
59
+ updatableSettings: [
60
+ "isEnabled",
61
+ "includeCoveringShifts",
62
+ "pastDays",
63
+ "futureDays",
64
+ ],
65
+ extraColumns: ["user", "userId", "includeCoveringShifts"],
66
+ defaults: {
67
+ isEnabled: true,
68
+ includeCoveringShifts: true,
69
+ pastDays: 2,
70
+ futureDays: 90,
71
+ fetchCount: 0,
72
+ lastRenderTruncated: false,
73
+ },
74
+ },
75
+ {
76
+ name: "OnCallDutyPolicyScheduleCalendarFeed",
77
+ modelType: OnCallDutyPolicyScheduleCalendarFeed,
78
+ crudApiPath: "/on-call-duty-policy-schedule-calendar-feed",
79
+ uniqueColumns: ["onCallDutyPolicyScheduleId"],
80
+ updatableSettings: [
81
+ "isEnabled",
82
+ "includeCoverageGaps",
83
+ "minimumGapMinutes",
84
+ "pastDays",
85
+ "futureDays",
86
+ "rotateWhenMemberLeaves",
87
+ ],
88
+ extraColumns: [
89
+ "onCallDutyPolicySchedule",
90
+ "onCallDutyPolicyScheduleId",
91
+ "includeCoverageGaps",
92
+ "minimumGapMinutes",
93
+ "rotateWhenMemberLeaves",
94
+ "createdByUser",
95
+ "createdByUserId",
96
+ ],
97
+ defaults: {
98
+ isEnabled: true,
99
+ includeCoverageGaps: false,
100
+ minimumGapMinutes: 60,
101
+ pastDays: 2,
102
+ futureDays: 90,
103
+ rotateWhenMemberLeaves: false,
104
+ fetchCount: 0,
105
+ lastRenderTruncated: false,
106
+ },
107
+ },
108
+ {
109
+ name: "ProjectOnCallCalendarFeed",
110
+ modelType: ProjectOnCallCalendarFeed,
111
+ crudApiPath: "/project-on-call-calendar-feed",
112
+ uniqueColumns: ["projectId"],
113
+ updatableSettings: [
114
+ "isEnabled",
115
+ "includeCoverageGaps",
116
+ "minimumGapMinutes",
117
+ "pastDays",
118
+ "futureDays",
119
+ "rotateWhenMemberLeaves",
120
+ ],
121
+ extraColumns: [
122
+ "includeCoverageGaps",
123
+ "minimumGapMinutes",
124
+ "rotateWhenMemberLeaves",
125
+ "createdByUser",
126
+ "createdByUserId",
127
+ ],
128
+ defaults: {
129
+ isEnabled: true,
130
+ includeCoverageGaps: false,
131
+ minimumGapMinutes: 60,
132
+ pastDays: 2,
133
+ futureDays: 90,
134
+ rotateWhenMemberLeaves: false,
135
+ fetchCount: 0,
136
+ lastRenderTruncated: false,
137
+ },
138
+ },
139
+ ];
140
+
141
+ const SECRET_COLUMNS: Array<string> = [
142
+ "tokenHash",
143
+ "token",
144
+ "previousTokenHash",
145
+ "previousTokenExpiresAt",
146
+ ];
147
+
148
+ const BOOKKEEPING_COLUMNS: Array<string> = [
149
+ "tokenHint",
150
+ "rotatedAt",
151
+ "lastFetchedAt",
152
+ "lastFetchedClient",
153
+ "fetchCount",
154
+ "lastRenderTruncated",
155
+ ];
156
+
157
+ const SHARED_COLUMNS: Array<string> = [
158
+ "project",
159
+ "projectId",
160
+ ...SECRET_COLUMNS,
161
+ ...BOOKKEEPING_COLUMNS,
162
+ "isEnabled",
163
+ "pastDays",
164
+ "futureDays",
165
+ "deletedByUser",
166
+ "deletedByUserId",
167
+ ];
168
+
169
+ function feedSpecs(): Array<[string, FeedSpec]> {
170
+ return FEEDS.map((spec: FeedSpec): [string, FeedSpec] => {
171
+ return [spec.name, spec];
172
+ });
173
+ }
174
+
175
+ function typeormColumn(
176
+ modelType: ModelType,
177
+ propertyName: string,
178
+ ): ColumnMetadataArgs {
179
+ const column: ColumnMetadataArgs | undefined =
180
+ getMetadataArgsStorage().columns.find((entry: ColumnMetadataArgs) => {
181
+ return entry.target === modelType && entry.propertyName === propertyName;
182
+ });
183
+
184
+ if (!column) {
185
+ throw new Error(`${modelType.name}.${propertyName} has no @Column`);
186
+ }
187
+
188
+ return column;
189
+ }
190
+
191
+ function typeormIndices(modelType: ModelType): Array<IndexMetadataArgs> {
192
+ return getMetadataArgsStorage().indices.filter((entry: IndexMetadataArgs) => {
193
+ return entry.target === modelType;
194
+ });
195
+ }
196
+
197
+ function indexColumns(index: IndexMetadataArgs): Array<string> {
198
+ if (Array.isArray(index.columns)) {
199
+ return index.columns as Array<string>;
200
+ }
201
+
202
+ return [];
203
+ }
204
+
205
+ function typeormRelation(
206
+ modelType: ModelType,
207
+ propertyName: string,
208
+ ): RelationMetadataArgs {
209
+ const relation: RelationMetadataArgs | undefined =
210
+ getMetadataArgsStorage().relations.find((entry: RelationMetadataArgs) => {
211
+ return entry.target === modelType && entry.propertyName === propertyName;
212
+ });
213
+
214
+ if (!relation) {
215
+ throw new Error(`${modelType.name}.${propertyName} has no relation`);
216
+ }
217
+
218
+ return relation;
219
+ }
220
+
221
+ describe("On-call calendar feed models", () => {
222
+ test.each(feedSpecs())(
223
+ "%s is registered in the model index",
224
+ (_name: string, spec: FeedSpec) => {
225
+ expect((AllModelTypes as Array<ModelType>).includes(spec.modelType)).toBe(
226
+ true,
227
+ );
228
+ },
229
+ );
230
+
231
+ test.each(feedSpecs())(
232
+ "%s uses its own table name",
233
+ (name: string, spec: FeedSpec) => {
234
+ expect(new spec.modelType().tableName).toBe(name);
235
+ },
236
+ );
237
+
238
+ test.each(feedSpecs())(
239
+ "%s is served from a CRUD route nothing else uses",
240
+ (_name: string, spec: FeedSpec) => {
241
+ const model: BaseModel = new spec.modelType();
242
+
243
+ expect(model.getCrudApiPath()?.toString()).toBe(spec.crudApiPath);
244
+
245
+ const owners: Array<string> = (AllModelTypes as Array<ModelType>)
246
+ .filter((modelType: ModelType) => {
247
+ return (
248
+ new modelType().getCrudApiPath()?.toString() === spec.crudApiPath
249
+ );
250
+ })
251
+ .map((modelType: ModelType) => {
252
+ return modelType.name;
253
+ });
254
+
255
+ expect(owners).toEqual([spec.modelType.name]);
256
+ },
257
+ );
258
+
259
+ test.each(feedSpecs())(
260
+ "%s is tenant scoped by projectId",
261
+ (_name: string, spec: FeedSpec) => {
262
+ const model: BaseModel = new spec.modelType();
263
+
264
+ expect(model.getTenantColumn()).toBe("projectId");
265
+ },
266
+ );
267
+
268
+ test.each(feedSpecs())(
269
+ "%s is Growth-gated for every operation and survives an unpaid subscription",
270
+ (_name: string, spec: FeedSpec) => {
271
+ const model: BaseModel = new spec.modelType();
272
+
273
+ expect(model.getCreateBillingPlan()).toBe(PlanType.Growth);
274
+ expect(model.getReadBillingPlan()).toBe(PlanType.Growth);
275
+ expect(model.getUpdateBillingPlan()).toBe(PlanType.Growth);
276
+ expect(model.getDeleteBillingPlan()).toBe(PlanType.Growth);
277
+ expect(model.allowAccessIfSubscriptionIsUnpaid).toBe(true);
278
+ },
279
+ );
280
+
281
+ test.each(feedSpecs())(
282
+ "%s is not documented",
283
+ (_name: string, spec: FeedSpec) => {
284
+ expect(new spec.modelType().enableDocumentation).toBeFalsy();
285
+ },
286
+ );
287
+
288
+ test.each(feedSpecs())(
289
+ "%s carries exactly the shared columns plus its own",
290
+ (_name: string, spec: FeedSpec) => {
291
+ const columns: Array<string> = new spec.modelType().getTableColumns()
292
+ .columns;
293
+
294
+ for (const column of [...SHARED_COLUMNS, ...spec.extraColumns]) {
295
+ expect(columns).toContain(column);
296
+ }
297
+
298
+ /*
299
+ * Base columns (_id, createdAt, ...) come from BaseModel; everything
300
+ * else must be accounted for above so a stray column is a deliberate
301
+ * edit here as well.
302
+ */
303
+ const base: Array<string> = new BaseModel().getTableColumns().columns;
304
+ const unexpected: Array<string> = columns.filter((column: string) => {
305
+ return (
306
+ !base.includes(column) &&
307
+ !SHARED_COLUMNS.includes(column) &&
308
+ !spec.extraColumns.includes(column)
309
+ );
310
+ });
311
+
312
+ expect(unexpected).toEqual([]);
313
+ },
314
+ );
315
+ });
316
+
317
+ describe("On-call calendar feed token columns", () => {
318
+ test.each(feedSpecs())(
319
+ "%s denies every operation on every secret column",
320
+ (_name: string, spec: FeedSpec) => {
321
+ const model: BaseModel = new spec.modelType();
322
+
323
+ for (const column of SECRET_COLUMNS) {
324
+ const accessControl: ColumnAccessControl | null =
325
+ model.getColumnAccessControlFor(column);
326
+
327
+ expect({ column, accessControl }).toEqual({
328
+ column,
329
+ accessControl: { create: [], read: [], update: [] },
330
+ });
331
+ }
332
+ },
333
+ );
334
+
335
+ test.each(feedSpecs())(
336
+ "%s hides every secret column from documentation and marks it server-minted",
337
+ (_name: string, spec: FeedSpec) => {
338
+ const model: BaseModel = new spec.modelType();
339
+
340
+ for (const column of SECRET_COLUMNS) {
341
+ const metadata: TableColumnMetadata =
342
+ model.getTableColumnMetadata(column);
343
+
344
+ expect({ column, hidden: metadata.hideColumnInDocumentation }).toEqual({
345
+ column,
346
+ hidden: true,
347
+ });
348
+ expect({ column, computed: metadata.computed }).toEqual({
349
+ column,
350
+ computed: true,
351
+ });
352
+ expect({ column, relation: metadata.canReadOnRelationQuery }).toEqual({
353
+ column,
354
+ relation: false,
355
+ });
356
+ }
357
+ },
358
+ );
359
+
360
+ test.each(feedSpecs())(
361
+ "%s encrypts the plaintext token at rest and nothing else",
362
+ (_name: string, spec: FeedSpec) => {
363
+ const model: BaseModel = new spec.modelType();
364
+
365
+ const encrypted: Array<string> = model
366
+ .getTableColumns()
367
+ .columns.filter((column: string) => {
368
+ return model.getTableColumnMetadata(column)?.encrypted === true;
369
+ });
370
+
371
+ expect(encrypted).toEqual(["token"]);
372
+ expect(model.getTableColumnMetadata("token").type).toBe(
373
+ TableColumnType.VeryLongText,
374
+ );
375
+ },
376
+ );
377
+
378
+ test.each(feedSpecs())(
379
+ "%s never uses TableColumn.unique on the hash (the pre-insert check would echo it in an error)",
380
+ (_name: string, spec: FeedSpec) => {
381
+ const model: BaseModel = new spec.modelType();
382
+
383
+ expect(model.getTableColumnMetadata("tokenHash").unique).toBeFalsy();
384
+ expect(
385
+ model.getTableColumnMetadata("previousTokenHash").unique,
386
+ ).toBeFalsy();
387
+ },
388
+ );
389
+
390
+ test.each(feedSpecs())(
391
+ "%s makes tokenHash NOT NULL and UNIQUE at the database",
392
+ (_name: string, spec: FeedSpec) => {
393
+ const column: ColumnMetadataArgs = typeormColumn(
394
+ spec.modelType,
395
+ "tokenHash",
396
+ );
397
+
398
+ expect(column.options.nullable).toBe(false);
399
+ expect(column.options.unique).toBe(true);
400
+ expect(
401
+ new spec.modelType().getTableColumnMetadata("tokenHash").required,
402
+ ).toBe(true);
403
+ },
404
+ );
405
+
406
+ test.each(feedSpecs())(
407
+ "%s indexes previousTokenHash (the public route falls back to it)",
408
+ (_name: string, spec: FeedSpec) => {
409
+ const indexed: boolean = typeormIndices(spec.modelType).some(
410
+ (index: IndexMetadataArgs) => {
411
+ return (
412
+ indexColumns(index).length === 1 &&
413
+ indexColumns(index)[0] === "previousTokenHash"
414
+ );
415
+ },
416
+ );
417
+
418
+ expect(indexed).toBe(true);
419
+ expect(
420
+ typeormColumn(spec.modelType, "previousTokenHash").options.nullable,
421
+ ).toBe(true);
422
+ expect(
423
+ typeormColumn(spec.modelType, "previousTokenExpiresAt").options
424
+ .nullable,
425
+ ).toBe(true);
426
+ },
427
+ );
428
+
429
+ test.each(feedSpecs())(
430
+ "%s lets readers see the hint but never write it",
431
+ (_name: string, spec: FeedSpec) => {
432
+ const model: BaseModel = new spec.modelType();
433
+ const accessControl: ColumnAccessControl | null =
434
+ model.getColumnAccessControlFor("tokenHint");
435
+
436
+ expect(accessControl?.read).toEqual(model.readRecordPermissions);
437
+ expect(accessControl?.read.length).toBeGreaterThan(0);
438
+ expect(accessControl?.create).toEqual([]);
439
+ expect(accessControl?.update).toEqual([]);
440
+ expect(model.getTableColumnMetadata("tokenHint").computed).toBe(true);
441
+ },
442
+ );
443
+ });
444
+
445
+ describe("On-call calendar feed settings and bookkeeping", () => {
446
+ test.each(feedSpecs())(
447
+ "%s lets its readers read and its editors update exactly the settings columns",
448
+ (_name: string, spec: FeedSpec) => {
449
+ const model: BaseModel = new spec.modelType();
450
+
451
+ /*
452
+ * BaseModel's own columns (_id, createdAt, ...) inherit the table lists;
453
+ * only the model's declared columns are the settings under test.
454
+ */
455
+ const base: Array<string> = new BaseModel().getTableColumns().columns;
456
+
457
+ const updatable: Array<string> = model
458
+ .getTableColumns()
459
+ .columns.filter((column: string) => {
460
+ return (
461
+ !base.includes(column) &&
462
+ (model.getColumnAccessControlFor(column)?.update || []).length > 0
463
+ );
464
+ })
465
+ .sort();
466
+
467
+ expect(updatable).toEqual([...spec.updatableSettings].sort());
468
+
469
+ for (const column of spec.updatableSettings) {
470
+ const accessControl: ColumnAccessControl | null =
471
+ model.getColumnAccessControlFor(column);
472
+
473
+ expect(accessControl?.read).toEqual(model.readRecordPermissions);
474
+ expect(accessControl?.update).toEqual(model.updateRecordPermissions);
475
+ }
476
+ },
477
+ );
478
+
479
+ test.each(feedSpecs())(
480
+ "%s bookkeeping is readable but root-written",
481
+ (_name: string, spec: FeedSpec) => {
482
+ const model: BaseModel = new spec.modelType();
483
+
484
+ for (const column of BOOKKEEPING_COLUMNS) {
485
+ const accessControl: ColumnAccessControl | null =
486
+ model.getColumnAccessControlFor(column);
487
+
488
+ expect({ column, read: accessControl?.read }).toEqual({
489
+ column,
490
+ read: model.readRecordPermissions,
491
+ });
492
+ expect({ column, create: accessControl?.create }).toEqual({
493
+ column,
494
+ create: [],
495
+ });
496
+ expect({ column, update: accessControl?.update }).toEqual({
497
+ column,
498
+ update: [],
499
+ });
500
+ expect({
501
+ column,
502
+ computed: model.getTableColumnMetadata(column).computed,
503
+ }).toEqual({ column, computed: true });
504
+ }
505
+ },
506
+ );
507
+
508
+ test.each(feedSpecs())(
509
+ "%s carries the documented defaults, in the model metadata AND the database column",
510
+ (_name: string, spec: FeedSpec) => {
511
+ const model: BaseModel = new spec.modelType();
512
+
513
+ for (const [column, value] of Object.entries(spec.defaults)) {
514
+ const metadata: TableColumnMetadata =
515
+ model.getTableColumnMetadata(column);
516
+
517
+ expect({ column, isDefault: metadata.isDefaultValueColumn }).toEqual({
518
+ column,
519
+ isDefault: true,
520
+ });
521
+ expect({ column, defaultValue: metadata.defaultValue }).toEqual({
522
+ column,
523
+ defaultValue: value,
524
+ });
525
+ expect({
526
+ column,
527
+ dbDefault: typeormColumn(spec.modelType, column).options.default,
528
+ }).toEqual({ column, dbDefault: value });
529
+ expect({
530
+ column,
531
+ nullable: typeormColumn(spec.modelType, column).options.nullable,
532
+ }).toEqual({ column, nullable: false });
533
+ }
534
+ },
535
+ );
536
+
537
+ test.each(feedSpecs())(
538
+ "%s is unique on the columns that make mint-or-rotate an upsert",
539
+ (_name: string, spec: FeedSpec) => {
540
+ const uniqueIndexes: Array<Array<string>> = typeormIndices(spec.modelType)
541
+ .filter((index: IndexMetadataArgs) => {
542
+ return index.unique === true;
543
+ })
544
+ .map(indexColumns);
545
+
546
+ expect(uniqueIndexes).toContainEqual(spec.uniqueColumns);
547
+ },
548
+ );
549
+
550
+ test.each(feedSpecs())(
551
+ "%s cascades away with its project",
552
+ (_name: string, spec: FeedSpec) => {
553
+ const relation: RelationMetadataArgs = typeormRelation(
554
+ spec.modelType,
555
+ "project",
556
+ );
557
+
558
+ expect(relation.options.onDelete).toBe("CASCADE");
559
+ expect(
560
+ new spec.modelType().getTableColumnMetadata("project").modelType,
561
+ ).toBe(Project);
562
+ },
563
+ );
564
+ });
565
+
566
+ describe("UserOnCallCalendarFeed (personal feed) specifics", () => {
567
+ const model: UserOnCallCalendarFeed = new UserOnCallCalendarFeed();
568
+
569
+ test("is owned by its user and readable/updatable/deletable by the owner only", () => {
570
+ expect(model.currentUserCanAccessColumnBy).toBe("userId");
571
+ expect(model.readRecordPermissions).toEqual([Permission.CurrentUser]);
572
+ expect(model.updateRecordPermissions).toEqual([Permission.CurrentUser]);
573
+ expect(model.deleteRecordPermissions).toEqual([Permission.CurrentUser]);
574
+ });
575
+
576
+ test("cannot be created through the CRUD API at all", () => {
577
+ expect(model.createRecordPermissions).toEqual([]);
578
+
579
+ for (const column of model.getTableColumns().columns) {
580
+ expect({
581
+ column,
582
+ create: model.getColumnAccessControlFor(column)?.create || [],
583
+ }).toEqual({ column, create: [] });
584
+ }
585
+ });
586
+
587
+ test("cascades away with its user", () => {
588
+ expect(
589
+ typeormRelation(UserOnCallCalendarFeed, "user").options.onDelete,
590
+ ).toBe("CASCADE");
591
+ expect(model.getTableColumnMetadata("user").modelType).toBe(User);
592
+ expect(
593
+ typeormColumn(UserOnCallCalendarFeed, "userId").options.nullable,
594
+ ).toBe(false);
595
+ });
596
+
597
+ test("is not label scoped (it has no labelled parent)", () => {
598
+ expect(model.canAccessIfCanReadOn).toBeFalsy();
599
+ });
600
+ });
601
+
602
+ describe("OnCallDutyPolicyScheduleCalendarFeed (shared schedule feed) specifics", () => {
603
+ test("cascades away with its schedule", () => {
604
+ const relation: RelationMetadataArgs = typeormRelation(
605
+ OnCallDutyPolicyScheduleCalendarFeed,
606
+ "onCallDutyPolicySchedule",
607
+ );
608
+
609
+ expect(relation.options.onDelete).toBe("CASCADE");
610
+ expect(
611
+ typeormColumn(
612
+ OnCallDutyPolicyScheduleCalendarFeed,
613
+ "onCallDutyPolicyScheduleId",
614
+ ).options.nullable,
615
+ ).toBe(false);
616
+ expect(
617
+ new OnCallDutyPolicyScheduleCalendarFeed().getTableColumnMetadata(
618
+ "onCallDutyPolicySchedule",
619
+ ).modelType,
620
+ ).toBe(OnCallDutyPolicySchedule);
621
+ });
622
+
623
+ test("keeps the creator informationally, without cascading", () => {
624
+ const relation: RelationMetadataArgs = typeormRelation(
625
+ OnCallDutyPolicyScheduleCalendarFeed,
626
+ "createdByUser",
627
+ );
628
+
629
+ expect(relation.options.onDelete).toBe("SET NULL");
630
+ });
631
+ });
632
+
633
+ describe("ProjectOnCallCalendarFeed (project-wide feed) specifics", () => {
634
+ test("keeps the creator informationally, without cascading", () => {
635
+ const relation: RelationMetadataArgs = typeormRelation(
636
+ ProjectOnCallCalendarFeed,
637
+ "createdByUser",
638
+ );
639
+
640
+ expect(relation.options.onDelete).toBe("SET NULL");
641
+ });
642
+ });