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

teableio / teable / 8389227144

22 Mar 2024 10:56AM UTC coverage: 26.087% (-53.9%) from 79.937%
8389227144

push

github

web-flow
refactor: move zod schema to openapi (#487)

2100 of 3363 branches covered (62.44%)

282 of 757 new or added lines in 74 files covered. (37.25%)

14879 existing lines in 182 files now uncovered.

25574 of 98035 relevant lines covered (26.09%)

5.17 hits per line

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

18.89
/apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts
1
import {
1✔
2
  BadRequestException,
1✔
3
  Injectable,
1✔
4
  InternalServerErrorException,
1✔
5
  Logger,
1✔
6
} from '@nestjs/common';
1✔
7
import type {
1✔
8
  IFieldPropertyKey,
1✔
9
  ILookupOptionsVo,
1✔
10
  IOtOperation,
1✔
11
  ISelectFieldChoice,
1✔
12
  IConvertFieldRo,
1✔
13
} from '@teable/core';
1✔
14
import {
1✔
15
  ColorUtils,
1✔
16
  DbFieldType,
1✔
17
  FIELD_VO_PROPERTIES,
1✔
18
  FieldOpBuilder,
1✔
19
  FieldType,
1✔
20
  generateChoiceId,
1✔
21
  isMultiValueLink,
1✔
22
  RecordOpBuilder,
1✔
23
} from '@teable/core';
1✔
24
import { PrismaService } from '@teable/db-main-prisma';
1✔
25
import { Knex } from 'knex';
1✔
26
import { difference, intersection, isEmpty, isEqual, keyBy, set } from 'lodash';
1✔
27
import { InjectModel } from 'nest-knexjs';
1✔
28
import { BatchService } from '../../calculation/batch.service';
1✔
29
import { FieldCalculationService } from '../../calculation/field-calculation.service';
1✔
30
import type { ICellContext } from '../../calculation/link.service';
1✔
31
import { LinkService } from '../../calculation/link.service';
1✔
32
import type { IOpsMap } from '../../calculation/reference.service';
1✔
33
import { ReferenceService } from '../../calculation/reference.service';
1✔
34
import { formatChangesToOps } from '../../calculation/utils/changes';
1✔
35
import { composeOpMaps } from '../../calculation/utils/compose-maps';
1✔
36
import { CollaboratorService } from '../../collaborator/collaborator.service';
1✔
37
import { FieldService } from '../field.service';
1✔
38
import type { IFieldInstance, IFieldMap } from '../model/factory';
1✔
39
import { createFieldInstanceByVo } from '../model/factory';
1✔
40
import { FormulaFieldDto } from '../model/field-dto/formula-field.dto';
1✔
41
import type { LinkFieldDto } from '../model/field-dto/link-field.dto';
1✔
42
import type { MultipleSelectFieldDto } from '../model/field-dto/multiple-select-field.dto';
1✔
43
import type { RatingFieldDto } from '../model/field-dto/rating-field.dto';
1✔
44
import { RollupFieldDto } from '../model/field-dto/rollup-field.dto';
1✔
45
import type { SingleSelectFieldDto } from '../model/field-dto/single-select-field.dto';
1✔
46
import type { UserFieldDto } from '../model/field-dto/user-field.dto';
1✔
47
import { FieldConvertingLinkService } from './field-converting-link.service';
1✔
48
import { FieldSupplementService } from './field-supplement.service';
1✔
49

1✔
50
interface IModifiedOps {
1✔
51
  recordOpsMap?: IOpsMap;
1✔
52
  fieldOps?: IOtOperation[];
1✔
53
}
1✔
54

1✔
55
@Injectable()
1✔
56
export class FieldConvertingService {
1✔
57
  private readonly logger = new Logger(FieldConvertingService.name);
41✔
58

41✔
59
  constructor(
41✔
60
    private readonly prismaService: PrismaService,
41✔
61
    private readonly fieldService: FieldService,
41✔
62
    private readonly linkService: LinkService,
41✔
63
    private readonly batchService: BatchService,
41✔
64
    private readonly referenceService: ReferenceService,
41✔
65
    private readonly fieldConvertingLinkService: FieldConvertingLinkService,
41✔
66
    private readonly fieldSupplementService: FieldSupplementService,
41✔
67
    private readonly fieldCalculationService: FieldCalculationService,
41✔
68
    private readonly collaboratorService: CollaboratorService,
41✔
69
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
41✔
70
  ) {}
41✔
71

41✔
72
  private fieldOpsMap() {
41✔
UNCOV
73
    const fieldOpsMap: IOpsMap = {};
×
UNCOV
74
    return {
×
UNCOV
75
      pushOpsMap: (tableId: string, fieldId: string, op: IOtOperation | IOtOperation[]) => {
×
UNCOV
76
        const ops = Array.isArray(op) ? op : [op];
×
UNCOV
77
        if (!fieldOpsMap[tableId]?.[fieldId]) {
×
UNCOV
78
          set(fieldOpsMap, [tableId, fieldId], ops);
×
UNCOV
79
        } else {
×
UNCOV
80
          fieldOpsMap[tableId][fieldId].push(...ops);
×
UNCOV
81
        }
×
UNCOV
82
      },
×
UNCOV
83
      getOpsMap: () => fieldOpsMap,
×
UNCOV
84
    };
×
UNCOV
85
  }
×
86

41✔
87
  /**
41✔
88
   * Mutate field instance directly, because we should update fieldInstance in fieldMap for next field operation
41✔
89
   */
41✔
90
  private buildOpAndMutateField(field: IFieldInstance, key: IFieldPropertyKey, value: unknown) {
41✔
UNCOV
91
    if (isEqual(field[key], value)) {
×
UNCOV
92
      return;
×
UNCOV
93
    }
×
UNCOV
94
    const oldValue = field[key];
×
UNCOV
95
    (field[key] as unknown) = value;
×
UNCOV
96
    return FieldOpBuilder.editor.setFieldProperty.build({ key, oldValue, newValue: value });
×
UNCOV
97
  }
×
98

41✔
99
  /**
41✔
100
   * 1. check if the lookup field is valid, if not mark error
41✔
101
   * 2. update lookup field properties
41✔
102
   */
41✔
103
  // eslint-disable-next-line sonarjs/cognitive-complexity
41✔
104
  private updateLookupField(field: IFieldInstance, fieldMap: IFieldMap): IOtOperation[] {
41✔
UNCOV
105
    const ops: (IOtOperation | undefined)[] = [];
×
UNCOV
106
    const lookupOptions = field.lookupOptions as ILookupOptionsVo;
×
UNCOV
107
    const linkField = fieldMap[lookupOptions.linkFieldId] as LinkFieldDto;
×
UNCOV
108
    const lookupField = fieldMap[lookupOptions.lookupFieldId];
×
UNCOV
109
    const { showAs: _, ...inheritableOptions } = lookupField.options as Record<string, unknown>;
×
UNCOV
110
    const {
×
UNCOV
111
      formatting = inheritableOptions.formatting,
×
UNCOV
112
      showAs,
×
UNCOV
113
      ...inheritOptions
×
UNCOV
114
    } = field.options as Record<string, unknown>;
×
UNCOV
115
    const cellValueTypeChanged = field.cellValueType !== lookupField.cellValueType;
×
UNCOV
116

×
UNCOV
117
    if (field.type !== lookupField.type) {
×
UNCOV
118
      ops.push(this.buildOpAndMutateField(field, 'type', lookupField.type));
×
UNCOV
119
    }
×
UNCOV
120

×
UNCOV
121
    if (lookupOptions.relationship !== linkField.options.relationship) {
×
122
      ops.push(
×
123
        this.buildOpAndMutateField(field, 'lookupOptions', {
×
124
          ...lookupOptions,
×
125
          relationship: linkField.options.relationship,
×
126
          fkHostTableName: linkField.options.fkHostTableName,
×
127
          selfKeyName: linkField.options.selfKeyName,
×
128
          foreignKeyName: linkField.options.foreignKeyName,
×
129
        } as ILookupOptionsVo)
×
130
      );
×
131
    }
×
UNCOV
132

×
UNCOV
133
    if (!isEqual(inheritOptions, inheritableOptions)) {
×
UNCOV
134
      ops.push(
×
UNCOV
135
        this.buildOpAndMutateField(field, 'options', {
×
UNCOV
136
          ...inheritableOptions,
×
UNCOV
137
          ...(formatting ? { formatting } : {}),
×
UNCOV
138
          ...(showAs ? { showAs } : {}),
×
UNCOV
139
        })
×
UNCOV
140
      );
×
UNCOV
141
    }
×
UNCOV
142

×
UNCOV
143
    if (cellValueTypeChanged) {
×
UNCOV
144
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', lookupField.cellValueType));
×
UNCOV
145
      if (formatting || showAs) {
×
UNCOV
146
        ops.push(this.buildOpAndMutateField(field, 'options', inheritableOptions));
×
UNCOV
147
      }
×
UNCOV
148
    }
×
UNCOV
149

×
UNCOV
150
    const isMultipleCellValue = lookupField.isMultipleCellValue || linkField.isMultipleCellValue;
×
UNCOV
151
    if (field.isMultipleCellValue !== isMultipleCellValue) {
×
152
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
153
      // clean showAs
×
154
      if (!cellValueTypeChanged && showAs) {
×
155
        ops.push(
×
156
          this.buildOpAndMutateField(field, 'options', {
×
157
            ...inheritableOptions,
×
158
            ...(formatting ? { formatting } : {}),
×
159
          })
×
160
        );
×
161
      }
×
162
    }
×
UNCOV
163

×
UNCOV
164
    return ops.filter(Boolean) as IOtOperation[];
×
UNCOV
165
  }
×
166

41✔
167
  private updateFormulaField(field: FormulaFieldDto, fieldMap: IFieldMap) {
41✔
UNCOV
168
    const ops: (IOtOperation | undefined)[] = [];
×
UNCOV
169
    const { cellValueType, isMultipleCellValue } = FormulaFieldDto.getParsedValueType(
×
UNCOV
170
      field.options.expression,
×
UNCOV
171
      fieldMap
×
UNCOV
172
    );
×
UNCOV
173

×
UNCOV
174
    if (field.cellValueType !== cellValueType) {
×
UNCOV
175
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', cellValueType));
×
UNCOV
176
    }
×
UNCOV
177
    if (field.isMultipleCellValue !== isMultipleCellValue) {
×
178
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
179
    }
×
UNCOV
180
    return ops.filter(Boolean) as IOtOperation[];
×
UNCOV
181
  }
×
182

41✔
183
  private updateRollupField(field: RollupFieldDto, fieldMap: IFieldMap) {
41✔
184
    const ops: (IOtOperation | undefined)[] = [];
×
185
    const { lookupFieldId, relationship } = field.lookupOptions;
×
186
    const lookupField = fieldMap[lookupFieldId];
×
187
    const { cellValueType, isMultipleCellValue } = RollupFieldDto.getParsedValueType(
×
188
      field.options.expression,
×
189
      lookupField.cellValueType,
×
190
      lookupField.isMultipleCellValue || isMultiValueLink(relationship)
×
191
    );
×
192

×
193
    if (field.cellValueType !== cellValueType) {
×
194
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', cellValueType));
×
195
    }
×
196
    if (field.isMultipleCellValue !== isMultipleCellValue) {
×
197
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
198
    }
×
199
    return ops.filter(Boolean) as IOtOperation[];
×
200
  }
×
201

41✔
202
  private updateDbFieldType(field: IFieldInstance) {
41✔
UNCOV
203
    const ops: IOtOperation[] = [];
×
UNCOV
204
    const dbFieldType = this.fieldSupplementService.getDbFieldType(
×
UNCOV
205
      field.type,
×
UNCOV
206
      field.cellValueType,
×
UNCOV
207
      field.isMultipleCellValue
×
UNCOV
208
    );
×
UNCOV
209

×
UNCOV
210
    if (field.dbFieldType !== dbFieldType) {
×
UNCOV
211
      const op1 = this.buildOpAndMutateField(field, 'dbFieldType', dbFieldType);
×
UNCOV
212
      op1 && ops.push(op1);
×
UNCOV
213
    }
×
UNCOV
214
    return ops;
×
UNCOV
215
  }
×
216

41✔
217
  private async generateReferenceFieldOps(fieldId: string) {
41✔
UNCOV
218
    const topoOrdersContext = await this.fieldCalculationService.getTopoOrdersContext([fieldId]);
×
UNCOV
219

×
UNCOV
220
    const { fieldMap, topoOrdersByFieldId, fieldId2TableId } = topoOrdersContext;
×
UNCOV
221
    const topoOrders = topoOrdersByFieldId[fieldId];
×
UNCOV
222
    if (topoOrders.length <= 1) {
×
UNCOV
223
      return {};
×
UNCOV
224
    }
×
UNCOV
225

×
UNCOV
226
    const { pushOpsMap, getOpsMap } = this.fieldOpsMap();
×
UNCOV
227

×
UNCOV
228
    for (let i = 1; i < topoOrders.length; i++) {
×
UNCOV
229
      const topoOrder = topoOrders[i];
×
UNCOV
230
      // curField will be mutate in loop
×
UNCOV
231
      const curField = fieldMap[topoOrder.id];
×
UNCOV
232
      const tableId = fieldId2TableId[curField.id];
×
UNCOV
233
      if (curField.isLookup) {
×
UNCOV
234
        pushOpsMap(tableId, curField.id, this.updateLookupField(curField, fieldMap));
×
UNCOV
235
      } else if (curField.type === FieldType.Formula) {
×
UNCOV
236
        pushOpsMap(tableId, curField.id, this.updateFormulaField(curField, fieldMap));
×
UNCOV
237
      } else if (curField.type === FieldType.Rollup) {
×
238
        pushOpsMap(tableId, curField.id, this.updateRollupField(curField, fieldMap));
×
239
      }
×
UNCOV
240
      pushOpsMap(tableId, curField.id, this.updateDbFieldType(curField));
×
UNCOV
241
    }
×
UNCOV
242

×
UNCOV
243
    return getOpsMap();
×
UNCOV
244
  }
×
245

41✔
246
  /**
41✔
247
   * get deep deference in options, and return changes
41✔
248
   * formatting, showAs should be ignore
41✔
249
   */
41✔
250
  private getOptionsChanges(
41✔
251
    newOptions: Record<string, unknown>,
3✔
252
    oldOptions: Record<string, unknown>,
3✔
253
    valueTypeChange?: boolean
3✔
254
  ): Record<string, unknown> {
3✔
255
    const optionsChanges: Record<string, unknown> = {};
3✔
256

3✔
257
    newOptions = { ...newOptions };
3✔
258
    oldOptions = { ...oldOptions };
3✔
259
    const nonInfectKeys = ['formatting', 'showAs'];
3✔
260
    nonInfectKeys.forEach((key) => {
3✔
261
      delete newOptions[key];
6✔
262
      delete oldOptions[key];
6✔
263
    });
6✔
264

3✔
265
    const newOptionsKeys = Object.keys(newOptions);
3✔
266
    const oldOptionsKeys = Object.keys(oldOptions);
3✔
267

3✔
268
    const addedOptionsKeys = difference(newOptionsKeys, oldOptionsKeys);
3✔
269
    const removedOptionsKeys = difference(oldOptionsKeys, newOptionsKeys);
3✔
270
    const editedOptionsKeys = intersection(newOptionsKeys, oldOptionsKeys).filter(
3✔
271
      (key) => !isEqual(oldOptions[key], newOptions[key])
3✔
272
    );
3✔
273

3✔
274
    addedOptionsKeys.forEach((key) => (optionsChanges[key] = newOptions[key]));
3✔
275
    editedOptionsKeys.forEach((key) => (optionsChanges[key] = newOptions[key]));
3✔
276
    removedOptionsKeys.forEach((key) => (optionsChanges[key] = null));
3✔
277

3✔
278
    // clean formatting, showAs when valueType change
3✔
279
    valueTypeChange && nonInfectKeys.forEach((key) => (optionsChanges[key] = null));
3✔
280

3✔
281
    return optionsChanges;
3✔
282
  }
3✔
283

41✔
284
  private infectPropertyChanged(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
285
    // those key will infect the reference field
×
UNCOV
286
    const infectProperties = ['type', 'cellValueType', 'isMultipleCellValue'] as const;
×
UNCOV
287
    const changedProperties = infectProperties.filter(
×
UNCOV
288
      (key) => !isEqual(newField[key], oldField[key])
×
UNCOV
289
    );
×
UNCOV
290

×
UNCOV
291
    const valueTypeChanged = changedProperties.some((key) =>
×
UNCOV
292
      ['cellValueType', 'isMultipleCellValue'].includes(key)
×
UNCOV
293
    );
×
UNCOV
294

×
UNCOV
295
    // options may infect the lookup field
×
UNCOV
296
    const optionsChanges = this.getOptionsChanges(
×
UNCOV
297
      newField.options,
×
UNCOV
298
      oldField.options,
×
UNCOV
299
      valueTypeChanged
×
UNCOV
300
    );
×
UNCOV
301

×
UNCOV
302
    return Boolean(changedProperties.length || !isEmpty(optionsChanges));
×
UNCOV
303
  }
×
304

41✔
305
  /**
41✔
306
   * modify a field will causes the properties of the field that depend on it to change
41✔
307
   * example:
41✔
308
   * 1. modify a field's type will cause the the lookup field's type change
41✔
309
   * 2. cellValueType / isMultipleCellValue change will cause the formula / rollup / lookup field's cellValueType / formatting change
41✔
310
   * 3. options change will cause the lookup field options change
41✔
311
   * 4. options in link field change may cause all lookup field run in to error, should mark them as error
41✔
312
   */
41✔
313
  private async updateReferencedFields(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
314
    if (!this.infectPropertyChanged(newField, oldField)) {
×
UNCOV
315
      return;
×
UNCOV
316
    }
×
UNCOV
317

×
UNCOV
318
    const fieldOpsMap = await this.generateReferenceFieldOps(newField.id);
×
UNCOV
319
    await this.submitFieldOpsMap(fieldOpsMap);
×
UNCOV
320
  }
×
321

41✔
322
  private async updateOptionsFromMultiSelectField(
41✔
UNCOV
323
    tableId: string,
×
UNCOV
324
    updatedChoiceMap: { [old: string]: string | null },
×
UNCOV
325
    field: MultipleSelectFieldDto
×
UNCOV
326
  ): Promise<IOpsMap | undefined> {
×
UNCOV
327
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
UNCOV
328
      where: { id: tableId, deletedTime: null },
×
UNCOV
329
      select: { dbTableName: true },
×
UNCOV
330
    });
×
UNCOV
331

×
UNCOV
332
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
UNCOV
333
    const nativeSql = this.knex(dbTableName)
×
UNCOV
334
      .select('__id', field.dbFieldName)
×
UNCOV
335
      .where((builder) => {
×
UNCOV
336
        for (const value of Object.keys(updatedChoiceMap)) {
×
UNCOV
337
          builder.orWhere(
×
UNCOV
338
            this.knex.raw(`CAST(?? AS text)`, [field.dbFieldName]),
×
UNCOV
339
            'LIKE',
×
UNCOV
340
            `%"${value}"%`
×
UNCOV
341
          );
×
UNCOV
342
        }
×
UNCOV
343
      })
×
UNCOV
344
      .toSQL()
×
UNCOV
345
      .toNative();
×
UNCOV
346

×
UNCOV
347
    const result = await this.prismaService
×
UNCOV
348
      .txClient()
×
UNCOV
349
      .$queryRawUnsafe<
×
UNCOV
350
        { __id: string; [dbFieldName: string]: string }[]
×
UNCOV
351
      >(nativeSql.sql, ...nativeSql.bindings);
×
UNCOV
352

×
UNCOV
353
    for (const row of result) {
×
UNCOV
354
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as string[];
×
UNCOV
355
      const newCellValue = oldCellValue.reduce<string[]>((pre, value) => {
×
UNCOV
356
        // if key not in updatedChoiceMap, we should keep it
×
UNCOV
357
        if (!(value in updatedChoiceMap)) {
×
358
          pre.push(value);
×
359
          return pre;
×
360
        }
×
UNCOV
361

×
UNCOV
362
        const newValue = updatedChoiceMap[value];
×
UNCOV
363
        if (newValue !== null) {
×
UNCOV
364
          pre.push(newValue);
×
UNCOV
365
        }
×
UNCOV
366
        return pre;
×
UNCOV
367
      }, []);
×
UNCOV
368

×
UNCOV
369
      opsMap[row.__id] = [
×
UNCOV
370
        RecordOpBuilder.editor.setRecord.build({
×
UNCOV
371
          fieldId: field.id,
×
UNCOV
372
          oldCellValue,
×
UNCOV
373
          newCellValue,
×
UNCOV
374
        }),
×
UNCOV
375
      ];
×
UNCOV
376
    }
×
UNCOV
377
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
×
UNCOV
378
  }
×
379

41✔
380
  private async updateOptionsFromSingleSelectField(
41✔
UNCOV
381
    tableId: string,
×
UNCOV
382
    updatedChoiceMap: { [old: string]: string | null },
×
UNCOV
383
    field: SingleSelectFieldDto
×
UNCOV
384
  ): Promise<IOpsMap | undefined> {
×
UNCOV
385
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
UNCOV
386
      where: { id: tableId, deletedTime: null },
×
UNCOV
387
      select: { dbTableName: true },
×
UNCOV
388
    });
×
UNCOV
389

×
UNCOV
390
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
UNCOV
391
    const nativeSql = this.knex(dbTableName)
×
UNCOV
392
      .select('__id', field.dbFieldName)
×
UNCOV
393
      .where((builder) => {
×
UNCOV
394
        for (const value of Object.keys(updatedChoiceMap)) {
×
UNCOV
395
          builder.orWhere(field.dbFieldName, value);
×
UNCOV
396
        }
×
UNCOV
397
      })
×
UNCOV
398
      .toSQL()
×
UNCOV
399
      .toNative();
×
UNCOV
400

×
UNCOV
401
    const result = await this.prismaService
×
UNCOV
402
      .txClient()
×
UNCOV
403
      .$queryRawUnsafe<
×
UNCOV
404
        { __id: string; [dbFieldName: string]: string }[]
×
UNCOV
405
      >(nativeSql.sql, ...nativeSql.bindings);
×
UNCOV
406

×
UNCOV
407
    for (const row of result) {
×
UNCOV
408
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as string;
×
UNCOV
409

×
UNCOV
410
      opsMap[row.__id] = [
×
UNCOV
411
        RecordOpBuilder.editor.setRecord.build({
×
UNCOV
412
          fieldId: field.id,
×
UNCOV
413
          oldCellValue,
×
UNCOV
414
          newCellValue: updatedChoiceMap[oldCellValue],
×
UNCOV
415
        }),
×
UNCOV
416
      ];
×
UNCOV
417
    }
×
UNCOV
418
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
×
UNCOV
419
  }
×
420

41✔
421
  private async updateOptionsFromSelectField(
41✔
UNCOV
422
    tableId: string,
×
UNCOV
423
    updatedChoiceMap: { [old: string]: string | null },
×
UNCOV
424
    field: SingleSelectFieldDto | MultipleSelectFieldDto
×
UNCOV
425
  ): Promise<IOpsMap | undefined> {
×
UNCOV
426
    if (field.type === FieldType.SingleSelect) {
×
UNCOV
427
      return this.updateOptionsFromSingleSelectField(tableId, updatedChoiceMap, field);
×
UNCOV
428
    }
×
UNCOV
429

×
UNCOV
430
    if (field.type === FieldType.MultipleSelect) {
×
UNCOV
431
      return this.updateOptionsFromMultiSelectField(tableId, updatedChoiceMap, field);
×
UNCOV
432
    }
×
433
    throw new Error('Invalid field type');
×
434
  }
×
435

41✔
436
  private async modifySelectOptions(
41✔
UNCOV
437
    tableId: string,
×
UNCOV
438
    newField: SingleSelectFieldDto | MultipleSelectFieldDto,
×
UNCOV
439
    oldField: SingleSelectFieldDto | MultipleSelectFieldDto
×
UNCOV
440
  ) {
×
UNCOV
441
    const newChoiceMap = keyBy(newField.options.choices, 'id');
×
UNCOV
442
    const updatedChoiceMap: { [old: string]: string | null } = {};
×
UNCOV
443

×
UNCOV
444
    oldField.options.choices.forEach((item) => {
×
UNCOV
445
      if (!newChoiceMap[item.id]) {
×
UNCOV
446
        updatedChoiceMap[item.name] = null;
×
UNCOV
447
        return;
×
UNCOV
448
      }
×
UNCOV
449

×
UNCOV
450
      if (newChoiceMap[item.id].name !== item.name) {
×
UNCOV
451
        updatedChoiceMap[item.name] = newChoiceMap[item.id].name;
×
UNCOV
452
      }
×
UNCOV
453
    });
×
UNCOV
454

×
UNCOV
455
    if (isEmpty(updatedChoiceMap)) {
×
UNCOV
456
      return;
×
UNCOV
457
    }
×
UNCOV
458

×
UNCOV
459
    return this.updateOptionsFromSelectField(tableId, updatedChoiceMap, newField);
×
UNCOV
460
  }
×
461

41✔
462
  private async updateOptionsFromRatingField(
41✔
UNCOV
463
    tableId: string,
×
UNCOV
464
    field: RatingFieldDto
×
UNCOV
465
  ): Promise<IOpsMap | undefined> {
×
UNCOV
466
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
UNCOV
467
      where: { id: tableId, deletedTime: null },
×
UNCOV
468
      select: { dbTableName: true },
×
UNCOV
469
    });
×
UNCOV
470

×
UNCOV
471
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
UNCOV
472
    const newMax = field.options.max;
×
UNCOV
473

×
UNCOV
474
    const nativeSql = this.knex(dbTableName)
×
UNCOV
475
      .select('__id', field.dbFieldName)
×
UNCOV
476
      .where(field.dbFieldName, '>', newMax)
×
UNCOV
477
      .toSQL()
×
UNCOV
478
      .toNative();
×
UNCOV
479

×
UNCOV
480
    const result = await this.prismaService
×
UNCOV
481
      .txClient()
×
UNCOV
482
      .$queryRawUnsafe<
×
UNCOV
483
        { __id: string; [dbFieldName: string]: string }[]
×
UNCOV
484
      >(nativeSql.sql, ...nativeSql.bindings);
×
UNCOV
485

×
UNCOV
486
    for (const row of result) {
×
UNCOV
487
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as number;
×
UNCOV
488

×
UNCOV
489
      opsMap[row.__id] = [
×
UNCOV
490
        RecordOpBuilder.editor.setRecord.build({
×
UNCOV
491
          fieldId: field.id,
×
UNCOV
492
          oldCellValue,
×
UNCOV
493
          newCellValue: newMax,
×
UNCOV
494
        }),
×
UNCOV
495
      ];
×
UNCOV
496
    }
×
UNCOV
497

×
UNCOV
498
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
×
UNCOV
499
  }
×
500

41✔
501
  private async modifyRatingOptions(
41✔
UNCOV
502
    tableId: string,
×
UNCOV
503
    newField: RatingFieldDto,
×
UNCOV
504
    oldField: RatingFieldDto
×
UNCOV
505
  ) {
×
UNCOV
506
    const newMax = newField.options.max;
×
UNCOV
507
    const oldMax = oldField.options.max;
×
UNCOV
508

×
UNCOV
509
    if (newMax >= oldMax) return;
×
UNCOV
510

×
UNCOV
511
    return await this.updateOptionsFromRatingField(tableId, newField);
×
UNCOV
512
  }
×
513

41✔
514
  private async updateOptionsFromUserField(
41✔
515
    tableId: string,
×
516
    field: UserFieldDto
×
517
  ): Promise<IOpsMap | undefined> {
×
518
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
519
      where: { id: tableId, deletedTime: null },
×
520
      select: { dbTableName: true },
×
521
    });
×
522

×
523
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
524
    const nativeSql = this.knex(dbTableName)
×
525
      .select('__id', field.dbFieldName)
×
526
      .whereNotNull(field.dbFieldName);
×
527

×
528
    const result = await this.prismaService
×
529
      .txClient()
×
530
      .$queryRawUnsafe<{ __id: string; [dbFieldName: string]: string }[]>(nativeSql.toQuery());
×
531

×
532
    for (const row of result) {
×
533
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]);
×
534
      let newCellValue;
×
535

×
536
      if (field.isMultipleCellValue && !Array.isArray(oldCellValue)) {
×
537
        newCellValue = [oldCellValue];
×
538
      } else if (!field.isMultipleCellValue && Array.isArray(oldCellValue)) {
×
539
        newCellValue = oldCellValue[0];
×
540
      } else {
×
541
        newCellValue = oldCellValue;
×
542
      }
×
543

×
544
      opsMap[row.__id] = [
×
545
        RecordOpBuilder.editor.setRecord.build({
×
546
          fieldId: field.id,
×
547
          oldCellValue,
×
548
          newCellValue: newCellValue,
×
549
        }),
×
550
      ];
×
551
    }
×
552

×
553
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
×
554
  }
×
555

41✔
556
  private async modifyUserOptions(tableId: string, newField: UserFieldDto, oldField: UserFieldDto) {
41✔
557
    const newOption = newField.options.isMultiple;
×
558
    const oldOption = oldField.options.isMultiple;
×
559

×
560
    if (newOption === oldOption) return;
×
561

×
562
    return await this.updateOptionsFromUserField(tableId, newField);
×
563
  }
×
564

41✔
565
  private async modifyOptions(
41✔
UNCOV
566
    tableId: string,
×
UNCOV
567
    newField: IFieldInstance,
×
UNCOV
568
    oldField: IFieldInstance
×
UNCOV
569
  ): Promise<IModifiedOps | undefined> {
×
UNCOV
570
    if (newField.isLookup) {
×
571
      return;
×
572
    }
×
UNCOV
573

×
UNCOV
574
    switch (newField.type) {
×
UNCOV
575
      case FieldType.Link:
×
UNCOV
576
        return this.fieldConvertingLinkService.modifyLinkOptions(
×
UNCOV
577
          tableId,
×
UNCOV
578
          newField as LinkFieldDto,
×
UNCOV
579
          oldField as LinkFieldDto
×
UNCOV
580
        );
×
UNCOV
581
      case FieldType.SingleSelect:
×
UNCOV
582
      case FieldType.MultipleSelect: {
×
UNCOV
583
        const rawOpsMap = await this.modifySelectOptions(
×
UNCOV
584
          tableId,
×
UNCOV
585
          newField as SingleSelectFieldDto,
×
UNCOV
586
          oldField as SingleSelectFieldDto
×
UNCOV
587
        );
×
UNCOV
588
        return { recordOpsMap: rawOpsMap };
×
UNCOV
589
      }
×
UNCOV
590
      case FieldType.Rating: {
×
UNCOV
591
        const rawOpsMap = await this.modifyRatingOptions(
×
UNCOV
592
          tableId,
×
UNCOV
593
          newField as RatingFieldDto,
×
UNCOV
594
          oldField as RatingFieldDto
×
UNCOV
595
        );
×
UNCOV
596
        return { recordOpsMap: rawOpsMap };
×
UNCOV
597
      }
×
UNCOV
598
      case FieldType.User: {
×
599
        const rawOpsMap = await this.modifyUserOptions(
×
600
          tableId,
×
601
          newField as UserFieldDto,
×
602
          oldField as UserFieldDto
×
603
        );
×
604
        return { recordOpsMap: rawOpsMap };
×
605
      }
×
UNCOV
606
    }
×
UNCOV
607
  }
×
608

41✔
609
  private getOriginFieldKeys(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
610
    return FIELD_VO_PROPERTIES.filter((key) => !isEqual(newField[key], oldField[key]));
×
UNCOV
611
  }
×
612

41✔
613
  private getOriginFieldOps(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
614
    return this.getOriginFieldKeys(newField, oldField).map((key) =>
×
UNCOV
615
      FieldOpBuilder.editor.setFieldProperty.build({
×
UNCOV
616
        key,
×
UNCOV
617
        newValue: newField[key],
×
UNCOV
618
        oldValue: oldField[key],
×
UNCOV
619
      })
×
UNCOV
620
    );
×
UNCOV
621
  }
×
622

41✔
623
  private async getDerivateByLink(tableId: string, innerOpsMap: IOpsMap['key']) {
41✔
UNCOV
624
    const changes: ICellContext[] = [];
×
UNCOV
625
    for (const recordId in innerOpsMap) {
×
UNCOV
626
      for (const op of innerOpsMap[recordId]) {
×
UNCOV
627
        const context = RecordOpBuilder.editor.setRecord.detect(op);
×
UNCOV
628
        if (!context) {
×
629
          throw new Error('Invalid operation');
×
630
        }
×
UNCOV
631
        changes.push({
×
UNCOV
632
          recordId,
×
UNCOV
633
          fieldId: context.fieldId,
×
UNCOV
634
          oldValue: null, // old value by no means when converting
×
UNCOV
635
          newValue: context.newCellValue,
×
UNCOV
636
        });
×
UNCOV
637
      }
×
UNCOV
638
    }
×
UNCOV
639

×
UNCOV
640
    const derivate = await this.linkService.getDerivateByLink(tableId, changes, true);
×
UNCOV
641
    const cellChanges = derivate?.cellChanges || [];
×
UNCOV
642

×
UNCOV
643
    const opsMapByLink = cellChanges.length ? formatChangesToOps(cellChanges) : {};
×
UNCOV
644

×
UNCOV
645
    return {
×
UNCOV
646
      opsMapByLink,
×
UNCOV
647
      saveForeignKeyToDb: derivate?.saveForeignKeyToDb,
×
UNCOV
648
    };
×
UNCOV
649
  }
×
650

41✔
651
  private async calculateAndSaveRecords(
41✔
UNCOV
652
    tableId: string,
×
UNCOV
653
    field: IFieldInstance,
×
UNCOV
654
    recordOpsMap: IOpsMap | void
×
UNCOV
655
  ) {
×
UNCOV
656
    if (!recordOpsMap || isEmpty(recordOpsMap)) {
×
UNCOV
657
      return;
×
UNCOV
658
    }
×
UNCOV
659

×
UNCOV
660
    let saveForeignKeyToDb: (() => Promise<void>) | undefined;
×
UNCOV
661
    if (field.type === FieldType.Link && !field.isLookup) {
×
UNCOV
662
      const result = await this.getDerivateByLink(tableId, recordOpsMap[tableId]);
×
UNCOV
663
      saveForeignKeyToDb = result?.saveForeignKeyToDb;
×
UNCOV
664
      recordOpsMap = composeOpMaps([recordOpsMap, result.opsMapByLink]);
×
UNCOV
665
    }
×
UNCOV
666

×
UNCOV
667
    const {
×
UNCOV
668
      opsMap: calculatedOpsMap,
×
UNCOV
669
      fieldMap,
×
UNCOV
670
      tableId2DbTableName,
×
UNCOV
671
    } = await this.referenceService.calculateOpsMap(recordOpsMap, saveForeignKeyToDb);
×
UNCOV
672

×
UNCOV
673
    const composedOpsMap = composeOpMaps([recordOpsMap, calculatedOpsMap]);
×
UNCOV
674

×
UNCOV
675
    // console.log('recordOpsMap', JSON.stringify(recordOpsMap));
×
UNCOV
676
    // console.log('composedOpsMap', JSON.stringify(composedOpsMap));
×
UNCOV
677
    // console.log('tableId2DbTableName', JSON.stringify(tableId2DbTableName));
×
UNCOV
678

×
UNCOV
679
    await this.batchService.updateRecords(composedOpsMap, fieldMap, tableId2DbTableName);
×
UNCOV
680
  }
×
681

41✔
682
  private async getExistRecords(tableId: string, newField: IFieldInstance) {
41✔
UNCOV
683
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
UNCOV
684
      where: { id: tableId },
×
UNCOV
685
      select: { dbTableName: true },
×
UNCOV
686
    });
×
UNCOV
687

×
UNCOV
688
    const result = await this.fieldCalculationService.getRecordsBatchByFields({
×
UNCOV
689
      [dbTableName]: [newField],
×
UNCOV
690
    });
×
UNCOV
691
    const records = result[dbTableName];
×
UNCOV
692
    if (!records) {
×
693
      throw new InternalServerErrorException(
×
694
        `Can't find recordMap for tableId: ${tableId} and fieldId: ${newField.id}`
×
695
      );
×
696
    }
×
UNCOV
697

×
UNCOV
698
    return records;
×
UNCOV
699
  }
×
700

41✔
701
  // eslint-disable-next-line sonarjs/cognitive-complexity
41✔
702
  private async convert2Select(
41✔
UNCOV
703
    tableId: string,
×
UNCOV
704
    newField: SingleSelectFieldDto | MultipleSelectFieldDto,
×
UNCOV
705
    oldField: IFieldInstance
×
UNCOV
706
  ) {
×
UNCOV
707
    const fieldId = newField.id;
×
UNCOV
708
    const records = await this.getExistRecords(tableId, oldField);
×
UNCOV
709
    const choices = newField.options.choices;
×
UNCOV
710
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
UNCOV
711
    const fieldOps: IOtOperation[] = [];
×
UNCOV
712
    const choicesMap = keyBy(choices, 'name');
×
UNCOV
713
    const newChoicesSet = new Set<string>();
×
UNCOV
714
    records.forEach((record) => {
×
UNCOV
715
      const oldCellValue = record.fields[fieldId];
×
UNCOV
716
      if (oldCellValue == null) {
×
717
        return;
×
718
      }
×
UNCOV
719

×
UNCOV
720
      if (!opsMap[record.id]) {
×
UNCOV
721
        opsMap[record.id] = [];
×
UNCOV
722
      }
×
UNCOV
723

×
UNCOV
724
      const cellStr = oldField.cellValue2String(oldCellValue);
×
UNCOV
725
      const newCellValue = newField.convertStringToCellValue(cellStr, true);
×
UNCOV
726
      if (Array.isArray(newCellValue)) {
×
UNCOV
727
        newCellValue.forEach((item) => {
×
UNCOV
728
          if (!choicesMap[item]) {
×
UNCOV
729
            newChoicesSet.add(item);
×
UNCOV
730
          }
×
UNCOV
731
        });
×
UNCOV
732
      } else if (newCellValue && !choicesMap[newCellValue]) {
×
UNCOV
733
        newChoicesSet.add(newCellValue);
×
UNCOV
734
      }
×
UNCOV
735
      opsMap[record.id].push(
×
UNCOV
736
        RecordOpBuilder.editor.setRecord.build({
×
UNCOV
737
          fieldId,
×
UNCOV
738
          newCellValue,
×
UNCOV
739
          oldCellValue,
×
UNCOV
740
        })
×
UNCOV
741
      );
×
UNCOV
742
    });
×
UNCOV
743

×
UNCOV
744
    if (newChoicesSet.size) {
×
UNCOV
745
      const colors = ColorUtils.randomColor(
×
UNCOV
746
        choices.map((item) => item.color),
×
UNCOV
747
        newChoicesSet.size
×
UNCOV
748
      );
×
UNCOV
749
      const newChoices = choices.concat(
×
UNCOV
750
        Array.from(newChoicesSet).map<ISelectFieldChoice>((item, i) => ({
×
UNCOV
751
          id: generateChoiceId(),
×
UNCOV
752
          name: item,
×
UNCOV
753
          color: colors[i],
×
UNCOV
754
        }))
×
UNCOV
755
      );
×
UNCOV
756
      const fieldOp = this.buildOpAndMutateField(newField, 'options', {
×
UNCOV
757
        ...newField.options,
×
UNCOV
758
        choices: newChoices,
×
UNCOV
759
      });
×
UNCOV
760
      fieldOp && fieldOps.push(fieldOp);
×
UNCOV
761
    }
×
UNCOV
762

×
UNCOV
763
    return {
×
UNCOV
764
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
×
UNCOV
765
      fieldOps,
×
UNCOV
766
    };
×
UNCOV
767
  }
×
768

41✔
769
  private async convert2User(tableId: string, newField: UserFieldDto, oldField: IFieldInstance) {
41✔
770
    const fieldId = newField.id;
×
771
    const records = await this.getExistRecords(tableId, oldField);
×
772
    const baseCollabs = await this.collaboratorService.getBaseCollabsWithPrimary(tableId);
×
773
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
774

×
775
    records.forEach((record) => {
×
776
      const oldCellValue = record.fields[fieldId];
×
777
      if (oldCellValue == null) {
×
778
        return;
×
779
      }
×
780

×
781
      if (!opsMap[record.id]) {
×
782
        opsMap[record.id] = [];
×
783
      }
×
784

×
785
      const cellStr = oldField.cellValue2String(oldCellValue);
×
786
      const newCellValue = newField.convertStringToCellValue(cellStr, { userSets: baseCollabs });
×
787

×
788
      opsMap[record.id].push(
×
789
        RecordOpBuilder.editor.setRecord.build({
×
790
          fieldId,
×
791
          newCellValue,
×
792
          oldCellValue,
×
793
        })
×
794
      );
×
795
    });
×
796

×
797
    return {
×
798
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
×
799
    };
×
800
  }
×
801

41✔
802
  private async basalConvert(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
803
    // simple value type change is not need to convert
×
UNCOV
804
    if (
×
UNCOV
805
      oldField.type !== FieldType.LongText &&
×
UNCOV
806
      newField.type !== FieldType.Rating &&
×
UNCOV
807
      newField.cellValueType === oldField.cellValueType &&
×
UNCOV
808
      newField.isMultipleCellValue !== true &&
×
UNCOV
809
      oldField.isMultipleCellValue !== true &&
×
UNCOV
810
      newField.dbFieldType !== DbFieldType.Json &&
×
UNCOV
811
      oldField.dbFieldType !== DbFieldType.Json &&
×
UNCOV
812
      newField.dbFieldType === oldField.dbFieldType
×
UNCOV
813
    ) {
×
UNCOV
814
      return;
×
UNCOV
815
    }
×
UNCOV
816

×
UNCOV
817
    const fieldId = newField.id;
×
UNCOV
818
    const records = await this.getExistRecords(tableId, oldField);
×
UNCOV
819
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
UNCOV
820
    records.forEach((record) => {
×
UNCOV
821
      const oldCellValue = record.fields[fieldId];
×
UNCOV
822
      if (oldCellValue == null) {
×
823
        return;
×
824
      }
×
UNCOV
825

×
UNCOV
826
      const cellStr = oldField.cellValue2String(oldCellValue);
×
UNCOV
827
      const newCellValue = newField.convertStringToCellValue(cellStr);
×
UNCOV
828

×
UNCOV
829
      if (!opsMap[record.id]) {
×
UNCOV
830
        opsMap[record.id] = [];
×
UNCOV
831
      }
×
UNCOV
832
      opsMap[record.id].push(
×
UNCOV
833
        RecordOpBuilder.editor.setRecord.build({
×
UNCOV
834
          fieldId,
×
UNCOV
835
          newCellValue,
×
UNCOV
836
          oldCellValue,
×
UNCOV
837
        })
×
UNCOV
838
      );
×
UNCOV
839
    });
×
UNCOV
840

×
UNCOV
841
    return {
×
UNCOV
842
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
×
UNCOV
843
    };
×
UNCOV
844
  }
×
845

41✔
846
  private async modifyType(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
847
    if (newField.isComputed) {
×
UNCOV
848
      return;
×
UNCOV
849
    }
×
UNCOV
850

×
UNCOV
851
    if (newField.type === FieldType.SingleSelect || newField.type === FieldType.MultipleSelect) {
×
UNCOV
852
      return this.convert2Select(tableId, newField, oldField);
×
UNCOV
853
    }
×
UNCOV
854

×
UNCOV
855
    if (newField.type === FieldType.Link) {
×
UNCOV
856
      return this.fieldConvertingLinkService.convertLink(tableId, newField, oldField);
×
UNCOV
857
    }
×
UNCOV
858

×
UNCOV
859
    if (newField.type === FieldType.User) {
×
860
      return this.convert2User(tableId, newField, oldField);
×
861
    }
×
UNCOV
862

×
UNCOV
863
    return this.basalConvert(tableId, newField, oldField);
×
UNCOV
864
  }
×
865

41✔
866
  private async updateReference(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
867
    if (!this.shouldUpdateReference(newField, oldField)) {
×
UNCOV
868
      return;
×
UNCOV
869
    }
×
UNCOV
870

×
UNCOV
871
    await this.prismaService.txClient().reference.deleteMany({
×
UNCOV
872
      where: { toFieldId: oldField.id },
×
UNCOV
873
    });
×
UNCOV
874

×
UNCOV
875
    await this.fieldSupplementService.createReference(newField);
×
UNCOV
876
  }
×
877

41✔
878
  private shouldUpdateReference(newField: IFieldInstance, oldField: IFieldInstance) {
41✔
UNCOV
879
    const keys = this.getOriginFieldKeys(newField, oldField);
×
UNCOV
880

×
UNCOV
881
    // lookup options change
×
UNCOV
882
    if (newField.isLookup && oldField.isLookup) {
×
UNCOV
883
      return keys.includes('lookupOptions');
×
UNCOV
884
    }
×
UNCOV
885

×
UNCOV
886
    // major change
×
UNCOV
887
    if (keys.includes('type') || keys.includes('isComputed') || keys.includes('isLookup')) {
×
UNCOV
888
      return true;
×
UNCOV
889
    }
×
UNCOV
890

×
UNCOV
891
    // for same field with options change
×
UNCOV
892
    if (keys.includes('options')) {
×
UNCOV
893
      return (
×
UNCOV
894
        (newField.type === FieldType.Rollup || newField.type === FieldType.Formula) &&
×
UNCOV
895
        newField.options.expression !== (oldField as FormulaFieldDto).options.expression
×
UNCOV
896
      );
×
UNCOV
897
    }
×
UNCOV
898

×
UNCOV
899
    // for same field with lookup options change
×
UNCOV
900
    return keys.includes('lookupOptions');
×
UNCOV
901
  }
×
902

41✔
903
  private async generateModifiedOps(
41✔
UNCOV
904
    tableId: string,
×
UNCOV
905
    newField: IFieldInstance,
×
UNCOV
906
    oldField: IFieldInstance
×
UNCOV
907
  ): Promise<IModifiedOps | undefined> {
×
UNCOV
908
    const keys = this.getOriginFieldKeys(newField, oldField);
×
UNCOV
909

×
UNCOV
910
    if (newField.isLookup && oldField.isLookup) {
×
UNCOV
911
      return;
×
UNCOV
912
    }
×
UNCOV
913

×
UNCOV
914
    // for field type change, isLookup change, isComputed change
×
UNCOV
915
    if (keys.includes('type') || keys.includes('isComputed') || keys.includes('isLookup')) {
×
UNCOV
916
      return this.modifyType(tableId, newField, oldField);
×
UNCOV
917
    }
×
UNCOV
918

×
UNCOV
919
    // for same field with options change
×
UNCOV
920
    if (keys.includes('options')) {
×
UNCOV
921
      return await this.modifyOptions(tableId, newField, oldField);
×
UNCOV
922
    }
×
UNCOV
923
  }
×
924

41✔
925
  majorKeysChanged(oldField: IFieldInstance, newField: IFieldInstance) {
41✔
UNCOV
926
    const keys = this.getOriginFieldKeys(newField, oldField);
×
UNCOV
927

×
UNCOV
928
    // filter property
×
UNCOV
929
    const majorKeys = difference(keys, ['name', 'description', 'dbFieldName']);
×
UNCOV
930

×
UNCOV
931
    if (!majorKeys.length) {
×
UNCOV
932
      return false;
×
UNCOV
933
    }
×
UNCOV
934

×
UNCOV
935
    // expression not change
×
UNCOV
936
    if (
×
UNCOV
937
      majorKeys.length === 1 &&
×
UNCOV
938
      majorKeys[0] === 'options' &&
×
939
      (oldField.options as { expression: string }).expression ===
×
940
        (newField.options as { expression: string }).expression
×
UNCOV
941
    ) {
×
942
      return false;
×
943
    }
×
UNCOV
944

×
UNCOV
945
    return true;
×
UNCOV
946
  }
×
947

41✔
948
  private async calculateField(
41✔
UNCOV
949
    tableId: string,
×
UNCOV
950
    newField: IFieldInstance,
×
UNCOV
951
    oldField: IFieldInstance
×
UNCOV
952
  ) {
×
UNCOV
953
    if (!newField.isComputed) {
×
UNCOV
954
      return;
×
UNCOV
955
    }
×
UNCOV
956

×
UNCOV
957
    if (!this.majorKeysChanged(oldField, newField)) {
×
958
      return;
×
959
    }
×
UNCOV
960

×
UNCOV
961
    this.logger.log(`calculating field: ${newField.name}`);
×
UNCOV
962

×
UNCOV
963
    if (newField.lookupOptions) {
×
UNCOV
964
      await this.fieldCalculationService.resetAndCalculateFields(tableId, [newField.id]);
×
UNCOV
965
    } else {
×
UNCOV
966
      await this.fieldCalculationService.calculateFields(tableId, [newField.id]);
×
UNCOV
967
    }
×
UNCOV
968
    await this.fieldService.resolvePending(tableId, [newField.id]);
×
UNCOV
969
  }
×
970

41✔
971
  private async submitFieldOpsMap(fieldOpsMap: IOpsMap | undefined) {
41✔
UNCOV
972
    if (!fieldOpsMap) {
×
973
      return;
×
974
    }
×
UNCOV
975

×
UNCOV
976
    for (const tableId in fieldOpsMap) {
×
UNCOV
977
      const opData = Object.entries(fieldOpsMap[tableId]).map(([fieldId, ops]) => ({
×
UNCOV
978
        fieldId,
×
UNCOV
979
        ops,
×
UNCOV
980
      }));
×
UNCOV
981
      await this.fieldService.batchUpdateFields(tableId, opData);
×
UNCOV
982
    }
×
UNCOV
983
  }
×
984

41✔
985
  async alterSupplementLink(
41✔
UNCOV
986
    tableId: string,
×
UNCOV
987
    newField: IFieldInstance,
×
UNCOV
988
    oldField: IFieldInstance,
×
UNCOV
989
    supplementChange?: { tableId: string; newField: IFieldInstance; oldField: IFieldInstance }
×
UNCOV
990
  ) {
×
UNCOV
991
    // for link ref and create or delete supplement link, (create, delete do not need calculate)
×
UNCOV
992
    await this.fieldConvertingLinkService.alterSupplementLink(tableId, newField, oldField);
×
UNCOV
993

×
UNCOV
994
    // for modify supplement link
×
UNCOV
995
    if (supplementChange) {
×
UNCOV
996
      const { tableId, newField, oldField } = supplementChange;
×
UNCOV
997
      await this.stageAlter(tableId, newField, oldField);
×
UNCOV
998
    }
×
UNCOV
999
  }
×
1000

41✔
1001
  async stageAnalysis(tableId: string, fieldId: string, updateFieldRo: IConvertFieldRo) {
41✔
UNCOV
1002
    const oldFieldVo = await this.fieldService.getField(tableId, fieldId);
×
UNCOV
1003
    if (!oldFieldVo) {
×
1004
      throw new BadRequestException(`Not found fieldId(${fieldId})`);
×
1005
    }
×
UNCOV
1006

×
UNCOV
1007
    const oldField = createFieldInstanceByVo(oldFieldVo);
×
UNCOV
1008
    const newFieldVo = await this.fieldSupplementService.prepareUpdateField(
×
UNCOV
1009
      tableId,
×
UNCOV
1010
      updateFieldRo,
×
UNCOV
1011
      oldField
×
UNCOV
1012
    );
×
UNCOV
1013

×
UNCOV
1014
    const newField = createFieldInstanceByVo(newFieldVo);
×
UNCOV
1015
    const modifiedOps = await this.generateModifiedOps(tableId, newField, oldField);
×
UNCOV
1016

×
UNCOV
1017
    // 2. collect changes effect by the supplement(link) field
×
UNCOV
1018
    const supplementChange = await this.fieldConvertingLinkService.analysisLink(newField, oldField);
×
UNCOV
1019

×
UNCOV
1020
    return {
×
UNCOV
1021
      newField,
×
UNCOV
1022
      oldField,
×
UNCOV
1023
      modifiedOps,
×
UNCOV
1024
      supplementChange,
×
UNCOV
1025
    };
×
UNCOV
1026
  }
×
1027

41✔
1028
  async stageAlter(
41✔
UNCOV
1029
    tableId: string,
×
UNCOV
1030
    newField: IFieldInstance,
×
UNCOV
1031
    oldField: IFieldInstance,
×
UNCOV
1032
    modifiedOps?: IModifiedOps
×
UNCOV
1033
  ) {
×
UNCOV
1034
    const ops = this.getOriginFieldOps(newField, oldField);
×
UNCOV
1035

×
UNCOV
1036
    // apply current field changes
×
UNCOV
1037
    await this.fieldService.batchUpdateFields(tableId, [
×
UNCOV
1038
      { fieldId: newField.id, ops: ops.concat(modifiedOps?.fieldOps || []) },
×
UNCOV
1039
    ]);
×
UNCOV
1040

×
UNCOV
1041
    // apply referenced fields changes
×
UNCOV
1042
    await this.updateReferencedFields(newField, oldField);
×
UNCOV
1043
  }
×
1044

41✔
1045
  async stageCalculate(
41✔
UNCOV
1046
    tableId: string,
×
UNCOV
1047
    newField: IFieldInstance,
×
UNCOV
1048
    oldField: IFieldInstance,
×
UNCOV
1049
    modifiedOps?: IModifiedOps
×
UNCOV
1050
  ) {
×
UNCOV
1051
    await this.updateReference(newField, oldField);
×
UNCOV
1052

×
UNCOV
1053
    // calculate and submit records
×
UNCOV
1054
    await this.calculateAndSaveRecords(tableId, newField, modifiedOps?.recordOpsMap);
×
UNCOV
1055

×
UNCOV
1056
    // calculate computed fields
×
UNCOV
1057
    await this.calculateField(tableId, newField, oldField);
×
UNCOV
1058
  }
×
1059
}
41✔
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