@oneuptime/common 7.0.5014 → 7.0.5020

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 (28) hide show
  1. package/Models/DatabaseModels/StatusPage.ts +39 -0
  2. package/Models/DatabaseModels/StatusPageSubscriber.ts +59 -0
  3. package/Server/API/StatusPageAPI.ts +48 -3
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1755775040650-MigrationName.ts +29 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1755778495455-MigrationName.ts +23 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1755778934927-MigrationName.ts +23 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  8. package/Server/Services/StatusPageSubscriberService.ts +67 -9
  9. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +113 -373
  10. package/build/dist/Models/DatabaseModels/StatusPage.js +40 -0
  11. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  12. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +61 -0
  13. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  14. package/build/dist/Server/API/StatusPageAPI.js +27 -3
  15. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755775040650-MigrationName.js +16 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755775040650-MigrationName.js.map +1 -0
  18. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755778495455-MigrationName.js +14 -0
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755778495455-MigrationName.js.map +1 -0
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755778934927-MigrationName.js +14 -0
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755778934927-MigrationName.js.map +1 -0
  22. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  23. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  24. package/build/dist/Server/Services/StatusPageSubscriberService.js +50 -9
  25. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  26. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +93 -302
  27. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  28. package/package.json +1 -1
@@ -3,414 +3,154 @@ import HTTPResponse from "../../../../Types/API/HTTPResponse";
3
3
  import URL from "../../../../Types/API/URL";
4
4
  import { JSONObject } from "../../../../Types/JSON";
5
5
  import API from "../../../../Utils/API";
6
- import WorkspaceMessagePayload from "../../../../Types/Workspace/WorkspaceMessagePayload";
7
6
  import logger from "../../Logger";
8
- import Dictionary from "../../../../Types/Dictionary";
9
- import WorkspaceBase, {
10
- WorkspaceChannel,
11
- WorkspaceSendMessageResponse,
12
- WorkspaceThread,
13
- } from "../WorkspaceBase";
14
- import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
15
- import OneUptimeDate from "../../../../Types/Date";
7
+ import WorkspaceBase from "../WorkspaceBase";
16
8
  import CaptureSpan from "../../Telemetry/CaptureSpan";
17
- import BadDataException from "../../../../Types/Exception/BadDataException";
18
9
 
19
10
  export default class MicrosoftTeams extends WorkspaceBase {
20
- @CaptureSpan()
21
- public static override async getAllWorkspaceChannels(data: {
22
- authToken: string;
23
- }): Promise<Dictionary<WorkspaceChannel>> {
24
- logger.debug("Getting all workspace channels with data:");
25
- logger.debug(data);
26
-
27
- const channels: Dictionary<WorkspaceChannel> = {};
28
- const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
29
- await API.get<JSONObject>(
30
- URL.fromString("https://graph.microsoft.com/v1.0/me/joinedTeams"),
31
- {
32
- Authorization: `Bearer ${data.authToken}`,
33
- },
34
- );
35
-
36
- logger.debug("Response from Microsoft Graph API for getting all channels:");
37
- logger.debug(JSON.stringify(response, null, 2));
38
-
39
- if (response instanceof HTTPErrorResponse) {
40
- logger.error("Error response from Microsoft Graph API:");
41
- logger.error(response);
42
- throw response;
43
- }
44
-
45
- for (const team of (response.jsonData as JSONObject)?.[
46
- "value"
47
- ] as Array<JSONObject>) {
48
- if (!team["id"] || !team["displayName"]) {
49
- continue;
50
- }
51
-
52
- channels[team["displayName"].toString()] = {
53
- id: team["id"] as string,
54
- name: team["displayName"] as string,
55
- workspaceType: WorkspaceType.MicrosoftTeams,
56
- };
57
- }
58
-
59
- logger.debug("All workspace channels obtained:");
60
- logger.debug(channels);
61
- return channels;
62
- }
63
-
64
- @CaptureSpan()
65
- public static override getDividerBlock(): JSONObject {
66
- return {
67
- type: "divider",
68
- };
69
- }
70
-
71
- @CaptureSpan()
72
- public static getValuesFromView(data: {
73
- view: JSONObject;
74
- }): Dictionary<string | number | Array<string | number> | Date> {
75
- logger.debug("Getting values from view with data:");
76
- logger.debug(JSON.stringify(data, null, 2));
77
-
78
- const teamsView: JSONObject = data.view;
79
- const values: Dictionary<string | number | Array<string | number> | Date> =
80
- {};
81
-
82
- if (!teamsView["state"] || !(teamsView["state"] as JSONObject)["values"]) {
83
- return {};
84
- }
85
-
86
- for (const valueId in (teamsView["state"] as JSONObject)[
87
- "values"
88
- ] as JSONObject) {
89
- for (const blockId in (
90
- (teamsView["state"] as JSONObject)["values"] as JSONObject
91
- )[valueId] as JSONObject) {
92
- const valueObject: JSONObject = (
93
- (teamsView["state"] as JSONObject)["values"] as JSONObject
94
- )[valueId] as JSONObject;
95
- const value: JSONObject = valueObject[blockId] as JSONObject;
96
- values[blockId] = value["value"] as string | number;
97
-
98
- if ((value["selected_option"] as JSONObject)?.["value"]) {
99
- values[blockId] = (value["selected_option"] as JSONObject)?.[
100
- "value"
101
- ] as string;
102
- }
103
-
104
- if (Array.isArray(value["selected_options"])) {
105
- values[blockId] = (
106
- value["selected_options"] as Array<JSONObject>
107
- ).map((option: JSONObject) => {
108
- return option["value"] as string | number;
109
- });
110
- }
111
-
112
- // if date picker
113
- if (value["selected_date_time"]) {
114
- values[blockId] = OneUptimeDate.fromUnixTimestamp(
115
- value["selected_date_time"] as number,
116
- );
117
- }
118
- }
119
- }
120
-
121
- logger.debug("Values obtained from view:");
122
- logger.debug(values);
123
-
124
- return values;
125
- }
126
-
127
- @CaptureSpan()
128
- public static override async inviteUserToChannelByChannelName(data: {
129
- authToken: string;
130
- channelName: string;
131
- workspaceUserId: string;
132
- }): Promise<void> {
133
- logger.debug("Inviting user to channel with data:");
134
- logger.debug(data);
135
-
136
- const channelId: string = (
137
- await this.getWorkspaceChannelFromChannelName({
138
- authToken: data.authToken,
139
- channelName: data.channelName,
11
+ private static buildMessageCardFromMarkdown(markdown: string): JSONObject {
12
+ // Teams MessageCard has limited markdown support. Headings like '##' are not supported
13
+ // and single newlines can collapse. Convert common patterns to a structured card.
14
+ const lines: Array<string> = markdown
15
+ .split("\n")
16
+ .map((l: string) => {
17
+ return l.trim();
140
18
  })
141
- ).id;
142
-
143
- return this.inviteUserToChannelByChannelId({
144
- authToken: data.authToken,
145
- channelId: channelId,
146
- workspaceUserId: data.workspaceUserId,
147
- });
148
- }
149
-
150
- @CaptureSpan()
151
- public static override async createChannelsIfDoesNotExist(data: {
152
- authToken: string;
153
- channelNames: Array<string>;
154
- }): Promise<Array<WorkspaceChannel>> {
155
- logger.debug("Creating channels if they do not exist with data:");
156
- logger.debug(data);
157
-
158
- const workspaceChannels: Array<WorkspaceChannel> = [];
159
- const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
160
- await this.getAllWorkspaceChannels({
161
- authToken: data.authToken,
19
+ .filter((l: string) => {
20
+ return l.length > 0;
162
21
  });
163
22
 
164
- logger.debug("Existing workspace channels:");
165
- logger.debug(existingWorkspaceChannels);
166
-
167
- for (let channelName of data.channelNames) {
168
- // if channel name starts with #, remove it
169
- if (channelName && channelName.startsWith("#")) {
170
- channelName = channelName.substring(1);
171
- }
172
-
173
- // convert channel name to lowercase
174
- channelName = channelName.toLowerCase();
175
-
176
- // replace spaces with hyphens
177
- channelName = channelName.replace(/\s+/g, "-");
23
+ let title: string = "";
24
+ const facts: Array<JSONObject> = [];
25
+ const actions: Array<JSONObject> = [];
26
+ const bodyTextParts: Array<string> = [];
27
+
28
+ // Extract title from the first non-empty line and strip markdown heading markers
29
+ if (lines.length > 0) {
30
+ const firstLine: string = lines[0] ?? "";
31
+ title = firstLine
32
+ .replace(/^#+\s*/, "") // remove leading markdown headers like ##
33
+ .replace(/^\*\*|\*\*$/g, "") // remove stray bold markers if any
34
+ .trim();
35
+ lines.shift();
36
+ }
178
37
 
179
- if (existingWorkspaceChannels[channelName]) {
180
- logger.debug(`Channel ${channelName} already exists.`);
181
- workspaceChannels.push(existingWorkspaceChannels[channelName]!);
182
- continue;
38
+ const linkRegex: RegExp = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](url)
39
+
40
+ for (const line of lines) {
41
+ // Extract links to actions and strip them from text
42
+ let lineWithoutLinks: string = line;
43
+ let match: RegExpExecArray | null = null;
44
+ while ((match = linkRegex.exec(line))) {
45
+ const name: string = match[1] ?? "";
46
+ const url: string = match[2] ?? "";
47
+ actions.push({
48
+ ["@type"]: "OpenUri",
49
+ name: name,
50
+ targets: [
51
+ {
52
+ os: "default",
53
+ uri: url,
54
+ },
55
+ ],
56
+ });
57
+ lineWithoutLinks = lineWithoutLinks.replace(match[0], "").trim();
183
58
  }
184
59
 
185
- logger.debug(`Channel ${channelName} does not exist. Creating channel.`);
186
- const channel: WorkspaceChannel = await this.createChannel({
187
- authToken: data.authToken,
188
- channelName: channelName,
189
- });
60
+ // Parse facts of the form **Label:** value
61
+ const factMatch: RegExpExecArray | null = /\*\*(.*?)\:\*\*\s*(.*)/.exec(
62
+ lineWithoutLinks,
63
+ );
190
64
 
191
- if (channel) {
192
- logger.debug(`Channel ${channelName} created successfully.`);
193
- workspaceChannels.push(channel);
65
+ if (factMatch) {
66
+ const name: string = (factMatch[1] ?? "").trim();
67
+ const value: string = (factMatch[2] ?? "").trim();
68
+ if (
69
+ name.toLowerCase() === "description" ||
70
+ name.toLowerCase() === "note"
71
+ ) {
72
+ bodyTextParts.push(`**${name}:** ${value}`);
73
+ } else {
74
+ facts.push({ name: name, value: value });
75
+ }
76
+ } else if (lineWithoutLinks) {
77
+ bodyTextParts.push(lineWithoutLinks);
194
78
  }
195
79
  }
196
80
 
197
- logger.debug("Channels created or found:");
198
- logger.debug(workspaceChannels);
199
- return workspaceChannels;
200
- }
201
-
202
- @CaptureSpan()
203
- public static override async getWorkspaceChannelFromChannelName(data: {
204
- authToken: string;
205
- channelName: string;
206
- }): Promise<WorkspaceChannel> {
207
- logger.debug("Getting workspace channel ID from channel name with data:");
208
- logger.debug(data);
209
-
210
- const channels: Dictionary<WorkspaceChannel> =
211
- await this.getAllWorkspaceChannels({
212
- authToken: data.authToken,
213
- });
214
-
215
- logger.debug("All workspace channels:");
216
- logger.debug(channels);
81
+ const payload: JSONObject = {
82
+ ["@type"]: "MessageCard",
83
+ ["@context"]: "https://schema.org/extensions",
84
+ title: title,
85
+ summary: title,
86
+ };
217
87
 
218
- if (!channels[data.channelName]) {
219
- logger.error("Channel not found.");
220
- throw new BadDataException("Channel not found.");
88
+ if (bodyTextParts.length > 0) {
89
+ payload["text"] = bodyTextParts.join("\n\n");
221
90
  }
222
91
 
223
- logger.debug("Workspace channel ID obtained:");
224
- logger.debug(channels[data.channelName]!.id);
225
-
226
- return channels[data.channelName]!;
227
- }
228
-
229
- @CaptureSpan()
230
- public static override async getWorkspaceChannelFromChannelId(data: {
231
- authToken: string;
232
- channelId: string;
233
- }): Promise<WorkspaceChannel> {
234
- logger.debug("Getting workspace channel from channel ID with data:");
235
- logger.debug(data);
236
-
237
- const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
238
- await API.get<JSONObject>(
239
- URL.fromString(
240
- `https://graph.microsoft.com/v1.0/teams/${data.channelId}`,
241
- ),
92
+ if (facts.length > 0) {
93
+ payload["sections"] = [
242
94
  {
243
- Authorization: `Bearer ${data.authToken}`,
95
+ facts: facts,
244
96
  },
245
- );
246
-
247
- logger.debug("Response from Microsoft Graph API for getting channel info:");
248
- logger.debug(response);
249
-
250
- if (response instanceof HTTPErrorResponse) {
251
- logger.error("Error response from Microsoft Graph API:");
252
- logger.error(response);
253
- throw response;
97
+ ];
254
98
  }
255
99
 
256
- if (!(response.jsonData as JSONObject)?.["displayName"]) {
257
- logger.error("Invalid response from Microsoft Graph API:");
258
- logger.error(response.jsonData);
259
- throw new Error("Invalid response");
100
+ if (actions.length > 0) {
101
+ payload["potentialAction"] = actions;
260
102
  }
261
103
 
262
- const channel: WorkspaceChannel = {
263
- name: (response.jsonData as JSONObject)["displayName"] as string,
264
- id: data.channelId,
265
- workspaceType: WorkspaceType.MicrosoftTeams,
266
- };
267
-
268
- logger.debug("Workspace channel obtained:");
269
- logger.debug(channel);
270
- return channel;
104
+ return payload;
271
105
  }
272
106
 
273
107
  @CaptureSpan()
274
- public static override async doesChannelExist(data: {
275
- authToken: string;
276
- channelName: string;
277
- }): Promise<boolean> {
278
- // if channel name starts with #, remove it
279
- if (data.channelName && data.channelName.startsWith("#")) {
280
- data.channelName = data.channelName.substring(1);
281
- }
282
-
283
- // convert channel name to lowercase
284
- data.channelName = data.channelName.toLowerCase();
285
-
286
- // get channel id from channel name
287
- const channels: Dictionary<WorkspaceChannel> =
288
- await this.getAllWorkspaceChannels({
289
- authToken: data.authToken,
290
- });
291
-
292
- // if this channel exists
293
- if (channels[data.channelName]) {
294
- return true;
295
- }
296
-
297
- return false;
298
- }
299
-
300
- @CaptureSpan()
301
- public static override async sendMessage(data: {
302
- workspaceMessagePayload: WorkspaceMessagePayload;
303
- authToken: string; // which auth token should we use to send.
304
- userId: string;
305
- }): Promise<WorkspaceSendMessageResponse> {
306
- logger.debug("Sending message to Microsoft Teams with data:");
108
+ public static override async sendMessageToChannelViaIncomingWebhook(data: {
109
+ url: URL;
110
+ text: string;
111
+ }): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
112
+ logger.debug("Sending message to Teams channel via incoming webhook:");
307
113
  logger.debug(data);
308
114
 
309
- const blocks: Array<JSONObject> = this.getBlocksFromWorkspaceMessagePayload(
310
- data.workspaceMessagePayload,
311
- );
312
-
313
- logger.debug("Blocks generated from workspace message payload:");
314
- logger.debug(blocks);
315
-
316
- const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
317
- await this.getAllWorkspaceChannels({
318
- authToken: data.authToken,
319
- });
320
-
321
- logger.debug("Existing workspace channels:");
322
- logger.debug(existingWorkspaceChannels);
323
-
324
- const workspaceChannelsToPostTo: Array<WorkspaceChannel> = [];
325
-
326
- for (let channelName of data.workspaceMessagePayload.channelNames) {
327
- if (channelName && channelName.startsWith("#")) {
328
- // trim # from channel name
329
- channelName = channelName.substring(1);
330
- }
331
-
332
- let channel: WorkspaceChannel | null = null;
115
+ // Build a structured MessageCard from markdown for better rendering in Teams
116
+ const payload: JSONObject = this.buildMessageCardFromMarkdown(data.text);
333
117
 
334
- if (existingWorkspaceChannels[channelName]) {
335
- channel = existingWorkspaceChannels[channelName]!;
336
- }
118
+ const apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null =
119
+ await API.post(data.url, payload);
337
120
 
338
- if (channel) {
339
- workspaceChannelsToPostTo.push(channel);
340
- } else {
341
- logger.debug(`Channel ${channelName} does not exist.`);
342
- }
121
+ if (!apiResult) {
122
+ logger.error(
123
+ "Could not send message to Teams channel via incoming webhook.",
124
+ );
125
+ throw new Error(
126
+ "Could not send message to Teams channel via incoming webhook.",
127
+ );
343
128
  }
344
129
 
345
- // add channel ids.
346
- for (const channelId of data.workspaceMessagePayload.channelIds) {
347
- try {
348
- // Get the channel info including name from channel ID
349
- const channel: WorkspaceChannel =
350
- await this.getWorkspaceChannelFromChannelId({
351
- authToken: data.authToken,
352
- channelId: channelId,
353
- });
354
-
355
- workspaceChannelsToPostTo.push(channel);
356
- } catch (err) {
357
- logger.error(`Error getting channel info for channel ID ${channelId}:`);
358
- logger.error(err);
359
-
360
- // Fallback: create channel object with empty name if API call fails
361
- const channel: WorkspaceChannel = {
362
- id: channelId,
363
- name: channelId,
364
- workspaceType: WorkspaceType.MicrosoftTeams,
365
- };
366
-
367
- workspaceChannelsToPostTo.push(channel);
368
- }
130
+ if (apiResult instanceof HTTPErrorResponse) {
131
+ logger.error(
132
+ "Error sending message to Teams channel via incoming webhook:",
133
+ );
134
+ logger.error(apiResult);
135
+ throw apiResult;
369
136
  }
370
137
 
371
- logger.debug("Channel IDs to post to:");
372
- logger.debug(workspaceChannelsToPostTo);
373
-
374
- const workspaceMessageResponse: WorkspaceSendMessageResponse = {
375
- threads: [],
376
- workspaceType: WorkspaceType.MicrosoftTeams,
377
- };
378
-
379
- for (const channel of workspaceChannelsToPostTo) {
380
- try {
381
- // check if the user is in the channel.
382
- const isUserInChannel: boolean = await this.isUserInChannel({
383
- authToken: data.authToken,
384
- channelId: channel.id,
385
- userId: data.userId,
386
- });
387
-
388
- if (!isUserInChannel) {
389
- // add user to the channel
390
- await this.joinChannel({
391
- authToken: data.authToken,
392
- channelId: channel.id,
393
- });
394
- }
395
-
396
- const thread: WorkspaceThread = await this.sendPayloadBlocksToChannel({
397
- authToken: data.authToken,
398
- workspaceChannel: channel,
399
- blocks: blocks,
400
- });
401
-
402
- workspaceMessageResponse.threads.push(thread);
403
-
404
- logger.debug(`Message sent to channel ID ${channel.id} successfully.`);
405
- } catch (e) {
406
- logger.error(`Error sending message to channel ID ${channel.id}:`);
407
- logger.error(e);
408
- }
409
- }
138
+ logger.debug(
139
+ "Message sent to Teams channel via incoming webhook successfully:",
140
+ );
141
+ logger.debug(apiResult);
410
142
 
411
- logger.debug("Message sent successfully.");
412
- logger.debug(workspaceMessageResponse);
143
+ return apiResult;
144
+ }
413
145
 
414
- return workspaceMessageResponse;
146
+ public static isValidMicrosoftTeamsIncomingWebhookUrl(
147
+ incomingWebhookUrl: URL,
148
+ ): boolean {
149
+ // Check if the URL contains outlook.office.com or office.com webhook pattern
150
+ const urlString: string = incomingWebhookUrl.toString();
151
+ return (
152
+ urlString.includes("outlook.office.com") ||
153
+ urlString.includes("office.com")
154
+ );
415
155
  }
416
156
  }
@@ -75,6 +75,7 @@ let StatusPage = class StatusPage extends BaseModel {
75
75
  this.allowSubscribersToChooseEventTypes = undefined;
76
76
  this.enableSmsSubscribers = undefined;
77
77
  this.enableSlackSubscribers = undefined;
78
+ this.enableMicrosoftTeamsSubscribers = undefined;
78
79
  this.copyrightText = undefined;
79
80
  this.customFields = undefined;
80
81
  this.requireSsoForLogin = undefined;
@@ -1198,6 +1199,45 @@ __decorate([
1198
1199
  }),
1199
1200
  __metadata("design:type", Boolean)
1200
1201
  ], StatusPage.prototype, "enableSlackSubscribers", void 0);
1202
+ __decorate([
1203
+ ColumnAccessControl({
1204
+ create: [
1205
+ Permission.ProjectOwner,
1206
+ Permission.ProjectAdmin,
1207
+ Permission.ProjectMember,
1208
+ Permission.CreateProjectStatusPage,
1209
+ ],
1210
+ read: [
1211
+ Permission.ProjectOwner,
1212
+ Permission.ProjectAdmin,
1213
+ Permission.ProjectMember,
1214
+ Permission.ReadProjectStatusPage,
1215
+ ],
1216
+ update: [
1217
+ Permission.ProjectOwner,
1218
+ Permission.ProjectAdmin,
1219
+ Permission.ProjectMember,
1220
+ Permission.EditProjectStatusPage,
1221
+ ],
1222
+ }),
1223
+ TableColumn({
1224
+ isDefaultValueColumn: true,
1225
+ type: TableColumnType.Boolean,
1226
+ title: "Enable Microsoft Teams Subscribers",
1227
+ description: "Can Microsoft Teams subscribers subscribe to this Status Page?",
1228
+ defaultValue: false,
1229
+ }),
1230
+ Column({
1231
+ type: ColumnType.Boolean,
1232
+ default: false,
1233
+ }),
1234
+ ColumnBillingAccessControl({
1235
+ read: PlanType.Free,
1236
+ update: PlanType.Scale,
1237
+ create: PlanType.Free,
1238
+ }),
1239
+ __metadata("design:type", Boolean)
1240
+ ], StatusPage.prototype, "enableMicrosoftTeamsSubscribers", void 0);
1201
1241
  __decorate([
1202
1242
  ColumnAccessControl({
1203
1243
  create: [