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

teableio / teable / 10523150885

23 Aug 2024 09:13AM UTC coverage: 83.355% (+0.3%) from 83.022%
10523150885

Pull #810

github

web-flow
Merge 65963bcd5 into c246febe5
Pull Request #810: fix: import relative

4644 of 4862 branches covered (95.52%)

130 of 149 new or added lines in 6 files covered. (87.25%)

36 existing lines in 4 files now uncovered.

30804 of 36955 relevant lines covered (83.36%)

1214.31 hits per line

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

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

4✔
59
interface IModifiedOps {
4✔
60
  recordOpsMap?: IOpsMap;
4✔
61
  fieldOps?: IOtOperation[];
4✔
62
}
4✔
63

4✔
64
@Injectable()
4✔
65
export class FieldConvertingService {
4✔
66
  private readonly logger = new Logger(FieldConvertingService.name);
160✔
67

160✔
68
  constructor(
160✔
69
    private readonly viewService: ViewService,
160✔
70
    private readonly linkService: LinkService,
160✔
71
    private readonly fieldService: FieldService,
160✔
72
    private readonly batchService: BatchService,
160✔
73
    private readonly prismaService: PrismaService,
160✔
74
    private readonly referenceService: ReferenceService,
160✔
75
    private readonly fieldConvertingLinkService: FieldConvertingLinkService,
160✔
76
    private readonly fieldSupplementService: FieldSupplementService,
160✔
77
    private readonly fieldCalculationService: FieldCalculationService,
160✔
78
    private readonly collaboratorService: CollaboratorService,
160✔
79
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
160✔
80
  ) {}
160✔
81

160✔
82
  private fieldOpsMap() {
160✔
83
    const fieldOpsMap: IOpsMap = {};
24✔
84
    return {
24✔
85
      pushOpsMap: (tableId: string, fieldId: string, op: IOtOperation | IOtOperation[]) => {
24✔
86
        const ops = Array.isArray(op) ? op : [op];
36✔
87
        if (!fieldOpsMap[tableId]?.[fieldId]) {
36✔
88
          set(fieldOpsMap, [tableId, fieldId], ops);
20✔
89
        } else {
36✔
90
          fieldOpsMap[tableId][fieldId].push(...ops);
16✔
91
        }
16✔
92
      },
36✔
93
      getOpsMap: () => fieldOpsMap,
24✔
94
    };
24✔
95
  }
24✔
96

160✔
97
  /**
160✔
98
   * Mutate field instance directly, because we should update fieldInstance in fieldMap for next field operation
160✔
99
   */
160✔
100
  private buildOpAndMutateField(field: IFieldInstance, key: IFieldPropertyKey, value: unknown) {
160✔
101
    if (isEqual(field[key], value)) {
44✔
102
      return;
4✔
103
    }
4✔
104
    const oldValue = field[key];
40✔
105
    (field[key] as unknown) = value;
40✔
106
    return FieldOpBuilder.editor.setFieldProperty.build({ key, oldValue, newValue: value });
40✔
107
  }
40✔
108

160✔
109
  /**
160✔
110
   * 1. check if the lookup field is valid, if not mark error
160✔
111
   * 2. update lookup field properties
160✔
112
   */
160✔
113
  // eslint-disable-next-line sonarjs/cognitive-complexity
160✔
114
  private updateLookupField(field: IFieldInstance, fieldMap: IFieldMap): IOtOperation[] {
160✔
115
    const ops: (IOtOperation | undefined)[] = [];
10✔
116
    const lookupOptions = field.lookupOptions as ILookupOptionsVo;
10✔
117
    const linkField = fieldMap[lookupOptions.linkFieldId] as LinkFieldDto;
10✔
118
    const lookupField = fieldMap[lookupOptions.lookupFieldId];
10✔
119
    const { showAs: _, ...inheritableOptions } = lookupField.options as Record<string, unknown>;
10✔
120
    const {
10✔
121
      formatting = inheritableOptions.formatting,
10✔
122
      showAs,
10✔
123
      ...inheritOptions
10✔
124
    } = field.options as Record<string, unknown>;
10✔
125
    const cellValueTypeChanged = field.cellValueType !== lookupField.cellValueType;
10✔
126

10✔
127
    if (field.type !== lookupField.type) {
10✔
128
      ops.push(this.buildOpAndMutateField(field, 'type', lookupField.type));
8✔
129
    }
8✔
130

10✔
131
    if (lookupOptions.relationship !== linkField.options.relationship) {
10✔
132
      ops.push(
×
133
        this.buildOpAndMutateField(field, 'lookupOptions', {
×
134
          ...lookupOptions,
×
135
          relationship: linkField.options.relationship,
×
136
          fkHostTableName: linkField.options.fkHostTableName,
×
137
          selfKeyName: linkField.options.selfKeyName,
×
138
          foreignKeyName: linkField.options.foreignKeyName,
×
139
        } as ILookupOptionsVo)
×
140
      );
×
UNCOV
141
    }
×
142

10✔
143
    if (!isEqual(inheritOptions, inheritableOptions)) {
10✔
144
      ops.push(
8✔
145
        this.buildOpAndMutateField(field, 'options', {
8✔
146
          ...inheritableOptions,
8✔
147
          ...(formatting ? { formatting } : {}),
8✔
148
          ...(showAs ? { showAs } : {}),
8✔
149
        })
8✔
150
      );
8✔
151
    }
8✔
152

10✔
153
    if (cellValueTypeChanged) {
10✔
154
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', lookupField.cellValueType));
6✔
155
      if (formatting || showAs) {
6✔
156
        ops.push(this.buildOpAndMutateField(field, 'options', inheritableOptions));
6✔
157
      }
6✔
158
    }
6✔
159

10✔
160
    const isMultipleCellValue = lookupField.isMultipleCellValue || linkField.isMultipleCellValue;
10✔
161
    if (field.isMultipleCellValue !== isMultipleCellValue) {
10✔
162
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
163
      // clean showAs
×
164
      if (!cellValueTypeChanged && showAs) {
×
165
        ops.push(
×
166
          this.buildOpAndMutateField(field, 'options', {
×
167
            ...inheritableOptions,
×
168
            ...(formatting ? { formatting } : {}),
×
169
          })
×
170
        );
×
171
      }
×
UNCOV
172
    }
×
173

10✔
174
    return ops.filter(Boolean) as IOtOperation[];
10✔
175
  }
10✔
176

160✔
177
  private updateFormulaField(field: FormulaFieldDto, fieldMap: IFieldMap) {
160✔
178
    const ops: (IOtOperation | undefined)[] = [];
6✔
179
    const { cellValueType, isMultipleCellValue } = FormulaFieldDto.getParsedValueType(
6✔
180
      field.options.expression,
6✔
181
      fieldMap
6✔
182
    );
6✔
183

6✔
184
    if (field.cellValueType !== cellValueType) {
6✔
185
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', cellValueType));
2✔
186
    }
2✔
187
    if (field.isMultipleCellValue !== isMultipleCellValue) {
6✔
188
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
UNCOV
189
    }
×
190
    return ops.filter(Boolean) as IOtOperation[];
6✔
191
  }
6✔
192

160✔
193
  private updateRollupField(field: RollupFieldDto, fieldMap: IFieldMap) {
160✔
194
    const ops: (IOtOperation | undefined)[] = [];
×
195
    const { lookupFieldId, relationship } = field.lookupOptions;
×
196
    const lookupField = fieldMap[lookupFieldId];
×
197
    const { cellValueType, isMultipleCellValue } = RollupFieldDto.getParsedValueType(
×
198
      field.options.expression,
×
199
      lookupField.cellValueType,
×
200
      lookupField.isMultipleCellValue || isMultiValueLink(relationship)
×
201
    );
×
202

×
203
    if (field.cellValueType !== cellValueType) {
×
204
      ops.push(this.buildOpAndMutateField(field, 'cellValueType', cellValueType));
×
205
    }
×
206
    if (field.isMultipleCellValue !== isMultipleCellValue) {
×
207
      ops.push(this.buildOpAndMutateField(field, 'isMultipleCellValue', isMultipleCellValue));
×
208
    }
×
209
    return ops.filter(Boolean) as IOtOperation[];
×
UNCOV
210
  }
×
211

160✔
212
  private updateDbFieldType(field: IFieldInstance) {
160✔
213
    const ops: IOtOperation[] = [];
16✔
214
    const dbFieldType = this.fieldSupplementService.getDbFieldType(
16✔
215
      field.type,
16✔
216
      field.cellValueType,
16✔
217
      field.isMultipleCellValue
16✔
218
    );
16✔
219

16✔
220
    if (field.dbFieldType !== dbFieldType) {
16✔
221
      const op1 = this.buildOpAndMutateField(field, 'dbFieldType', dbFieldType);
4✔
222
      op1 && ops.push(op1);
4✔
223
    }
4✔
224
    return ops;
16✔
225
  }
16✔
226

160✔
227
  private async generateReferenceFieldOps(fieldId: string) {
160✔
228
    const topoOrdersContext = await this.fieldCalculationService.getTopoOrdersContext([fieldId]);
132✔
229

132✔
230
    const { fieldMap, topoOrdersByFieldId, fieldId2TableId } = topoOrdersContext;
132✔
231
    const topoOrders = topoOrdersByFieldId[fieldId];
132✔
232
    if (topoOrders.length <= 1) {
132✔
233
      return {};
118✔
234
    }
118✔
235

14✔
236
    const { pushOpsMap, getOpsMap } = this.fieldOpsMap();
14✔
237

14✔
238
    for (let i = 1; i < topoOrders.length; i++) {
114✔
239
      const topoOrder = topoOrders[i];
16✔
240
      // curField will be mutate in loop
16✔
241
      const curField = fieldMap[topoOrder.id];
16✔
242
      const tableId = fieldId2TableId[curField.id];
16✔
243
      if (curField.isLookup) {
16✔
244
        pushOpsMap(tableId, curField.id, this.updateLookupField(curField, fieldMap));
10✔
245
      } else if (curField.type === FieldType.Formula) {
16✔
246
        pushOpsMap(tableId, curField.id, this.updateFormulaField(curField, fieldMap));
6✔
247
      } else if (curField.type === FieldType.Rollup) {
6✔
248
        pushOpsMap(tableId, curField.id, this.updateRollupField(curField, fieldMap));
×
UNCOV
249
      }
×
250
      pushOpsMap(tableId, curField.id, this.updateDbFieldType(curField));
16✔
251
    }
16✔
252

14✔
253
    return getOpsMap();
14✔
254
  }
14✔
255

160✔
256
  /**
160✔
257
   * get deep deference in options, and return changes
160✔
258
   * formatting, showAs should be ignore
160✔
259
   */
160✔
260
  private getOptionsChanges(
160✔
261
    newOptions: Record<string, unknown>,
163✔
262
    oldOptions: Record<string, unknown>,
163✔
263
    valueTypeChange?: boolean
163✔
264
  ): Record<string, unknown> {
163✔
265
    const optionsChanges: Record<string, unknown> = {};
163✔
266

163✔
267
    newOptions = { ...newOptions };
163✔
268
    oldOptions = { ...oldOptions };
163✔
269
    const nonInfectKeys = ['formatting', 'showAs'];
163✔
270
    nonInfectKeys.forEach((key) => {
163✔
271
      delete newOptions[key];
326✔
272
      delete oldOptions[key];
326✔
273
    });
326✔
274

163✔
275
    const newOptionsKeys = Object.keys(newOptions);
163✔
276
    const oldOptionsKeys = Object.keys(oldOptions);
163✔
277

163✔
278
    const addedOptionsKeys = difference(newOptionsKeys, oldOptionsKeys);
163✔
279
    const removedOptionsKeys = difference(oldOptionsKeys, newOptionsKeys);
163✔
280
    const editedOptionsKeys = intersection(newOptionsKeys, oldOptionsKeys).filter(
163✔
281
      (key) => !isEqual(oldOptions[key], newOptions[key])
163✔
282
    );
163✔
283

163✔
284
    addedOptionsKeys.forEach((key) => (optionsChanges[key] = newOptions[key]));
163✔
285
    editedOptionsKeys.forEach((key) => (optionsChanges[key] = newOptions[key]));
163✔
286
    removedOptionsKeys.forEach((key) => (optionsChanges[key] = null));
163✔
287

163✔
288
    // clean formatting, showAs when valueType change
163✔
289
    valueTypeChange && nonInfectKeys.forEach((key) => (optionsChanges[key] = null));
163✔
290

163✔
291
    return optionsChanges;
163✔
292
  }
163✔
293

160✔
294
  private infectPropertyChanged(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
295
    // those key will infect the reference field
157✔
296
    const infectProperties = ['type', 'cellValueType', 'isMultipleCellValue'] as const;
157✔
297
    const changedProperties = infectProperties.filter(
157✔
298
      (key) => !isEqual(newField[key], oldField[key])
157✔
299
    );
157✔
300

157✔
301
    const valueTypeChanged = changedProperties.some((key) =>
157✔
302
      ['cellValueType', 'isMultipleCellValue'].includes(key)
170✔
303
    );
157✔
304

157✔
305
    // options may infect the lookup field
157✔
306
    const optionsChanges = this.getOptionsChanges(
157✔
307
      newField.options,
157✔
308
      oldField.options,
157✔
309
      valueTypeChanged
157✔
310
    );
157✔
311

157✔
312
    return Boolean(changedProperties.length || !isEmpty(optionsChanges));
157✔
313
  }
157✔
314

160✔
315
  // lookupOptions of lookup field and rollup field must be consistent with linkField Settings
160✔
316
  // And they don't belong in the referenceField
160✔
317
  private async updateLookupRollupRef(
160✔
318
    newField: IFieldInstance,
132✔
319
    oldField: IFieldInstance
132✔
320
  ): Promise<IOpsMap | undefined> {
132✔
321
    if (newField.type !== FieldType.Link || oldField.type !== FieldType.Link) {
132✔
322
      return;
110✔
323
    }
110✔
324

22✔
325
    // ignore foreignTableId change
22✔
326
    if (newField.options.foreignTableId !== oldField.options.foreignTableId) {
114✔
327
      return;
10✔
328
    }
10✔
329

12✔
330
    const { relationship, fkHostTableName, foreignKeyName, selfKeyName } = newField.options;
12✔
331
    if (
12✔
332
      relationship === oldField.options.relationship &&
12✔
333
      fkHostTableName === oldField.options.fkHostTableName &&
132✔
334
      foreignKeyName === oldField.options.foreignKeyName &&
132✔
335
      selfKeyName === oldField.options.selfKeyName
2✔
336
    ) {
132✔
337
      return;
2✔
338
    }
2✔
339

10✔
340
    const relatedFieldsRaw = await this.prismaService.field.findMany({
10✔
341
      where: {
10✔
342
        lookupLinkedFieldId: newField.id,
10✔
343
        deletedTime: null,
10✔
344
      },
10✔
345
    });
10✔
346

10✔
347
    const relatedFields = relatedFieldsRaw.map(createFieldInstanceByRaw);
10✔
348

10✔
349
    const lookupToFields = await this.prismaService.field.findMany({
10✔
350
      where: {
10✔
351
        id: {
10✔
352
          in: relatedFields.map((field) => field.lookupOptions?.lookupFieldId as string),
10✔
353
        },
10✔
354
      },
10✔
355
    });
10✔
356
    const relatedFieldsRawMap = keyBy(relatedFieldsRaw, 'id');
10✔
357
    const lookupToFieldsMap = keyBy(lookupToFields, 'id');
10✔
358

10✔
359
    const { pushOpsMap, getOpsMap } = this.fieldOpsMap();
10✔
360

10✔
361
    relatedFields.forEach((field) => {
10✔
362
      const lookupOptions = field.lookupOptions!;
4✔
363
      const ops: IOtOperation[] = [];
4✔
364
      ops.push(
4✔
365
        FieldOpBuilder.editor.setFieldProperty.build({
4✔
366
          key: 'lookupOptions',
4✔
367
          newValue: {
4✔
368
            ...lookupOptions,
4✔
369
            relationship,
4✔
370
            fkHostTableName,
4✔
371
            foreignKeyName,
4✔
372
            selfKeyName,
4✔
373
          },
4✔
374
          oldValue: lookupOptions,
4✔
375
        })
4✔
376
      );
4✔
377

4✔
378
      const lookupToFieldRaw = lookupToFieldsMap[lookupOptions.lookupFieldId];
4✔
379

4✔
380
      if (field.isLookup) {
4✔
381
        const isMultipleCellValue =
2✔
382
          newField.isMultipleCellValue || lookupToFieldRaw.isMultipleCellValue || false;
2✔
383

2✔
384
        if (isMultipleCellValue !== field.isMultipleCellValue) {
2✔
385
          ops.push(
2✔
386
            FieldOpBuilder.editor.setFieldProperty.build({
2✔
387
              key: 'isMultipleCellValue',
2✔
388
              newValue: isMultipleCellValue,
2✔
389
              oldValue: field.isMultipleCellValue,
2✔
390
            }),
2✔
391
            FieldOpBuilder.editor.setFieldProperty.build({
2✔
392
              key: 'dbFieldType',
2✔
393
              newValue: this.fieldSupplementService.getDbFieldType(
2✔
394
                field.type,
2✔
395
                field.cellValueType,
2✔
396
                isMultipleCellValue
2✔
397
              ),
2✔
398
              oldValue: field.dbFieldType,
2✔
399
            })
2✔
400
          );
2✔
401
        }
2✔
402

2✔
403
        const newOptions = this.fieldSupplementService.prepareFormattingShowAs(
2✔
404
          field.options,
2✔
405
          JSON.parse(lookupToFieldRaw.options as string),
2✔
406
          field.cellValueType,
2✔
407
          isMultipleCellValue
2✔
408
        );
2✔
409

2✔
410
        if (!isEqual(newOptions, field.options)) {
2✔
411
          ops.push(
×
412
            FieldOpBuilder.editor.setFieldProperty.build({
×
413
              key: 'options',
×
414
              newValue: newOptions,
×
415
              oldValue: field.options,
×
416
            })
×
417
          );
×
UNCOV
418
        }
×
419
      }
2✔
420

4✔
421
      pushOpsMap(relatedFieldsRawMap[field.id].tableId, field.id, ops);
4✔
422
    });
4✔
423

10✔
424
    return getOpsMap();
10✔
425
  }
10✔
426

160✔
427
  /**
160✔
428
   * modify a field will causes the properties of the field that depend on it to change
160✔
429
   * example:
160✔
430
   * 1. modify a field's type will cause the the lookup field's type change
160✔
431
   * 2. cellValueType / isMultipleCellValue change will cause the formula / rollup / lookup field's cellValueType / formatting change
160✔
432
   * 3. options change will cause the lookup field options change
160✔
433
   * 4. options in link field change may cause all lookup field run in to error, should mark them as error
160✔
434
   */
160✔
435
  private async updateReferencedFields(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
436
    if (!this.infectPropertyChanged(newField, oldField)) {
157✔
437
      return;
25✔
438
    }
25✔
439

132✔
440
    const refFieldOpsMap = await this.updateLookupRollupRef(newField, oldField);
132✔
441

132✔
442
    const fieldOpsMap = await this.generateReferenceFieldOps(newField.id);
132✔
443

132✔
444
    await this.submitFieldOpsMap(composeOpMaps([refFieldOpsMap, fieldOpsMap]));
132✔
445
  }
132✔
446

160✔
447
  private async updateOptionsFromMultiSelectField(
160✔
448
    tableId: string,
2✔
449
    updatedChoiceMap: { [old: string]: string | null },
2✔
450
    field: MultipleSelectFieldDto
2✔
451
  ): Promise<IOpsMap | undefined> {
2✔
452
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
2✔
453
      where: { id: tableId, deletedTime: null },
2✔
454
      select: { dbTableName: true },
2✔
455
    });
2✔
456

2✔
457
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
2✔
458
    const nativeSql = this.knex(dbTableName)
2✔
459
      .select('__id', field.dbFieldName)
2✔
460
      .where((builder) => {
2✔
461
        for (const value of Object.keys(updatedChoiceMap)) {
2✔
462
          builder.orWhere(
4✔
463
            this.knex.raw(`CAST(?? AS text)`, [field.dbFieldName]),
4✔
464
            'LIKE',
4✔
465
            `%"${value}"%`
4✔
466
          );
4✔
467
        }
4✔
468
      })
2✔
469
      .toSQL()
2✔
470
      .toNative();
2✔
471

2✔
472
    const result = await this.prismaService
2✔
473
      .txClient()
2✔
474
      .$queryRawUnsafe<
2✔
475
        { __id: string; [dbFieldName: string]: string }[]
2✔
476
      >(nativeSql.sql, ...nativeSql.bindings);
2✔
477

2✔
478
    for (const row of result) {
2✔
479
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as string[];
6✔
480
      const newCellValue = oldCellValue.reduce<string[]>((pre, value) => {
6✔
481
        // if key not in updatedChoiceMap, we should keep it
8✔
482
        if (!(value in updatedChoiceMap)) {
8✔
483
          pre.push(value);
×
484
          return pre;
×
UNCOV
485
        }
×
486

8✔
487
        const newValue = updatedChoiceMap[value];
8✔
488
        if (newValue !== null) {
8✔
489
          pre.push(newValue);
4✔
490
        }
4✔
491
        return pre;
8✔
492
      }, []);
6✔
493

6✔
494
      opsMap[row.__id] = [
6✔
495
        RecordOpBuilder.editor.setRecord.build({
6✔
496
          fieldId: field.id,
6✔
497
          oldCellValue,
6✔
498
          newCellValue,
6✔
499
        }),
6✔
500
      ];
6✔
501
    }
6✔
502
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
2✔
503
  }
2✔
504

160✔
505
  private async updateOptionsFromSingleSelectField(
160✔
506
    tableId: string,
4✔
507
    updatedChoiceMap: { [old: string]: string | null },
4✔
508
    field: SingleSelectFieldDto
4✔
509
  ): Promise<IOpsMap | undefined> {
4✔
510
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
4✔
511
      where: { id: tableId, deletedTime: null },
4✔
512
      select: { dbTableName: true },
4✔
513
    });
4✔
514

4✔
515
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
4✔
516
    const nativeSql = this.knex(dbTableName)
4✔
517
      .select('__id', field.dbFieldName)
4✔
518
      .where((builder) => {
4✔
519
        for (const value of Object.keys(updatedChoiceMap)) {
4✔
520
          builder.orWhere(field.dbFieldName, value);
6✔
521
        }
6✔
522
      })
4✔
523
      .toSQL()
4✔
524
      .toNative();
4✔
525

4✔
526
    const result = await this.prismaService
4✔
527
      .txClient()
4✔
528
      .$queryRawUnsafe<
4✔
529
        { __id: string; [dbFieldName: string]: string }[]
4✔
530
      >(nativeSql.sql, ...nativeSql.bindings);
4✔
531

4✔
532
    for (const row of result) {
4✔
533
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as string;
4✔
534

4✔
535
      opsMap[row.__id] = [
4✔
536
        RecordOpBuilder.editor.setRecord.build({
4✔
537
          fieldId: field.id,
4✔
538
          oldCellValue,
4✔
539
          newCellValue: updatedChoiceMap[oldCellValue],
4✔
540
        }),
4✔
541
      ];
4✔
542
    }
4✔
543
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
4✔
544
  }
4✔
545

160✔
546
  private async updateOptionsFromSelectField(
160✔
547
    tableId: string,
6✔
548
    updatedChoiceMap: { [old: string]: string | null },
6✔
549
    field: SingleSelectFieldDto | MultipleSelectFieldDto
6✔
550
  ): Promise<IOpsMap | undefined> {
6✔
551
    if (field.type === FieldType.SingleSelect) {
6✔
552
      return this.updateOptionsFromSingleSelectField(tableId, updatedChoiceMap, field);
4✔
553
    }
4✔
554

2✔
555
    if (field.type === FieldType.MultipleSelect) {
2✔
556
      return this.updateOptionsFromMultiSelectField(tableId, updatedChoiceMap, field);
2✔
557
    }
2✔
558
    throw new Error('Invalid field type');
×
UNCOV
559
  }
×
560

160✔
561
  private async modifySelectOptions(
160✔
562
    tableId: string,
10✔
563
    newField: SingleSelectFieldDto | MultipleSelectFieldDto,
10✔
564
    oldField: SingleSelectFieldDto | MultipleSelectFieldDto
10✔
565
  ) {
10✔
566
    const newChoiceMap = keyBy(newField.options.choices, 'id');
10✔
567
    const updatedChoiceMap: { [old: string]: string | null } = {};
10✔
568

10✔
569
    oldField.options.choices.forEach((item) => {
10✔
570
      if (!newChoiceMap[item.id]) {
16✔
571
        updatedChoiceMap[item.name] = null;
4✔
572
        return;
4✔
573
      }
4✔
574

12✔
575
      if (newChoiceMap[item.id].name !== item.name) {
16✔
576
        updatedChoiceMap[item.name] = newChoiceMap[item.id].name;
6✔
577
      }
6✔
578
    });
16✔
579

10✔
580
    if (isEmpty(updatedChoiceMap)) {
10✔
581
      return;
4✔
582
    }
4✔
583

6✔
584
    return this.updateOptionsFromSelectField(tableId, updatedChoiceMap, newField);
6✔
585
  }
6✔
586

160✔
587
  private async updateOptionsFromRatingField(
160✔
588
    tableId: string,
2✔
589
    field: RatingFieldDto
2✔
590
  ): Promise<IOpsMap | undefined> {
2✔
591
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
2✔
592
      where: { id: tableId, deletedTime: null },
2✔
593
      select: { dbTableName: true },
2✔
594
    });
2✔
595

2✔
596
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
2✔
597
    const newMax = field.options.max;
2✔
598

2✔
599
    const nativeSql = this.knex(dbTableName)
2✔
600
      .select('__id', field.dbFieldName)
2✔
601
      .where(field.dbFieldName, '>', newMax)
2✔
602
      .toSQL()
2✔
603
      .toNative();
2✔
604

2✔
605
    const result = await this.prismaService
2✔
606
      .txClient()
2✔
607
      .$queryRawUnsafe<
2✔
608
        { __id: string; [dbFieldName: string]: string }[]
2✔
609
      >(nativeSql.sql, ...nativeSql.bindings);
2✔
610

2✔
611
    for (const row of result) {
2✔
612
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]) as number;
2✔
613

2✔
614
      opsMap[row.__id] = [
2✔
615
        RecordOpBuilder.editor.setRecord.build({
2✔
616
          fieldId: field.id,
2✔
617
          oldCellValue,
2✔
618
          newCellValue: newMax,
2✔
619
        }),
2✔
620
      ];
2✔
621
    }
2✔
622

2✔
623
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
2✔
624
  }
2✔
625

160✔
626
  private async modifyRatingOptions(
160✔
627
    tableId: string,
2✔
628
    newField: RatingFieldDto,
2✔
629
    oldField: RatingFieldDto
2✔
630
  ) {
2✔
631
    const newMax = newField.options.max;
2✔
632
    const oldMax = oldField.options.max;
2✔
633

2✔
634
    if (newMax >= oldMax) return;
2✔
635

2✔
636
    return await this.updateOptionsFromRatingField(tableId, newField);
2✔
637
  }
2✔
638

160✔
639
  private async updateOptionsFromUserField(
160✔
640
    tableId: string,
×
641
    field: UserFieldDto
×
642
  ): Promise<IOpsMap | undefined> {
×
643
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
×
644
      where: { id: tableId, deletedTime: null },
×
645
      select: { dbTableName: true },
×
646
    });
×
647

×
648
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
649
    const nativeSql = this.knex(dbTableName)
×
650
      .select('__id', field.dbFieldName)
×
651
      .whereNotNull(field.dbFieldName);
×
652

×
653
    const result = await this.prismaService
×
654
      .txClient()
×
655
      .$queryRawUnsafe<{ __id: string; [dbFieldName: string]: string }[]>(nativeSql.toQuery());
×
656

×
657
    for (const row of result) {
×
658
      const oldCellValue = field.convertDBValue2CellValue(row[field.dbFieldName]);
×
659
      let newCellValue;
×
660

×
661
      if (field.isMultipleCellValue && !Array.isArray(oldCellValue)) {
×
662
        newCellValue = [oldCellValue];
×
663
      } else if (!field.isMultipleCellValue && Array.isArray(oldCellValue)) {
×
664
        newCellValue = oldCellValue[0];
×
665
      } else {
×
666
        newCellValue = oldCellValue;
×
667
      }
×
668

×
669
      opsMap[row.__id] = [
×
670
        RecordOpBuilder.editor.setRecord.build({
×
671
          fieldId: field.id,
×
672
          oldCellValue,
×
673
          newCellValue: newCellValue,
×
674
        }),
×
675
      ];
×
676
    }
×
677

×
678
    return isEmpty(opsMap) ? undefined : { [tableId]: opsMap };
×
UNCOV
679
  }
×
680

160✔
681
  private async modifyUserOptions(tableId: string, newField: UserFieldDto, oldField: UserFieldDto) {
160✔
682
    const newOption = newField.options.isMultiple;
×
683
    const oldOption = oldField.options.isMultiple;
×
684

×
685
    if (newOption === oldOption) return;
×
686

×
687
    return await this.updateOptionsFromUserField(tableId, newField);
×
UNCOV
688
  }
×
689

160✔
690
  private async modifyOptions(
160✔
691
    tableId: string,
34✔
692
    newField: IFieldInstance,
34✔
693
    oldField: IFieldInstance
34✔
694
  ): Promise<IModifiedOps | undefined> {
34✔
695
    if (newField.isLookup) {
34✔
696
      return;
×
UNCOV
697
    }
×
698

34✔
699
    switch (newField.type) {
34✔
700
      case FieldType.Link:
34✔
701
        return this.fieldConvertingLinkService.modifyLinkOptions(
16✔
702
          tableId,
16✔
703
          newField as LinkFieldDto,
16✔
704
          oldField as LinkFieldDto
16✔
705
        );
34✔
706
      case FieldType.SingleSelect:
34✔
707
      case FieldType.MultipleSelect: {
34✔
708
        const rawOpsMap = await this.modifySelectOptions(
10✔
709
          tableId,
10✔
710
          newField as SingleSelectFieldDto,
10✔
711
          oldField as SingleSelectFieldDto
10✔
712
        );
10✔
713
        return { recordOpsMap: rawOpsMap };
10✔
714
      }
10✔
715
      case FieldType.Rating: {
34✔
716
        const rawOpsMap = await this.modifyRatingOptions(
2✔
717
          tableId,
2✔
718
          newField as RatingFieldDto,
2✔
719
          oldField as RatingFieldDto
2✔
720
        );
2✔
721
        return { recordOpsMap: rawOpsMap };
2✔
722
      }
2✔
723
      case FieldType.User: {
34✔
724
        const rawOpsMap = await this.modifyUserOptions(
×
725
          tableId,
×
726
          newField as UserFieldDto,
×
727
          oldField as UserFieldDto
×
728
        );
×
729
        return { recordOpsMap: rawOpsMap };
×
UNCOV
730
      }
×
731
    }
34✔
732
  }
34✔
733

160✔
734
  private getOriginFieldKeys(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
735
    return FIELD_VO_PROPERTIES.filter((key) => !isEqual(newField[key], oldField[key]));
469✔
736
  }
469✔
737

160✔
738
  private getOriginFieldOps(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
739
    return this.getOriginFieldKeys(newField, oldField).map((key) =>
159✔
740
      FieldOpBuilder.editor.setFieldProperty.build({
443✔
741
        key,
443✔
742
        newValue: newField[key],
443✔
743
        oldValue: oldField[key],
443✔
744
      })
443✔
745
    );
159✔
746
  }
159✔
747

160✔
748
  private async getDerivateByLink(tableId: string, innerOpsMap: IOpsMap['key']) {
160✔
749
    const changes: ICellContext[] = [];
22✔
750
    for (const recordId in innerOpsMap) {
22✔
751
      for (const op of innerOpsMap[recordId]) {
34✔
752
        const context = RecordOpBuilder.editor.setRecord.detect(op);
34✔
753
        if (!context) {
34✔
754
          throw new Error('Invalid operation');
×
UNCOV
755
        }
×
756
        changes.push({
34✔
757
          recordId,
34✔
758
          fieldId: context.fieldId,
34✔
759
          oldValue: null, // old value by no means when converting
34✔
760
          newValue: context.newCellValue,
34✔
761
        });
34✔
762
      }
34✔
763
    }
34✔
764

20✔
765
    const derivate = await this.linkService.getDerivateByLink(tableId, changes, true);
20✔
766
    const cellChanges = derivate?.cellChanges || [];
22✔
767

22✔
768
    const opsMapByLink = cellChanges.length ? formatChangesToOps(cellChanges) : {};
22✔
769

22✔
770
    return {
22✔
771
      opsMapByLink,
22✔
772
      saveForeignKeyToDb: derivate?.saveForeignKeyToDb,
22✔
773
    };
22✔
774
  }
22✔
775

160✔
776
  private async calculateAndSaveRecords(
160✔
777
    tableId: string,
155✔
778
    field: IFieldInstance,
155✔
779
    recordOpsMap: IOpsMap | void
155✔
780
  ) {
155✔
781
    if (!recordOpsMap || isEmpty(recordOpsMap)) {
155✔
782
      return;
73✔
783
    }
73✔
784

82✔
785
    let saveForeignKeyToDb: (() => Promise<void>) | undefined;
82✔
786
    if (field.type === FieldType.Link && !field.isLookup) {
155✔
787
      const result = await this.getDerivateByLink(tableId, recordOpsMap[tableId]);
22✔
788
      saveForeignKeyToDb = result?.saveForeignKeyToDb;
22✔
789
      recordOpsMap = composeOpMaps([recordOpsMap, result.opsMapByLink]);
22✔
790
    }
22✔
791

82✔
792
    const {
82✔
793
      opsMap: calculatedOpsMap,
82✔
794
      fieldMap,
82✔
795
      tableId2DbTableName,
82✔
796
    } = await this.referenceService.calculateOpsMap(recordOpsMap, saveForeignKeyToDb);
82✔
797

82✔
798
    const composedOpsMap = composeOpMaps([recordOpsMap, calculatedOpsMap]);
82✔
799

82✔
800
    await this.batchService.updateRecords(composedOpsMap, fieldMap, tableId2DbTableName);
82✔
801
  }
82✔
802

160✔
803
  private async getExistRecords(tableId: string, newField: IFieldInstance) {
160✔
804
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
58✔
805
      where: { id: tableId },
58✔
806
      select: { dbTableName: true },
58✔
807
    });
58✔
808

58✔
809
    const result = await this.fieldCalculationService.getRecordsBatchByFields({
58✔
810
      [dbTableName]: [newField],
58✔
811
    });
58✔
812
    const records = result[dbTableName];
58✔
813
    if (!records) {
58✔
814
      throw new InternalServerErrorException(
×
815
        `Can't find recordMap for tableId: ${tableId} and fieldId: ${newField.id}`
×
816
      );
×
UNCOV
817
    }
×
818

58✔
819
    return records;
58✔
820
  }
58✔
821

160✔
822
  // eslint-disable-next-line sonarjs/cognitive-complexity
160✔
823
  private async convert2Select(
160✔
824
    tableId: string,
10✔
825
    newField: SingleSelectFieldDto | MultipleSelectFieldDto,
10✔
826
    oldField: IFieldInstance
10✔
827
  ) {
10✔
828
    const fieldId = newField.id;
10✔
829
    const records = await this.getExistRecords(tableId, oldField);
10✔
830
    const choices = newField.options.choices;
10✔
831
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
10✔
832
    const fieldOps: IOtOperation[] = [];
10✔
833
    const choicesMap = keyBy(choices, 'name');
10✔
834
    const newChoicesSet = new Set<string>();
10✔
835
    records.forEach((record) => {
10✔
836
      const oldCellValue = record.fields[fieldId];
32✔
837
      if (oldCellValue == null) {
32✔
838
        return;
×
UNCOV
839
      }
×
840

32✔
841
      if (!opsMap[record.id]) {
32✔
842
        opsMap[record.id] = [];
32✔
843
      }
32✔
844

32✔
845
      const cellStr = oldField.cellValue2String(oldCellValue);
32✔
846
      const newCellValue = newField.convertStringToCellValue(cellStr, true);
32✔
847
      if (Array.isArray(newCellValue)) {
32✔
848
        newCellValue.forEach((item) => {
18✔
849
          if (!choicesMap[item]) {
32✔
850
            newChoicesSet.add(item);
4✔
851
          }
4✔
852
        });
32✔
853
      } else if (newCellValue && !choicesMap[newCellValue]) {
32✔
854
        newChoicesSet.add(newCellValue);
10✔
855
      }
10✔
856
      opsMap[record.id].push(
32✔
857
        RecordOpBuilder.editor.setRecord.build({
32✔
858
          fieldId,
32✔
859
          newCellValue,
32✔
860
          oldCellValue,
32✔
861
        })
32✔
862
      );
32✔
863
    });
32✔
864

10✔
865
    if (newChoicesSet.size) {
10✔
866
      const colors = ColorUtils.randomColor(
10✔
867
        choices.map((item) => item.color),
10✔
868
        newChoicesSet.size
10✔
869
      );
10✔
870
      const newChoices = choices.concat(
10✔
871
        Array.from(newChoicesSet).map<ISelectFieldChoice>((item, i) => ({
10✔
872
          id: generateChoiceId(),
14✔
873
          name: item,
14✔
874
          color: colors[i],
14✔
875
        }))
14✔
876
      );
10✔
877
      const fieldOp = this.buildOpAndMutateField(newField, 'options', {
10✔
878
        ...newField.options,
10✔
879
        choices: newChoices,
10✔
880
      });
10✔
881
      fieldOp && fieldOps.push(fieldOp);
10✔
882
    }
10✔
883

10✔
884
    return {
10✔
885
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
10✔
886
      fieldOps,
10✔
887
    };
10✔
888
  }
10✔
889

160✔
890
  private async convert2User(tableId: string, newField: UserFieldDto, oldField: IFieldInstance) {
160✔
891
    const fieldId = newField.id;
×
892
    const records = await this.getExistRecords(tableId, oldField);
×
893
    const baseCollabs = await this.collaboratorService.getBaseCollabsWithPrimary(tableId);
×
894
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
×
895

×
896
    records.forEach((record) => {
×
897
      const oldCellValue = record.fields[fieldId];
×
898
      if (oldCellValue == null) {
×
899
        return;
×
900
      }
×
901

×
902
      if (!opsMap[record.id]) {
×
903
        opsMap[record.id] = [];
×
904
      }
×
905

×
906
      const cellStr = oldField.cellValue2String(oldCellValue);
×
907
      const newCellValue = newField.convertStringToCellValue(cellStr, { userSets: baseCollabs });
×
908

×
909
      opsMap[record.id].push(
×
910
        RecordOpBuilder.editor.setRecord.build({
×
911
          fieldId,
×
912
          newCellValue,
×
913
          oldCellValue,
×
914
        })
×
915
      );
×
916
    });
×
917

×
918
    return {
×
919
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
×
920
    };
×
UNCOV
921
  }
×
922

160✔
923
  private async basalConvert(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) {
160✔
924
    // simple value type change is not need to convert
50✔
925
    if (
50✔
926
      oldField.type !== FieldType.LongText &&
50✔
927
      newField.type !== FieldType.Rating &&
50✔
928
      newField.cellValueType === oldField.cellValueType &&
50✔
929
      newField.isMultipleCellValue !== true &&
22✔
930
      oldField.isMultipleCellValue !== true &&
20✔
931
      newField.dbFieldType !== DbFieldType.Json &&
50✔
932
      oldField.dbFieldType !== DbFieldType.Json &&
50✔
933
      newField.dbFieldType === oldField.dbFieldType
2✔
934
    ) {
50✔
935
      return;
2✔
936
    }
2✔
937

48✔
938
    const fieldId = newField.id;
48✔
939
    const records = await this.getExistRecords(tableId, oldField);
48✔
940
    const opsMap: { [recordId: string]: IOtOperation[] } = {};
48✔
941
    records.forEach((record) => {
48✔
942
      const oldCellValue = record.fields[fieldId];
60✔
943
      if (oldCellValue == null) {
60✔
944
        return;
×
UNCOV
945
      }
×
946

60✔
947
      const cellStr = oldField.cellValue2String(oldCellValue);
60✔
948
      const newCellValue = newField.convertStringToCellValue(cellStr);
60✔
949

60✔
950
      if (!opsMap[record.id]) {
60✔
951
        opsMap[record.id] = [];
60✔
952
      }
60✔
953
      opsMap[record.id].push(
60✔
954
        RecordOpBuilder.editor.setRecord.build({
60✔
955
          fieldId,
60✔
956
          newCellValue,
60✔
957
          oldCellValue,
60✔
958
        })
60✔
959
      );
60✔
960
    });
60✔
961

48✔
962
    return {
48✔
963
      recordOpsMap: isEmpty(opsMap) ? undefined : { [tableId]: opsMap },
50✔
964
    };
50✔
965
  }
50✔
966

160✔
967
  private async modifyType(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) {
160✔
968
    if (newField.isComputed) {
88✔
969
      return;
22✔
970
    }
22✔
971

66✔
972
    if (newField.type === FieldType.SingleSelect || newField.type === FieldType.MultipleSelect) {
88✔
973
      return this.convert2Select(tableId, newField, oldField);
10✔
974
    }
10✔
975

56✔
976
    if (newField.type === FieldType.Link) {
88✔
977
      return this.fieldConvertingLinkService.convertLink(tableId, newField, oldField);
6✔
978
    }
6✔
979

50✔
980
    if (newField.type === FieldType.User) {
88✔
981
      return this.convert2User(tableId, newField, oldField);
×
UNCOV
982
    }
✔
983

50✔
984
    return this.basalConvert(tableId, newField, oldField);
50✔
985
  }
50✔
986

160✔
987
  private async updateReference(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
988
    if (!this.shouldUpdateReference(newField, oldField)) {
155✔
989
      return;
53✔
990
    }
53✔
991

102✔
992
    await this.prismaService.txClient().reference.deleteMany({
102✔
993
      where: { toFieldId: oldField.id },
102✔
994
    });
102✔
995

102✔
996
    await this.fieldSupplementService.createReference(newField);
102✔
997
  }
102✔
998

160✔
999
  private shouldUpdateReference(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
1000
    const keys = this.getOriginFieldKeys(newField, oldField);
155✔
1001

155✔
1002
    // lookup options change
155✔
1003
    if (newField.isLookup && oldField.isLookup) {
155✔
1004
      return keys.includes('lookupOptions');
10✔
1005
    }
10✔
1006

145✔
1007
    // major change
145✔
1008
    if (keys.includes('type') || keys.includes('isComputed') || keys.includes('isLookup')) {
155✔
1009
      return true;
88✔
1010
    }
88✔
1011

57✔
1012
    // for same field with options change
57✔
1013
    if (keys.includes('options')) {
143✔
1014
      return (
36✔
1015
        (newField.type === FieldType.Rollup || newField.type === FieldType.Formula) &&
36✔
1016
        newField.options.expression !== (oldField as FormulaFieldDto).options.expression
6✔
1017
      );
36✔
1018
    }
36✔
1019

21✔
1020
    // for same field with lookup options change
21✔
1021
    return keys.includes('lookupOptions');
21✔
1022
  }
21✔
1023

160✔
1024
  private async generateModifiedOps(
160✔
1025
    tableId: string,
155✔
1026
    newField: IFieldInstance,
155✔
1027
    oldField: IFieldInstance
155✔
1028
  ): Promise<IModifiedOps | undefined> {
155✔
1029
    const keys = this.getOriginFieldKeys(newField, oldField);
155✔
1030

155✔
1031
    if (newField.isLookup && oldField.isLookup) {
155✔
1032
      return;
10✔
1033
    }
10✔
1034

145✔
1035
    // for field type change, isLookup change, isComputed change
145✔
1036
    if (keys.includes('type') || keys.includes('isComputed') || keys.includes('isLookup')) {
155✔
1037
      return this.modifyType(tableId, newField, oldField);
88✔
1038
    }
88✔
1039

57✔
1040
    // for same field with options change
57✔
1041
    if (keys.includes('options')) {
143✔
1042
      return await this.modifyOptions(tableId, newField, oldField);
34✔
1043
    }
34✔
1044
  }
155✔
1045

160✔
1046
  needCalculate(newField: IFieldInstance, oldField: IFieldInstance) {
160✔
1047
    if (!newField.isComputed) {
159✔
1048
      return false;
115✔
1049
    }
115✔
1050

44✔
1051
    return majorFieldKeysChanged(oldField, newField);
44✔
1052
  }
44✔
1053

160✔
1054
  private async calculateField(
160✔
1055
    tableId: string,
155✔
1056
    newField: IFieldInstance,
155✔
1057
    oldField: IFieldInstance
155✔
1058
  ) {
155✔
1059
    if (!newField.isComputed) {
155✔
1060
      return;
111✔
1061
    }
111✔
1062

44✔
1063
    if (!majorFieldKeysChanged(oldField, newField)) {
140✔
1064
      return;
8✔
1065
    }
8✔
1066

36✔
1067
    this.logger.log(`calculating field: ${newField.name}`);
36✔
1068

36✔
1069
    if (newField.lookupOptions) {
140✔
1070
      await this.fieldCalculationService.resetAndCalculateFields(tableId, [newField.id]);
20✔
1071
    } else {
140✔
1072
      await this.fieldCalculationService.calculateFields(tableId, [newField.id]);
16✔
1073
    }
16✔
1074
    await this.fieldService.resolvePending(tableId, [newField.id]);
36✔
1075
  }
36✔
1076

160✔
1077
  private async submitFieldOpsMap(fieldOpsMap: IOpsMap | undefined) {
160✔
1078
    if (!fieldOpsMap) {
132✔
1079
      return;
×
UNCOV
1080
    }
×
1081

132✔
1082
    for (const tableId in fieldOpsMap) {
132✔
1083
      const opData = Object.entries(fieldOpsMap[tableId]).map(([fieldId, ops]) => ({
12✔
1084
        fieldId,
16✔
1085
        ops,
16✔
1086
      }));
16✔
1087
      await this.fieldService.batchUpdateFields(tableId, opData);
12✔
1088
    }
12✔
1089
  }
132✔
1090

160✔
1091
  async alterSupplementLink(
160✔
1092
    tableId: string,
151✔
1093
    newField: IFieldInstance,
151✔
1094
    oldField: IFieldInstance,
151✔
1095
    supplementChange?: { tableId: string; newField: IFieldInstance; oldField: IFieldInstance }
151✔
1096
  ) {
151✔
1097
    // for link ref and create or delete supplement link, (create, delete do not need calculate)
151✔
1098
    await this.fieldConvertingLinkService.alterSupplementLink(tableId, newField, oldField);
151✔
1099

151✔
1100
    // for modify supplement link
151✔
1101
    if (supplementChange) {
151✔
1102
      const { tableId, newField, oldField } = supplementChange;
4✔
1103
      await this.stageAlter(tableId, newField, oldField);
4✔
1104
    }
4✔
1105
  }
151✔
1106

160✔
1107
  async supplementFieldConstraint(tableId: string, field: IFieldInstance) {
160✔
1108
    const { dbTableName } = await this.prismaService.tableMeta.findUniqueOrThrow({
×
1109
      where: { id: tableId },
×
1110
      select: { dbTableName: true },
×
1111
    });
×
1112

×
1113
    const { unique, notNull, dbFieldName } = field;
×
1114

×
1115
    const fieldValidationQuery = this.knex.schema
×
1116
      .alterTable(dbTableName, (table) => {
×
1117
        if (unique) table.unique(dbFieldName);
×
1118
        if (notNull) table.dropNullable(dbFieldName);
×
1119
      })
×
1120
      .toQuery();
×
1121

×
1122
    await this.prismaService.$executeRawUnsafe(fieldValidationQuery);
×
UNCOV
1123
  }
×
1124

160✔
1125
  async stageAnalysis(tableId: string, fieldId: string, updateFieldRo: IConvertFieldRo) {
160✔
1126
    const oldFieldVo = await this.fieldService.getField(tableId, fieldId);
157✔
1127
    if (!oldFieldVo) {
157✔
1128
      throw new BadRequestException(`Not found fieldId(${fieldId})`);
×
UNCOV
1129
    }
×
1130

157✔
1131
    const oldField = createFieldInstanceByVo(oldFieldVo);
157✔
1132
    const newFieldVo = await this.fieldSupplementService.prepareUpdateField(
157✔
1133
      tableId,
157✔
1134
      updateFieldRo,
157✔
1135
      oldField
157✔
1136
    );
155✔
1137

155✔
1138
    const newField = createFieldInstanceByVo(newFieldVo);
155✔
1139
    const modifiedOps = await this.generateModifiedOps(tableId, newField, oldField);
155✔
1140

155✔
1141
    // 2. collect changes effect by the supplement(link) field
155✔
1142
    const supplementChange = await this.fieldConvertingLinkService.analysisLink(newField, oldField);
155✔
1143

155✔
1144
    // 3. preprocessing field validation
155✔
1145
    let needSupplementFieldConstraint = false;
155✔
1146

155✔
1147
    if (majorFieldKeysChanged(oldField, newField) && (oldField.unique || oldField.notNull)) {
157✔
1148
      needSupplementFieldConstraint = true;
×
1149

×
1150
      const { dbTableName } = await this.prismaService.tableMeta.findUniqueOrThrow({
×
1151
        where: { id: tableId },
×
1152
        select: { dbTableName: true },
×
1153
      });
×
1154

×
1155
      const { unique, notNull, dbFieldName } = oldField;
×
1156

×
1157
      const fieldValidationQuery = this.knex.schema
×
1158
        .alterTable(dbTableName, (table) => {
×
1159
          if (unique) table.dropUnique([dbFieldName]);
×
1160
          if (notNull) table.setNullable(dbFieldName);
×
1161
        })
×
1162
        .toQuery();
×
1163

×
1164
      await this.prismaService.$executeRawUnsafe(fieldValidationQuery);
×
UNCOV
1165
    }
✔
1166

155✔
1167
    return {
155✔
1168
      newField,
155✔
1169
      oldField,
155✔
1170
      modifiedOps,
155✔
1171
      supplementChange,
155✔
1172
      needSupplementFieldConstraint,
155✔
1173
    };
155✔
1174
  }
155✔
1175

160✔
1176
  async stageAlter(
160✔
1177
    tableId: string,
159✔
1178
    newField: IFieldInstance,
159✔
1179
    oldField: IFieldInstance,
159✔
1180
    modifiedOps?: IModifiedOps
159✔
1181
  ) {
159✔
1182
    const ops = this.getOriginFieldOps(newField, oldField);
159✔
1183

159✔
1184
    if (this.needCalculate(newField, oldField)) {
159✔
1185
      ops.push(
36✔
1186
        FieldOpBuilder.editor.setFieldProperty.build({
36✔
1187
          key: 'isPending',
36✔
1188
          newValue: true,
36✔
1189
          oldValue: undefined,
36✔
1190
        })
36✔
1191
      );
36✔
1192
    }
36✔
1193

159✔
1194
    // apply current field changes
159✔
1195
    await this.fieldService.batchUpdateFields(tableId, [
159✔
1196
      { fieldId: newField.id, ops: ops.concat(modifiedOps?.fieldOps || []) },
159✔
1197
    ]);
159✔
1198

157✔
1199
    // apply referenced fields changes
157✔
1200
    await this.updateReferencedFields(newField, oldField);
157✔
1201
  }
157✔
1202

160✔
1203
  async stageCalculate(
160✔
1204
    tableId: string,
155✔
1205
    newField: IFieldInstance,
155✔
1206
    oldField: IFieldInstance,
155✔
1207
    modifiedOps?: IModifiedOps
155✔
1208
  ) {
155✔
1209
    await this.updateReference(newField, oldField);
155✔
1210

155✔
1211
    // calculate and submit records
155✔
1212
    await this.calculateAndSaveRecords(tableId, newField, modifiedOps?.recordOpsMap);
155✔
1213

155✔
1214
    // calculate computed fields
155✔
1215
    await this.calculateField(tableId, newField, oldField);
155✔
1216
  }
155✔
1217
}
160✔
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