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

teableio / teable / 14397495341

11 Apr 2025 07:01AM UTC coverage: 80.597% (-0.04%) from 80.639%
14397495341

Pull #1432

github

web-flow
Merge af7857d0b into f20a45456
Pull Request #1432: fix: duplicate primary dependent field base

7743 of 8207 branches covered (94.35%)

180 of 309 new or added lines in 5 files covered. (58.25%)

1 existing line in 1 file now uncovered.

36292 of 45029 relevant lines covered (80.6%)

1758.96 hits per line

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

75.02
/apps/nestjs-backend/src/features/base/base-import.service.ts
1
import type { Readable } from 'stream';
4✔
2
import { BadGatewayException, Injectable, Logger } from '@nestjs/common';
3
import type { IFormulaFieldOptions, ILinkFieldOptions, ILookupOptionsRo } from '@teable/core';
4
import {
5
  FieldType,
6
  generateBaseId,
7
  generateDashboardId,
8
  generatePluginInstallId,
9
  generatePluginPanelId,
10
  generateShareId,
11
  Role,
12
  ViewType,
13
} from '@teable/core';
14
import { PrismaService } from '@teable/db-main-prisma';
15
import { UploadType, PluginPosition, PrincipalType, ResourceType } from '@teable/openapi';
16
import type {
17
  ICreateBaseVo,
18
  IBaseJson,
19
  ImportBaseRo,
20
  IFieldJson,
21
  IFieldWithTableIdJson,
22
} from '@teable/openapi';
23

24
import { Knex } from 'knex';
25
import { get, pick } from 'lodash';
26
import { InjectModel } from 'nest-knexjs';
27
import { ClsService } from 'nestjs-cls';
28
import streamJson from 'stream-json';
29
import streamValues from 'stream-json/streamers/StreamValues';
30
import * as unzipper from 'unzipper';
31
import { InjectDbProvider } from '../../db-provider/db.provider';
32
import { IDbProvider } from '../../db-provider/db.provider.interface';
33
import type { IClsStore } from '../../types/cls';
34
import StorageAdapter from '../attachments/plugins/adapter';
35
import { InjectStorageAdapter } from '../attachments/plugins/storage';
36
import { createFieldInstanceByRaw } from '../field/model/factory';
37
import { FieldOpenApiService } from '../field/open-api/field-open-api.service';
38
import { dbType2knexFormat } from '../field/util';
39
import { TableService } from '../table/table.service';
40
import { ViewOpenApiService } from '../view/open-api/view-open-api.service';
41
import { BaseImportAttachmentsQueueProcessor } from './base-import-processor/base-import-attachments.processor';
42
import { BaseImportCsvQueueProcessor } from './base-import-processor/base-import-csv.processor';
43
import { DEFAULT_EXPRESSION } from './constant';
44
import { replaceStringByMap } from './utils';
45

46
@Injectable()
47
export class BaseImportService {
4✔
48
  private logger = new Logger(BaseImportService.name);
125✔
49

50
  constructor(
125✔
51
    private readonly prismaService: PrismaService,
125✔
52
    private readonly cls: ClsService<IClsStore>,
125✔
53
    private readonly tableService: TableService,
125✔
54
    private readonly fieldOpenApiService: FieldOpenApiService,
125✔
55
    private readonly viewOpenApiService: ViewOpenApiService,
125✔
56
    private readonly baseImportAttachmentsQueueProcessor: BaseImportAttachmentsQueueProcessor,
125✔
57
    private readonly baseImportCsvQueueProcessor: BaseImportCsvQueueProcessor,
125✔
58
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
125✔
59
    @InjectDbProvider() private readonly dbProvider: IDbProvider,
125✔
60
    @InjectStorageAdapter() private readonly storageAdapter: StorageAdapter
125✔
61
  ) {}
125✔
62

63
  private async getMaxOrder(spaceId: string) {
125✔
64
    const spaceAggregate = await this.prismaService.txClient().base.aggregate({
13✔
65
      where: { spaceId, deletedTime: null },
13✔
66
      _max: { order: true },
13✔
67
    });
13✔
68
    return spaceAggregate._max.order || 0;
13✔
69
  }
13✔
70

71
  private async createBase(spaceId: string, name: string, icon?: string) {
125✔
72
    const userId = this.cls.get('user.id');
13✔
73

74
    return this.prismaService.$tx(async (prisma) => {
13✔
75
      const order = (await this.getMaxOrder(spaceId)) + 1;
13✔
76

77
      const base = await prisma.base.create({
13✔
78
        data: {
13✔
79
          id: generateBaseId(),
13✔
80
          name: name || 'Untitled Base',
13✔
81
          spaceId,
13✔
82
          order,
13✔
83
          icon,
13✔
84
          createdBy: userId,
13✔
85
        },
13✔
86
        select: {
13✔
87
          id: true,
13✔
88
          name: true,
13✔
89
          icon: true,
13✔
90
          spaceId: true,
13✔
91
        },
13✔
92
      });
13✔
93

94
      const sqlList = this.dbProvider.createSchema(base.id);
13✔
95
      if (sqlList) {
13✔
96
        for (const sql of sqlList) {
11✔
97
          await prisma.$executeRawUnsafe(sql);
22✔
98
        }
22✔
99
      }
11✔
100

101
      return base;
13✔
102
    });
13✔
103
  }
13✔
104

105
  async importBase(importBaseRo: ImportBaseRo) {
125✔
106
    // 1. create base structure from json
2✔
107
    // 2. upload attachments
2✔
108
    // 3. create import table data task
2✔
109
    const structureStream = await this.storageAdapter.downloadFile(
2✔
110
      StorageAdapter.getBucket(UploadType.Import),
2✔
111
      importBaseRo.notify.path
2✔
112
    );
113

114
    const { base, tableIdMap, viewIdMap, fieldIdMap, structure } = await this.prismaService.$tx(
2✔
115
      async () => {
2✔
116
        return await this.processStructure(structureStream, importBaseRo);
2✔
117
      }
2✔
118
    );
119

120
    this.uploadAttachments(importBaseRo.notify.path);
2✔
121

122
    this.appendTableData(importBaseRo.notify.path, tableIdMap, fieldIdMap, viewIdMap, structure);
2✔
123

124
    return {
2✔
125
      base,
2✔
126
      tableIdMap,
2✔
127
      fieldIdMap,
2✔
128
      viewIdMap,
2✔
129
    };
2✔
130
  }
2✔
131

132
  private async processStructure(
125✔
133
    zipStream: Readable,
2✔
134
    importBaseRo: ImportBaseRo
2✔
135
  ): Promise<{
136
    base: ICreateBaseVo;
137
    tableIdMap: Record<string, string>;
138
    fieldIdMap: Record<string, string>;
139
    viewIdMap: Record<string, string>;
140
    structure: IBaseJson;
141
  }> {
2✔
142
    const { spaceId } = importBaseRo;
2✔
143
    const parser = unzipper.Parse();
2✔
144
    zipStream.pipe(parser);
2✔
145
    return new Promise((resolve, reject) => {
2✔
146
      parser.on('entry', (entry) => {
2✔
147
        const filePath = entry.path;
10✔
148
        if (filePath === 'structure.json') {
10✔
149
          const parser = streamJson.parser();
2✔
150
          const pipeline = entry.pipe(parser).pipe(streamValues.streamValues());
2✔
151

152
          let structureObject: IBaseJson | null = null;
2✔
153
          pipeline
2✔
154
            .on('data', (data: { key: number; value: IBaseJson }) => {
2✔
155
              structureObject = data.value;
2✔
156
            })
2✔
157
            .on('end', async () => {
2✔
158
              if (!structureObject) {
2✔
159
                reject(new Error('import base structure.json resolve error'));
×
160
              }
×
161

162
              try {
2✔
163
                const result = await this.createBaseStructure(spaceId, structureObject!);
2✔
164
                resolve(result);
2✔
165
              } catch (error) {
2✔
166
                reject(error);
×
167
              }
×
168
            })
2✔
169
            .on('error', (err: Error) => {
2✔
170
              parser.destroy(new Error(`resolve structure.json error: ${err.message}`));
×
171
              reject(Error);
×
172
            });
×
173
        } else {
10✔
174
          entry.autodrain();
8✔
175
        }
8✔
176
      });
10✔
177
    });
2✔
178
  }
2✔
179

180
  private async uploadAttachments(path: string) {
125✔
181
    const userId = this.cls.get('user.id');
2✔
182
    await this.baseImportAttachmentsQueueProcessor.queue.add(
2✔
183
      'import_base_attachments',
2✔
184
      {
2✔
185
        path,
2✔
186
        userId,
2✔
187
      },
2✔
188
      {
2✔
189
        jobId: `import_attachments_${path}_${userId}`,
2✔
190
      }
2✔
191
    );
192
  }
2✔
193

194
  private async appendTableData(
125✔
195
    path: string,
2✔
196
    tableIdMap: Record<string, string>,
2✔
197
    fieldIdMap: Record<string, string>,
2✔
198
    viewIdMap: Record<string, string>,
2✔
199
    structure: IBaseJson
2✔
200
  ) {
2✔
201
    const userId = this.cls.get('user.id');
2✔
202
    await this.baseImportCsvQueueProcessor.queue.add(
2✔
203
      'base_import_csv',
2✔
204
      {
2✔
205
        path,
2✔
206
        userId,
2✔
207
        tableIdMap,
2✔
208
        fieldIdMap,
2✔
209
        viewIdMap,
2✔
210
        structure,
2✔
211
      },
2✔
212
      {
2✔
213
        jobId: `import_csv_${path}_${userId}`,
2✔
214
      }
2✔
215
    );
216
  }
2✔
217

218
  async createBaseStructure(spaceId: string, structure: IBaseJson) {
125✔
219
    const { name, icon, tables, plugins } = structure;
13✔
220

221
    // create base
13✔
222
    const newBase = await this.createBase(spaceId, name, icon || undefined);
13✔
223

224
    // create table
13✔
225
    const { tableIdMap, fieldIdMap, viewIdMap } = await this.createTables(newBase.id, tables);
13✔
226

227
    // create plugins
13✔
228
    await this.createPlugins(newBase.id, plugins, tableIdMap, fieldIdMap, viewIdMap);
13✔
229

230
    return {
13✔
231
      base: newBase,
13✔
232
      tableIdMap,
13✔
233
      fieldIdMap,
13✔
234
      viewIdMap,
13✔
235
      structure,
13✔
236
    };
13✔
237
  }
13✔
238

239
  private async createTables(baseId: string, tables: IBaseJson['tables']) {
125✔
240
    const tableIdMap: Record<string, string> = {};
13✔
241

242
    for (const table of tables) {
13✔
243
      const { name, icon, description, id: tableId } = table;
14✔
244
      const newTableVo = await this.tableService.createTable(baseId, {
14✔
245
        name,
14✔
246
        icon,
14✔
247
        description,
14✔
248
      });
14✔
249
      tableIdMap[tableId] = newTableVo.id;
14✔
250
    }
14✔
251

252
    const fieldIdMap = await this.createFields(tables, tableIdMap);
13✔
253

254
    const viewIdMap = await this.createViews(tables, tableIdMap, fieldIdMap);
13✔
255

256
    await this.repairFieldOptions(tables, tableIdMap, fieldIdMap, viewIdMap);
13✔
257

258
    return { tableIdMap, fieldIdMap, viewIdMap };
13✔
259
  }
13✔
260

261
  private async createFields(tables: IBaseJson['tables'], tableIdMap: Record<string, string>) {
125✔
262
    const fieldMap: Record<string, string> = {};
13✔
263

264
    const allFields = tables
13✔
265
      .reduce((acc, cur) => {
13✔
266
        const fieldWithTableId = cur.fields.map((field) => ({
14✔
267
          ...field,
81✔
268
          sourceTableId: cur.id,
81✔
269
          targetTableId: tableIdMap[cur.id],
81✔
270
        }));
81✔
271
        return [...acc, ...fieldWithTableId];
14✔
272
      }, [] as IFieldWithTableIdJson[])
13✔
273
      .sort((a, b) => a.createTime.localeCompare(b.createTime));
13✔
274

275
    const nonCommonFieldTypes = [FieldType.Link, FieldType.Rollup, FieldType.Formula];
13✔
276

277
    const commonFields = allFields.filter(
13✔
278
      ({ type, isLookup }) => !nonCommonFieldTypes.includes(type) && !isLookup
13✔
279
    );
280

281
    // the primary formula which rely on other fields
13✔
282
    const primaryFormulaFields = allFields.filter(
13✔
283
      ({ type, isLookup, isPrimary }) => type === FieldType.Formula && !isLookup && isPrimary
13✔
284
    );
285

286
    const linkFields = allFields.filter(
13✔
287
      ({ type, isLookup }) => type === FieldType.Link && !isLookup
13✔
288
    );
289

290
    // rest fields, like formula, rollup, lookup fields
13✔
291
    const dependencyFields = allFields.filter(
13✔
292
      ({ id }) =>
13✔
293
        ![...primaryFormulaFields, ...linkFields, ...commonFields].map(({ id }) => id).includes(id)
81✔
294
    );
295

296
    await this.createCommonFields(commonFields, fieldMap);
13✔
297

298
    await this.createTmpPrimaryFormulaFields(primaryFormulaFields, fieldMap);
13✔
299

300
    await this.createLinkFields(linkFields, tableIdMap, fieldMap);
13✔
301

302
    await this.createDependencyFields(dependencyFields, tableIdMap, fieldMap);
13✔
303

304
    await this.repairPrimaryFormulaFields(primaryFormulaFields, fieldMap);
13✔
305

306
    return fieldMap;
13✔
307
  }
13✔
308

309
  private async createTmpPrimaryFormulaFields(
125✔
310
    primaryFormulaFields: IFieldWithTableIdJson[],
13✔
311
    fieldMap: Record<string, string>
13✔
312
  ) {
13✔
313
    for (const field of primaryFormulaFields) {
13✔
314
      const {
1✔
315
        type,
1✔
316
        dbFieldName,
1✔
317
        name,
1✔
318
        options,
1✔
319
        id,
1✔
320
        notNull,
1✔
321
        unique,
1✔
322
        description,
1✔
323
        isPrimary,
1✔
324
        targetTableId,
1✔
325
        order,
1✔
326
        hasError,
1✔
327
      } = field;
1✔
328
      const newField = await this.fieldOpenApiService.createField(targetTableId, {
1✔
329
        type,
1✔
330
        dbFieldName,
1✔
331
        description,
1✔
332
        options: {
1✔
333
          ...options,
1✔
334
          expression: DEFAULT_EXPRESSION,
1✔
335
        },
1✔
336
        name,
1✔
337
      });
1✔
338
      await this.replenishmentConstraint(newField.id, targetTableId, order, {
1✔
339
        notNull,
1✔
340
        unique,
1✔
341
        dbFieldName,
1✔
342
        isPrimary,
1✔
343
      });
1✔
344
      fieldMap[id] = newField.id;
1✔
345

346
      if (hasError) {
1✔
NEW
347
        await this.prismaService.txClient().field.update({
×
NEW
348
          where: {
×
NEW
349
            id: newField.id,
×
NEW
350
          },
×
NEW
351
          data: {
×
NEW
352
            hasError,
×
NEW
353
          },
×
NEW
354
        });
×
NEW
355
      }
×
356
    }
1✔
357
  }
13✔
358

359
  private async repairPrimaryFormulaFields(
125✔
360
    primaryFormulaFields: IFieldWithTableIdJson[],
13✔
361
    fieldMap: Record<string, string>
13✔
362
  ) {
13✔
363
    for (const field of primaryFormulaFields) {
13✔
364
      const {
1✔
365
        id,
1✔
366
        options,
1✔
367
        dbFieldType,
1✔
368
        targetTableId,
1✔
369
        dbFieldName,
1✔
370
        cellValueType,
1✔
371
        isMultipleCellValue,
1✔
372
      } = field;
1✔
373
      const { dbTableName } = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
1✔
374
        where: {
1✔
375
          id: targetTableId,
1✔
376
        },
1✔
377
        select: {
1✔
378
          dbTableName: true,
1✔
379
        },
1✔
380
      });
1✔
381
      const newOptions = replaceStringByMap(options, { fieldMap });
1✔
382
      const { dbFieldType: currentDbFieldType } = await this.prismaService.txClient().field.update({
1✔
383
        where: {
1✔
384
          id: fieldMap[id],
1✔
385
        },
1✔
386
        data: {
1✔
387
          options: newOptions,
1✔
388
          cellValueType,
1✔
389
        },
1✔
390
      });
1✔
391
      if (currentDbFieldType !== dbFieldType) {
1✔
392
        const schemaType = dbType2knexFormat(this.knex, dbFieldType);
1✔
393
        const modifyColumnSql = this.dbProvider.modifyColumnSchema(
1✔
394
          dbTableName,
1✔
395
          dbFieldName,
1✔
396
          schemaType
1✔
397
        );
398

399
        for (const alterTableQuery of modifyColumnSql) {
1✔
400
          await this.prismaService.txClient().$executeRawUnsafe(alterTableQuery);
2✔
401
        }
2✔
402
        await this.prismaService.txClient().field.update({
1✔
403
          where: {
1✔
404
            id: fieldMap[id],
1✔
405
          },
1✔
406
          data: {
1✔
407
            cellValueType,
1✔
408
            dbFieldType,
1✔
409
            isMultipleCellValue,
1✔
410
          },
1✔
411
        });
1✔
412
      }
1✔
413
    }
1✔
414
  }
13✔
415

416
  private async createCommonFields(
125✔
417
    fields: IFieldWithTableIdJson[],
13✔
418
    fieldMap: Record<string, string>
13✔
419
  ) {
13✔
420
    for (const field of fields) {
13✔
421
      const {
49✔
422
        name,
49✔
423
        type,
49✔
424
        options,
49✔
425
        targetTableId,
49✔
426
        isPrimary,
49✔
427
        notNull,
49✔
428
        dbFieldName,
49✔
429
        description,
49✔
430
        unique,
49✔
431
      } = field;
49✔
432
      const newFieldVo = await this.fieldOpenApiService.createField(targetTableId, {
49✔
433
        name,
49✔
434
        type,
49✔
435
        options,
49✔
436
        dbFieldName,
49✔
437
        description,
49✔
438
      });
49✔
439
      await this.replenishmentConstraint(newFieldVo.id, targetTableId, field.order, {
49✔
440
        notNull,
49✔
441
        unique,
49✔
442
        dbFieldName: newFieldVo.dbFieldName,
49✔
443
        isPrimary,
49✔
444
      });
49✔
445
      fieldMap[field.id] = newFieldVo.id;
49✔
446
      await this.prismaService.txClient().field.update({
49✔
447
        where: {
49✔
448
          id: newFieldVo.id,
49✔
449
        },
49✔
450
        data: {
49✔
451
          order: field.order,
49✔
452
        },
49✔
453
      });
49✔
454
    }
49✔
455
  }
13✔
456

457
  private async createLinkFields(
125✔
458
    // filter lookup fields
13✔
459
    linkFields: IFieldWithTableIdJson[],
13✔
460
    tableIdMap: Record<string, string>,
13✔
461
    fieldMap: Record<string, string>
13✔
462
  ) {
13✔
463
    const selfLinkFields = linkFields.filter(
13✔
464
      ({ options, sourceTableId }) =>
13✔
465
        (options as ILinkFieldOptions).foreignTableId === sourceTableId
10✔
466
    );
467

468
    // cross base link fields should convert to one-way link field
13✔
469
    // only for base-duplicate
13✔
470
    const crossBaseLinkFields = linkFields
13✔
471
      .filter(({ options }) => Boolean((options as ILinkFieldOptions)?.baseId))
13✔
472
      .map((f) => ({
13✔
473
        ...f,
×
474
        options: {
×
475
          ...f.options,
×
476
          isOneWay: true,
×
477
        },
×
478
      })) as IFieldWithTableIdJson[];
×
479

480
    // already converted to text field in export side, prevent unexpected error
13✔
481
    // if (crossBaseLinkFields.length > 0) {
13✔
482
    //   throw new BadRequestException('cross base link fields are not supported');
13✔
483
    // }
13✔
484

485
    // common cross table link fields
13✔
486
    const commonLinkFields = linkFields.filter(
13✔
487
      ({ id }) => ![...selfLinkFields, ...crossBaseLinkFields].map(({ id }) => id).includes(id)
13✔
488
    );
489

490
    await this.createSelfLinkFields(selfLinkFields, fieldMap);
13✔
491

492
    // deal with cross base link fields
13✔
493
    await this.createCommonLinkFields(crossBaseLinkFields, tableIdMap, fieldMap, true);
13✔
494

495
    await this.createCommonLinkFields(commonLinkFields, tableIdMap, fieldMap);
13✔
496
  }
13✔
497

498
  private async createSelfLinkFields(
125✔
499
    fields: IFieldWithTableIdJson[],
13✔
500
    fieldMap: Record<string, string>
13✔
501
  ) {
13✔
502
    const twoWaySelfLinkFields = fields.filter(
13✔
503
      ({ options }) => !(options as ILinkFieldOptions).isOneWay
13✔
504
    );
505

506
    const mergedTwoWaySelfLinkFields = [] as [IFieldWithTableIdJson, IFieldWithTableIdJson][];
13✔
507

508
    twoWaySelfLinkFields.forEach((f) => {
13✔
509
      // two-way self link field should only create one of it
×
510
      if (!mergedTwoWaySelfLinkFields.some((group) => group.some(({ id: fId }) => fId === f.id))) {
×
511
        const groupField = twoWaySelfLinkFields.find(
×
512
          ({ options }) => get(options, 'symmetricFieldId') === f.id
×
513
        );
514
        groupField && mergedTwoWaySelfLinkFields.push([f, groupField]);
×
515
      }
×
516
    });
×
517

518
    const oneWaySelfLinkFields = fields.filter(
13✔
519
      ({ options }) => (options as ILinkFieldOptions).isOneWay
13✔
520
    );
521

522
    for (const field of oneWaySelfLinkFields) {
13✔
523
      const {
×
524
        name,
×
525
        targetTableId,
×
526
        type,
×
527
        options,
×
528
        description,
×
529
        notNull,
×
530
        unique,
×
531
        dbFieldName,
×
532
        isPrimary,
×
533
      } = field;
×
534
      const { relationship } = options as ILinkFieldOptions;
×
535
      const newFieldVo = await this.fieldOpenApiService.createField(targetTableId, {
×
536
        name,
×
537
        type,
×
538
        dbFieldName,
×
539
        description,
×
540
        options: {
×
541
          foreignTableId: targetTableId,
×
542
          relationship,
×
543
          isOneWay: true,
×
544
        },
×
545
      });
×
546
      await this.replenishmentConstraint(newFieldVo.id, targetTableId, field.order, {
×
547
        notNull,
×
548
        unique,
×
549
        dbFieldName,
×
550
        isPrimary,
×
551
      });
×
552
      fieldMap[field.id] = newFieldVo.id;
×
553
    }
×
554

555
    for (const field of mergedTwoWaySelfLinkFields) {
13✔
556
      const f = field[0];
×
557
      const groupField = field[1];
×
558
      const {
×
559
        name,
×
560
        type,
×
561
        id,
×
562
        description,
×
563
        targetTableId,
×
564
        notNull,
×
565
        unique,
×
566
        dbFieldName,
×
567
        isPrimary,
×
568
      } = f;
×
569
      const options = f.options as ILinkFieldOptions;
×
570
      const newField = await this.fieldOpenApiService.createField(targetTableId, {
×
571
        type: type as FieldType,
×
572
        dbFieldName,
×
573
        name,
×
574
        description,
×
575
        options: {
×
576
          ...pick(options, [
×
577
            'relationship',
×
578
            'isOneWay',
×
579
            'filterByViewId',
×
580
            'filter',
×
581
            'visibleFieldIds',
×
582
          ]),
×
583
          foreignTableId: targetTableId,
×
584
        },
×
585
      });
×
586
      await this.replenishmentConstraint(newField.id, targetTableId, f.order, {
×
587
        notNull,
×
588
        unique,
×
589
        dbFieldName,
×
590
        isPrimary,
×
591
      });
×
592
      fieldMap[id] = newField.id;
×
593
      fieldMap[groupField.id] = (newField.options as ILinkFieldOptions).symmetricFieldId!;
×
594

595
      // self link should updated the opposite field dbFieldName and name
×
596
      const { dbTableName: targetDbTableName } = await this.prismaService
×
597
        .txClient()
×
598
        .tableMeta.findUniqueOrThrow({
×
599
          where: {
×
600
            id: targetTableId,
×
601
          },
×
602
          select: {
×
603
            dbTableName: true,
×
604
          },
×
605
        });
×
606

607
      const { dbFieldName: genDbFieldName } = await this.prismaService
×
608
        .txClient()
×
609
        .field.findUniqueOrThrow({
×
610
          where: {
×
611
            id: fieldMap[groupField.id],
×
612
          },
×
613
          select: {
×
614
            dbFieldName: true,
×
615
          },
×
616
        });
×
617

618
      await this.prismaService.txClient().field.update({
×
619
        where: {
×
620
          id: fieldMap[groupField.id],
×
621
        },
×
622
        data: {
×
623
          dbFieldName: groupField.dbFieldName,
×
624
          name: groupField.name,
×
625
          description: groupField.description,
×
626
          order: groupField.order,
×
627
        },
×
628
      });
×
629

630
      if (genDbFieldName !== groupField.dbFieldName) {
×
631
        const alterTableSql = this.dbProvider.renameColumn(
×
632
          targetDbTableName,
×
633
          genDbFieldName,
×
634
          groupField.dbFieldName
×
635
        );
636

637
        for (const sql of alterTableSql) {
×
638
          await this.prismaService.txClient().$executeRawUnsafe(sql);
×
639
        }
×
640
      }
×
641
    }
×
642
  }
13✔
643

644
  private async createCommonLinkFields(
125✔
645
    fields: IFieldWithTableIdJson[],
26✔
646
    tableIdMap: Record<string, string>,
26✔
647
    fieldMap: Record<string, string>,
26✔
648
    allowCrossBase: boolean = false
26✔
649
  ) {
26✔
650
    const oneWayFields = fields.filter(({ options }) => (options as ILinkFieldOptions).isOneWay);
26✔
651
    const twoWayFields = fields.filter(({ options }) => !(options as ILinkFieldOptions).isOneWay);
26✔
652

653
    for (const field of oneWayFields) {
26✔
654
      const {
×
655
        name,
×
656
        type,
×
657
        options,
×
658
        targetTableId,
×
659
        description,
×
660
        notNull,
×
661
        unique,
×
662
        dbFieldName,
×
663
        isPrimary,
×
664
      } = field;
×
665
      const { foreignTableId, relationship } = options as ILinkFieldOptions;
×
666
      const newFieldVo = await this.fieldOpenApiService.createField(targetTableId, {
×
667
        name,
×
668
        type,
×
669
        description,
×
670
        options: {
×
671
          foreignTableId: allowCrossBase ? foreignTableId : tableIdMap[foreignTableId],
×
672
          relationship,
×
673
          isOneWay: true,
×
674
        },
×
675
      });
×
676
      fieldMap[field.id] = newFieldVo.id;
×
677
      await this.replenishmentConstraint(newFieldVo.id, targetTableId, field.order, {
×
678
        notNull,
×
679
        unique,
×
680
        dbFieldName,
×
681
        isPrimary,
×
682
      });
×
683
    }
×
684

685
    const groupedTwoWayFields = [] as [IFieldWithTableIdJson, IFieldWithTableIdJson][];
26✔
686

687
    twoWayFields.forEach((f) => {
26✔
688
      // two-way link field should only create one of it
10✔
689
      if (!groupedTwoWayFields.some((group) => group.some(({ id: fId }) => fId === f.id))) {
10✔
690
        const symmetricField = twoWayFields.find(
5✔
691
          ({ options }) => get(options, 'symmetricFieldId') === f.id
5✔
692
        );
693
        symmetricField && groupedTwoWayFields.push([f, symmetricField]);
5✔
694
      }
5✔
695
    });
10✔
696

697
    for (const field of groupedTwoWayFields) {
26✔
698
      const {
5✔
699
        name,
5✔
700
        type,
5✔
701
        options,
5✔
702
        targetTableId,
5✔
703
        description,
5✔
704
        id: fieldId,
5✔
705
        notNull,
5✔
706
        unique,
5✔
707
        dbFieldName,
5✔
708
        isPrimary,
5✔
709
        order,
5✔
710
      } = field[0];
5✔
711
      const symmetricField = field[1];
5✔
712
      const { foreignTableId, relationship } = options as ILinkFieldOptions;
5✔
713
      const newFieldVo = await this.fieldOpenApiService.createField(targetTableId, {
5✔
714
        name,
5✔
715
        type,
5✔
716
        description,
5✔
717
        dbFieldName,
5✔
718
        options: {
5✔
719
          foreignTableId: tableIdMap[foreignTableId],
5✔
720
          relationship,
5✔
721
          isOneWay: false,
5✔
722
        },
5✔
723
      });
5✔
724
      fieldMap[fieldId] = newFieldVo.id;
5✔
725
      fieldMap[symmetricField.id] = (newFieldVo.options as ILinkFieldOptions).symmetricFieldId!;
5✔
726
      await this.replenishmentConstraint(newFieldVo.id, targetTableId, order, {
5✔
727
        notNull,
5✔
728
        unique,
5✔
729
        dbFieldName,
5✔
730
        isPrimary,
5✔
731
      });
5✔
732
      await this.repairSymmetricField(
5✔
733
        symmetricField,
5✔
734
        (newFieldVo.options as ILinkFieldOptions).foreignTableId,
5✔
735
        (newFieldVo.options as ILinkFieldOptions).symmetricFieldId!
5✔
736
      );
737
    }
5✔
738
  }
26✔
739

740
  // create two-way link, the symmetricFieldId created automatically, and need to update config
125✔
741
  private async repairSymmetricField(
125✔
742
    symmetricField: IFieldWithTableIdJson,
5✔
743
    targetTableId: string,
5✔
744
    newFieldId: string
5✔
745
  ) {
5✔
746
    const { notNull, unique, dbFieldName, isPrimary, description, name, order } = symmetricField;
5✔
747
    await this.replenishmentConstraint(newFieldId, targetTableId, order, {
5✔
748
      notNull,
5✔
749
      unique,
5✔
750
      dbFieldName,
5✔
751
      isPrimary,
5✔
752
    });
5✔
753
    const { dbTableName: targetDbTableName } = await this.prismaService
5✔
754
      .txClient()
5✔
755
      .tableMeta.findUniqueOrThrow({
5✔
756
        where: {
5✔
757
          id: targetTableId,
5✔
758
        },
5✔
759
        select: {
5✔
760
          dbTableName: true,
5✔
761
        },
5✔
762
      });
5✔
763

764
    const { dbFieldName: genDbFieldName } = await this.prismaService
5✔
765
      .txClient()
5✔
766
      .field.findUniqueOrThrow({
5✔
767
        where: {
5✔
768
          id: newFieldId,
5✔
769
        },
5✔
770
        select: {
5✔
771
          dbFieldName: true,
5✔
772
        },
5✔
773
      });
5✔
774

775
    await this.prismaService.txClient().field.update({
5✔
776
      where: {
5✔
777
        id: newFieldId,
5✔
778
      },
5✔
779
      data: {
5✔
780
        dbFieldName,
5✔
781
        name,
5✔
782
        description,
5✔
783
      },
5✔
784
    });
5✔
785

786
    if (genDbFieldName !== dbFieldName) {
5✔
787
      const alterTableSql = this.dbProvider.renameColumn(
×
788
        targetDbTableName,
×
789
        genDbFieldName,
×
790
        dbFieldName
×
791
      );
792

793
      for (const sql of alterTableSql) {
×
794
        await this.prismaService.txClient().$executeRawUnsafe(sql);
×
795
      }
×
796
    }
×
797
  }
5✔
798

799
  private async repairFieldOptions(
125✔
800
    tables: IBaseJson['tables'],
13✔
801
    tableIdMap: Record<string, string>,
13✔
802
    fieldIdMap: Record<string, string>,
13✔
803
    viewIdMap: Record<string, string>
13✔
804
  ) {
13✔
805
    const prisma = this.prismaService.txClient();
13✔
806

807
    const sourceFields = tables.map(({ fields }) => fields).flat();
13✔
808

809
    const targetFieldRaws = await prisma.field.findMany({
13✔
810
      where: {
13✔
811
        id: { in: Object.values(fieldIdMap) },
13✔
812
      },
13✔
813
    });
13✔
814

815
    const targetFields = targetFieldRaws.map((fieldRaw) => createFieldInstanceByRaw(fieldRaw));
13✔
816

817
    const linkFields = targetFields.filter(
13✔
818
      (field) => field.type === FieldType.Link && !field.isLookup
13✔
819
    );
820
    const lookupFields = targetFields.filter((field) => field.isLookup);
13✔
821
    const rollupFields = targetFields.filter((field) => field.type === FieldType.Rollup);
13✔
822

823
    for (const field of linkFields) {
13✔
824
      const { options, id } = field;
10✔
825
      const sourceField = sourceFields.find((f) => fieldIdMap[f.id] === id);
10✔
826
      const { filter, filterByViewId, visibleFieldIds } = sourceField?.options as ILinkFieldOptions;
10✔
827
      const moreConfigStr = {
10✔
828
        filter,
10✔
829
        filterByViewId,
10✔
830
        visibleFieldIds,
10✔
831
      };
10✔
832

833
      const newMoreConfigStr = replaceStringByMap(moreConfigStr, {
10✔
834
        tableIdMap,
10✔
835
        fieldIdMap,
10✔
836
        viewIdMap,
10✔
837
      });
10✔
838

839
      const newOptions = {
10✔
840
        ...options,
10✔
841
        ...JSON.parse(newMoreConfigStr || '{}'),
10✔
842
      };
10✔
843

844
      await prisma.field.update({
10✔
845
        where: {
10✔
846
          id,
10✔
847
        },
10✔
848
        data: {
10✔
849
          options: JSON.stringify(newOptions),
10✔
850
        },
10✔
851
      });
10✔
852
    }
10✔
853
    for (const field of [...lookupFields, ...rollupFields]) {
13✔
854
      const { lookupOptions, id } = field;
19✔
855
      const sourceField = sourceFields.find((f) => fieldIdMap[f.id] === id);
19✔
856
      const { filter } = sourceField?.lookupOptions as ILookupOptionsRo;
19✔
857
      const moreConfigStr = {
19✔
858
        filter,
19✔
859
      };
19✔
860

861
      const newMoreConfigStr = replaceStringByMap(moreConfigStr, {
19✔
862
        tableIdMap,
19✔
863
        fieldIdMap,
19✔
864
        viewIdMap,
19✔
865
      });
19✔
866

867
      const newLookupOptions = {
19✔
868
        ...lookupOptions,
19✔
869
        ...JSON.parse(newMoreConfigStr || '{}'),
19✔
870
      };
19✔
871

872
      await prisma.field.update({
19✔
873
        where: {
19✔
874
          id,
19✔
875
        },
19✔
876
        data: {
19✔
877
          lookupOptions: JSON.stringify(newLookupOptions),
19✔
878
        },
19✔
879
      });
19✔
880
    }
19✔
881
  }
13✔
882

883
  private async createDependencyFields(
125✔
884
    dependFields: IFieldWithTableIdJson[],
13✔
885
    tableIdMap: Record<string, string>,
13✔
886
    fieldMap: Record<string, string>
13✔
887
  ) {
13✔
888
    if (!dependFields.length) return;
13✔
889

890
    const checkedField = [] as IFieldJson[];
3✔
891

892
    while (dependFields.length) {
3✔
893
      const curField = dependFields.shift();
21✔
894
      if (!curField) continue;
21✔
895

896
      const { sourceTableId, targetTableId } = curField;
21✔
897

898
      const isChecked = checkedField.some((f) => f.id === curField.id);
21✔
899
      // InDegree all ready
21✔
900
      const isInDegreeReady = this.isInDegreeReady(curField, fieldMap);
21✔
901

902
      if (isInDegreeReady) {
21✔
903
        await this.duplicateSingleDependField(
21✔
904
          sourceTableId,
21✔
905
          targetTableId,
21✔
906
          curField,
21✔
907
          tableIdMap,
21✔
908
          fieldMap
21✔
909
        );
910
        continue;
21✔
911
      }
21✔
912

913
      if (isChecked) {
×
914
        if (curField.hasError) {
×
915
          await this.duplicateSingleDependField(
×
916
            sourceTableId,
×
917
            targetTableId,
×
918
            curField,
×
919
            tableIdMap,
×
920
            fieldMap,
×
921
            true
×
922
          );
923
        } else {
×
924
          throw new BadGatewayException('Create circular field');
×
925
        }
×
926
      } else {
×
927
        dependFields.push(curField);
×
928
        checkedField.push(curField);
×
929
      }
×
930
    }
21✔
931
  }
13✔
932

933
  private async duplicateSingleDependField(
125✔
934
    sourceTableId: string,
21✔
935
    targetTableId: string,
21✔
936
    field: IFieldWithTableIdJson,
21✔
937
    tableIdMap: Record<string, string>,
21✔
938
    sourceToTargetFieldMap: Record<string, string>,
21✔
939
    hasError = false
21✔
940
  ) {
21✔
941
    if (field.type === FieldType.Formula && !field.isLookup) {
21✔
942
      await this.duplicateFormulaField(targetTableId, field, sourceToTargetFieldMap, hasError);
2✔
943
    } else if (field.isLookup) {
21✔
944
      await this.duplicateLookupField(
19✔
945
        sourceTableId,
19✔
946
        targetTableId,
19✔
947
        field,
19✔
948
        tableIdMap,
19✔
949
        sourceToTargetFieldMap
19✔
950
      );
951
    } else if (field.type === FieldType.Rollup) {
19✔
952
      await this.duplicateRollupField(
×
953
        sourceTableId,
×
954
        targetTableId,
×
955
        field,
×
956
        tableIdMap,
×
957
        sourceToTargetFieldMap
×
958
      );
959
    }
×
960
  }
21✔
961

962
  private async duplicateLookupField(
125✔
963
    sourceTableId: string,
19✔
964
    targetTableId: string,
19✔
965
    field: IFieldWithTableIdJson,
19✔
966
    tableIdMap: Record<string, string>,
19✔
967
    sourceToTargetFieldMap: Record<string, string>
19✔
968
  ) {
19✔
969
    const {
19✔
970
      dbFieldName,
19✔
971
      name,
19✔
972
      lookupOptions,
19✔
973
      id,
19✔
974
      hasError,
19✔
975
      options,
19✔
976
      notNull,
19✔
977
      unique,
19✔
978
      description,
19✔
979
      isPrimary,
19✔
980
      type: lookupFieldType,
19✔
981
    } = field;
19✔
982
    const { foreignTableId, linkFieldId, lookupFieldId } = lookupOptions as ILookupOptionsRo;
19✔
983
    const isSelfLink = foreignTableId === sourceTableId;
19✔
984

985
    const mockFieldId = Object.values(sourceToTargetFieldMap)[0];
19✔
986
    const { type: mockType } = await this.prismaService.txClient().field.findUniqueOrThrow({
19✔
987
      where: {
19✔
988
        id: mockFieldId,
19✔
989
        deletedTime: null,
19✔
990
      },
19✔
991
      select: {
19✔
992
        type: true,
19✔
993
      },
19✔
994
    });
19✔
995
    const newField = await this.fieldOpenApiService.createField(targetTableId, {
19✔
996
      type: (hasError ? mockType : lookupFieldType) as FieldType,
19✔
997
      dbFieldName,
19✔
998
      description,
19✔
999
      isLookup: true,
19✔
1000
      lookupOptions: {
19✔
1001
        // foreignTableId may are cross base table id, so we need to use tableIdMap to get the target table id
19✔
1002
        foreignTableId: (isSelfLink ? targetTableId : tableIdMap[foreignTableId]) || foreignTableId,
19✔
1003
        linkFieldId: sourceToTargetFieldMap[linkFieldId],
19✔
1004
        lookupFieldId: isSelfLink
19✔
1005
          ? hasError
×
1006
            ? mockFieldId
×
1007
            : sourceToTargetFieldMap[lookupFieldId]
×
1008
          : hasError
19✔
1009
            ? mockFieldId
×
1010
            : sourceToTargetFieldMap[lookupFieldId] || lookupFieldId,
19✔
1011
      },
19✔
1012
      name,
19✔
1013
    });
19✔
1014
    await this.replenishmentConstraint(newField.id, targetTableId, field.order, {
19✔
1015
      notNull,
19✔
1016
      unique,
19✔
1017
      dbFieldName,
19✔
1018
      isPrimary,
19✔
1019
    });
19✔
1020
    sourceToTargetFieldMap[id] = newField.id;
19✔
1021
    if (hasError) {
19✔
1022
      await this.prismaService.txClient().field.update({
×
1023
        where: {
×
1024
          id: newField.id,
×
1025
        },
×
1026
        data: {
×
1027
          hasError,
×
1028
          type: lookupFieldType,
×
1029
          lookupOptions: JSON.stringify({
×
1030
            ...newField.lookupOptions,
×
1031
            lookupFieldId: lookupFieldId,
×
1032
          }),
×
1033
          options: JSON.stringify(options),
×
1034
        },
×
1035
      });
×
1036
    }
×
1037
  }
19✔
1038

1039
  private async duplicateRollupField(
125✔
1040
    sourceTableId: string,
×
1041
    targetTableId: string,
×
NEW
1042
    fieldInstance: IFieldWithTableIdJson,
×
1043
    tableIdMap: Record<string, string>,
×
1044
    sourceToTargetFieldMap: Record<string, string>
×
1045
  ) {
×
1046
    const {
×
1047
      dbFieldName,
×
1048
      name,
×
1049
      lookupOptions,
×
1050
      id,
×
1051
      hasError,
×
1052
      options,
×
1053
      notNull,
×
1054
      unique,
×
1055
      description,
×
1056
      isPrimary,
×
1057
      type: lookupFieldType,
×
1058
    } = fieldInstance;
×
1059
    const { foreignTableId, linkFieldId, lookupFieldId } = lookupOptions as ILookupOptionsRo;
×
1060
    const isSelfLink = foreignTableId === sourceTableId;
×
1061

1062
    const mockFieldId = Object.values(sourceToTargetFieldMap)[0];
×
1063
    const newField = await this.fieldOpenApiService.createField(targetTableId, {
×
1064
      type: FieldType.Rollup,
×
1065
      dbFieldName,
×
1066
      description,
×
1067
      lookupOptions: {
×
1068
        // foreignTableId may are cross base table id, so we need to use tableIdMap to get the target table id
×
1069
        foreignTableId: (isSelfLink ? targetTableId : tableIdMap[foreignTableId]) || foreignTableId,
×
1070
        linkFieldId: sourceToTargetFieldMap[linkFieldId],
×
1071
        lookupFieldId: isSelfLink
×
1072
          ? hasError
×
1073
            ? mockFieldId
×
1074
            : sourceToTargetFieldMap[lookupFieldId]
×
1075
          : hasError
×
1076
            ? mockFieldId
×
1077
            : sourceToTargetFieldMap[lookupFieldId] || lookupFieldId,
×
1078
      },
×
1079
      options,
×
1080
      name,
×
1081
    });
×
1082
    await this.replenishmentConstraint(newField.id, targetTableId, fieldInstance.order, {
×
1083
      notNull,
×
1084
      unique,
×
1085
      dbFieldName,
×
1086
      isPrimary,
×
1087
    });
×
1088
    sourceToTargetFieldMap[id] = newField.id;
×
1089
    if (hasError) {
×
1090
      await this.prismaService.txClient().field.update({
×
1091
        where: {
×
1092
          id: newField.id,
×
1093
        },
×
1094
        data: {
×
1095
          hasError,
×
1096
          type: lookupFieldType,
×
1097
          lookupOptions: JSON.stringify({
×
1098
            ...newField.lookupOptions,
×
1099
            lookupFieldId: lookupFieldId,
×
1100
          }),
×
1101
          options: JSON.stringify(options),
×
1102
        },
×
1103
      });
×
1104
    }
×
1105
  }
×
1106

1107
  private async duplicateFormulaField(
125✔
1108
    targetTableId: string,
2✔
1109
    fieldInstance: IFieldWithTableIdJson,
2✔
1110
    sourceToTargetFieldMap: Record<string, string>,
2✔
1111
    hasError: boolean = false
2✔
1112
  ) {
2✔
1113
    const {
2✔
1114
      type,
2✔
1115
      dbFieldName,
2✔
1116
      name,
2✔
1117
      options,
2✔
1118
      id,
2✔
1119
      notNull,
2✔
1120
      unique,
2✔
1121
      description,
2✔
1122
      isPrimary,
2✔
1123
      dbFieldType,
2✔
1124
      cellValueType,
2✔
1125
      isMultipleCellValue,
2✔
1126
    } = fieldInstance;
2✔
1127
    const { expression } = options as IFormulaFieldOptions;
2✔
1128
    const newExpression = replaceStringByMap(expression, { sourceToTargetFieldMap });
2✔
1129
    const newField = await this.fieldOpenApiService.createField(targetTableId, {
2✔
1130
      type,
2✔
1131
      dbFieldName: dbFieldName,
2✔
1132
      description,
2✔
1133
      options: {
2✔
1134
        ...options,
2✔
1135
        expression: hasError
2✔
NEW
1136
          ? DEFAULT_EXPRESSION
×
1137
          : newExpression
2✔
1138
            ? JSON.parse(newExpression)
2✔
1139
            : undefined,
×
1140
      },
2✔
1141
      name,
2✔
1142
    });
2✔
1143
    await this.replenishmentConstraint(newField.id, targetTableId, fieldInstance.order, {
2✔
1144
      notNull,
2✔
1145
      unique,
2✔
1146
      dbFieldName,
2✔
1147
      isPrimary,
2✔
1148
    });
2✔
1149
    sourceToTargetFieldMap[id] = newField.id;
2✔
1150

1151
    if (hasError) {
2✔
1152
      await this.prismaService.txClient().field.update({
×
1153
        where: {
×
1154
          id: newField.id,
×
1155
        },
×
1156
        data: {
×
1157
          hasError,
×
1158
          options: JSON.stringify({
×
1159
            ...options,
×
1160
            expression: newExpression ? JSON.parse(newExpression) : undefined,
×
1161
          }),
×
1162
        },
×
1163
      });
×
1164
    }
×
1165

1166
    if (dbFieldType !== newField.dbFieldType) {
2✔
NEW
1167
      const { dbTableName } = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
×
NEW
1168
        where: {
×
NEW
1169
          id: targetTableId,
×
NEW
1170
        },
×
NEW
1171
        select: {
×
NEW
1172
          dbTableName: true,
×
NEW
1173
        },
×
NEW
1174
      });
×
NEW
1175
      const schemaType = dbType2knexFormat(this.knex, dbFieldType);
×
NEW
1176
      const modifyColumnSql = this.dbProvider.modifyColumnSchema(
×
NEW
1177
        dbTableName,
×
NEW
1178
        dbFieldName,
×
NEW
1179
        schemaType
×
1180
      );
1181

NEW
1182
      for (const alterTableQuery of modifyColumnSql) {
×
NEW
1183
        await this.prismaService.txClient().$executeRawUnsafe(alterTableQuery);
×
NEW
1184
      }
×
1185

NEW
1186
      await this.prismaService.txClient().field.update({
×
NEW
1187
        where: {
×
NEW
1188
          id: newField.id,
×
NEW
1189
        },
×
NEW
1190
        data: {
×
NEW
1191
          dbFieldType,
×
NEW
1192
          cellValueType,
×
NEW
1193
          isMultipleCellValue,
×
NEW
1194
        },
×
NEW
1195
      });
×
NEW
1196
    }
×
1197
  }
2✔
1198

1199
  // field could not set constraint when create
125✔
1200
  private async replenishmentConstraint(
125✔
1201
    fId: string,
81✔
1202
    targetTableId: string,
81✔
1203
    order: number,
81✔
1204
    {
81✔
1205
      notNull,
81✔
1206
      unique,
81✔
1207
      dbFieldName,
81✔
1208
      isPrimary,
81✔
1209
    }: { notNull?: boolean; unique?: boolean; dbFieldName: string; isPrimary?: boolean }
81✔
1210
  ) {
81✔
1211
    await this.prismaService.txClient().field.update({
81✔
1212
      where: {
81✔
1213
        id: fId,
81✔
1214
      },
81✔
1215
      data: {
81✔
1216
        order,
81✔
1217
      },
81✔
1218
    });
81✔
1219
    if (!notNull && !unique && !isPrimary) {
81✔
1220
      return;
67✔
1221
    }
67✔
1222

1223
    const { dbTableName } = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
14✔
1224
      where: {
14✔
1225
        id: targetTableId,
14✔
1226
        deletedTime: null,
14✔
1227
      },
14✔
1228
      select: {
14✔
1229
        dbTableName: true,
14✔
1230
      },
14✔
1231
    });
14✔
1232

1233
    await this.prismaService.txClient().field.update({
14✔
1234
      where: {
14✔
1235
        id: fId,
14✔
1236
      },
14✔
1237
      data: {
14✔
1238
        notNull: notNull ?? null,
14✔
1239
        unique: unique ?? null,
81✔
1240
        isPrimary: isPrimary ?? null,
81✔
1241
      },
81✔
1242
    });
81✔
1243

1244
    if (notNull || unique) {
81✔
1245
      const fieldValidationQuery = this.knex.schema
×
1246
        .alterTable(dbTableName, (table) => {
×
1247
          if (unique) table.dropUnique([dbFieldName]);
×
1248
          if (notNull) table.setNullable(dbFieldName);
×
1249
        })
×
1250
        .toQuery();
×
1251

1252
      await this.prismaService.txClient().$executeRawUnsafe(fieldValidationQuery);
×
1253
    }
×
1254
  }
81✔
1255

1256
  private isInDegreeReady(field: IFieldWithTableIdJson, fieldMap: Record<string, string>) {
125✔
1257
    if (field.type === FieldType.Formula) {
21✔
1258
      const formulaOptions = field.options as IFormulaFieldOptions;
4✔
1259
      const referencedFields = this.extractFieldIds(formulaOptions.expression);
4✔
1260
      const keys = Object.keys(fieldMap);
4✔
1261
      return referencedFields.every((field) => keys.includes(field));
4✔
1262
    }
4✔
1263

1264
    if (field.isLookup || field.type === FieldType.Rollup) {
21✔
1265
      const { lookupOptions } = field;
17✔
1266
      const { linkFieldId, lookupFieldId } = lookupOptions as ILookupOptionsRo;
17✔
1267
      // const isSelfLink = foreignTableId === sourceTableId;
17✔
1268
      return Boolean(fieldMap[lookupFieldId] && fieldMap[linkFieldId]);
17✔
1269
      // return isSelfLink ? Boolean(fieldMap[lookupFieldId] && fieldMap[linkFieldId]) : true;
17✔
1270
    }
17✔
1271

1272
    return false;
×
1273
  }
×
1274

1275
  private extractFieldIds(expression: string): string[] {
125✔
1276
    const matches = expression.match(/\{fld[a-zA-Z0-9]+\}/g);
4✔
1277

1278
    if (!matches) {
4✔
1279
      return [];
4✔
1280
    }
4✔
1281
    return matches.map((match) => match.slice(1, -1));
×
1282
  }
×
1283

1284
  /* eslint-disable sonarjs/cognitive-complexity */
125✔
1285
  private async createViews(
125✔
1286
    tables: IBaseJson['tables'],
13✔
1287
    tableIdMap: Record<string, string>,
13✔
1288
    fieldMap: Record<string, string>
13✔
1289
  ) {
13✔
1290
    const viewMap: Record<string, string> = {};
13✔
1291
    for (const table of tables) {
13✔
1292
      const { views: originalViews, id: tableId } = table;
14✔
1293
      const views = originalViews.filter((view) => view.type !== ViewType.Plugin);
14✔
1294
      for (const view of views) {
14✔
1295
        const {
14✔
1296
          name,
14✔
1297
          type,
14✔
1298
          id: viewId,
14✔
1299
          description,
14✔
1300
          enableShare,
14✔
1301
          isLocked,
14✔
1302
          order,
14✔
1303
          columnMeta,
14✔
1304
          shareMeta,
14✔
1305
          shareId,
14✔
1306
        } = view;
14✔
1307

1308
        const keys = ['options', 'columnMeta', 'filter', 'group', 'sort'] as (keyof typeof view)[];
14✔
1309
        const obj = {} as Record<string, unknown>;
14✔
1310

1311
        for (const key of keys) {
14✔
1312
          const keyString = replaceStringByMap(view[key], { fieldMap });
70✔
1313
          const newValue = keyString ? JSON.parse(keyString) : null;
70✔
1314
          obj[key] = newValue;
70✔
1315
        }
70✔
1316
        const newViewVo = await this.viewOpenApiService.createView(tableIdMap[tableId], {
14✔
1317
          name,
14✔
1318
          type,
14✔
1319
          description,
14✔
1320
          enableShare,
14✔
1321
          isLocked,
14✔
1322
          ...obj,
14✔
1323
        });
14✔
1324

1325
        viewMap[viewId] = newViewVo.id;
14✔
1326

1327
        await this.prismaService.txClient().view.update({
14✔
1328
          where: {
14✔
1329
            id: newViewVo.id,
14✔
1330
          },
14✔
1331
          data: {
14✔
1332
            order,
14✔
1333
            columnMeta: columnMeta ? replaceStringByMap(columnMeta, { fieldMap }) : columnMeta,
14✔
1334
            shareId: shareId ? generateShareId() : undefined,
14✔
1335
            shareMeta: shareMeta ? JSON.stringify(shareMeta) : undefined,
14✔
1336
            enableShare,
14✔
1337
            isLocked,
14✔
1338
          },
14✔
1339
        });
14✔
1340
      }
14✔
1341
    }
14✔
1342

1343
    return viewMap;
13✔
1344
  }
13✔
1345

1346
  private async createPlugins(
125✔
1347
    baseId: string,
13✔
1348
    plugins: IBaseJson['plugins'],
13✔
1349
    tableIdMap: Record<string, string>,
13✔
1350
    fieldMap: Record<string, string>,
13✔
1351
    viewIdMap: Record<string, string>
13✔
1352
  ) {
13✔
1353
    await this.createDashboard(baseId, plugins[PluginPosition.Dashboard], tableIdMap, fieldMap);
13✔
1354
    await this.createPanel(baseId, plugins[PluginPosition.Panel], tableIdMap, fieldMap);
13✔
1355
    await this.createPluginViews(
13✔
1356
      baseId,
13✔
1357
      plugins[PluginPosition.View],
13✔
1358
      tableIdMap,
13✔
1359
      fieldMap,
13✔
1360
      viewIdMap
13✔
1361
    );
1362
  }
13✔
1363

1364
  private async createDashboard(
125✔
1365
    baseId: string,
13✔
1366
    plugins: IBaseJson['plugins'][PluginPosition.Dashboard],
13✔
1367
    tableMap: Record<string, string>,
13✔
1368
    fieldMap: Record<string, string>
13✔
1369
  ) {
13✔
1370
    const dashboardMap: Record<string, string> = {};
13✔
1371
    const pluginInstallMap: Record<string, string> = {};
13✔
1372
    const userId = this.cls.get('user.id');
13✔
1373
    const prisma = this.prismaService.txClient();
13✔
1374
    const pluginInstalls = plugins.map(({ pluginInstall }) => pluginInstall).flat();
13✔
1375

1376
    for (const plugin of plugins) {
13✔
1377
      const { id, name } = plugin;
6✔
1378
      const newDashBoardId = generateDashboardId();
6✔
1379
      await prisma.dashboard.create({
6✔
1380
        data: {
6✔
1381
          id: newDashBoardId,
6✔
1382
          baseId,
6✔
1383
          name,
6✔
1384
          createdBy: userId,
6✔
1385
        },
6✔
1386
      });
6✔
1387
      dashboardMap[id] = newDashBoardId;
6✔
1388
    }
6✔
1389

1390
    for (const pluginInstall of pluginInstalls) {
13✔
1391
      const { id, pluginId, positionId, position, name, storage } = pluginInstall;
9✔
1392
      const newPluginInstallId = generatePluginInstallId();
9✔
1393
      const newStorage = replaceStringByMap(storage, { tableMap, fieldMap });
9✔
1394
      await prisma.pluginInstall.create({
9✔
1395
        data: {
9✔
1396
          id: newPluginInstallId,
9✔
1397
          createdBy: userId,
9✔
1398
          baseId,
9✔
1399
          pluginId,
9✔
1400
          name,
9✔
1401
          positionId: dashboardMap[positionId],
9✔
1402
          position,
9✔
1403
          storage: newStorage,
9✔
1404
        },
9✔
1405
      });
9✔
1406
      pluginInstallMap[id] = newPluginInstallId;
9✔
1407
    }
9✔
1408

1409
    // replace pluginId in layout with new pluginInstallId
13✔
1410
    for (const plugin of plugins) {
13✔
1411
      const { id, layout } = plugin;
6✔
1412
      const newLayout = replaceStringByMap(layout, { pluginInstallMap });
6✔
1413
      await prisma.dashboard.update({
6✔
1414
        where: { id: dashboardMap[id] },
6✔
1415
        data: {
6✔
1416
          layout: newLayout,
6✔
1417
        },
6✔
1418
      });
6✔
1419
    }
6✔
1420

1421
    // create char user to collaborator
13✔
1422
    await prisma.collaborator.create({
13✔
1423
      data: {
13✔
1424
        roleName: Role.Owner,
13✔
1425
        createdBy: userId,
13✔
1426
        resourceId: baseId,
13✔
1427
        resourceType: ResourceType.Base,
13✔
1428
        principalType: PrincipalType.User,
13✔
1429
        principalId: 'pluchartuser',
13✔
1430
      },
13✔
1431
    });
13✔
1432
  }
13✔
1433

1434
  private async createPanel(
125✔
1435
    baseId: string,
13✔
1436
    plugins: IBaseJson['plugins'][PluginPosition.Panel],
13✔
1437
    tableMap: Record<string, string>,
13✔
1438
    fieldMap: Record<string, string>
13✔
1439
  ) {
13✔
1440
    const panelMap: Record<string, string> = {};
13✔
1441
    const pluginInstallMap: Record<string, string> = {};
13✔
1442
    const userId = this.cls.get('user.id');
13✔
1443
    const prisma = this.prismaService.txClient();
13✔
1444
    const pluginInstalls = plugins.map(({ pluginInstall }) => pluginInstall).flat();
13✔
1445

1446
    for (const plugin of plugins) {
13✔
1447
      const { id, name, tableId } = plugin;
6✔
1448
      const newPluginPanelId = generatePluginPanelId();
6✔
1449
      await prisma.pluginPanel.create({
6✔
1450
        data: {
6✔
1451
          id: newPluginPanelId,
6✔
1452
          tableId: tableMap[tableId],
6✔
1453
          name,
6✔
1454
          createdBy: userId,
6✔
1455
        },
6✔
1456
      });
6✔
1457
      panelMap[id] = newPluginPanelId;
6✔
1458
    }
6✔
1459

1460
    for (const pluginInstall of pluginInstalls) {
13✔
1461
      const { id, pluginId, positionId, position, name, storage } = pluginInstall;
9✔
1462
      const newPluginInstallId = generatePluginInstallId();
9✔
1463
      const newStorage = replaceStringByMap(storage, { tableMap, fieldMap });
9✔
1464
      await prisma.pluginInstall.create({
9✔
1465
        data: {
9✔
1466
          id: newPluginInstallId,
9✔
1467
          createdBy: userId,
9✔
1468
          baseId,
9✔
1469
          pluginId,
9✔
1470
          name,
9✔
1471
          positionId: panelMap[positionId],
9✔
1472
          position,
9✔
1473
          storage: newStorage,
9✔
1474
        },
9✔
1475
      });
9✔
1476
      pluginInstallMap[id] = newPluginInstallId;
9✔
1477
    }
9✔
1478

1479
    // replace pluginId in layout with new pluginInstallId
13✔
1480
    for (const plugin of plugins) {
13✔
1481
      const { id, layout } = plugin;
6✔
1482
      const newLayout = replaceStringByMap(layout, { pluginInstallMap });
6✔
1483
      await prisma.pluginPanel.update({
6✔
1484
        where: { id: panelMap[id] },
6✔
1485
        data: {
6✔
1486
          layout: newLayout,
6✔
1487
        },
6✔
1488
      });
6✔
1489
    }
6✔
1490
  }
13✔
1491

1492
  private async createPluginViews(
125✔
1493
    baseId: string,
13✔
1494
    pluginViews: IBaseJson['plugins'][PluginPosition.View],
13✔
1495
    tableIdMap: Record<string, string>,
13✔
1496
    fieldIdMap: Record<string, string>,
13✔
1497
    viewIdMap: Record<string, string>
13✔
1498
  ) {
13✔
1499
    const prisma = this.prismaService.txClient();
13✔
1500

1501
    for (const pluginView of pluginViews) {
13✔
1502
      const {
6✔
1503
        id,
6✔
1504
        name,
6✔
1505
        description,
6✔
1506
        enableShare,
6✔
1507
        shareMeta,
6✔
1508
        isLocked,
6✔
1509
        tableId,
6✔
1510
        pluginInstall,
6✔
1511
        order,
6✔
1512
      } = pluginView;
6✔
1513
      const { pluginId } = pluginInstall;
6✔
1514
      const { viewId: newViewId, pluginInstallId } = await this.viewOpenApiService.pluginInstall(
6✔
1515
        tableIdMap[tableId],
6✔
1516
        {
6✔
1517
          name,
6✔
1518
          pluginId,
6✔
1519
        }
6✔
1520
      );
1521
      viewIdMap[id] = newViewId;
6✔
1522

1523
      await prisma.view.update({
6✔
1524
        where: { id: newViewId },
6✔
1525
        data: {
6✔
1526
          order,
6✔
1527
        },
6✔
1528
      });
6✔
1529

1530
      // 1. update view options
6✔
1531
      const configProperties = ['columnMeta', 'options', 'sort', 'group', 'filter'] as const;
6✔
1532
      const updateConfig = {} as Record<(typeof configProperties)[number], string>;
6✔
1533
      for (const property of configProperties) {
6✔
1534
        const result = replaceStringByMap(pluginView[property], {
30✔
1535
          tableIdMap,
30✔
1536
          fieldIdMap,
30✔
1537
          viewIdMap,
30✔
1538
        });
30✔
1539

1540
        if (result) {
30✔
1541
          updateConfig[property] = result;
12✔
1542
        }
12✔
1543
      }
30✔
1544
      await prisma.view.update({
6✔
1545
        where: { id: newViewId },
6✔
1546
        data: {
6✔
1547
          description,
6✔
1548
          isLocked,
6✔
1549
          enableShare,
6✔
1550
          shareMeta: shareMeta ? JSON.stringify(shareMeta) : undefined,
6✔
1551
          ...updateConfig,
6✔
1552
        },
6✔
1553
      });
6✔
1554

1555
      // 2. update plugin install
6✔
1556
      const newStorage = replaceStringByMap(pluginInstall.storage, {
6✔
1557
        tableIdMap,
6✔
1558
        fieldIdMap,
6✔
1559
        viewIdMap,
6✔
1560
      });
6✔
1561
      await prisma.pluginInstall.update({
6✔
1562
        where: { id: pluginInstallId },
6✔
1563
        data: {
6✔
1564
          storage: newStorage,
6✔
1565
        },
6✔
1566
      });
6✔
1567
    }
6✔
1568
  }
13✔
1569
}
125✔
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