• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 12881890017

21 Jan 2025 07:13AM CUT coverage: 80.91%. First build
12881890017

Pull #1263

github

web-flow
Merge 1aab39586 into 2be93a15f
Pull Request #1263: feat: webhooks

6485 of 6849 branches covered (94.69%)

58 of 370 new or added lines in 7 files covered. (15.68%)

30774 of 38035 relevant lines covered (80.91%)

1827.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.06
/apps/nestjs-backend/src/features/webhook/webhook.service.ts
1
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
2✔
2
import type { IUnPromisify } from '@teable/core';
3
import { generateWebHookId, generateWebHookRunHistoryId } from '@teable/core';
4
import { PrismaService } from '@teable/db-main-prisma';
5
import type {
6
  ContentType,
7
  ICreateWebhookRo,
8
  IGetWebhookRunHistoryListQuery,
9
  IUpdateWebhookRo,
10
  IWebhookListVo,
11
  IWebhookRunHistoriesVo,
12
  IWebhookRunHistoryVo,
13
  IWebhookVo,
14
} from '@teable/openapi';
15
import { WebhookRunStatus } from '@teable/openapi';
16
import { ClsService } from 'nestjs-cls';
17
import { IThresholdConfig, ThresholdConfig } from '../../configs/threshold.config';
18
import type { IClsStore } from '../../types/cls';
19

20
@Injectable()
21
export class WebhookService {
2✔
22
  private logger = new Logger(WebhookService.name);
101✔
23

24
  constructor(
101✔
25
    private readonly prismaService: PrismaService,
101✔
26
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig,
101✔
27
    private readonly cls: ClsService<IClsStore>
101✔
28
  ) {}
101✔
29

30
  async getWebhookList(spaceId: string): Promise<IWebhookListVo> {
101✔
NEW
31
    const rawWebHookList = await this.prismaService.webhook.findMany({
×
NEW
32
      select: {
×
NEW
33
        id: true,
×
NEW
34
        url: true,
×
NEW
35
        contentType: true,
×
NEW
36
        secret: true,
×
NEW
37
        events: true,
×
NEW
38
        isEnabled: true,
×
NEW
39
        createdTime: true,
×
NEW
40
        lastModifiedTime: true,
×
NEW
41
      },
×
NEW
42
      where: {
×
NEW
43
        spaceId,
×
NEW
44
      },
×
NEW
45
    });
×
46

NEW
47
    return rawWebHookList.map((item) => this.wrapRawWebhook(item));
×
NEW
48
  }
×
49

50
  async getWebhookById(webhookId: string): Promise<IWebhookVo> {
101✔
NEW
51
    const rawData = await this.prismaService.webhook.findUniqueOrThrow({
×
NEW
52
      select: {
×
NEW
53
        id: true,
×
NEW
54
        url: true,
×
NEW
55
        contentType: true,
×
NEW
56
        secret: true,
×
NEW
57
        events: true,
×
NEW
58
        isEnabled: true,
×
NEW
59
        createdTime: true,
×
NEW
60
        lastModifiedTime: true,
×
NEW
61
      },
×
NEW
62
      where: {
×
NEW
63
        id: webhookId,
×
NEW
64
      },
×
NEW
65
    });
×
66

NEW
67
    return this.wrapRawWebhook(rawData);
×
NEW
68
  }
×
69

70
  async getWebhookListBySpaceId(
101✔
NEW
71
    spaceId: string
×
NEW
72
  ): Promise<(IWebhookVo & { secret: string | null })[]> {
×
NEW
73
    const rawDataList = await this.prismaService.webhook.findMany({
×
NEW
74
      select: {
×
NEW
75
        id: true,
×
NEW
76
        url: true,
×
NEW
77
        contentType: true,
×
NEW
78
        secret: true,
×
NEW
79
        events: true,
×
NEW
80
        isEnabled: true,
×
NEW
81
        createdTime: true,
×
NEW
82
        lastModifiedTime: true,
×
NEW
83
      },
×
NEW
84
      where: {
×
NEW
85
        spaceId,
×
NEW
86
      },
×
NEW
87
    });
×
88

NEW
89
    return rawDataList.map((item) => {
×
NEW
90
      return {
×
NEW
91
        ...this.wrapRawWebhook(item),
×
NEW
92
        secret: item.secret,
×
NEW
93
      };
×
NEW
94
    });
×
NEW
95
  }
×
96

97
  async createWebhook(body: ICreateWebhookRo): Promise<IWebhookVo> {
101✔
NEW
98
    const { spaceId } = body;
×
NEW
99
    await this.checkWebhookLimit(spaceId);
×
100

NEW
101
    const rawWebHook = await this.create(body);
×
NEW
102
    return this.wrapRawWebhook(rawWebHook);
×
NEW
103
  }
×
104

105
  async deleteWebhook(webhookId: string) {
101✔
NEW
106
    await this.prismaService.webhook.delete({
×
NEW
107
      where: {
×
NEW
108
        id: webhookId,
×
NEW
109
      },
×
NEW
110
    });
×
NEW
111
  }
×
112

113
  async updateWebhook(webhookId: string, body: IUpdateWebhookRo): Promise<IWebhookVo> {
101✔
NEW
114
    const rawWebHook = await this.update(webhookId, body);
×
NEW
115
    return this.wrapRawWebhook(rawWebHook);
×
NEW
116
  }
×
117

118
  async getWebhookRunHistoryList(
101✔
NEW
119
    webhookId: string,
×
NEW
120
    query: IGetWebhookRunHistoryListQuery
×
NEW
121
  ): Promise<IWebhookRunHistoriesVo> {
×
NEW
122
    const { cursor } = query;
×
NEW
123
    const limit = 10;
×
124

NEW
125
    const rawDataList = await this.prismaService.webhookRunHistory.findMany({
×
NEW
126
      where: {
×
NEW
127
        webhookId,
×
NEW
128
      },
×
NEW
129
      take: limit + 1,
×
NEW
130
      cursor: cursor ? { id: cursor } : undefined,
×
NEW
131
      orderBy: {
×
NEW
132
        createdTime: 'desc',
×
NEW
133
      },
×
NEW
134
    });
×
135

NEW
136
    const runHistories = rawDataList.map((v) => {
×
NEW
137
      return {
×
NEW
138
        id: v.id,
×
NEW
139
        webhookId: v.webhookId,
×
NEW
140
        event: v.event,
×
NEW
141
        status: v.status,
×
NEW
142
        request: v.request && JSON.parse(v.request),
×
NEW
143
        response: v.response && JSON.parse(v.response),
×
NEW
144
        createdTime: v.createdTime.toISOString(),
×
NEW
145
        finishedTime: v.finishedTime?.toISOString(),
×
NEW
146
      };
×
NEW
147
    });
×
148

NEW
149
    let nextCursor: typeof cursor | undefined = undefined;
×
NEW
150
    if (rawDataList.length > limit) {
×
NEW
151
      const nextItem = rawDataList.pop();
×
NEW
152
      nextCursor = nextItem!.id;
×
NEW
153
    }
×
NEW
154
    return {
×
NEW
155
      runHistories,
×
NEW
156
      nextCursor,
×
NEW
157
    };
×
NEW
158
  }
×
159

160
  async getWebhookRunHistoryById(runHistoryId: string): Promise<IWebhookRunHistoryVo> {
101✔
NEW
161
    const rawData = await this.prismaService.webhookRunHistory.findUniqueOrThrow({
×
NEW
162
      where: {
×
NEW
163
        id: runHistoryId,
×
NEW
164
      },
×
NEW
165
    });
×
166

NEW
167
    return {
×
NEW
168
      id: rawData.id,
×
NEW
169
      webhookId: rawData.webhookId,
×
NEW
170
      event: rawData.event,
×
NEW
171
      status: rawData.status,
×
NEW
172
      request: rawData.request && JSON.parse(rawData.request),
×
NEW
173
      response: rawData.response && JSON.parse(rawData.response),
×
NEW
174
      createdTime: rawData.createdTime.toISOString(),
×
NEW
175
      finishedTime: rawData.finishedTime?.toISOString(),
×
NEW
176
    };
×
NEW
177
  }
×
178

179
  async startRunLog(webhookId: string, event: string, request: string) {
101✔
NEW
180
    const userId = this.cls.get('user.id');
×
NEW
181
    const runHistoryId = generateWebHookRunHistoryId();
×
182

NEW
183
    await this.prismaService.webhookRunHistory.create({
×
NEW
184
      data: {
×
NEW
185
        id: runHistoryId,
×
NEW
186
        webhookId,
×
NEW
187
        event: '',
×
NEW
188
        status: WebhookRunStatus.Running,
×
NEW
189
        request: '',
×
NEW
190
        response: '{}',
×
NEW
191
        createdBy: userId,
×
NEW
192
        createdTime: new Date().toISOString(),
×
NEW
193
      },
×
NEW
194
    });
×
NEW
195
  }
×
196

197
  async endRunLog(runHistoryId: string, response: string, isError?: boolean, errorMsg?: string) {
101✔
NEW
198
    await this.prismaService.webhookRunHistory.update({
×
NEW
199
      data: {
×
NEW
200
        status: WebhookRunStatus.Finished,
×
NEW
201
        response: '',
×
NEW
202
        isError,
×
NEW
203
        errorMsg,
×
NEW
204
        finishedTime: new Date().toISOString(),
×
NEW
205
      },
×
NEW
206
      where: {
×
NEW
207
        id: runHistoryId,
×
NEW
208
      },
×
NEW
209
    });
×
NEW
210
  }
×
211

212
  private wrapRawWebhook(model: IUnPromisify<ReturnType<typeof this.create>>) {
101✔
NEW
213
    const { secret, contentType, ...other } = model;
×
214

NEW
215
    return {
×
NEW
216
      ...other,
×
NEW
217
      contentType: contentType as ContentType,
×
NEW
218
      events: JSON.parse(other.events!),
×
NEW
219
      hasSecret: !secret,
×
NEW
220
      createdTime: other.createdTime?.toISOString(),
×
NEW
221
      lastModifiedTime: other.lastModifiedTime?.toISOString(),
×
NEW
222
    };
×
NEW
223
  }
×
224

225
  private async create(input: ICreateWebhookRo) {
101✔
NEW
226
    const { spaceId, baseIds, url, contentType, secret, events, isEnabled } = input;
×
NEW
227
    const userId = this.cls.get('user.id');
×
NEW
228
    const webhookId = generateWebHookId();
×
229

NEW
230
    return this.prismaService.webhook.create({
×
NEW
231
      select: {
×
NEW
232
        id: true,
×
NEW
233
        url: true,
×
NEW
234
        contentType: true,
×
NEW
235
        secret: true,
×
NEW
236
        events: true,
×
NEW
237
        isEnabled: true,
×
NEW
238
        createdTime: true,
×
NEW
239
        lastModifiedTime: true,
×
NEW
240
      },
×
NEW
241
      data: {
×
NEW
242
        id: webhookId,
×
NEW
243
        spaceId: spaceId,
×
NEW
244
        baseIds: baseIds && JSON.stringify(baseIds),
×
NEW
245
        url: url,
×
NEW
246
        method: 'POST',
×
NEW
247
        contentType: contentType.toString(),
×
NEW
248
        secret: secret,
×
NEW
249
        events: events && JSON.stringify(events),
×
NEW
250
        isEnabled: isEnabled,
×
NEW
251
        createdBy: userId,
×
NEW
252
      },
×
NEW
253
    });
×
NEW
254
  }
×
255

256
  private async update(webhookId: string, input: IUpdateWebhookRo) {
101✔
NEW
257
    const { spaceId, baseIds, url, contentType, secret, events, isEnabled } = input;
×
NEW
258
    const userId = this.cls.get('user.id');
×
259

NEW
260
    return this.prismaService.webhook.update({
×
NEW
261
      select: {
×
NEW
262
        id: true,
×
NEW
263
        url: true,
×
NEW
264
        contentType: true,
×
NEW
265
        secret: true,
×
NEW
266
        events: true,
×
NEW
267
        isEnabled: true,
×
NEW
268
        createdTime: true,
×
NEW
269
        lastModifiedTime: true,
×
NEW
270
      },
×
NEW
271
      data: {
×
NEW
272
        spaceId: spaceId,
×
NEW
273
        baseIds: JSON.stringify(baseIds),
×
NEW
274
        url: url,
×
NEW
275
        method: 'POST',
×
NEW
276
        contentType: contentType,
×
NEW
277
        secret: secret,
×
NEW
278
        events: JSON.stringify(events),
×
NEW
279
        isEnabled: isEnabled,
×
NEW
280
        createdBy: userId,
×
NEW
281
      },
×
NEW
282
      where: {
×
NEW
283
        id: webhookId,
×
NEW
284
      },
×
NEW
285
    });
×
NEW
286
  }
×
287

288
  private async checkWebhookLimit(spaceId: string) {
101✔
NEW
289
    const webhookCount = await this.prismaService.webhook.count({
×
NEW
290
      where: {
×
NEW
291
        spaceId,
×
NEW
292
      },
×
NEW
293
    });
×
294

NEW
295
    const { maxCreateWebhookLimit } = this.thresholdConfig;
×
NEW
296
    if (webhookCount >= maxCreateWebhookLimit) {
×
NEW
297
      throw new BadRequestException(
×
NEW
298
        `Exceed the maximum limit of creating webhooks, the limit is ${maxCreateWebhookLimit}`
×
299
      );
NEW
300
    }
×
NEW
301
  }
×
302
}
101✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc