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

teableio / teable / 8389034568

22 Mar 2024 10:38AM UTC coverage: 79.934% (+51.7%) from 28.208%
8389034568

Pull #487

github

web-flow
Merge 3045b1f94 into a06c6afb1
Pull Request #487: refactor: move zod schema to openapi

3263 of 3860 branches covered (84.53%)

67 of 70 new or added lines in 23 files covered. (95.71%)

762 existing lines in 27 files now uncovered.

25152 of 31466 relevant lines covered (79.93%)

1188.31 hits per line

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

95.49
/apps/nestjs-backend/src/features/field/field-calculate/field-supplement.service.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
2✔
2
import { BadRequestException, Injectable } from '@nestjs/common';
2✔
3
import type {
2✔
4
  IFieldRo,
2✔
5
  IFieldVo,
2✔
6
  IFormulaFieldOptions,
2✔
7
  ILinkFieldOptions,
2✔
8
  ILinkFieldOptionsRo,
2✔
9
  ILookupOptionsRo,
2✔
10
  ILookupOptionsVo,
2✔
11
  IRollupFieldOptions,
2✔
12
  ISelectFieldOptionsRo,
2✔
13
  IConvertFieldRo,
2✔
14
  IUserFieldOptions,
2✔
15
} from '@teable/core';
2✔
16
import {
2✔
17
  assertNever,
2✔
18
  AttachmentFieldCore,
2✔
19
  AutoNumberFieldCore,
2✔
20
  CellValueType,
2✔
21
  CheckboxFieldCore,
2✔
22
  ColorUtils,
2✔
23
  CreatedTimeFieldCore,
2✔
24
  DateFieldCore,
2✔
25
  DbFieldType,
2✔
26
  FieldType,
2✔
27
  generateChoiceId,
2✔
28
  generateFieldId,
2✔
29
  getDefaultFormatting,
2✔
30
  getFormattingSchema,
2✔
31
  getRandomString,
2✔
32
  getShowAsSchema,
2✔
33
  getUniqName,
2✔
34
  isMultiValueLink,
2✔
35
  LastModifiedTimeFieldCore,
2✔
36
  LongTextFieldCore,
2✔
37
  NumberFieldCore,
2✔
38
  RatingFieldCore,
2✔
39
  Relationship,
2✔
40
  RelationshipRevert,
2✔
41
  SelectFieldCore,
2✔
42
  SingleLineTextFieldCore,
2✔
43
  UserFieldCore,
2✔
44
} from '@teable/core';
2✔
45
import { PrismaService } from '@teable/db-main-prisma';
2✔
46
import { Knex } from 'knex';
2✔
47
import { keyBy, merge } from 'lodash';
2✔
48
import { InjectModel } from 'nest-knexjs';
2✔
49
import type { z } from 'zod';
2✔
50
import { fromZodError } from 'zod-validation-error';
2✔
51
import { InjectDbProvider } from '../../../db-provider/db.provider';
2✔
52
import { IDbProvider } from '../../../db-provider/db.provider.interface';
2✔
53
import { ReferenceService } from '../../calculation/reference.service';
2✔
54
import { hasCycle } from '../../calculation/utils/dfs';
2✔
55
import { FieldService } from '../field.service';
2✔
56
import type { IFieldInstance } from '../model/factory';
2✔
57
import { createFieldInstanceByRaw, createFieldInstanceByVo } from '../model/factory';
2✔
58
import { FormulaFieldDto } from '../model/field-dto/formula-field.dto';
2✔
59
import type { LinkFieldDto } from '../model/field-dto/link-field.dto';
2✔
60
import { RollupFieldDto } from '../model/field-dto/rollup-field.dto';
2✔
61

2✔
62
@Injectable()
2✔
63
export class FieldSupplementService {
2✔
64
  constructor(
64✔
65
    private readonly fieldService: FieldService,
64✔
66
    private readonly prismaService: PrismaService,
64✔
67
    private readonly referenceService: ReferenceService,
64✔
68
    @InjectDbProvider() private readonly dbProvider: IDbProvider,
64✔
69
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
64✔
70
  ) {}
64✔
71

64✔
72
  private async getDbTableName(tableId: string) {
64✔
73
    const tableMeta = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
742✔
74
      where: { id: tableId },
742✔
75
      select: { dbTableName: true },
742✔
76
    });
742✔
77
    return tableMeta.dbTableName;
742✔
78
  }
742✔
79

64✔
80
  private getForeignKeyFieldName(fieldId: string | undefined) {
64✔
81
    if (!fieldId) {
456✔
82
      return `__fk_rad${getRandomString(16)}`;
48✔
83
    }
48✔
84
    return `__fk_${fieldId}`;
408✔
85
  }
408✔
86

64✔
87
  private async getJunctionTableName(
64✔
88
    tableId: string,
85✔
89
    fieldId: string,
85✔
90
    symmetricFieldId: string | undefined
85✔
91
  ) {
85✔
92
    const { baseId } = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
85✔
93
      where: { id: tableId, deletedTime: null },
85✔
94
      select: { baseId: true },
85✔
95
    });
85✔
96

85✔
97
    const junctionTableName = symmetricFieldId
85✔
98
      ? `junction_${fieldId}_${symmetricFieldId}`
37✔
99
      : `junction_${fieldId}`;
85✔
100
    return this.dbProvider.generateDbTableName(baseId, junctionTableName);
85✔
101
  }
85✔
102

64✔
103
  private async getDefaultLinkName(foreignTableId: string) {
64✔
104
    const tableRaw = await this.prismaService.tableMeta.findUnique({
106✔
105
      where: { id: foreignTableId },
106✔
106
      select: { name: true },
106✔
107
    });
106✔
108
    if (!tableRaw) {
106!
109
      throw new BadRequestException(`foreignTableId ${foreignTableId} is invalid`);
×
110
    }
×
111
    return tableRaw.name;
106✔
112
  }
106✔
113

64✔
114
  private async generateLinkOptionsVo(params: {
64✔
115
    tableId: string;
371✔
116
    optionsRo: ILinkFieldOptionsRo;
371✔
117
    fieldId: string;
371✔
118
    symmetricFieldId: string | undefined;
371✔
119
    lookupFieldId: string;
371✔
120
    dbTableName: string;
371✔
121
    foreignTableName: string;
371✔
122
  }): Promise<ILinkFieldOptions> {
371✔
123
    const {
371✔
124
      tableId,
371✔
125
      optionsRo,
371✔
126
      fieldId,
371✔
127
      symmetricFieldId,
371✔
128
      lookupFieldId,
371✔
129
      dbTableName,
371✔
130
      foreignTableName,
371✔
131
    } = params;
371✔
132
    const { relationship, isOneWay } = optionsRo;
371✔
133
    const common = {
371✔
134
      ...optionsRo,
371✔
135
      symmetricFieldId,
371✔
136
      lookupFieldId,
371✔
137
    };
371✔
138

371✔
139
    if (relationship === Relationship.ManyMany) {
371✔
140
      const fkHostTableName = await this.getJunctionTableName(tableId, fieldId, symmetricFieldId);
55✔
141
      return {
55✔
142
        ...common,
55✔
143
        fkHostTableName,
55✔
144
        selfKeyName: this.getForeignKeyFieldName(symmetricFieldId),
55✔
145
        foreignKeyName: this.getForeignKeyFieldName(fieldId),
55✔
146
      };
55✔
147
    }
55✔
148

316✔
149
    if (relationship === Relationship.ManyOne) {
371✔
150
      return {
150✔
151
        ...common,
150✔
152
        fkHostTableName: dbTableName,
150✔
153
        selfKeyName: '__id',
150✔
154
        foreignKeyName: this.getForeignKeyFieldName(fieldId),
150✔
155
      };
150✔
156
    }
150✔
157

166✔
158
    if (relationship === Relationship.OneMany) {
282✔
159
      return {
106✔
160
        ...common,
106✔
161
        /**
106✔
162
         * Semantically, one way link should not cause any side effects on the foreign table,
106✔
163
         * so we should not modify the foreign table when `isOneWay` enable.
106✔
164
         * Instead, we will create a junction table to store the foreign key.
106✔
165
         */
106✔
166
        fkHostTableName: isOneWay
106✔
167
          ? await this.getJunctionTableName(tableId, fieldId, symmetricFieldId)
30✔
168
          : foreignTableName,
76✔
169
        selfKeyName: this.getForeignKeyFieldName(symmetricFieldId),
106✔
170
        foreignKeyName: isOneWay ? this.getForeignKeyFieldName(fieldId) : '__id',
106✔
171
      };
106✔
172
    }
106✔
173

60✔
174
    if (relationship === Relationship.OneOne) {
60✔
175
      return {
60✔
176
        ...common,
60✔
177
        fkHostTableName: dbTableName,
60✔
178
        selfKeyName: '__id',
60✔
179
        foreignKeyName: this.getForeignKeyFieldName(fieldId),
60✔
180
      };
60✔
181
    }
60!
182

×
183
    throw new BadRequestException('relationship is invalid');
×
184
  }
×
185

64✔
186
  async generateNewLinkOptionsVo(
64✔
187
    tableId: string,
355✔
188
    fieldId: string,
355✔
189
    optionsRo: ILinkFieldOptionsRo
355✔
190
  ): Promise<ILinkFieldOptions> {
355✔
191
    const { foreignTableId, isOneWay } = optionsRo;
355✔
192
    const symmetricFieldId = isOneWay ? undefined : generateFieldId();
355✔
193
    const dbTableName = await this.getDbTableName(tableId);
355✔
194
    const foreignTableName = await this.getDbTableName(foreignTableId);
355✔
195

355✔
196
    const { id: lookupFieldId } = await this.prismaService.field.findFirstOrThrow({
355✔
197
      where: { tableId: foreignTableId, isPrimary: true },
355✔
198
      select: { id: true },
355✔
199
    });
355✔
200

355✔
201
    return this.generateLinkOptionsVo({
355✔
202
      tableId,
355✔
203
      optionsRo,
355✔
204
      fieldId,
355✔
205
      symmetricFieldId,
355✔
206
      lookupFieldId,
355✔
207
      dbTableName,
355✔
208
      foreignTableName,
355✔
209
    });
355✔
210
  }
355✔
211

64✔
212
  async generateUpdatedLinkOptionsVo(
64✔
213
    tableId: string,
16✔
214
    fieldId: string,
16✔
215
    oldOptions: ILinkFieldOptions,
16✔
216
    newOptionsRo: ILinkFieldOptionsRo
16✔
217
  ): Promise<ILinkFieldOptions> {
16✔
218
    const { foreignTableId, isOneWay } = newOptionsRo;
16✔
219

16✔
220
    const dbTableName = await this.getDbTableName(tableId);
16✔
221
    const foreignTableName = await this.getDbTableName(foreignTableId);
16✔
222

16✔
223
    const symmetricFieldId = isOneWay
16!
224
      ? undefined
×
225
      : oldOptions.foreignTableId === newOptionsRo.foreignTableId
16✔
226
        ? oldOptions.symmetricFieldId
6✔
227
        : generateFieldId();
10✔
228

16✔
229
    const lookupFieldId =
16✔
230
      oldOptions.foreignTableId === foreignTableId
16✔
231
        ? oldOptions.lookupFieldId
6✔
232
        : (
10✔
233
            await this.prismaService.field.findFirstOrThrow({
10✔
234
              where: { tableId: foreignTableId, isPrimary: true, deletedTime: null },
10✔
235
              select: { id: true },
10✔
236
            })
10✔
237
          ).id;
16✔
238

16✔
239
    return this.generateLinkOptionsVo({
16✔
240
      tableId,
16✔
241
      optionsRo: newOptionsRo,
16✔
242
      fieldId,
16✔
243
      symmetricFieldId,
16✔
244
      lookupFieldId,
16✔
245
      dbTableName,
16✔
246
      foreignTableName,
16✔
247
    });
16✔
248
  }
16✔
249

64✔
250
  private async prepareLinkField(tableId: string, field: IFieldRo) {
64✔
251
    const options = field.options as ILinkFieldOptionsRo;
355✔
252
    const { relationship, foreignTableId } = options;
355✔
253

355✔
254
    const fieldId = field.id ?? generateFieldId();
355✔
255
    const optionsVo = await this.generateNewLinkOptionsVo(tableId, fieldId, options);
355✔
256

355✔
257
    return {
355✔
258
      ...field,
355✔
259
      id: fieldId,
355✔
260
      name: field.name ?? (await this.getDefaultLinkName(foreignTableId)),
355✔
261
      options: optionsVo,
106✔
262
      isMultipleCellValue: isMultiValueLink(relationship) || undefined,
348✔
263
      dbFieldType: DbFieldType.Json,
355✔
264
      cellValueType: CellValueType.String,
355✔
265
    };
355✔
266
  }
355✔
267

64✔
268
  // only for linkField to linkField
64✔
269
  private async prepareUpdateLinkField(tableId: string, fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
270
    const newOptionsRo = fieldRo.options as ILinkFieldOptionsRo;
18✔
271
    const oldOptions = oldFieldVo.options as ILinkFieldOptions;
18✔
272
    // isOneWay may be undefined or false, so we should convert it to boolean
18✔
273
    const oldIsOneWay = Boolean(oldOptions.isOneWay);
18✔
274
    const newIsOneWay = Boolean(newOptionsRo.isOneWay);
18✔
275
    if (
18✔
276
      oldOptions.foreignTableId === newOptionsRo.foreignTableId &&
18✔
277
      oldOptions.relationship === newOptionsRo.relationship &&
18✔
278
      oldIsOneWay !== newIsOneWay
4✔
279
    ) {
18✔
280
      return {
2✔
281
        ...oldFieldVo,
2✔
282
        ...fieldRo,
2✔
283
        options: {
2✔
284
          ...oldOptions,
2✔
285
          ...newOptionsRo,
2✔
286
          symmetricFieldId: newOptionsRo.isOneWay ? undefined : generateFieldId(),
2!
287
        },
2✔
288
      };
2✔
289
    }
2✔
290

16✔
291
    const fieldId = oldFieldVo.id;
16✔
292

16✔
293
    const optionsVo = await this.generateUpdatedLinkOptionsVo(
16✔
294
      tableId,
16✔
295
      fieldId,
16✔
296
      oldOptions,
16✔
297
      newOptionsRo
16✔
298
    );
16✔
299

16✔
300
    return {
16✔
301
      ...oldFieldVo,
16✔
302
      ...fieldRo,
16✔
303
      options: optionsVo,
16✔
304
      isMultipleCellValue: isMultiValueLink(optionsVo.relationship) || undefined,
18✔
305
      dbFieldType: DbFieldType.Json,
18✔
306
      cellValueType: CellValueType.String,
18✔
307
    };
18✔
308
  }
18✔
309

64✔
310
  private async prepareLookupOptions(field: IFieldRo, batchFieldVos?: IFieldVo[]) {
64✔
311
    const { lookupOptions } = field;
155✔
312
    if (!lookupOptions) {
155!
313
      throw new BadRequestException('lookupOptions is required');
×
314
    }
×
315

155✔
316
    const { linkFieldId, lookupFieldId, foreignTableId } = lookupOptions;
155✔
317
    const linkFieldRaw = await this.prismaService.field.findFirst({
155✔
318
      where: { id: linkFieldId, deletedTime: null, type: FieldType.Link },
155✔
319
      select: { name: true, options: true, isMultipleCellValue: true },
155✔
320
    });
155✔
321

155✔
322
    const optionsRaw = linkFieldRaw?.options || null;
155!
323
    const linkFieldOptions: ILinkFieldOptions =
155✔
324
      (optionsRaw && JSON.parse(optionsRaw as string)) ||
155!
325
      batchFieldVos?.find((field) => field.id === linkFieldId)?.options;
×
326

155✔
327
    if (!linkFieldOptions || !linkFieldRaw) {
155!
328
      throw new BadRequestException(`linkFieldId ${linkFieldId} is invalid`);
×
329
    }
×
330

155✔
331
    if (foreignTableId !== linkFieldOptions.foreignTableId) {
155!
332
      throw new BadRequestException(`foreignTableId ${foreignTableId} is invalid`);
×
333
    }
×
334

155✔
335
    const lookupFieldRaw = await this.prismaService.field.findFirst({
155✔
336
      where: { id: lookupFieldId, deletedTime: null },
155✔
337
    });
155✔
338

155✔
339
    if (!lookupFieldRaw) {
155!
340
      throw new BadRequestException(`Lookup field ${lookupFieldId} is not exist`);
×
341
    }
×
342

155✔
343
    return {
155✔
344
      lookupOptions: {
155✔
345
        linkFieldId,
155✔
346
        lookupFieldId,
155✔
347
        foreignTableId,
155✔
348
        relationship: linkFieldOptions.relationship,
155✔
349
        fkHostTableName: linkFieldOptions.fkHostTableName,
155✔
350
        selfKeyName: linkFieldOptions.selfKeyName,
155✔
351
        foreignKeyName: linkFieldOptions.foreignKeyName,
155✔
352
      },
155✔
353
      lookupFieldRaw,
155✔
354
      linkFieldRaw,
155✔
355
    };
155✔
356
  }
155✔
357

64✔
358
  getDbFieldType(
64✔
359
    fieldType: FieldType,
285✔
360
    cellValueType: CellValueType,
285✔
361
    isMultipleCellValue?: boolean
285✔
362
  ) {
285✔
363
    if (isMultipleCellValue) {
285✔
364
      return DbFieldType.Json;
97✔
365
    }
97✔
366

188✔
367
    if (fieldType === FieldType.Link) {
285✔
368
      return DbFieldType.Json;
×
369
    }
✔
370

188✔
371
    switch (cellValueType) {
188✔
372
      case CellValueType.Number:
266✔
373
        return DbFieldType.Real;
80✔
374
      case CellValueType.DateTime:
285✔
375
        return DbFieldType.DateTime;
4✔
376
      case CellValueType.Boolean:
285✔
377
        return DbFieldType.Boolean;
4✔
378
      case CellValueType.String:
285✔
379
        return DbFieldType.Text;
100✔
380
      default:
285!
381
        assertNever(cellValueType);
×
382
    }
285✔
383
  }
285✔
384

64✔
385
  private prepareFormattingShowAs(
64✔
386
    options: IFieldRo['options'] = {},
113✔
387
    sourceOptions: IFieldVo['options'],
113✔
388
    cellValueType: CellValueType,
113✔
389
    isMultipleCellValue?: boolean
113✔
390
  ) {
113✔
391
    const sourceFormatting = 'formatting' in sourceOptions ? sourceOptions.formatting : undefined;
113✔
392
    const showAsSchema = getShowAsSchema(cellValueType, isMultipleCellValue);
113✔
393
    let sourceShowAs = 'showAs' in sourceOptions ? sourceOptions.showAs : undefined;
113!
394

113✔
395
    // if source showAs is invalid, we should ignore it
113✔
396
    if (sourceShowAs && !showAsSchema.safeParse(sourceShowAs).success) {
113!
397
      sourceShowAs = undefined;
×
398
    }
×
399

113✔
400
    const formatting =
113✔
401
      'formatting' in options
113✔
402
        ? options.formatting
18✔
403
        : sourceFormatting
95✔
404
          ? sourceFormatting
14✔
405
          : getDefaultFormatting(cellValueType);
81✔
406

113✔
407
    const showAs = 'showAs' in options ? options.showAs : sourceShowAs;
113✔
408

113✔
409
    return {
113✔
410
      ...sourceOptions,
113✔
411
      ...(formatting ? { formatting } : {}),
113✔
412
      ...(showAs ? { showAs } : {}),
113✔
413
    };
113✔
414
  }
113✔
415

64✔
416
  private async prepareLookupField(fieldRo: IFieldRo, batchFieldVos?: IFieldVo[]) {
64✔
417
    const { lookupOptions, lookupFieldRaw, linkFieldRaw } = await this.prepareLookupOptions(
113✔
418
      fieldRo,
113✔
419
      batchFieldVos
113✔
420
    );
113✔
421

113✔
422
    if (lookupFieldRaw.type !== fieldRo.type) {
113!
423
      throw new BadRequestException(
×
424
        `Current field type ${fieldRo.type} is not equal to lookup field (${lookupFieldRaw.type})`
×
425
      );
×
426
    }
×
427

113✔
428
    const isMultipleCellValue =
113✔
429
      linkFieldRaw.isMultipleCellValue || lookupFieldRaw.isMultipleCellValue || false;
113✔
430

113✔
431
    const cellValueType = lookupFieldRaw.cellValueType as CellValueType;
113✔
432

113✔
433
    const options = this.prepareFormattingShowAs(
113✔
434
      fieldRo.options,
113✔
435
      JSON.parse(lookupFieldRaw.options as string),
113✔
436
      cellValueType,
113✔
437
      isMultipleCellValue
113✔
438
    );
113✔
439

113✔
440
    return {
113✔
441
      ...fieldRo,
113✔
442
      name: fieldRo.name ?? `${lookupFieldRaw.name} (from ${linkFieldRaw.name})`,
113✔
443
      options,
113✔
444
      lookupOptions,
113✔
445
      isMultipleCellValue,
113✔
446
      isComputed: true,
113✔
447
      cellValueType,
113✔
448
      dbFieldType: this.getDbFieldType(fieldRo.type, cellValueType, isMultipleCellValue),
113✔
449
    };
113✔
450
  }
113✔
451

64✔
452
  private async prepareUpdateLookupField(fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
453
    const newLookupOptions = fieldRo.lookupOptions as ILookupOptionsRo;
10✔
454
    const oldLookupOptions = oldFieldVo.lookupOptions as ILookupOptionsVo;
10✔
455
    if (
10✔
456
      oldFieldVo.isLookup &&
10✔
457
      newLookupOptions.lookupFieldId === oldLookupOptions.lookupFieldId &&
10✔
458
      newLookupOptions.linkFieldId === oldLookupOptions.linkFieldId &&
10✔
459
      newLookupOptions.foreignTableId === oldLookupOptions.foreignTableId
4✔
460
    ) {
10✔
461
      return merge(
4✔
462
        {},
4✔
463
        fieldRo.options
4✔
464
          ? {
2✔
465
              ...oldFieldVo,
2✔
466
              options: { ...oldFieldVo.options, showAs: undefined }, // clean showAs
2✔
467
            }
2✔
468
          : oldFieldVo,
2✔
469
        fieldRo
4✔
470
      );
4✔
471
    }
4✔
472

6✔
473
    return this.prepareLookupField(fieldRo);
6✔
474
  }
6✔
475

64✔
476
  private async prepareFormulaField(fieldRo: IFieldRo, batchFieldVos?: IFieldVo[]) {
64✔
477
    let fieldIds;
114✔
478
    try {
114✔
479
      fieldIds = FormulaFieldDto.getReferenceFieldIds(
114✔
480
        (fieldRo.options as IFormulaFieldOptions).expression
114✔
481
      );
114✔
482
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
114✔
483
    } catch (e: any) {
114!
484
      throw new BadRequestException('expression parse error');
×
485
    }
×
486

114✔
487
    const fieldRaws = await this.prismaService.field.findMany({
114✔
488
      where: { id: { in: fieldIds }, deletedTime: null },
114✔
489
    });
114✔
490

114✔
491
    const fields = fieldRaws.map((fieldRaw) => createFieldInstanceByRaw(fieldRaw));
114✔
492
    const batchFields = batchFieldVos?.map((fieldVo) => createFieldInstanceByVo(fieldVo));
114✔
493
    const fieldMap = keyBy(fields.concat(batchFields || []), 'id');
114✔
494

114✔
495
    if (fieldIds.find((id) => !fieldMap[id])) {
114✔
496
      throw new BadRequestException(`formula field reference ${fieldIds.join()} not found`);
×
497
    }
×
498

114✔
499
    const { cellValueType, isMultipleCellValue } = FormulaFieldDto.getParsedValueType(
114✔
500
      (fieldRo.options as IFormulaFieldOptions).expression,
114✔
501
      fieldMap
114✔
502
    );
114✔
503

114✔
504
    const formatting =
114✔
505
      (fieldRo.options as IFormulaFieldOptions)?.formatting ?? getDefaultFormatting(cellValueType);
114✔
506

114✔
507
    return {
114✔
508
      ...fieldRo,
114✔
509
      name: fieldRo.name ?? 'Calculation',
114✔
510
      options: {
114✔
511
        ...fieldRo.options,
114✔
512
        ...(formatting ? { formatting } : {}),
114✔
513
      },
114✔
514
      cellValueType,
114✔
515
      isMultipleCellValue,
114✔
516
      isComputed: true,
114✔
517
      dbFieldType: this.getDbFieldType(
114✔
518
        fieldRo.type,
114✔
519
        cellValueType as CellValueType,
114✔
520
        isMultipleCellValue
114✔
521
      ),
114✔
522
    };
114✔
523
  }
114✔
524

64✔
525
  private async prepareUpdateFormulaField(fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
526
    const newOptions = fieldRo.options as IFormulaFieldOptions;
10✔
527
    const oldOptions = oldFieldVo.options as IFormulaFieldOptions;
10✔
528

10✔
529
    if (newOptions.expression === oldOptions.expression) {
10✔
530
      return merge({}, oldFieldVo, fieldRo);
2✔
531
    }
2✔
532

8✔
533
    return this.prepareFormulaField(fieldRo);
8✔
534
  }
8✔
535

64✔
536
  private async prepareRollupField(field: IFieldRo, batchFieldVos?: IFieldVo[]) {
64✔
537
    const { lookupOptions, linkFieldRaw, lookupFieldRaw } = await this.prepareLookupOptions(
42✔
538
      field,
42✔
539
      batchFieldVos
42✔
540
    );
42✔
541
    const options = field.options as IRollupFieldOptions;
42✔
542
    const lookupField = createFieldInstanceByRaw(lookupFieldRaw);
42✔
543
    if (!options) {
42!
544
      throw new BadRequestException('rollup field options is required');
×
545
    }
×
546

42✔
547
    let valueType;
42✔
548
    try {
42✔
549
      valueType = RollupFieldDto.getParsedValueType(
42✔
550
        options.expression,
42✔
551
        lookupField.cellValueType,
42✔
552
        lookupField.isMultipleCellValue || linkFieldRaw.isMultipleCellValue || false
42✔
553
      );
42✔
554
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
42✔
555
    } catch (e: any) {
42!
556
      throw new BadRequestException(`Parse rollUp Error: ${e.message}`);
×
557
    }
×
558

42✔
559
    const { cellValueType, isMultipleCellValue } = valueType;
42✔
560

42✔
561
    const formatting = options.formatting ?? getDefaultFormatting(cellValueType);
42✔
562

42✔
563
    return {
42✔
564
      ...field,
42✔
565
      name: field.name ?? `${lookupFieldRaw.name} Rollup (from ${linkFieldRaw.name})`,
42✔
566
      options: {
42✔
567
        ...options,
42✔
568
        ...(formatting ? { formatting } : {}),
42✔
569
      },
42✔
570
      lookupOptions,
42✔
571
      cellValueType,
42✔
572
      isComputed: true,
42✔
573
      isMultipleCellValue,
42✔
574
      dbFieldType: this.getDbFieldType(
42✔
575
        field.type,
42✔
576
        cellValueType as CellValueType,
42✔
577
        isMultipleCellValue
42✔
578
      ),
42✔
579
    };
42✔
580
  }
42✔
581

64✔
582
  private async prepareUpdateRollupField(fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
583
    const newOptions = fieldRo.options as IRollupFieldOptions;
4✔
584
    const oldOptions = oldFieldVo.options as IRollupFieldOptions;
4✔
585

4✔
586
    const newLookupOptions = fieldRo.lookupOptions as ILookupOptionsRo;
4✔
587
    const oldLookupOptions = oldFieldVo.lookupOptions as ILookupOptionsVo;
4✔
588
    if (
4✔
589
      newOptions.expression === oldOptions.expression &&
4✔
590
      newLookupOptions.lookupFieldId === oldLookupOptions.lookupFieldId &&
4✔
591
      newLookupOptions.linkFieldId === oldLookupOptions.linkFieldId &&
4✔
592
      newLookupOptions.foreignTableId === oldLookupOptions.foreignTableId
2✔
593
    ) {
4✔
594
      return merge({}, oldFieldVo, fieldRo);
2✔
595
    }
2✔
596

2✔
597
    return this.prepareRollupField(fieldRo);
2✔
598
  }
2✔
599

64✔
600
  private prepareSingleTextField(field: IFieldRo) {
64✔
601
    const { name, options } = field;
1,132✔
602

1,132✔
603
    return {
1,132✔
604
      ...field,
1,132✔
605
      name: name ?? 'Label',
1,132✔
606
      options: options ?? SingleLineTextFieldCore.defaultOptions(),
1,132✔
607
      cellValueType: CellValueType.String,
1,132✔
608
      dbFieldType: DbFieldType.Text,
1,132✔
609
    };
1,132✔
610
  }
1,132✔
611

64✔
612
  private prepareLongTextField(field: IFieldRo) {
64✔
613
    const { name, options } = field;
26✔
614

26✔
615
    return {
26✔
616
      ...field,
26✔
617
      name: name ?? 'Notes',
26!
618
      options: options ?? LongTextFieldCore.defaultOptions(),
26✔
619
      cellValueType: CellValueType.String,
26✔
620
      dbFieldType: DbFieldType.Text,
26✔
621
    };
26✔
622
  }
26✔
623

64✔
624
  private prepareNumberField(field: IFieldRo) {
64✔
625
    const { name, options } = field;
1,014✔
626

1,014✔
627
    return {
1,014✔
628
      ...field,
1,014✔
629
      name: name ?? 'Number',
1,014✔
630
      options: options ?? NumberFieldCore.defaultOptions(),
1,014✔
631
      cellValueType: CellValueType.Number,
1,014✔
632
      dbFieldType: DbFieldType.Real,
1,014✔
633
    };
1,014✔
634
  }
1,014✔
635

64✔
636
  private prepareRatingField(field: IFieldRo) {
64✔
637
    const { name, options } = field;
8✔
638

8✔
639
    return {
8✔
640
      ...field,
8✔
641
      name: name ?? 'Rating',
8✔
642
      options: options ?? RatingFieldCore.defaultOptions(),
8!
643
      cellValueType: CellValueType.Number,
8✔
644
      dbFieldType: DbFieldType.Real,
8✔
645
    };
8✔
646
  }
8✔
647

64✔
648
  private prepareSelectOptions(options: ISelectFieldOptionsRo) {
64✔
649
    const optionsRo = (options ?? SelectFieldCore.defaultOptions()) as ISelectFieldOptionsRo;
694✔
650
    const nameSet = new Set<string>();
694✔
651
    return {
694✔
652
      ...optionsRo,
694✔
653
      choices: optionsRo.choices.map((choice) => {
694✔
654
        if (nameSet.has(choice.name)) {
2,022✔
655
          throw new BadRequestException(`choice name ${choice.name} is duplicated`);
2✔
656
        }
2✔
657
        nameSet.add(choice.name);
2,020✔
658
        return {
2,020✔
659
          name: choice.name,
2,020✔
660
          id: choice.id ?? generateChoiceId(),
2,022✔
661
          color: choice.color ?? ColorUtils.randomColor()[0],
2,022!
662
        };
2,022✔
663
      }),
2,022✔
664
    };
694✔
665
  }
694✔
666

64✔
667
  private prepareSingleSelectField(field: IFieldRo) {
64✔
668
    const { name, options } = field;
664✔
669

664✔
670
    return {
664✔
671
      ...field,
664✔
672
      name: name ?? 'Select',
664✔
673
      options: this.prepareSelectOptions(options as ISelectFieldOptionsRo),
664✔
674
      cellValueType: CellValueType.String,
664✔
675
      dbFieldType: DbFieldType.Text,
664✔
676
    };
664✔
677
  }
664✔
678

64✔
679
  private prepareMultipleSelectField(field: IFieldRo) {
64✔
680
    const { name, options } = field;
30✔
681

30✔
682
    return {
30✔
683
      ...field,
30✔
684
      name: name ?? 'Tags',
30✔
685
      options: this.prepareSelectOptions(options as ISelectFieldOptionsRo),
30✔
686
      cellValueType: CellValueType.String,
30✔
687
      dbFieldType: DbFieldType.Json,
30✔
688
      isMultipleCellValue: true,
30✔
689
    };
30✔
690
  }
30✔
691

64✔
692
  private prepareAttachmentField(field: IFieldRo) {
64✔
693
    const { name, options } = field;
18✔
694

18✔
695
    return {
18✔
696
      ...field,
18✔
697
      name: name ?? 'Attachments',
18✔
698
      options: options ?? AttachmentFieldCore.defaultOptions(),
18✔
699
      cellValueType: CellValueType.String,
18✔
700
      dbFieldType: DbFieldType.Json,
18✔
701
      isMultipleCellValue: true,
18✔
702
    };
18✔
703
  }
18✔
704

64✔
705
  private async prepareUpdateUserField(fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
706
    const mergeObj = merge({}, oldFieldVo, fieldRo);
×
707

×
708
    return this.prepareUserField(mergeObj);
×
709
  }
×
710

64✔
711
  private prepareUserField(field: IFieldRo) {
64✔
712
    const { name, options = UserFieldCore.defaultOptions() } = field;
20✔
713
    const { isMultiple } = options as IUserFieldOptions;
20✔
714

20✔
715
    return {
20✔
716
      ...field,
20✔
717
      name: name ?? `Collaborator${isMultiple ? 's' : ''}`,
20!
718
      options: options,
20✔
719
      cellValueType: CellValueType.String,
20✔
720
      dbFieldType: DbFieldType.Json,
20✔
721
      isMultipleCellValue: isMultiple || undefined,
20✔
722
    };
20✔
723
  }
20✔
724

64✔
725
  private prepareDateField(field: IFieldRo) {
64✔
726
    const { name, options } = field;
38✔
727

38✔
728
    return {
38✔
729
      ...field,
38✔
730
      name: name ?? 'Date',
38✔
731
      options: options ?? DateFieldCore.defaultOptions(),
38✔
732
      cellValueType: CellValueType.DateTime,
38✔
733
      dbFieldType: DbFieldType.DateTime,
38✔
734
    };
38✔
735
  }
38✔
736

64✔
737
  private prepareAutoNumberField(field: IFieldRo) {
64✔
738
    const { name } = field;
2✔
739
    const options = field.options ?? AutoNumberFieldCore.defaultOptions();
2!
740

2✔
741
    return {
2✔
742
      ...field,
2✔
743
      name: name ?? 'ID',
2!
744
      options: { ...options, expression: 'AUTO_NUMBER()' },
2✔
745
      cellValueType: CellValueType.Number,
2✔
746
      dbFieldType: DbFieldType.Integer,
2✔
747
      isComputed: true,
2✔
748
    };
2✔
749
  }
2✔
750

64✔
751
  private prepareCreatedTimeField(field: IFieldRo) {
64✔
752
    const { name } = field;
2✔
753
    const options = field.options ?? CreatedTimeFieldCore.defaultOptions();
2!
754

2✔
755
    return {
2✔
756
      ...field,
2✔
757
      name: name ?? 'Created Time',
2!
758
      options: { ...options, expression: 'CREATED_TIME()' },
2✔
759
      cellValueType: CellValueType.DateTime,
2✔
760
      dbFieldType: DbFieldType.DateTime,
2✔
761
      isComputed: true,
2✔
762
    };
2✔
763
  }
2✔
764

64✔
765
  private prepareLastModifiedTimeField(field: IFieldRo) {
64✔
766
    const { name } = field;
2✔
767
    const options = field.options ?? LastModifiedTimeFieldCore.defaultOptions();
2!
768

2✔
769
    return {
2✔
770
      ...field,
2✔
771
      name: name ?? 'Last Modified Time',
2!
772
      options: { ...options, expression: 'LAST_MODIFIED_TIME()' },
2✔
773
      cellValueType: CellValueType.DateTime,
2✔
774
      dbFieldType: DbFieldType.DateTime,
2✔
775
      isComputed: true,
2✔
776
    };
2✔
777
  }
2✔
778

64✔
779
  private prepareCheckboxField(field: IFieldRo) {
64✔
780
    const { name, options } = field;
22✔
781

22✔
782
    return {
22✔
783
      ...field,
22✔
784
      name: name ?? 'Done',
22✔
785
      options: options ?? CheckboxFieldCore.defaultOptions(),
22✔
786
      cellValueType: CellValueType.Boolean,
22✔
787
      dbFieldType: DbFieldType.Boolean,
22✔
788
    };
22✔
789
  }
22✔
790

64✔
791
  private async prepareCreateFieldInner(
64✔
792
    tableId: string,
3,564✔
793
    fieldRo: IFieldRo,
3,564✔
794
    batchFieldVos?: IFieldVo[]
3,564✔
795
  ) {
3,564✔
796
    if (fieldRo.isLookup) {
3,564✔
797
      return this.prepareLookupField(fieldRo, batchFieldVos);
107✔
798
    }
107✔
799

3,457✔
800
    switch (fieldRo.type) {
3,457✔
801
      case FieldType.Link:
3,564✔
802
        return this.prepareLinkField(tableId, fieldRo);
355✔
803
      case FieldType.Rollup:
3,564✔
804
        return this.prepareRollupField(fieldRo, batchFieldVos);
40✔
805
      case FieldType.Formula:
3,564✔
806
        return this.prepareFormulaField(fieldRo, batchFieldVos);
106✔
807
      case FieldType.SingleLineText:
3,564✔
808
        return this.prepareSingleTextField(fieldRo);
1,124✔
809
      case FieldType.LongText:
3,564✔
810
        return this.prepareLongTextField(fieldRo);
26✔
811
      case FieldType.Number:
3,564✔
812
        return this.prepareNumberField(fieldRo);
1,014✔
813
      case FieldType.Rating:
3,564✔
814
        return this.prepareRatingField(fieldRo);
6✔
815
      case FieldType.SingleSelect:
3,564✔
816
        return this.prepareSingleSelectField(fieldRo);
658✔
817
      case FieldType.MultipleSelect:
3,564✔
818
        return this.prepareMultipleSelectField(fieldRo);
26✔
819
      case FieldType.Attachment:
3,564✔
820
        return this.prepareAttachmentField(fieldRo);
16✔
821
      case FieldType.User:
3,564✔
822
        return this.prepareUserField(fieldRo);
20✔
823
      case FieldType.Date:
3,564✔
824
        return this.prepareDateField(fieldRo);
38✔
825
      case FieldType.AutoNumber:
3,564✔
826
        return this.prepareAutoNumberField(fieldRo);
2✔
827
      case FieldType.CreatedTime:
3,564✔
828
        return this.prepareCreatedTimeField(fieldRo);
2✔
829
      case FieldType.LastModifiedTime:
3,564✔
830
        return this.prepareLastModifiedTimeField(fieldRo);
2✔
831
      case FieldType.Checkbox:
3,564✔
832
        return this.prepareCheckboxField(fieldRo);
22✔
833
      default:
3,564!
834
        throw new Error('invalid field type');
×
835
    }
3,564✔
836
  }
3,564✔
837

64✔
838
  private async prepareUpdateFieldInner(tableId: string, fieldRo: IFieldRo, oldFieldVo: IFieldVo) {
64✔
839
    if (fieldRo.type !== oldFieldVo.type) {
150✔
840
      return this.prepareCreateFieldInner(tableId, fieldRo);
86✔
841
    }
86✔
842

64✔
843
    if (fieldRo.isLookup) {
146✔
844
      return this.prepareUpdateLookupField(fieldRo, oldFieldVo);
10✔
845
    }
10✔
846

54✔
847
    switch (fieldRo.type) {
54✔
848
      case FieldType.Link: {
146✔
849
        return this.prepareUpdateLinkField(tableId, fieldRo, oldFieldVo);
18✔
850
      }
18✔
851
      case FieldType.Rollup:
150✔
852
        return this.prepareUpdateRollupField(fieldRo, oldFieldVo);
4✔
853
      case FieldType.Formula:
150✔
854
        return this.prepareUpdateFormulaField(fieldRo, oldFieldVo);
10✔
855
      case FieldType.SingleLineText:
150✔
856
        return this.prepareSingleTextField(fieldRo);
8✔
857
      case FieldType.LongText:
150!
858
        return this.prepareLongTextField(fieldRo);
×
859
      case FieldType.Number:
150!
860
        return this.prepareNumberField(fieldRo);
×
861
      case FieldType.Rating:
150✔
862
        return this.prepareRatingField(fieldRo);
2✔
863
      case FieldType.SingleSelect:
150✔
864
        return this.prepareSingleSelectField(fieldRo);
6✔
865
      case FieldType.MultipleSelect:
150✔
866
        return this.prepareMultipleSelectField(fieldRo);
4✔
867
      case FieldType.Attachment:
150✔
868
        return this.prepareAttachmentField(fieldRo);
2✔
869
      case FieldType.User:
150!
870
        return this.prepareUpdateUserField(fieldRo, oldFieldVo);
×
871
      case FieldType.Date:
150!
872
        return this.prepareDateField(fieldRo);
×
873
      case FieldType.AutoNumber:
150!
874
        return this.prepareAutoNumberField(fieldRo);
×
875
      case FieldType.CreatedTime:
150!
876
        return this.prepareCreatedTimeField(fieldRo);
×
877
      case FieldType.LastModifiedTime:
150!
878
        return this.prepareLastModifiedTimeField(fieldRo);
×
879
      case FieldType.Checkbox:
150!
880
        return this.prepareCheckboxField(fieldRo);
×
881
      default:
150!
882
        throw new Error('invalid field type');
×
883
    }
150✔
884
  }
150✔
885

64✔
886
  private zodParse(schema: z.Schema, value: unknown) {
64✔
887
    const result = (schema as z.Schema).safeParse(value);
1,176✔
888

1,176✔
889
    if (!result.success) {
1,176!
890
      throw new BadRequestException(fromZodError(result.error));
×
891
    }
×
892
  }
1,176✔
893

64✔
894
  private validateFormattingShowAs(field: IFieldVo) {
64✔
895
    const { cellValueType, isMultipleCellValue } = field;
3,624✔
896
    const showAsSchema = getShowAsSchema(cellValueType, isMultipleCellValue);
3,624✔
897

3,624✔
898
    const showAs = 'showAs' in field.options ? field.options.showAs : undefined;
3,624✔
899
    const formatting = 'formatting' in field.options ? field.options.formatting : undefined;
3,624✔
900

3,624✔
901
    if (showAs) {
3,624✔
902
      this.zodParse(showAsSchema, showAs);
10✔
903
    }
10✔
904

3,624✔
905
    if (formatting) {
3,624✔
906
      const formattingSchema = getFormattingSchema(cellValueType);
1,166✔
907
      this.zodParse(formattingSchema, formatting);
1,166✔
908
    }
1,166✔
909
  }
3,624✔
910
  /**
64✔
911
   * prepare properties for computed field to make sure it's valid
64✔
912
   * this method do not do any db update
64✔
913
   */
64✔
914
  async prepareCreateField(tableId: string, fieldRo: IFieldRo, batchFieldVos?: IFieldVo[]) {
64✔
915
    const field = (await this.prepareCreateFieldInner(tableId, fieldRo, batchFieldVos)) as IFieldVo;
3,478✔
916

3,478✔
917
    const fieldId = field.id || generateFieldId();
3,478✔
918
    const fieldName = await this.uniqFieldName(tableId, field.name);
3,478✔
919

3,478✔
920
    const dbFieldName =
3,478✔
921
      fieldRo.dbFieldName ?? (await this.fieldService.generateDbFieldName(tableId, fieldName));
3,478✔
922

3,472✔
923
    if (fieldRo.dbFieldName) {
3,478✔
924
      const existField = await this.prismaService.txClient().field.findFirst({
6✔
925
        where: { tableId, dbFieldName: fieldRo.dbFieldName },
6✔
926
        select: { id: true },
6✔
927
      });
6✔
928
      if (existField) {
6✔
929
        throw new BadRequestException(`dbFieldName ${fieldRo.dbFieldName} is duplicated`);
2✔
930
      }
2✔
931
    }
6✔
932

3,476✔
933
    const fieldVo: IFieldVo = {
3,476✔
934
      ...field,
3,476✔
935
      id: fieldId,
3,476✔
936
      name: fieldName,
3,476✔
937
      dbFieldName,
3,476✔
938
      isPending: field.isComputed ? true : undefined,
3,478✔
939
    };
3,478✔
940

3,478✔
941
    this.validateFormattingShowAs(fieldVo);
3,478✔
942

3,478✔
943
    return fieldVo;
3,478✔
944
  }
3,478✔
945

64✔
946
  async prepareUpdateField(
64✔
947
    tableId: string,
150✔
948
    fieldRo: IConvertFieldRo,
150✔
949
    oldField: IFieldInstance
150✔
950
  ): Promise<IFieldVo> {
150✔
951
    const fieldVo = (await this.prepareUpdateFieldInner(
150✔
952
      tableId,
150✔
953
      {
150✔
954
        ...fieldRo,
150✔
955
        name: fieldRo.name ?? oldField.name,
150✔
956
        dbFieldName: fieldRo.dbFieldName ?? oldField.dbFieldName,
150✔
957
        description: fieldRo.description === undefined ? oldField.description : fieldRo.description,
150✔
958
      }, // for convenience, we fallback name adn dbFieldName when it be undefined
150✔
959
      oldField
150✔
960
    )) as IFieldVo;
148✔
961

148✔
962
    this.validateFormattingShowAs(fieldVo);
148✔
963

148✔
964
    return {
148✔
965
      ...fieldVo,
148✔
966
      id: oldField.id,
148✔
967
      isPrimary: oldField.isPrimary,
148✔
968
      isPending: fieldVo.isComputed ? true : undefined,
150✔
969
    };
150✔
970
  }
150✔
971

64✔
972
  private async uniqFieldName(tableId: string, fieldName: string) {
64✔
973
    const fieldRaw = await this.prismaService.txClient().field.findMany({
3,737✔
974
      where: { tableId, deletedTime: null },
3,737✔
975
      select: { name: true },
3,737✔
976
    });
3,737✔
977

3,737✔
978
    const names = fieldRaw.map((item) => item.name);
3,737✔
979
    const uniqName = getUniqName(fieldName, names);
3,737✔
980
    if (uniqName !== fieldName) {
3,737✔
981
      return uniqName;
76✔
982
    }
76✔
983
    return fieldName;
3,661✔
984
  }
3,661✔
985

64✔
986
  async generateSymmetricField(tableId: string, field: LinkFieldDto) {
64✔
987
    if (!field.options.symmetricFieldId) {
259!
988
      throw new Error('symmetricFieldId is required');
×
989
    }
×
990

259✔
991
    const prisma = this.prismaService.txClient();
259✔
992
    const { name: tableName } = await prisma.tableMeta.findUniqueOrThrow({
259✔
993
      where: { id: tableId },
259✔
994
      select: { name: true },
259✔
995
    });
259✔
996

259✔
997
    const fieldName = await this.uniqFieldName(tableId, tableName);
259✔
998

259✔
999
    // lookup field id is the primary field of the table to which it is linked
259✔
1000
    const { id: lookupFieldId } = await prisma.field.findFirstOrThrow({
259✔
1001
      where: { tableId, isPrimary: true },
259✔
1002
      select: { id: true },
259✔
1003
    });
259✔
1004

259✔
1005
    const relationship = RelationshipRevert[field.options.relationship];
259✔
1006
    const isMultipleCellValue = isMultiValueLink(relationship) || undefined;
259✔
1007
    const dbFieldName = await this.fieldService.generateDbFieldName(
259✔
1008
      field.options.foreignTableId,
259✔
1009
      fieldName
259✔
1010
    );
259✔
1011

259✔
1012
    return createFieldInstanceByVo({
259✔
1013
      id: field.options.symmetricFieldId,
259✔
1014
      name: fieldName,
259✔
1015
      dbFieldName,
259✔
1016
      type: FieldType.Link,
259✔
1017
      options: {
259✔
1018
        relationship,
259✔
1019
        foreignTableId: tableId,
259✔
1020
        lookupFieldId,
259✔
1021
        fkHostTableName: field.options.fkHostTableName,
259✔
1022
        selfKeyName: field.options.foreignKeyName,
259✔
1023
        foreignKeyName: field.options.selfKeyName,
259✔
1024
        symmetricFieldId: field.id,
259✔
1025
      },
259✔
1026
      isMultipleCellValue,
259✔
1027
      dbFieldType: DbFieldType.Json,
259✔
1028
      cellValueType: CellValueType.String,
259✔
1029
    } as IFieldVo) as LinkFieldDto;
259✔
1030
  }
259✔
1031

64✔
1032
  async createForeignKey(options: ILinkFieldOptions) {
64✔
1033
    const { relationship, fkHostTableName, selfKeyName, foreignKeyName, isOneWay } = options;
369✔
1034

369✔
1035
    let alterTableSchema: Knex.SchemaBuilder | undefined;
369✔
1036

369✔
1037
    if (relationship === Relationship.ManyMany) {
369✔
1038
      alterTableSchema = this.knex.schema.createTable(fkHostTableName, (table) => {
55✔
1039
        table.increments('__id').primary();
55✔
1040
        table.string(selfKeyName);
55✔
1041
        table.string(foreignKeyName);
55✔
1042

55✔
1043
        table.index([foreignKeyName], `index_${foreignKeyName}`);
55✔
1044
        table.unique([selfKeyName, foreignKeyName], {
55✔
1045
          indexName: `index_${selfKeyName}_${foreignKeyName}`,
55✔
1046
        });
55✔
1047
      });
55✔
1048
    }
55✔
1049

369✔
1050
    if (relationship === Relationship.ManyOne) {
369✔
1051
      alterTableSchema = this.knex.schema.alterTable(fkHostTableName, (table) => {
148✔
1052
        table.string(foreignKeyName);
148✔
1053
        table.index([foreignKeyName], `index_${foreignKeyName}`);
148✔
1054
      });
148✔
1055
    }
148✔
1056

369✔
1057
    if (relationship === Relationship.OneMany) {
369✔
1058
      if (isOneWay) {
106✔
1059
        alterTableSchema = this.knex.schema.createTable(fkHostTableName, (table) => {
30✔
1060
          table.increments('__id').primary();
30✔
1061
          table.string(selfKeyName);
30✔
1062
          table.string(foreignKeyName);
30✔
1063

30✔
1064
          table.index([foreignKeyName], `index_${foreignKeyName}`);
30✔
1065
          table.unique([selfKeyName, foreignKeyName], {
30✔
1066
            indexName: `index_${selfKeyName}_${foreignKeyName}`,
30✔
1067
          });
30✔
1068
        });
30✔
1069
      } else {
100✔
1070
        alterTableSchema = this.knex.schema.alterTable(fkHostTableName, (table) => {
76✔
1071
          table.string(selfKeyName);
76✔
1072
          table.index([selfKeyName], `index_${selfKeyName}`);
76✔
1073
        });
76✔
1074
      }
76✔
1075
    }
106✔
1076

369✔
1077
    // assume options is from the main field (user created one)
369✔
1078
    if (relationship === Relationship.OneOne) {
369✔
1079
      alterTableSchema = this.knex.schema.alterTable(fkHostTableName, (table) => {
60✔
1080
        if (foreignKeyName === '__id') {
60!
1081
          throw new Error('can not use __id for foreignKeyName');
×
1082
        }
×
1083
        table.string(foreignKeyName);
60✔
1084
        table.unique([foreignKeyName], {
60✔
1085
          indexName: `index_${foreignKeyName}`,
60✔
1086
        });
60✔
1087
      });
60✔
1088
    }
60✔
1089

369✔
1090
    if (!alterTableSchema) {
369!
UNCOV
1091
      throw new Error('alterTableSchema is undefined');
×
UNCOV
1092
    }
×
1093

369✔
1094
    for (const sql of alterTableSchema.toSQL()) {
369✔
1095
      await this.prismaService.txClient().$executeRawUnsafe(sql.sql);
823✔
1096
    }
823✔
1097
  }
369✔
1098

64✔
1099
  async cleanForeignKey(options: ILinkFieldOptions) {
64✔
1100
    const { fkHostTableName, relationship, selfKeyName, foreignKeyName, isOneWay } = options;
34✔
1101
    const dropTable = async (tableName: string) => {
34✔
1102
      const alterTableSchema = this.knex.schema.dropTable(tableName);
8✔
1103

8✔
1104
      for (const sql of alterTableSchema.toSQL()) {
8✔
1105
        await this.prismaService.txClient().$executeRawUnsafe(sql.sql);
8✔
1106
      }
8✔
1107
    };
8✔
1108

34✔
1109
    const dropColumn = async (tableName: string, columnName: string) => {
34✔
1110
      const alterTableQuery = this.dbProvider.dropColumnAndIndex(
26✔
1111
        tableName,
26✔
1112
        columnName,
26✔
1113
        `index_${columnName}`
26✔
1114
      );
26✔
1115

26✔
1116
      for (const query of alterTableQuery) {
26✔
1117
        await this.prismaService.txClient().$executeRawUnsafe(query);
39✔
1118
      }
39✔
1119
    };
26✔
1120

34✔
1121
    if (relationship === Relationship.ManyMany) {
34✔
1122
      await dropTable(fkHostTableName);
4✔
1123
    }
4✔
1124

34✔
1125
    if (relationship === Relationship.ManyOne) {
34✔
1126
      await dropColumn(fkHostTableName, foreignKeyName);
18✔
1127
    }
18✔
1128

34✔
1129
    if (relationship === Relationship.OneMany) {
34✔
1130
      if (isOneWay) {
8✔
1131
        await dropTable(fkHostTableName);
4✔
1132
      } else {
4✔
1133
        await dropColumn(fkHostTableName, selfKeyName);
4✔
1134
      }
4✔
1135
    }
8✔
1136

34✔
1137
    if (relationship === Relationship.OneOne) {
34✔
1138
      await dropColumn(fkHostTableName, foreignKeyName === '__id' ? selfKeyName : foreignKeyName);
4!
1139
    }
4✔
1140
  }
34✔
1141

64✔
1142
  async createReference(field: IFieldInstance) {
64✔
1143
    if (field.isLookup) {
3,841✔
1144
      return this.createComputedFieldReference(field);
109✔
1145
    }
109✔
1146

3,732✔
1147
    switch (field.type) {
3,732✔
1148
      case FieldType.Formula:
3,841✔
1149
      case FieldType.Rollup:
3,841✔
1150
      case FieldType.Link:
3,841✔
1151
        return this.createComputedFieldReference(field);
780✔
1152
      default:
3,841✔
1153
        break;
2,952✔
1154
    }
3,841✔
1155
  }
3,841✔
1156

64✔
1157
  async deleteReference(fieldId: string): Promise<string[]> {
64✔
1158
    const prisma = this.prismaService.txClient();
68✔
1159
    const refRaw = await prisma.reference.findMany({
68✔
1160
      where: {
68✔
1161
        fromFieldId: fieldId,
68✔
1162
      },
68✔
1163
    });
68✔
1164

68✔
1165
    await prisma.reference.deleteMany({
68✔
1166
      where: {
68✔
1167
        OR: [{ toFieldId: fieldId }, { fromFieldId: fieldId }],
68✔
1168
      },
68✔
1169
    });
68✔
1170

68✔
1171
    return refRaw.map((ref) => ref.toFieldId);
68✔
1172
  }
68✔
1173

64✔
1174
  /**
64✔
1175
   * the lookup field that attach to the deleted, should delete to field reference
64✔
1176
   */
64✔
1177
  async deleteLookupFieldReference(linkFieldId: string): Promise<string[]> {
64✔
1178
    const prisma = this.prismaService.txClient();
72✔
1179
    const fieldsRaw = await prisma.field.findMany({
72✔
1180
      where: { lookupLinkedFieldId: linkFieldId, deletedTime: null },
72✔
1181
      select: { id: true },
72✔
1182
    });
72✔
1183

72✔
1184
    for (const field of fieldsRaw) {
72✔
1185
      await prisma.field.update({
14✔
1186
        data: { lookupLinkedFieldId: null },
14✔
1187
        where: { id: field.id },
14✔
1188
      });
14✔
1189
    }
14✔
1190

72✔
1191
    const lookupFieldIds = fieldsRaw.map((field) => field.id);
72✔
1192

72✔
1193
    // just need delete to field id, because lookup field still exist
72✔
1194
    await prisma.reference.deleteMany({
72✔
1195
      where: {
72✔
1196
        OR: [{ toFieldId: { in: lookupFieldIds } }],
72✔
1197
      },
72✔
1198
    });
72✔
1199
    return lookupFieldIds;
72✔
1200
  }
72✔
1201

64✔
1202
  getFieldReferenceIds(field: IFieldInstance): string[] {
64✔
1203
    if (field.lookupOptions) {
901✔
1204
      return [field.lookupOptions.lookupFieldId];
155✔
1205
    }
155✔
1206

746✔
1207
    if (field.type === FieldType.Link) {
880✔
1208
      return [field.options.lookupFieldId];
630✔
1209
    }
630✔
1210

116✔
1211
    if (field.type === FieldType.Formula) {
138✔
1212
      return (field as FormulaFieldDto).getReferenceFieldIds();
114✔
1213
    }
114✔
1214

2✔
1215
    return [];
2✔
1216
  }
2✔
1217

64✔
1218
  private async createComputedFieldReference(field: IFieldInstance) {
64✔
1219
    const toFieldId = field.id;
889✔
1220

889✔
1221
    const graphItems = await this.referenceService.getFieldGraphItems([field.id]);
889✔
1222
    const fieldIds = this.getFieldReferenceIds(field);
889✔
1223

889✔
1224
    fieldIds.forEach((fromFieldId) => {
889✔
1225
      graphItems.push({ fromFieldId, toFieldId });
873✔
1226
    });
873✔
1227

889✔
1228
    if (hasCycle(graphItems)) {
889!
1229
      throw new BadRequestException('field reference has cycle');
×
1230
    }
×
1231

889✔
1232
    for (const fromFieldId of fieldIds) {
889✔
1233
      await this.prismaService.txClient().reference.create({
873✔
1234
        data: {
873✔
1235
          fromFieldId,
873✔
1236
          toFieldId,
873✔
1237
        },
873✔
1238
      });
873✔
1239
    }
873✔
1240
  }
889✔
1241
}
64✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc