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

teableio / teable / 8421654220

25 Mar 2024 02:22PM CUT coverage: 79.934% (+53.8%) from 26.087%
8421654220

Pull #495

github

web-flow
Merge 4faeebea5 into 1869c986d
Pull Request #495: chore: add licenses for non-NPM packages

3256 of 3853 branches covered (84.51%)

25152 of 31466 relevant lines covered (79.93%)

1188.29 hits per line

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

69.74
/apps/nestjs-backend/src/features/selection/selection.service.ts
1
import { BadRequestException, Injectable } from '@nestjs/common';
2✔
2
import type {
2✔
3
  IDateFieldOptions,
2✔
4
  IFieldOptionsRo,
2✔
5
  IFieldOptionsVo,
2✔
6
  IFieldRo,
2✔
7
  IFieldVo,
2✔
8
  INumberFieldOptionsRo,
2✔
9
  IRecord,
2✔
10
  ISingleLineTextFieldOptions,
2✔
11
  IUserFieldOptions,
2✔
12
} from '@teable/core';
2✔
13
import {
2✔
14
  CellValueType,
2✔
15
  FieldKeyType,
2✔
16
  FieldType,
2✔
17
  datetimeFormattingSchema,
2✔
18
  defaultDatetimeFormatting,
2✔
19
  defaultNumberFormatting,
2✔
20
  defaultUserFieldOptions,
2✔
21
  nullsToUndefined,
2✔
22
  numberFormattingSchema,
2✔
23
  parseClipboardText,
2✔
24
  singleLineTextShowAsSchema,
2✔
25
  singleNumberShowAsSchema,
2✔
26
  stringifyClipboardText,
2✔
27
} from '@teable/core';
2✔
28
import { PrismaService } from '@teable/db-main-prisma';
2✔
29
import type {
2✔
30
  IUpdateRecordsRo,
2✔
31
  IRangesToIdQuery,
2✔
32
  IRangesToIdVo,
2✔
33
  IPasteRo,
2✔
34
  IPasteVo,
2✔
35
  IRangesRo,
2✔
36
} from '@teable/openapi';
2✔
37
import { IdReturnType, RangeType } from '@teable/openapi';
2✔
38
import { isNumber, isString, map, pick } from 'lodash';
2✔
39
import { ClsService } from 'nestjs-cls';
2✔
40
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
2✔
41
import type { IClsStore } from '../../types/cls';
2✔
42
import { AggregationService } from '../aggregation/aggregation.service';
2✔
43
import { CollaboratorService } from '../collaborator/collaborator.service';
2✔
44
import { FieldCreatingService } from '../field/field-calculate/field-creating.service';
2✔
45
import { FieldSupplementService } from '../field/field-calculate/field-supplement.service';
2✔
46
import { FieldService } from '../field/field.service';
2✔
47
import type { IFieldInstance } from '../field/model/factory';
2✔
48
import { createFieldInstanceByVo } from '../field/model/factory';
2✔
49
import { AttachmentFieldDto } from '../field/model/field-dto/attachment-field.dto';
2✔
50
import { RecordOpenApiService } from '../record/open-api/record-open-api.service';
2✔
51
import { RecordService } from '../record/record.service';
2✔
52

2✔
53
@Injectable()
2✔
54
export class SelectionService {
2✔
55
  constructor(
64✔
56
    private readonly recordService: RecordService,
64✔
57
    private readonly fieldService: FieldService,
64✔
58
    private readonly prismaService: PrismaService,
64✔
59
    private readonly aggregationService: AggregationService,
64✔
60
    private readonly recordOpenApiService: RecordOpenApiService,
64✔
61
    private readonly fieldCreatingService: FieldCreatingService,
64✔
62
    private readonly fieldSupplementService: FieldSupplementService,
64✔
63
    private readonly collaboratorService: CollaboratorService,
64✔
64
    private readonly cls: ClsService<IClsStore>,
64✔
65
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
64✔
66
  ) {}
64✔
67

64✔
68
  async getIdsFromRanges(tableId: string, query: IRangesToIdQuery): Promise<IRangesToIdVo> {
64✔
69
    const { returnType } = query;
18✔
70
    if (returnType === IdReturnType.RecordId) {
18✔
71
      return {
6✔
72
        recordIds: await this.rowSelectionToIds(tableId, query),
6✔
73
      };
6✔
74
    }
6✔
75

12✔
76
    if (returnType === IdReturnType.FieldId) {
18✔
77
      return {
6✔
78
        fieldIds: await this.columnSelectionToIds(tableId, query),
6✔
79
      };
6✔
80
    }
6✔
81

6✔
82
    if (returnType === IdReturnType.All) {
6✔
83
      return {
6✔
84
        fieldIds: await this.columnSelectionToIds(tableId, query),
6✔
85
        recordIds: await this.rowSelectionToIds(tableId, query),
6✔
86
      };
6✔
87
    }
6!
88

×
89
    throw new BadRequestException('Invalid return type');
×
90
  }
×
91

64✔
92
  private async columnSelectionToIds(tableId: string, query: IRangesToIdQuery): Promise<string[]> {
64✔
93
    const { type, viewId, ranges } = query;
12✔
94
    const result = await this.fieldService.getDocIdsByQuery(tableId, {
12✔
95
      viewId,
12✔
96
      filterHidden: true,
12✔
97
    });
12✔
98

12✔
99
    if (type === RangeType.Rows) {
12✔
100
      return result.ids;
4✔
101
    }
4✔
102

8✔
103
    if (type === RangeType.Columns) {
12✔
104
      return ranges.reduce<string[]>((acc, range) => {
4✔
105
        return acc.concat(result.ids.slice(range[0], range[1] + 1));
4✔
106
      }, []);
4✔
107
    }
4✔
108

4✔
109
    const [start, end] = ranges;
4✔
110
    return result.ids.slice(start[0], end[0] + 1);
4✔
111
  }
4✔
112

64✔
113
  private async rowSelectionToIds(tableId: string, query: IRangesToIdQuery): Promise<string[]> {
64✔
114
    const { type, ranges } = query;
12✔
115
    if (type === RangeType.Columns) {
12✔
116
      const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
117
        ...query,
4✔
118
        skip: 0,
4✔
119
        take: -1,
4✔
120
      });
4✔
121
      return result.ids;
4✔
122
    }
4✔
123

8✔
124
    if (type === RangeType.Rows) {
12✔
125
      let recordIds: string[] = [];
4✔
126
      const total = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
4✔
127
      if (total > this.thresholdConfig.maxReadRows) {
4!
128
        throw new BadRequestException(`Exceed max read rows ${this.thresholdConfig.maxReadRows}`);
×
129
      }
×
130
      for (const [start, end] of ranges) {
4✔
131
        const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
132
          ...query,
4✔
133
          skip: start,
4✔
134
          take: end + 1 - start,
4✔
135
        });
4✔
136
        recordIds = recordIds.concat(result.ids);
4✔
137
      }
4✔
138

4✔
139
      return ranges.reduce<string[]>((acc, range) => {
4✔
140
        return acc.concat(recordIds.slice(range[0], range[1] + 1));
4✔
141
      }, []);
4✔
142
    }
4✔
143

4✔
144
    const [start, end] = ranges;
4✔
145
    const total = end[1] - start[1] + 1;
4✔
146
    if (total > this.thresholdConfig.maxReadRows) {
12!
147
      throw new BadRequestException(`Exceed max read rows ${this.thresholdConfig.maxReadRows}`);
×
148
    }
✔
149
    const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
150
      ...query,
4✔
151
      skip: start[1],
4✔
152
      take: end[1] + 1 - start[1],
4✔
153
    });
4✔
154

4✔
155
    return result.ids;
4✔
156
  }
4✔
157

64✔
158
  private fieldsToProjection(fields: IFieldVo[], fieldKeyType: FieldKeyType) {
64✔
159
    return fields.map((f) => f[fieldKeyType]);
2✔
160
  }
2✔
161

64✔
162
  private async columnsSelectionCtx(tableId: string, rangesRo: IRangesRo) {
64✔
163
    const { ranges, type, ...queryRo } = rangesRo;
×
164

×
165
    const fields = await this.fieldService.getFieldsByQuery(tableId, {
×
166
      viewId: queryRo.viewId,
×
167
      filterHidden: true,
×
168
    });
×
169

×
170
    const records = await this.recordService.getRecordsFields(tableId, {
×
171
      ...queryRo,
×
172
      skip: 0,
×
173
      take: -1,
×
174
      fieldKeyType: FieldKeyType.Id,
×
175
      projection: this.fieldsToProjection(fields, FieldKeyType.Id),
×
176
    });
×
177

×
178
    return {
×
179
      records,
×
180
      fields: ranges.reduce((acc, range) => {
×
181
        return acc.concat(fields.slice(range[0], range[1] + 1));
×
182
      }, [] as IFieldVo[]),
×
183
    };
×
184
  }
×
185

64✔
186
  private async rowsSelectionCtx(tableId: string, rangesRo: IRangesRo) {
64✔
187
    const { ranges, type, ...queryRo } = rangesRo;
×
188
    const fields = await this.fieldService.getFieldsByQuery(tableId, {
×
189
      viewId: queryRo.viewId,
×
190
      filterHidden: true,
×
191
    });
×
192
    let records: Pick<IRecord, 'id' | 'fields'>[] = [];
×
193
    for (const [start, end] of ranges) {
×
194
      const recordsFields = await this.recordService.getRecordsFields(tableId, {
×
195
        ...queryRo,
×
196
        skip: start,
×
197
        take: end + 1 - start,
×
198
        fieldKeyType: FieldKeyType.Id,
×
199
        projection: this.fieldsToProjection(fields, FieldKeyType.Id),
×
200
      });
×
201
      records = records.concat(recordsFields);
×
202
    }
×
203

×
204
    return {
×
205
      records,
×
206
      fields,
×
207
    };
×
208
  }
×
209

64✔
210
  private async defaultSelectionCtx(tableId: string, rangesRo: IRangesRo) {
64✔
211
    const { ranges, type, ...queryRo } = rangesRo;
2✔
212
    const [start, end] = ranges;
2✔
213
    const fields = await this.fieldService.getFieldInstances(tableId, {
2✔
214
      viewId: queryRo.viewId,
2✔
215
      filterHidden: true,
2✔
216
    });
2✔
217

2✔
218
    const records = await this.recordService.getRecordsFields(tableId, {
2✔
219
      ...queryRo,
2✔
220
      skip: start[1],
2✔
221
      take: end[1] + 1 - start[1],
2✔
222
      fieldKeyType: FieldKeyType.Id,
2✔
223
      projection: this.fieldsToProjection(fields, FieldKeyType.Id),
2✔
224
    });
2✔
225
    return { records, fields: fields.slice(start[0], end[0] + 1) };
2✔
226
  }
2✔
227

64✔
228
  private async parseRange(
64✔
229
    tableId: string,
8✔
230
    rangesRo: IRangesRo
8✔
231
  ): Promise<{ cellCount: number; columnCount: number; rowCount: number }> {
8✔
232
    const { ranges, type, ...queryRo } = rangesRo;
8✔
233
    switch (type) {
8✔
234
      case RangeType.Columns: {
8!
235
        const { rowCount } = await this.aggregationService.performRowCount(tableId, queryRo);
×
236
        const columnCount = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
×
237
        const cellCount = rowCount * columnCount;
×
238

×
239
        return { cellCount, columnCount, rowCount };
×
240
      }
×
241
      case RangeType.Rows: {
8!
242
        const fields = await this.fieldService.getFieldsByQuery(tableId, {
×
243
          viewId: queryRo.viewId,
×
244
          filterHidden: true,
×
245
        });
×
246
        const columnCount = fields.length;
×
247
        const rowCount = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
×
248
        const cellCount = rowCount * columnCount;
×
249

×
250
        return { cellCount, columnCount, rowCount };
×
251
      }
×
252
      default: {
8✔
253
        const [start, end] = ranges;
8✔
254
        const columnCount = end[0] - start[0] + 1;
8✔
255
        const rowCount = end[1] - start[1] + 1;
8✔
256
        const cellCount = rowCount * columnCount;
8✔
257

8✔
258
        return { cellCount, columnCount, rowCount };
8✔
259
      }
8✔
260
    }
8✔
261
  }
8✔
262

64✔
263
  private async getSelectionCtxByRange(tableId: string, rangesRo: IRangesRo) {
64✔
264
    const { type } = rangesRo;
2✔
265
    switch (type) {
2✔
266
      case RangeType.Columns: {
2!
267
        return await this.columnsSelectionCtx(tableId, rangesRo);
×
268
      }
×
269
      case RangeType.Rows: {
2!
270
        return await this.rowsSelectionCtx(tableId, rangesRo);
×
271
      }
×
272
      default:
2✔
273
        return await this.defaultSelectionCtx(tableId, rangesRo);
2✔
274
    }
2✔
275
  }
2✔
276

64✔
277
  private async expandRows({
64✔
278
    tableId,
6✔
279
    numRowsToExpand,
6✔
280
  }: {
6✔
281
    tableId: string;
6✔
282
    numRowsToExpand: number;
6✔
283
  }) {
6✔
284
    if (numRowsToExpand === 0) {
6✔
285
      return [];
6✔
286
    }
6!
287
    const records = Array.from({ length: numRowsToExpand }, () => ({ fields: {} }));
×
288
    const createdRecords = await this.recordOpenApiService.createRecords(tableId, { records });
×
289
    return createdRecords.records.map(({ id, fields }) => ({ id, fields }));
×
290
  }
×
291

64✔
292
  private optionsRoToVoByCvType(
64✔
293
    cellValueType: CellValueType,
2✔
294
    options: IFieldOptionsVo = {}
2✔
295
  ): { type: FieldType; options: IFieldOptionsRo } {
2✔
296
    switch (cellValueType) {
2✔
297
      case CellValueType.Number: {
2✔
298
        const numberOptions = options as INumberFieldOptionsRo;
2✔
299
        const formattingRes = numberFormattingSchema.safeParse(numberOptions?.formatting);
2✔
300
        const showAsRes = singleNumberShowAsSchema.safeParse(numberOptions?.showAs);
2✔
301
        return {
2✔
302
          type: FieldType.Number,
2✔
303
          options: {
2✔
304
            formatting: formattingRes.success ? formattingRes?.data : defaultNumberFormatting,
2!
305
            showAs: showAsRes.success ? showAsRes?.data : undefined,
2!
306
          },
2✔
307
        };
2✔
308
      }
2✔
309
      case CellValueType.DateTime: {
2!
310
        const dateOptions = options as IDateFieldOptions;
×
311
        const formattingRes = datetimeFormattingSchema.safeParse(dateOptions?.formatting);
×
312
        return {
×
313
          type: FieldType.Date,
×
314
          options: {
×
315
            formatting: formattingRes.success ? formattingRes?.data : defaultDatetimeFormatting,
×
316
          },
×
317
        };
×
318
      }
×
319
      case CellValueType.String: {
2!
320
        const singleLineTextOptions = options as ISingleLineTextFieldOptions;
×
321
        const showAsRes = singleLineTextShowAsSchema.safeParse(singleLineTextOptions.showAs);
×
322
        return {
×
323
          type: FieldType.SingleLineText,
×
324
          options: {
×
325
            showAs: showAsRes.success ? showAsRes?.data : undefined,
×
326
          },
×
327
        };
×
328
      }
×
329
      case CellValueType.Boolean: {
2!
330
        return {
×
331
          type: FieldType.Checkbox,
×
332
          options: {},
×
333
        };
×
334
      }
×
335
      default:
2!
336
        throw new BadRequestException('Invalid cellValueType');
×
337
    }
2✔
338
  }
2✔
339

64✔
340
  private lookupOptionsRoToVo(field: IFieldVo): { type: FieldType; options: IFieldOptionsRo } {
64✔
341
    const { type, isMultipleCellValue, options } = field;
×
342
    if (type === FieldType.SingleSelect && isMultipleCellValue) {
×
343
      return {
×
344
        type: FieldType.MultipleSelect,
×
345
        options,
×
346
      };
×
347
    }
×
348
    if (type === FieldType.User && isMultipleCellValue) {
×
349
      const userOptions = options as IUserFieldOptions;
×
350
      return {
×
351
        type,
×
352
        options: {
×
353
          ...userOptions,
×
354
          isMultiple: true,
×
355
        },
×
356
      };
×
357
    }
×
358
    return { type, options };
×
359
  }
×
360

64✔
361
  private fieldVoToRo(field?: IFieldVo): IFieldRo {
64✔
362
    if (!field) {
2!
363
      return {
×
364
        type: FieldType.SingleLineText,
×
365
      };
×
366
    }
×
367
    const { isComputed, isLookup } = field;
2✔
368
    const baseField = pick(field, 'name', 'type', 'options', 'description');
2✔
369

2✔
370
    if (isComputed && !isLookup) {
2✔
371
      if ([FieldType.CreatedBy, FieldType.LastModifiedBy].includes(field.type)) {
2!
372
        return {
×
373
          ...baseField,
×
374
          type: FieldType.User,
×
375
          options: defaultUserFieldOptions,
×
376
        };
×
377
      }
×
378
      return {
2✔
379
        ...baseField,
2✔
380
        ...this.optionsRoToVoByCvType(field.cellValueType, field.options),
2✔
381
      };
2✔
382
    }
2!
383

×
384
    if (isLookup) {
×
385
      return {
×
386
        ...baseField,
×
387
        ...this.lookupOptionsRoToVo(field),
×
388
      };
×
389
    }
×
390

×
391
    return baseField;
×
392
  }
×
393

64✔
394
  private async expandColumns({
64✔
395
    tableId,
6✔
396
    header,
6✔
397
    numColsToExpand,
6✔
398
  }: {
6✔
399
    tableId: string;
6✔
400
    header: IFieldVo[];
6✔
401
    numColsToExpand: number;
6✔
402
  }) {
6✔
403
    const colLen = header.length;
6✔
404
    const res: IFieldVo[] = [];
6✔
405
    for (let i = colLen - numColsToExpand; i < colLen; i++) {
6✔
406
      const field = this.fieldVoToRo(header[i]);
2✔
407
      const fieldVo = await this.fieldSupplementService.prepareCreateField(tableId, field);
2✔
408
      const fieldInstance = createFieldInstanceByVo(fieldVo);
2✔
409
      // expend columns do not need to calculate
2✔
410
      await this.fieldCreatingService.alterCreateField(tableId, fieldInstance);
2✔
411
      res.push(fieldVo);
2✔
412
    }
2✔
413
    return res;
6✔
414
  }
6✔
415

64✔
416
  private async collectionAttachment({
64✔
417
    fields,
×
418
    tableData,
×
419
  }: {
×
420
    tableData: string[][];
×
421
    fields: IFieldInstance[];
×
422
  }) {
×
423
    const attachmentFieldsIndex = fields
×
424
      .map((field, index) => (field.type === FieldType.Attachment ? index : null))
×
425
      .filter(isNumber);
×
426

×
427
    const tokens = tableData.reduce((acc, recordData) => {
×
428
      const tokensInRecord = attachmentFieldsIndex.reduce((acc, index) => {
×
429
        const tokensAndNames = recordData[index]
×
430
          .split(',')
×
431
          .map(AttachmentFieldDto.getTokenAndNameByString);
×
432
        return acc.concat(map(tokensAndNames, 'token').filter(isString));
×
433
      }, [] as string[]);
×
434
      return acc.concat(tokensInRecord);
×
435
    }, [] as string[]);
×
436

×
437
    const attachments = await this.prismaService.attachments.findMany({
×
438
      where: {
×
439
        token: {
×
440
          in: tokens,
×
441
        },
×
442
      },
×
443
      select: {
×
444
        token: true,
×
445
        size: true,
×
446
        mimetype: true,
×
447
        width: true,
×
448
        height: true,
×
449
        path: true,
×
450
      },
×
451
    });
×
452
    return attachments.map(nullsToUndefined);
×
453
  }
×
454

64✔
455
  private parseCopyContent(content: string): string[][] {
64✔
456
    return parseClipboardText(content);
6✔
457
  }
6✔
458

64✔
459
  private stringifyCopyContent(content: string[][]): string {
64✔
460
    return stringifyClipboardText(content);
2✔
461
  }
2✔
462

64✔
463
  private calculateExpansion(
64✔
464
    tableSize: [number, number],
6✔
465
    cell: [number, number],
6✔
466
    tableDataSize: [number, number]
6✔
467
  ): [number, number] {
6✔
468
    const permissions = this.cls.get('permissions');
6✔
469
    const [numCols, numRows] = tableSize;
6✔
470
    const [dataNumCols, dataNumRows] = tableDataSize;
6✔
471

6✔
472
    const endCol = cell[0] + dataNumCols;
6✔
473
    const endRow = cell[1] + dataNumRows;
6✔
474

6✔
475
    const numRowsToExpand = Math.max(0, endRow - numRows);
6✔
476
    const numColsToExpand = Math.max(0, endCol - numCols);
6✔
477

6✔
478
    const hasFieldCreatePermission = permissions.includes('field|create');
6✔
479
    const hasRecordCreatePermission = permissions.includes('record|create');
6✔
480
    return [
6✔
481
      hasFieldCreatePermission ? numColsToExpand : 0,
6!
482
      hasRecordCreatePermission ? numRowsToExpand : 0,
6!
483
    ];
6✔
484
  }
6✔
485

64✔
486
  private async fillCells({
64✔
487
    tableId,
6✔
488
    tableData,
6✔
489
    fields,
6✔
490
    records,
6✔
491
  }: {
6✔
492
    tableId: string;
6✔
493
    tableData: string[][];
6✔
494
    fields: IFieldInstance[];
6✔
495
    records: Pick<IRecord, 'id' | 'fields'>[];
6✔
496
  }) {
6✔
497
    const fieldConvertContext = await this.fieldConvertContext(tableId, tableData, fields);
6✔
498

6✔
499
    const updateRecordsRo: IUpdateRecordsRo = {
6✔
500
      fieldKeyType: FieldKeyType.Id,
6✔
501
      typecast: true,
6✔
502
      records: [],
6✔
503
    };
6✔
504
    fields.forEach((field, col) => {
6✔
505
      if (field.isComputed) {
12!
506
        return;
×
507
      }
×
508
      records.forEach((record, row) => {
12✔
509
        const stringValue = tableData?.[row]?.[col] ?? null;
20!
510
        const recordField = updateRecordsRo.records[row]?.fields || {};
20✔
511

20✔
512
        if (stringValue === null) {
20!
513
          recordField[field.id] = null;
×
514
        } else {
20✔
515
          switch (field.type) {
20✔
516
            case FieldType.Attachment:
20!
517
              {
×
518
                recordField[field.id] = field.convertStringToCellValue(
×
519
                  stringValue,
×
520
                  fieldConvertContext?.attachments
×
521
                );
×
522
              }
×
523
              break;
×
524
            case FieldType.SingleSelect:
20!
525
            case FieldType.MultipleSelect:
20!
526
              recordField[field.id] = field.convertStringToCellValue(stringValue, true);
×
527
              break;
×
528
            case FieldType.User:
20!
529
              recordField[field.id] = field.convertStringToCellValue(stringValue, {
×
530
                userSets: fieldConvertContext?.userSets,
×
531
              });
×
532
              break;
×
533
            default:
20✔
534
              recordField[field.id] = field.convertStringToCellValue(stringValue);
20✔
535
          }
20✔
536
        }
20✔
537

20✔
538
        updateRecordsRo.records[row] = {
20✔
539
          id: record.id,
20✔
540
          fields: recordField,
20✔
541
        };
20✔
542
      });
20✔
543
    });
12✔
544
    return updateRecordsRo;
6✔
545
  }
6✔
546

64✔
547
  private async fieldConvertContext(
64✔
548
    tableId: string,
6✔
549
    tableData: string[][],
6✔
550
    fields: IFieldInstance[]
6✔
551
  ) {
6✔
552
    const hasFieldType = (type: FieldType) => fields.some((field) => field.type === type);
6✔
553

6✔
554
    const loadAttachments = hasFieldType(FieldType.Attachment)
6!
555
      ? this.collectionAttachment({ fields, tableData })
×
556
      : Promise.resolve(undefined);
6✔
557

6✔
558
    const loadUserSets = hasFieldType(FieldType.User)
6!
559
      ? this.collaboratorService.getBaseCollabsWithPrimary(tableId)
×
560
      : Promise.resolve(undefined);
6✔
561

6✔
562
    const [attachments, userSets] = await Promise.all([loadAttachments, loadUserSets]);
6✔
563

6✔
564
    return {
6✔
565
      attachments: attachments,
6✔
566
      userSets: userSets,
6✔
567
    };
6✔
568
  }
6✔
569

64✔
570
  async copy(tableId: string, rangesRo: IRangesRo) {
64✔
571
    const { cellCount } = await this.parseRange(tableId, rangesRo);
2✔
572

2✔
573
    if (cellCount > this.thresholdConfig.maxCopyCells) {
2!
574
      throw new BadRequestException(`Exceed max copy cells ${this.thresholdConfig.maxCopyCells}`);
×
575
    }
×
576

2✔
577
    const { fields, records } = await this.getSelectionCtxByRange(tableId, rangesRo);
2✔
578
    const fieldInstances = fields.map(createFieldInstanceByVo);
2✔
579
    const rectangleData = records.map((record) =>
2✔
580
      fieldInstances.map((fieldInstance) =>
6✔
581
        fieldInstance.cellValue2String(record.fields[fieldInstance.id] as never)
12✔
582
      )
2✔
583
    );
2✔
584
    return {
2✔
585
      content: this.stringifyCopyContent(rectangleData),
2✔
586
      header: fields,
2✔
587
    };
2✔
588
  }
2✔
589

64✔
590
  // If the pasted selection is twice the size of the content,
64✔
591
  // the content is automatically expanded to the selection size
64✔
592
  private expandPasteContent(pasteData: string[][], range: [[number, number], [number, number]]) {
64✔
593
    const [start, end] = range;
6✔
594
    const [startCol, startRow] = start;
6✔
595
    const [endCol, endRow] = end;
6✔
596

6✔
597
    const rangeRows = endRow - startRow + 1;
6✔
598
    const rangeCols = endCol - startCol + 1;
6✔
599

6✔
600
    const pasteRows = pasteData.length;
6✔
601
    const pasteCols = pasteData[0].length;
6✔
602

6✔
603
    if (rangeRows % pasteRows !== 0 || rangeCols % pasteCols !== 0) {
6✔
604
      return pasteData;
6✔
605
    }
6!
606

×
607
    return Array.from({ length: rangeRows }, (_, i) =>
×
608
      Array.from({ length: rangeCols }, (_, j) => pasteData[i % pasteRows][j % pasteCols])
×
609
    );
×
610
  }
×
611

64✔
612
  // Paste does not support non-contiguous selections,
64✔
613
  // the first selection is taken by default.
64✔
614
  private getRangeCell(
64✔
615
    maxRange: [number, number][],
6✔
616
    range: [number, number][],
6✔
617
    type?: RangeType
6✔
618
  ): [[number, number], [number, number]] {
6✔
619
    const [maxStart, maxEnd] = maxRange;
6✔
620
    const [maxStartCol, maxStartRow] = maxStart;
6✔
621
    const [maxEndCol, maxEndRow] = maxEnd;
6✔
622

6✔
623
    if (type === RangeType.Columns) {
6!
624
      return [
×
625
        [range[0][0], maxStartRow],
×
626
        [range[0][1], maxEndRow],
×
627
      ];
×
628
    }
×
629

6✔
630
    if (type === RangeType.Rows) {
6!
631
      return [
×
632
        [maxStartCol, range[0][0]],
×
633
        [maxEndCol, range[0][1]],
×
634
      ];
×
635
    }
×
636
    return [range[0], range[1]];
6✔
637
  }
6✔
638

64✔
639
  async paste(tableId: string, pasteRo: IPasteRo) {
64✔
640
    const { content, header = [], ...rangesRo } = pasteRo;
6✔
641
    const { ranges, type, ...queryRo } = rangesRo;
6✔
642
    const { viewId } = queryRo;
6✔
643
    const { cellCount } = await this.parseRange(tableId, rangesRo);
6✔
644

6✔
645
    if (cellCount > this.thresholdConfig.maxPasteCells) {
6!
646
      throw new BadRequestException(`Exceed max paste cells ${this.thresholdConfig.maxPasteCells}`);
×
647
    }
×
648

6✔
649
    const { rowCount: rowCountInView } = await this.aggregationService.performRowCount(
6✔
650
      tableId,
6✔
651
      queryRo
6✔
652
    );
6✔
653
    const fields = await this.fieldService.getFieldInstances(tableId, {
6✔
654
      viewId,
6✔
655
      filterHidden: true,
6✔
656
    });
6✔
657

6✔
658
    const tableSize: [number, number] = [fields.length, rowCountInView];
6✔
659
    const rangeCell = this.getRangeCell(
6✔
660
      [
6✔
661
        [0, 0],
6✔
662
        [tableSize[0] - 1, tableSize[1] - 1],
6✔
663
      ],
6✔
664
      ranges,
6✔
665
      type
6✔
666
    );
6✔
667

6✔
668
    const tableData = this.expandPasteContent(this.parseCopyContent(content), rangeCell);
6✔
669
    const tableColCount = tableData[0].length;
6✔
670
    const tableRowCount = tableData.length;
6✔
671

6✔
672
    const cell = rangeCell[0];
6✔
673
    const [col, row] = cell;
6✔
674

6✔
675
    const effectFields = fields.slice(col, col + tableColCount);
6✔
676

6✔
677
    const projection = effectFields.map((f) => f.id);
6✔
678

6✔
679
    const records = await this.recordService.getRecordsFields(tableId, {
6✔
680
      ...queryRo,
6✔
681
      projection,
6✔
682
      skip: row,
6✔
683
      take: tableData.length,
6✔
684
      fieldKeyType: FieldKeyType.Id,
6✔
685
    });
6✔
686

6✔
687
    const [numColsToExpand, numRowsToExpand] = this.calculateExpansion(tableSize, cell, [
6✔
688
      tableColCount,
6✔
689
      tableRowCount,
6✔
690
    ]);
6✔
691

6✔
692
    const updateRange: IPasteVo['ranges'] = [cell, cell];
6✔
693

6✔
694
    const expandColumns = await this.prismaService.$tx(async () => {
6✔
695
      // Expansion col
6✔
696
      return await this.expandColumns({
6✔
697
        tableId,
6✔
698
        header,
6✔
699
        numColsToExpand,
6✔
700
      });
6✔
701
    });
6✔
702

6✔
703
    await this.prismaService.$tx(async () => {
6✔
704
      // Expansion row
6✔
705
      const expandRows = await this.expandRows({ tableId, numRowsToExpand });
6✔
706

6✔
707
      const updateFields = effectFields.concat(expandColumns.map(createFieldInstanceByVo));
6✔
708
      const updateRecords = records.concat(expandRows);
6✔
709

6✔
710
      // Fill cells
6✔
711
      const updateRecordsRo = await this.fillCells({
6✔
712
        tableId,
6✔
713
        tableData,
6✔
714
        fields: updateFields,
6✔
715
        records: updateRecords,
6✔
716
      });
6✔
717

6✔
718
      updateRange[1] = [col + updateFields.length - 1, row + updateFields.length - 1];
6✔
719
      await this.recordOpenApiService.updateRecords(tableId, updateRecordsRo);
6✔
720
    });
6✔
721

6✔
722
    return updateRange;
6✔
723
  }
6✔
724

64✔
725
  async clear(tableId: string, rangesRo: IRangesRo) {
64✔
726
    const { fields, records } = await this.getSelectionCtxByRange(tableId, rangesRo);
×
727
    const fieldInstances = fields.map(createFieldInstanceByVo);
×
728
    const updateRecordsRo = await this.fillCells({
×
729
      tableId,
×
730
      tableData: [],
×
731
      fields: fieldInstances,
×
732
      records,
×
733
    });
×
734

×
735
    await this.recordOpenApiService.updateRecords(tableId, updateRecordsRo);
×
736
  }
×
737
}
64✔
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