• 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

93.2
/apps/nestjs-backend/src/features/record/typecast.validate.ts
1
import { BadRequestException } from '@nestjs/common';
2✔
2
import type { IAttachmentCellValue, ILinkCellValue } from '@teable/core';
2✔
3
import { ColorUtils, FieldType, generateChoiceId } from '@teable/core';
2✔
4
import type { PrismaService } from '@teable/db-main-prisma';
2✔
5
import { UploadType } from '@teable/openapi';
2✔
6
import { isUndefined, keyBy, map } from 'lodash';
2✔
7
import { fromZodError } from 'zod-validation-error';
2✔
8
import type { AttachmentsStorageService } from '../attachments/attachments-storage.service';
2✔
9
import StorageAdapter from '../attachments/plugins/adapter';
2✔
10
import type { FieldConvertingService } from '../field/field-calculate/field-converting.service';
2✔
11
import type { IFieldInstance } from '../field/model/factory';
2✔
12
import type { LinkFieldDto } from '../field/model/field-dto/link-field.dto';
2✔
13
import type { MultipleSelectFieldDto } from '../field/model/field-dto/multiple-select-field.dto';
2✔
14
import type { SingleSelectFieldDto } from '../field/model/field-dto/single-select-field.dto';
2✔
15
import type { RecordService } from './record.service';
2✔
16

2✔
17
interface IServices {
2✔
18
  prismaService: PrismaService;
2✔
19
  fieldConvertingService: FieldConvertingService;
2✔
20
  recordService: RecordService;
2✔
21
  attachmentsStorageService: AttachmentsStorageService;
2✔
22
}
2✔
23

2✔
24
/**
2✔
25
 * Cell type conversion:
2✔
26
 * Because there are some merge operations, we choose column-by-column conversion here.
2✔
27
 */
2✔
28
export class TypeCastAndValidate {
2✔
29
  private readonly services: IServices;
1,532✔
30
  private readonly field: IFieldInstance;
1,532✔
31
  private readonly tableId: string;
1,532✔
32
  private readonly typecast?: boolean;
1,532✔
33

1,532✔
34
  constructor({
1,532✔
35
    services,
1,532✔
36
    field,
1,532✔
37
    typecast,
1,532✔
38
    tableId,
1,532✔
39
  }: {
1,532✔
40
    services: IServices;
1,532✔
41
    field: IFieldInstance;
1,532✔
42
    typecast?: boolean;
1,532✔
43
    tableId: string;
1,532✔
44
  }) {
1,532✔
45
    this.services = services;
1,532✔
46
    this.field = field;
1,532✔
47
    this.typecast = typecast;
1,532✔
48
    this.tableId = tableId;
1,532✔
49
  }
1,532✔
50

1,532✔
51
  /**
1,532✔
52
   * Attempts to cast a cell value to the appropriate type based on the field configuration.
1,532✔
53
   * Calls the appropriate typecasting method depending on the field type.
1,532✔
54
   */
1,532✔
55
  async typecastCellValuesWithField(cellValues: unknown[]) {
1,532✔
56
    const { type, isComputed } = this.field;
1,532✔
57
    if (isComputed) {
1,532!
58
      return cellValues;
×
59
    }
×
60
    switch (type) {
1,532✔
61
      case FieldType.SingleSelect:
1,532✔
62
        return await this.castToSingleSelect(cellValues);
24✔
63
      case FieldType.MultipleSelect:
1,532✔
64
        return await this.castToMultipleSelect(cellValues);
20✔
65
      case FieldType.Link: {
1,532✔
66
        return await this.castToLink(cellValues);
557✔
67
      }
557✔
68
      case FieldType.User:
1,532✔
69
        return await this.castToUser(cellValues);
20✔
70
      case FieldType.Attachment:
1,532✔
71
        return await this.castToAttachment(cellValues);
2✔
72
      default:
1,532✔
73
        return this.defaultCastTo(cellValues);
909✔
74
    }
1,532✔
75
  }
1,532✔
76

1,532✔
77
  private defaultCastTo(cellValues: unknown[]) {
1,532✔
78
    return this.mapFieldsCellValuesWithValidate(cellValues, (cellValue: unknown) => {
931✔
79
      return this.field.repair(cellValue);
20✔
80
    });
20✔
81
  }
931✔
82

1,532✔
83
  /**
1,532✔
84
   * Traverse fieldRecords, and do validation here.
1,532✔
85
   */
1,532✔
86
  private mapFieldsCellValuesWithValidate(
1,532✔
87
    cellValues: unknown[],
1,532✔
88
    callBack: (cellValue: unknown) => unknown
1,532✔
89
  ) {
1,532✔
90
    return cellValues.map((cellValue) => {
1,532✔
91
      const validate = this.field.validateCellValue(cellValue);
4,746✔
92
      if (cellValue === undefined) {
4,746✔
93
        return;
986✔
94
      }
986✔
95
      if (!validate.success) {
4,746✔
96
        if (this.typecast) {
52✔
97
          return callBack(cellValue);
42✔
98
        } else {
50✔
99
          throw new BadRequestException(fromZodError(validate.error).message);
10✔
100
        }
10✔
101
      }
52✔
102
      return cellValue;
3,708✔
103
    });
3,708✔
104
  }
1,532✔
105

1,532✔
106
  /**
1,532✔
107
   * Converts the provided value to a string array.
1,532✔
108
   * Handles multiple types of input such as arrays, strings, and other types.
1,532✔
109
   */
1,532✔
110
  private valueToStringArray(value: unknown): string[] | null {
1,532✔
111
    if (value == null) {
22!
112
      return null;
×
113
    }
×
114
    if (Array.isArray(value)) {
22✔
115
      return value.filter((v) => v != null && v !== '').map(String);
8✔
116
    }
8✔
117
    if (typeof value === 'string') {
14✔
118
      return [value];
14✔
119
    }
14!
120
    const strValue = String(value);
×
121
    if (strValue != null) {
×
122
      return [String(value)];
×
123
    }
×
124
    return null;
×
125
  }
×
126

1,532✔
127
  /**
1,532✔
128
   * Creates select options if they do not already exist in the field.
1,532✔
129
   * Also updates the field with the newly created options.
1,532✔
130
   */
1,532✔
131
  private async createOptionsIfNotExists(choicesNames: string[]) {
1,532✔
132
    if (!choicesNames.length) {
42✔
133
      return;
40✔
134
    }
40✔
135
    const { id, type, options } = this.field as SingleSelectFieldDto | MultipleSelectFieldDto;
2✔
136
    const existsChoicesNameMap = keyBy(options.choices, 'name');
2✔
137
    const notExists = choicesNames.filter((name) => !existsChoicesNameMap[name]);
2✔
138
    const colors = ColorUtils.randomColor(map(options.choices, 'color'), notExists.length);
2✔
139
    const newChoices = notExists.map((name, index) => ({
2✔
140
      id: generateChoiceId(),
2✔
141
      name,
2✔
142
      color: colors[index],
2✔
143
    }));
2✔
144

2✔
145
    const { newField, modifiedOps } = await this.services.fieldConvertingService.stageAnalysis(
2✔
146
      this.tableId,
2✔
147
      id,
2✔
148
      {
2✔
149
        type,
2✔
150
        options: {
2✔
151
          ...options,
2✔
152
          choices: options.choices.concat(newChoices),
2✔
153
        },
2✔
154
      }
2✔
155
    );
2✔
156

2✔
157
    await this.services.fieldConvertingService.stageAlter(
2✔
158
      this.tableId,
2✔
159
      newField,
2✔
160
      this.field,
2✔
161
      modifiedOps
2✔
162
    );
2✔
163
  }
2✔
164

1,532✔
165
  /**
1,532✔
166
   * Casts the value to a single select option.
1,532✔
167
   * Creates the option if it does not already exist.
1,532✔
168
   */
1,532✔
169
  private async castToSingleSelect(cellValues: unknown[]): Promise<unknown[]> {
1,532✔
170
    const allValuesSet = new Set<string>();
24✔
171
    const newCellValues = this.mapFieldsCellValuesWithValidate(cellValues, (cellValue: unknown) => {
24✔
172
      const valueArr = this.valueToStringArray(cellValue);
2✔
173
      const newCellValue: string | null = valueArr?.length ? valueArr[0] : null;
2!
174
      newCellValue && allValuesSet.add(newCellValue);
2✔
175
      return newCellValue;
2✔
176
    });
2✔
177
    await this.createOptionsIfNotExists([...allValuesSet]);
24✔
178
    return newCellValues;
22✔
179
  }
22✔
180

1,532✔
181
  /**
1,532✔
182
   * Casts the value to multiple select options.
1,532✔
183
   * Creates the option if it does not already exist.
1,532✔
184
   */
1,532✔
185
  private async castToMultipleSelect(cellValues: unknown[]): Promise<unknown[]> {
1,532✔
186
    const allValuesSet = new Set<string>();
20✔
187
    const newCellValues = this.mapFieldsCellValuesWithValidate(cellValues, (cellValue: unknown) => {
20✔
188
      const valueArr = this.valueToStringArray(cellValue);
×
189
      const newCellValue: string[] | null = valueArr?.length ? valueArr : null;
×
190
      // collect all options
×
191
      newCellValue?.forEach((v) => v && allValuesSet.add(v));
×
192
      return newCellValue;
×
193
    });
×
194
    await this.createOptionsIfNotExists([...allValuesSet]);
20✔
195
    return newCellValues;
20✔
196
  }
20✔
197

1,532✔
198
  /**
1,532✔
199
   * Casts the value to a link type, associating it with another table.
1,532✔
200
   * Try to find the rows with matching titles from the associated table and write them to the cell.
1,532✔
201
   */
1,532✔
202
  private async castToLink(cellValues: unknown[]): Promise<unknown[]> {
1,532✔
203
    const linkRecordMap = this.typecast ? await this.getLinkTableRecordMap(cellValues) : {};
557✔
204
    return this.mapFieldsCellValuesWithValidate(cellValues, (cellValue: unknown) => {
557✔
205
      const newCellValue: ILinkCellValue[] | ILinkCellValue | null = this.castToLinkOne(
20✔
206
        cellValue,
20✔
207
        linkRecordMap
20✔
208
      );
20✔
209
      return newCellValue;
20✔
210
    });
20✔
211
  }
557✔
212

1,532✔
213
  private async castToUser(cellValues: unknown[]): Promise<unknown[]> {
1,532✔
214
    const newCellValues = this.defaultCastTo(cellValues);
20✔
215
    return newCellValues.map((cellValues) => {
20✔
216
      return this.field.convertDBValue2CellValue(cellValues);
460✔
217
    });
460✔
218
  }
20✔
219

1,532✔
220
  private async castToAttachment(cellValues: unknown[]): Promise<unknown[]> {
1,532✔
221
    const newCellValues = this.defaultCastTo(cellValues);
2✔
222

2✔
223
    const allAttachmentsPromises = newCellValues.map((cellValues) => {
2✔
224
      const attachmentCellValue = cellValues as IAttachmentCellValue;
2✔
225
      if (!attachmentCellValue) {
2!
226
        return attachmentCellValue;
×
227
      }
×
228

2✔
229
      const attachmentsWithPresignedUrls = attachmentCellValue.map(async (item) => {
2✔
230
        const { path, mimetype, token } = item;
2✔
231
        const presignedUrl = await this.services.attachmentsStorageService.getPreviewUrlByPath(
2✔
232
          StorageAdapter.getBucket(UploadType.Table),
2✔
233
          path,
2✔
234
          token,
2✔
235
          undefined,
2✔
236
          {
2✔
237
            // eslint-disable-next-line @typescript-eslint/naming-convention
2✔
238
            'Content-Type': mimetype,
2✔
239
          }
2✔
240
        );
2✔
241
        return {
2✔
242
          ...item,
2✔
243
          presignedUrl,
2✔
244
        };
2✔
245
      });
2✔
246

2✔
247
      return Promise.all(attachmentsWithPresignedUrls);
2✔
248
    });
2✔
249
    return await Promise.all(allAttachmentsPromises);
2✔
250
  }
2✔
251

1,532✔
252
  /**
1,532✔
253
   * Get the recordMap of the associated table, the format is: {[title]: [id]}.
1,532✔
254
   */
1,532✔
255
  private async getLinkTableRecordMap(cellValues: unknown[]) {
1,532✔
256
    const titles = cellValues.flat().filter(Boolean) as string[];
20✔
257

20✔
258
    const linkRecords = await this.services.recordService.getRecordsWithPrimary(
20✔
259
      (this.field as LinkFieldDto).options.foreignTableId,
20✔
260
      titles
20✔
261
    );
20✔
262

20✔
263
    return linkRecords.reduce(
20✔
264
      (result, { id, title }) => {
20✔
265
        if (!result[title]) {
20✔
266
          result[title] = id;
20✔
267
        }
20✔
268
        return result;
20✔
269
      },
20✔
270
      {} as Record<string, string>
20✔
271
    );
20✔
272
  }
20✔
273

1,532✔
274
  /**
1,532✔
275
   * The conversion of cellValue here is mainly about the difference between filtering null values,
1,532✔
276
   * returning data based on isMultipleCellValue.
1,532✔
277
   */
1,532✔
278
  private castToLinkOne(
1,532✔
279
    value: unknown,
20✔
280
    linkTableRecordMap: Record<string, string>
20✔
281
  ): ILinkCellValue[] | ILinkCellValue | null {
20✔
282
    const { isMultipleCellValue } = this.field;
20✔
283
    let valueArr = this.valueToStringArray(value);
20✔
284
    if (!valueArr?.length) {
20!
285
      return null;
×
286
    }
×
287
    valueArr = isMultipleCellValue ? valueArr : valueArr.slice(0, 1);
20✔
288
    const valueArrNotEmpty = valueArr.map(String).filter((v) => v !== undefined || v !== '');
20✔
289
    const result = valueArrNotEmpty
20✔
290
      .map((v) => ({ title: v, id: linkTableRecordMap[v] }))
20✔
291
      .filter((v) => !isUndefined(v.id)) as ILinkCellValue[];
20✔
292
    return isMultipleCellValue ? result : result[0] ?? null;
20✔
293
  }
20✔
294
}
1,532✔
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