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

teableio / teable / 10315078073

09 Aug 2024 07:01AM UTC coverage: 82.673% (-0.002%) from 82.675%
10315078073

Pull #806

github

web-flow
Merge 85e7898ed into 7701966d5
Pull Request #806: fix: the attachments rendering in the record history

4426 of 4647 branches covered (95.24%)

76 of 83 new or added lines in 2 files covered. (91.57%)

1 existing line in 1 file now uncovered.

29324 of 35470 relevant lines covered (82.67%)

1241.57 hits per line

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

92.41
/apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts
1
import { Injectable, NotFoundException } from '@nestjs/common';
4✔
2
import type { IAttachmentCellValue } from '@teable/core';
4✔
3
import { FieldKeyType, FieldType } from '@teable/core';
4✔
4
import { PrismaService } from '@teable/db-main-prisma';
4✔
5
import { UploadType } from '@teable/openapi';
4✔
6
import type {
4✔
7
  IRecordHistoryItemVo,
4✔
8
  ICreateRecordsRo,
4✔
9
  ICreateRecordsVo,
4✔
10
  IGetRecordHistoryQuery,
4✔
11
  IRecord,
4✔
12
  IRecordHistoryVo,
4✔
13
  IRecordInsertOrderRo,
4✔
14
  IUpdateRecordRo,
4✔
15
  IUpdateRecordsRo,
4✔
16
} from '@teable/openapi';
4✔
17
import { forEach, keyBy, map } from 'lodash';
4✔
18
import { AttachmentsStorageService } from '../../attachments/attachments-storage.service';
4✔
19
import StorageAdapter from '../../attachments/plugins/adapter';
4✔
20
import { getFullStorageUrl } from '../../attachments/plugins/utils';
4✔
21
import { CollaboratorService } from '../../collaborator/collaborator.service';
4✔
22
import { FieldConvertingService } from '../../field/field-calculate/field-converting.service';
4✔
23
import { createFieldInstanceByRaw } from '../../field/model/factory';
4✔
24
import { ViewOpenApiService } from '../../view/open-api/view-open-api.service';
4✔
25
import { ViewService } from '../../view/view.service';
4✔
26
import { RecordCalculateService } from '../record-calculate/record-calculate.service';
4✔
27
import { RecordService } from '../record.service';
4✔
28
import { TypeCastAndValidate } from '../typecast.validate';
4✔
29

4✔
30
@Injectable()
4✔
31
export class RecordOpenApiService {
4✔
32
  constructor(
150✔
33
    private readonly recordCalculateService: RecordCalculateService,
150✔
34
    private readonly prismaService: PrismaService,
150✔
35
    private readonly recordService: RecordService,
150✔
36
    private readonly fieldConvertingService: FieldConvertingService,
150✔
37
    private readonly attachmentsStorageService: AttachmentsStorageService,
150✔
38
    private readonly collaboratorService: CollaboratorService,
150✔
39
    private readonly viewService: ViewService,
150✔
40
    private readonly viewOpenApiService: ViewOpenApiService
150✔
41
  ) {}
150✔
42

150✔
43
  async multipleCreateRecords(
150✔
44
    tableId: string,
96✔
45
    createRecordsRo: ICreateRecordsRo
96✔
46
  ): Promise<ICreateRecordsVo> {
96✔
47
    return await this.prismaService.$tx(async () => {
96✔
48
      return await this.createRecords(tableId, createRecordsRo);
96✔
49
    });
96✔
50
  }
96✔
51

150✔
52
  /**
150✔
53
   * create records without any ops, only typecast and sql
150✔
54
   * @param tableId
150✔
55
   * @param createRecordsRo
150✔
56
   */
150✔
57
  async createRecordsOnlySql(tableId: string, createRecordsRo: ICreateRecordsRo): Promise<void> {
150✔
58
    await this.prismaService.$tx(async () => {
6✔
59
      return await this.createPureRecords(tableId, createRecordsRo);
6✔
60
    });
6✔
61
  }
6✔
62

150✔
63
  private async getRecordOrderIndexes(
150✔
64
    tableId: string,
6✔
65
    orderRo: IRecordInsertOrderRo,
6✔
66
    recordCount: number
6✔
67
  ) {
6✔
68
    const dbTableName = await this.recordService.getDbTableName(tableId);
6✔
69

6✔
70
    const indexField = await this.viewService.getOrCreateViewIndexField(
6✔
71
      dbTableName,
6✔
72
      orderRo.viewId
6✔
73
    );
6✔
74
    let indexes: number[] = [];
6✔
75
    await this.viewOpenApiService.updateRecordOrdersInner({
6✔
76
      tableId,
6✔
77
      dbTableName,
6✔
78
      itemLength: recordCount,
6✔
79
      indexField,
6✔
80
      orderRo,
6✔
81
      update: async (result) => {
6✔
82
        indexes = result;
6✔
83
      },
6✔
84
    });
6✔
85

6✔
86
    return indexes;
6✔
87
  }
6✔
88

150✔
89
  async createRecords(
150✔
90
    tableId: string,
1,149✔
91
    createRecordsRo: ICreateRecordsRo
1,149✔
92
  ): Promise<ICreateRecordsVo> {
1,149✔
93
    const { fieldKeyType = FieldKeyType.Name, records, typecast, order } = createRecordsRo;
1,149✔
94

1,149✔
95
    const typecastRecords = await this.validateFieldsAndTypecast(
1,149✔
96
      tableId,
1,149✔
97
      records,
1,149✔
98
      fieldKeyType,
1,149✔
99
      typecast
1,149✔
100
    );
1,141✔
101

1,141✔
102
    const indexes = order && (await this.getRecordOrderIndexes(tableId, order, records.length));
1,149✔
103
    const orderIndex = indexes ? { viewId: order.viewId, indexes } : undefined;
1,149✔
104

1,149✔
105
    return await this.recordCalculateService.createRecords(
1,149✔
106
      tableId,
1,149✔
107
      typecastRecords,
1,149✔
108
      fieldKeyType,
1,149✔
109
      orderIndex
1,149✔
110
    );
1,149✔
111
  }
1,149✔
112

150✔
113
  private async createPureRecords(
150✔
114
    tableId: string,
6✔
115
    createRecordsRo: ICreateRecordsRo
6✔
116
  ): Promise<void> {
6✔
117
    const { fieldKeyType = FieldKeyType.Name, records, typecast } = createRecordsRo;
6✔
118
    const typecastRecords = await this.validateFieldsAndTypecast(
6✔
119
      tableId,
6✔
120
      records,
6✔
121
      fieldKeyType,
6✔
122
      typecast
6✔
123
    );
6✔
124

6✔
125
    await this.recordService.createRecordsOnlySql(tableId, typecastRecords);
6✔
126
  }
6✔
127

150✔
128
  async updateRecords(tableId: string, updateRecordsRo: IUpdateRecordsRo) {
150✔
129
    return await this.prismaService.$tx(async () => {
6✔
130
      // validate cellValue and typecast
6✔
131
      const typecastRecords = await this.validateFieldsAndTypecast(
6✔
132
        tableId,
6✔
133
        updateRecordsRo.records,
6✔
134
        updateRecordsRo.fieldKeyType,
6✔
135
        updateRecordsRo.typecast
6✔
136
      );
6✔
137

6✔
138
      await this.recordCalculateService.calculateUpdatedRecord(
6✔
139
        tableId,
6✔
140
        updateRecordsRo.fieldKeyType,
6✔
141
        typecastRecords
6✔
142
      );
6✔
143
    });
6✔
144
  }
6✔
145

150✔
146
  private async getEffectFieldInstances(
150✔
147
    tableId: string,
2,109✔
148
    recordsFields: Record<string, unknown>[],
2,109✔
149
    fieldKeyType: FieldKeyType = FieldKeyType.Name
2,109✔
150
  ) {
2,109✔
151
    const fieldIdsOrNamesSet = recordsFields.reduce<Set<string>>((acc, recordFields) => {
2,109✔
152
      const fieldIds = Object.keys(recordFields);
5,005✔
153
      forEach(fieldIds, (fieldId) => acc.add(fieldId));
5,005✔
154
      return acc;
5,005✔
155
    }, new Set());
2,109✔
156

2,109✔
157
    const usedFieldIdsOrNames = Array.from(fieldIdsOrNamesSet);
2,109✔
158

2,109✔
159
    const usedFields = await this.prismaService.txClient().field.findMany({
2,109✔
160
      where: {
2,109✔
161
        tableId,
2,109✔
162
        [fieldKeyType]: { in: usedFieldIdsOrNames },
2,109✔
163
        deletedTime: null,
2,109✔
164
      },
2,109✔
165
    });
2,109✔
166

2,109✔
167
    if (usedFields.length !== usedFieldIdsOrNames.length) {
2,109✔
168
      const usedSet = new Set(map(usedFields, fieldKeyType));
×
169
      const missedFields = usedFieldIdsOrNames.filter(
×
170
        (fieldIdOrName) => !usedSet.has(fieldIdOrName)
×
171
      );
×
172
      throw new NotFoundException(`Field ${fieldKeyType}: ${missedFields.join()} not found`);
×
173
    }
×
174
    return map(usedFields, createFieldInstanceByRaw);
2,109✔
175
  }
2,109✔
176

150✔
177
  async validateFieldsAndTypecast<
150✔
178
    T extends {
2,109✔
179
      fields: Record<string, unknown>;
2,109✔
180
    },
2,109✔
181
  >(
2,109✔
182
    tableId: string,
2,109✔
183
    records: T[],
2,109✔
184
    fieldKeyType: FieldKeyType = FieldKeyType.Name,
2,109✔
185
    typecast?: boolean
2,109✔
186
  ): Promise<T[]> {
2,109✔
187
    const recordsFields = map(records, 'fields');
2,109✔
188
    const effectFieldInstance = await this.getEffectFieldInstances(
2,109✔
189
      tableId,
2,109✔
190
      recordsFields,
2,109✔
191
      fieldKeyType
2,109✔
192
    );
2,109✔
193

2,109✔
194
    const newRecordsFields: Record<string, unknown>[] = recordsFields.map(() => ({}));
2,109✔
195
    for (const field of effectFieldInstance) {
2,109✔
196
      const typeCastAndValidate = new TypeCastAndValidate({
1,656✔
197
        services: {
1,656✔
198
          prismaService: this.prismaService,
1,656✔
199
          fieldConvertingService: this.fieldConvertingService,
1,656✔
200
          recordService: this.recordService,
1,656✔
201
          attachmentsStorageService: this.attachmentsStorageService,
1,656✔
202
          collaboratorService: this.collaboratorService,
1,656✔
203
        },
1,656✔
204
        field,
1,656✔
205
        tableId,
1,656✔
206
        typecast,
1,656✔
207
      });
1,656✔
208
      const fieldIdOrName = field[fieldKeyType];
1,656✔
209

1,656✔
210
      const cellValues = recordsFields.map((recordFields) => recordFields[fieldIdOrName]);
1,656✔
211

1,656✔
212
      const newCellValues = await typeCastAndValidate.typecastCellValuesWithField(cellValues);
1,656✔
213
      newRecordsFields.forEach((recordField, i) => {
1,646✔
214
        // do not generate undefined field key
5,966✔
215
        if (newCellValues[i] !== undefined) {
5,966✔
216
          recordField[fieldIdOrName] = newCellValues[i];
4,499✔
217
        }
4,499✔
218
      });
5,966✔
219
    }
1,646✔
220

2,099✔
221
    return records.map((record, i) => ({
2,099✔
222
      ...record,
4,995✔
223
      fields: newRecordsFields[i],
4,995✔
224
    }));
4,995✔
225
  }
2,099✔
226

150✔
227
  async updateRecord(
150✔
228
    tableId: string,
948✔
229
    recordId: string,
948✔
230
    updateRecordRo: IUpdateRecordRo
948✔
231
  ): Promise<IRecord> {
948✔
232
    return await this.prismaService.$tx(async () => {
948✔
233
      const { order, ...recordRo } = updateRecordRo;
948✔
234
      if (order != null) {
948✔
235
        const { viewId, anchorId, position } = order;
×
236
        await this.viewOpenApiService.updateRecordOrders(tableId, viewId, {
×
237
          anchorId,
×
238
          position,
×
239
          recordIds: [recordId],
×
240
        });
×
241
      }
×
242

948✔
243
      const { fieldKeyType = FieldKeyType.Name, typecast, record } = recordRo;
948✔
244

948✔
245
      const typecastRecords = await this.validateFieldsAndTypecast(
948✔
246
        tableId,
948✔
247
        [{ id: recordId, fields: record.fields }],
948✔
248
        fieldKeyType,
948✔
249
        typecast
948✔
250
      );
946✔
251

946✔
252
      await this.recordCalculateService.calculateUpdatedRecord(
946✔
253
        tableId,
946✔
254
        fieldKeyType,
946✔
255
        typecastRecords
946✔
256
      );
932✔
257

932✔
258
      // return record result
932✔
259
      const snapshots = await this.recordService.getSnapshotBulk(
932✔
260
        tableId,
932✔
261
        [recordId],
932✔
262
        undefined,
932✔
263
        fieldKeyType
932✔
264
      );
932✔
265

932✔
266
      if (snapshots.length !== 1) {
948✔
267
        throw new Error('update record failed');
×
268
      }
✔
269
      return snapshots[0].data;
932✔
270
    });
932✔
271
  }
948✔
272

150✔
273
  async deleteRecord(tableId: string, recordId: string) {
150✔
274
    return this.deleteRecords(tableId, [recordId]);
20✔
275
  }
20✔
276

150✔
277
  async deleteRecords(tableId: string, recordIds: string[]) {
150✔
278
    return await this.prismaService.$tx(async () => {
36✔
279
      await this.recordCalculateService.calculateDeletedRecord(tableId, recordIds);
36✔
280

36✔
281
      await this.recordService.batchDeleteRecords(tableId, recordIds);
36✔
282
    });
36✔
283
  }
36✔
284

150✔
285
  async getRecordHistory(
150✔
286
    tableId: string,
5✔
287
    query: IGetRecordHistoryQuery
5✔
288
  ): Promise<IRecordHistoryVo> {
5✔
289
    const { recordId, cursor, startDate, endDate } = query;
5✔
290
    const limit = 20;
5✔
291

5✔
292
    const dateFilter: { [key: string]: Date } = {};
5✔
293
    if (startDate) {
5✔
294
      dateFilter['gte'] = new Date(startDate);
×
295
    }
×
296
    if (endDate) {
5✔
297
      dateFilter['lte'] = new Date(endDate);
×
298
    }
×
299

5✔
300
    const list = await this.prismaService.recordHistory.findMany({
5✔
301
      where: {
5✔
302
        tableId,
5✔
303
        ...(recordId ? { recordId } : {}),
5✔
304
        ...(Object.keys(dateFilter).length > 0 ? { createdTime: dateFilter } : {}),
5✔
305
      },
5✔
306
      select: {
5✔
307
        id: true,
5✔
308
        recordId: true,
5✔
309
        fieldId: true,
5✔
310
        before: true,
5✔
311
        after: true,
5✔
312
        createdTime: true,
5✔
313
        createdBy: true,
5✔
314
      },
5✔
315
      take: limit + 1,
5✔
316
      cursor: cursor ? { id: cursor } : undefined,
5✔
317
      orderBy: {
5✔
318
        createdTime: 'desc',
5✔
319
      },
5✔
320
    });
5✔
321

5✔
322
    let nextCursor: typeof cursor | undefined = undefined;
5✔
323

5✔
324
    if (list.length > limit) {
5✔
325
      const nextItem = list.pop();
×
326
      nextCursor = nextItem?.id;
×
327
    }
×
328

5✔
329
    const createdBySet: Set<string> = new Set();
5✔
330
    const historyList: IRecordHistoryItemVo[] = [];
5✔
331

5✔
332
    for (const item of list) {
5✔
333
      const { id, recordId, fieldId, before, after, createdTime, createdBy } = item;
4✔
334

4✔
335
      createdBySet.add(createdBy);
4✔
336
      const beforeObj = JSON.parse(before as string);
4✔
337
      const afterObj = JSON.parse(after as string);
4✔
338
      const { meta: beforeMeta, data: beforeData } = beforeObj as IRecordHistoryItemVo['before'];
4✔
339
      const { meta: afterMeta, data: afterData } = afterObj as IRecordHistoryItemVo['after'];
4✔
340
      const { type: beforeType } = beforeMeta;
4✔
341
      const { type: afterType } = afterMeta;
4✔
342

4✔
343
      if (beforeType === FieldType.Attachment) {
4✔
NEW
344
        beforeObj.data = await this.recordService.getAttachmentPresignedCellValue(
×
NEW
345
          beforeData as IAttachmentCellValue
×
NEW
346
        );
×
NEW
347
      }
×
348

4✔
349
      if (afterType === FieldType.Attachment) {
4✔
NEW
350
        afterObj.data = await this.recordService.getAttachmentPresignedCellValue(
×
NEW
351
          afterData as IAttachmentCellValue
×
NEW
352
        );
×
UNCOV
353
      }
×
354

4✔
355
      historyList.push({
4✔
356
        id,
4✔
357
        tableId,
4✔
358
        recordId,
4✔
359
        fieldId,
4✔
360
        before: beforeObj,
4✔
361
        after: afterObj,
4✔
362
        createdTime: createdTime.toISOString(),
4✔
363
        createdBy,
4✔
364
      });
4✔
365
    }
4✔
366

5✔
367
    const userList = await this.prismaService.user.findMany({
5✔
368
      where: {
5✔
369
        id: {
5✔
370
          in: Array.from(createdBySet),
5✔
371
        },
5✔
372
      },
5✔
373
      select: {
5✔
374
        id: true,
5✔
375
        name: true,
5✔
376
        email: true,
5✔
377
        avatar: true,
5✔
378
      },
5✔
379
    });
5✔
380

5✔
381
    const handledUserList = userList.map((user) => {
5✔
382
      const { avatar } = user;
4✔
383
      return {
4✔
384
        ...user,
4✔
385
        avatar: avatar && getFullStorageUrl(StorageAdapter.getBucket(UploadType.Avatar), avatar),
4✔
386
      };
4✔
387
    });
4✔
388

5✔
389
    return {
5✔
390
      historyList,
5✔
391
      userMap: keyBy(handledUserList, 'id'),
5✔
392
      nextCursor,
5✔
393
    };
5✔
394
  }
5✔
395
}
150✔
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