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

teableio / teable / 16002989744

01 Jul 2025 03:00PM UTC coverage: 80.704% (+0.002%) from 80.702%
16002989744

Pull #1530

github

web-flow
Merge aff823760 into 4e9b67f77
Pull Request #1530: Chore/preview ai feature

8068 of 8579 branches covered (94.04%)

2 of 4 new or added lines in 1 file covered. (50.0%)

41 existing lines in 2 files now uncovered.

38436 of 47626 relevant lines covered (80.7%)

1705.19 hits per line

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

80.79
/apps/nestjs-backend/src/features/table/open-api/table-open-api.service.ts
1
import {
4✔
2
  BadRequestException,
3
  NotFoundException,
4
  Injectable,
5
  Logger,
6
  ForbiddenException,
7
} from '@nestjs/common';
8
import type {
9
  FieldAction,
10
  IFieldRo,
11
  IFieldVo,
12
  ILinkFieldOptions,
13
  ILookupOptionsVo,
14
  IViewRo,
15
  RecordAction,
16
  IRole,
17
  TableAction,
18
  ViewAction,
19
} from '@teable/core';
20
import {
21
  ActionPrefix,
22
  FieldKeyType,
23
  FieldType,
24
  actionPrefixMap,
25
  getBasePermission,
26
} from '@teable/core';
27
import { PrismaService } from '@teable/db-main-prisma';
28
import { ResourceType } from '@teable/openapi';
29
import type {
30
  ICreateRecordsRo,
31
  ICreateTableRo,
32
  ICreateTableWithDefault,
33
  IDuplicateTableRo,
34
  ITableFullVo,
35
  ITablePermissionVo,
36
  ITableVo,
37
  IUpdateOrderRo,
38
} from '@teable/openapi';
39
import { Knex } from 'knex';
40
import { nanoid } from 'nanoid';
41
import { InjectModel } from 'nest-knexjs';
42
import { ThresholdConfig, IThresholdConfig } from '../../../configs/threshold.config';
43
import { InjectDbProvider } from '../../../db-provider/db.provider';
44
import { IDbProvider } from '../../../db-provider/db.provider.interface';
45
import { updateOrder } from '../../../utils/update-order';
46
import { PermissionService } from '../../auth/permission.service';
47
import { LinkService } from '../../calculation/link.service';
48
import { FieldCreatingService } from '../../field/field-calculate/field-creating.service';
49
import { FieldSupplementService } from '../../field/field-calculate/field-supplement.service';
50
import { createFieldInstanceByVo } from '../../field/model/factory';
51
import { FieldOpenApiService } from '../../field/open-api/field-open-api.service';
52
import { RecordOpenApiService } from '../../record/open-api/record-open-api.service';
53
import { RecordService } from '../../record/record.service';
54
import { ViewOpenApiService } from '../../view/open-api/view-open-api.service';
55
import { TableDuplicateService } from '../table-duplicate.service';
56
import { TableService } from '../table.service';
57

58
@Injectable()
59
export class TableOpenApiService {
4✔
60
  private logger = new Logger(TableOpenApiService.name);
133✔
61
  constructor(
133✔
62
    private readonly prismaService: PrismaService,
133✔
63
    private readonly recordOpenApiService: RecordOpenApiService,
133✔
64
    private readonly viewOpenApiService: ViewOpenApiService,
133✔
65
    private readonly recordService: RecordService,
133✔
66
    private readonly tableService: TableService,
133✔
67
    private readonly linkService: LinkService,
133✔
68
    private readonly fieldOpenApiService: FieldOpenApiService,
133✔
69
    private readonly fieldCreatingService: FieldCreatingService,
133✔
70
    private readonly fieldSupplementService: FieldSupplementService,
133✔
71
    private readonly permissionService: PermissionService,
133✔
72
    private readonly tableDuplicateService: TableDuplicateService,
133✔
73
    @InjectDbProvider() private readonly dbProvider: IDbProvider,
133✔
74
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig,
133✔
75
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
133✔
76
  ) {}
133✔
77

78
  private async createView(tableId: string, viewRos: IViewRo[]) {
133✔
79
    const viewCreationPromises = viewRos.map(async (viewRo) => {
1,697✔
80
      return this.viewOpenApiService.createView(tableId, viewRo);
1,719✔
81
    });
1,719✔
82
    return await Promise.all(viewCreationPromises);
1,697✔
83
  }
1,697✔
84

85
  private async createField(tableId: string, fieldVos: IFieldVo[]) {
133✔
86
    const fieldSnapshots: IFieldVo[] = [];
×
87
    const fieldNameSet = new Set<string>();
×
88
    for (const fieldVo of fieldVos) {
×
89
      if (fieldNameSet.has(fieldVo.name)) {
×
90
        throw new BadRequestException(`duplicate field name: ${fieldVo.name}`);
×
91
      }
×
92
      fieldNameSet.add(fieldVo.name);
×
93
      const fieldInstance = createFieldInstanceByVo(fieldVo);
×
94
      await this.fieldCreatingService.alterCreateField(tableId, fieldInstance);
×
95
      fieldSnapshots.push(fieldVo);
×
96
    }
×
97
    return fieldSnapshots;
×
98
  }
×
99

100
  private async createFields(tableId: string, fieldVos: IFieldVo[]) {
133✔
101
    const fieldNameSet = new Set<string>();
1,697✔
102

103
    for (const fieldVo of fieldVos) {
1,697✔
104
      if (fieldNameSet.has(fieldVo.name)) {
5,132✔
105
        throw new BadRequestException(`duplicate field name: ${fieldVo.name}`);
×
106
      }
×
107
      fieldNameSet.add(fieldVo.name);
5,132✔
108
    }
5,132✔
109

110
    const fieldInstances = fieldVos.map((fieldVo) => createFieldInstanceByVo(fieldVo));
1,697✔
111

112
    await this.fieldCreatingService.alterCreateFields(tableId, fieldInstances);
1,697✔
113

114
    return fieldVos;
1,697✔
115
  }
1,697✔
116

117
  private async createRecords(tableId: string, data: ICreateRecordsRo) {
133✔
118
    return this.recordOpenApiService.createRecords(tableId, data);
1,667✔
119
  }
1,667✔
120

121
  private async prepareFields(tableId: string, fieldRos: IFieldRo[]) {
133✔
122
    const simpleFields: IFieldRo[] = [];
1,697✔
123
    const computeFields: IFieldRo[] = [];
1,697✔
124
    fieldRos.forEach((field) => {
1,697✔
125
      if (field.type === FieldType.Link || field.type === FieldType.Formula || field.isLookup) {
5,132✔
126
        computeFields.push(field);
147✔
127
      } else {
5,132✔
128
        simpleFields.push(field);
4,985✔
129
      }
4,985✔
130
    });
5,132✔
131

132
    const fields: IFieldVo[] = await this.fieldSupplementService.prepareCreateFields(
1,697✔
133
      tableId,
1,697✔
134
      simpleFields
1,697✔
135
    );
136

137
    const allFieldRos = simpleFields.concat(computeFields);
1,697✔
138

139
    const fieldVoMap = new Map<IFieldRo, IFieldVo>();
1,697✔
140
    simpleFields.forEach((f, i) => fieldVoMap.set(f, fields[i]));
1,697✔
141

142
    for (const fieldRo of computeFields) {
1,697✔
143
      const computedFieldVo = await this.fieldSupplementService.prepareCreateField(
147✔
144
        tableId,
147✔
145
        fieldRo,
147✔
146
        allFieldRos.filter((ro) => ro !== fieldRo) as IFieldVo[]
147✔
147
      );
148
      fieldVoMap.set(fieldRo, computedFieldVo);
147✔
149
    }
147✔
150

151
    const orderedFields = fieldRos.map((ro) => fieldVoMap.get(ro)).filter(Boolean) as IFieldVo[];
1,697✔
152

153
    const repeatedDbFieldNames = orderedFields
1,697✔
154
      .map((f) => f.dbFieldName)
1,697✔
155
      .filter((value, index, self) => self.indexOf(value) !== index);
1,697✔
156

157
    // generator dbFieldName may repeat, this is fix it.
1,697✔
158
    return orderedFields.map((f) => {
1,697✔
159
      const newField = { ...f };
5,132✔
160
      const { dbFieldName } = newField;
5,132✔
161

162
      if (repeatedDbFieldNames.includes(dbFieldName)) {
5,132✔
163
        newField.dbFieldName = `${dbFieldName}_${nanoid(3)}`;
×
164
      }
×
165

166
      return newField;
5,132✔
167
    });
5,132✔
168
  }
1,697✔
169

170
  async createTable(baseId: string, tableRo: ICreateTableWithDefault): Promise<ITableFullVo> {
133✔
171
    const schema = await this.prismaService.$tx(async () => {
1,697✔
172
      const tableVo = await this.createTableMeta(baseId, tableRo);
1,697✔
173
      const tableId = tableVo.id;
1,697✔
174

175
      const preparedFields = await this.prepareFields(tableId, tableRo.fields);
1,697✔
176

177
      // set the first field to be the primary field if not set
1,697✔
178
      if (!preparedFields.find((field) => field.isPrimary)) {
1,697✔
NEW
179
        preparedFields[0].isPrimary = true;
×
NEW
180
      }
×
181

182
      // create teable should not set computed field isPending, because noting need to calculate when create
1,697✔
183
      preparedFields.forEach((field) => delete field.isPending);
1,697✔
184
      const fieldVos = await this.createFields(tableId, preparedFields);
1,697✔
185

186
      const viewVos = await this.createView(tableId, tableRo.views);
1,697✔
187

188
      return {
1,697✔
189
        ...tableVo,
1,697✔
190
        total: tableRo.records?.length || 0,
1,697✔
191
        fields: fieldVos,
1,697✔
192
        views: viewVos,
1,697✔
193
        defaultViewId: viewVos[0].id,
1,697✔
194
      };
1,697✔
195
    });
1,697✔
196

197
    const records = await this.prismaService.$tx(async () => {
1,697✔
198
      const recordsVo =
1,697✔
199
        tableRo.records?.length &&
1,697✔
200
        (await this.createRecords(schema.id, {
1,667✔
201
          records: tableRo.records,
1,667✔
202
          fieldKeyType: tableRo.fieldKeyType ?? FieldKeyType.Name,
1,667✔
203
        }));
1,667✔
204

205
      return recordsVo ? recordsVo.records : [];
1,697✔
206
    });
1,697✔
207

208
    return {
1,695✔
209
      ...schema,
1,695✔
210
      records,
1,695✔
211
    };
1,695✔
212
  }
1,695✔
213

214
  async duplicateTable(baseId: string, tableId: string, tableRo: IDuplicateTableRo) {
133✔
215
    return await this.tableDuplicateService.duplicateTable(baseId, tableId, tableRo);
10✔
216
  }
10✔
217

218
  async createTableMeta(baseId: string, tableRo: ICreateTableRo) {
133✔
219
    return await this.tableService.createTable(baseId, tableRo);
1,697✔
220
  }
1,697✔
221

222
  async getTable(baseId: string, tableId: string): Promise<ITableVo> {
133✔
223
    return await this.tableService.getTableMeta(baseId, tableId);
34✔
224
  }
34✔
225

226
  async getTables(baseId: string, includeTableIds?: string[]): Promise<ITableVo[]> {
133✔
227
    const tablesMeta = await this.prismaService.txClient().tableMeta.findMany({
21✔
228
      orderBy: { order: 'asc' },
21✔
229
      where: {
21✔
230
        baseId,
21✔
231
        deletedTime: null,
21✔
232
        id: includeTableIds ? { in: includeTableIds } : undefined,
21✔
233
      },
21✔
234
    });
21✔
235
    const tableIds = tablesMeta.map((tableMeta) => tableMeta.id);
21✔
236
    const tableTime = await this.tableService.getTableLastModifiedTime(tableIds);
21✔
237
    const tableDefaultViewIds = await this.tableService.getTableDefaultViewId(tableIds);
21✔
238
    return tablesMeta.map((tableMeta, i) => {
21✔
239
      const time = tableTime[i];
39✔
240
      const defaultViewId = tableDefaultViewIds[i];
39✔
241
      if (!defaultViewId) {
39✔
242
        throw new Error('defaultViewId is not found');
×
243
      }
×
244
      return {
39✔
245
        ...tableMeta,
39✔
246
        description: tableMeta.description ?? undefined,
39✔
247
        icon: tableMeta.icon ?? undefined,
39✔
248
        lastModifiedTime: time || tableMeta.lastModifiedTime?.toISOString(),
39✔
249
        defaultViewId,
39✔
250
      };
39✔
251
    });
39✔
252
  }
21✔
253

254
  async detachLink(tableId: string) {
133✔
255
    // handle the link field in this table
1,652✔
256
    const linkFields = await this.prismaService.txClient().field.findMany({
1,652✔
257
      where: { tableId, type: FieldType.Link, isLookup: null, deletedTime: null },
1,652✔
258
      select: { id: true, options: true },
1,652✔
259
    });
1,652✔
260

261
    for (const field of linkFields) {
1,652✔
262
      if (field.options) {
436✔
263
        const options = JSON.parse(field.options as string) as ILinkFieldOptions;
436✔
264
        // if the link field is a self-link field, skip it
436✔
265
        if (options.foreignTableId === tableId) {
436✔
266
          continue;
24✔
267
        }
24✔
268
      }
436✔
269
      await this.fieldOpenApiService.convertField(tableId, field.id, {
412✔
270
        type: FieldType.SingleLineText,
412✔
271
      });
412✔
272
    }
407✔
273

274
    // handle the link field in related tables
1,647✔
275
    const relatedLinkFieldRaws = await this.linkService.getRelatedLinkFieldRaws(tableId);
1,647✔
276

277
    for (const field of relatedLinkFieldRaws) {
1,652✔
278
      if (field.tableId === tableId) {
80✔
279
        continue;
24✔
280
      }
24✔
281
      await this.fieldOpenApiService.convertField(field.tableId, field.id, {
56✔
282
        type: FieldType.SingleLineText,
56✔
283
      });
56✔
284
    }
56✔
285
  }
1,635✔
286

287
  async permanentDeleteTables(baseId: string, tableIds: string[]) {
133✔
288
    // If the table has already been deleted, exceptions may occur
1,582✔
289
    // If the table hasn't been deleted and permanent deletion is executed directly,
1,582✔
290
    // we need to handle the deletion of associated data
1,582✔
291
    try {
1,582✔
292
      for (const tableId of tableIds) {
1,582✔
293
        await this.detachLink(tableId);
1,582✔
294
      }
1,565✔
295
    } catch (e) {
1,582✔
296
      console.log('Permanent delete tables error:', e);
17✔
297
    }
17✔
298

299
    return await this.prismaService.$tx(
1,582✔
300
      async () => {
1,582✔
301
        await this.dropTables(tableIds);
1,582✔
302
        await this.cleanTaskRelatedData(tableIds);
1,577✔
303
        await this.cleanTablesRelatedData(baseId, tableIds);
1,577✔
304
      },
1,577✔
305
      {
1,582✔
306
        timeout: this.thresholdConfig.bigTransactionTimeout,
1,582✔
307
      }
1,582✔
308
    );
309
  }
1,582✔
310

311
  async dropTables(tableIds: string[]) {
133✔
312
    const tables = await this.prismaService.txClient().tableMeta.findMany({
1,625✔
313
      where: { id: { in: tableIds } },
1,625✔
314
      select: { dbTableName: true },
1,625✔
315
    });
1,625✔
316

317
    for (const table of tables) {
1,625✔
318
      await this.prismaService
1,584✔
319
        .txClient()
1,584✔
320
        .$executeRawUnsafe(this.dbProvider.dropTable(table.dbTableName));
1,584✔
321
    }
1,579✔
322
  }
1,620✔
323

324
  async cleanTaskRelatedData(tableIds: string[]) {
133✔
325
    const alternativeFields = await this.prismaService.txClient().field.findMany({
1,577✔
326
      where: { tableId: { in: tableIds } },
1,577✔
327
      select: { id: true },
1,577✔
328
    });
1,577✔
329
    const alternativeFieldIds = alternativeFields.map((field) => field.id);
1,577✔
330

331
    // clean task reference for fields
1,577✔
332
    await this.prismaService.txClient().taskReference.deleteMany({
1,577✔
333
      where: {
1,577✔
334
        OR: [
1,577✔
335
          { fromFieldId: { in: alternativeFieldIds } },
1,577✔
336
          { toFieldId: { in: alternativeFieldIds } },
1,577✔
337
        ],
338
      },
1,577✔
339
    });
1,577✔
340

341
    // clean task for table
1,577✔
342
    await this.prismaService.txClient().task.deleteMany({
1,577✔
343
      where: {
1,577✔
344
        OR: tableIds.map((tableId) => ({
1,577✔
345
          snapshot: {
1,581✔
346
            contains: `"tableId":"${tableId}"`,
1,581✔
347
          },
1,581✔
348
        })),
1,581✔
349
      },
1,577✔
350
    });
1,577✔
351
  }
1,577✔
352

353
  async cleanReferenceFieldIds(tableIds: string[]) {
133✔
354
    const fields = await this.prismaService.txClient().field.findMany({
86✔
355
      where: { tableId: { in: tableIds }, type: { in: [FieldType.Link, FieldType.Formula] } },
86✔
356
      select: { id: true },
86✔
357
    });
86✔
358
    const fieldIds = fields.map((field) => field.id);
86✔
359
    await this.prismaService.txClient().reference.deleteMany({
86✔
360
      where: { OR: [{ fromFieldId: { in: fieldIds } }, { toFieldId: { in: fieldIds } }] },
86✔
361
    });
86✔
362
  }
86✔
363

364
  async cleanTablesRelatedData(baseId: string, tableIds: string[]) {
133✔
365
    // delete field for table
1,663✔
366
    await this.prismaService.txClient().field.deleteMany({
1,663✔
367
      where: { tableId: { in: tableIds } },
1,663✔
368
    });
1,663✔
369

370
    // delete view for table
1,663✔
371
    await this.prismaService.txClient().view.deleteMany({
1,663✔
372
      where: { tableId: { in: tableIds } },
1,663✔
373
    });
1,663✔
374

375
    // clean attachment for table
1,663✔
376
    await this.prismaService.txClient().attachmentsTable.deleteMany({
1,663✔
377
      where: { tableId: { in: tableIds } },
1,663✔
378
    });
1,663✔
379

380
    // clear ops for view/field/record
1,663✔
381
    await this.prismaService.txClient().ops.deleteMany({
1,663✔
382
      where: { collection: { in: tableIds } },
1,663✔
383
    });
1,663✔
384

385
    // clean ops for table
1,663✔
386
    await this.prismaService.txClient().ops.deleteMany({
1,663✔
387
      where: { collection: baseId, docId: { in: tableIds } },
1,663✔
388
    });
1,663✔
389

390
    await this.prismaService.txClient().tableMeta.deleteMany({
1,663✔
391
      where: { id: { in: tableIds } },
1,663✔
392
    });
1,663✔
393

394
    // clean record history for table
1,663✔
395
    await this.prismaService.txClient().recordHistory.deleteMany({
1,663✔
396
      where: { tableId: { in: tableIds } },
1,663✔
397
    });
1,663✔
398

399
    // clean trash for table
1,663✔
400
    await this.prismaService.txClient().trash.deleteMany({
1,663✔
401
      where: { resourceId: { in: tableIds }, resourceType: ResourceType.Table },
1,663✔
402
    });
1,663✔
403

404
    // clean table trash
1,663✔
405
    await this.prismaService.txClient().tableTrash.deleteMany({
1,663✔
406
      where: { tableId: { in: tableIds } },
1,663✔
407
    });
1,663✔
408

409
    // clean record trash
1,663✔
410
    await this.prismaService.txClient().recordTrash.deleteMany({
1,663✔
411
      where: { tableId: { in: tableIds } },
1,663✔
412
    });
1,663✔
413
  }
1,663✔
414

415
  async deleteTable(baseId: string, tableId: string) {
133✔
416
    try {
70✔
417
      await this.detachLink(tableId);
70✔
418
    } catch (e) {
70✔
419
      console.log(`Detach link error in table ${tableId}:`, e);
×
420
    }
×
421

422
    return await this.prismaService.$tx(
70✔
423
      async (prisma) => {
70✔
424
        const deletedTime = new Date();
70✔
425

426
        await this.tableService.deleteTable(baseId, tableId, deletedTime);
70✔
427

428
        await prisma.field.updateMany({
70✔
429
          where: { tableId, deletedTime: null },
70✔
430
          data: { deletedTime },
70✔
431
        });
70✔
432

433
        await prisma.view.updateMany({
70✔
434
          where: { tableId, deletedTime: null },
70✔
435
          data: { deletedTime },
70✔
436
        });
70✔
437
      },
70✔
438
      {
70✔
439
        timeout: this.thresholdConfig.bigTransactionTimeout,
70✔
440
      }
70✔
441
    );
442
  }
70✔
443

444
  async restoreTable(baseId: string, tableId: string) {
133✔
445
    return await this.prismaService.$tx(
2✔
446
      async (prisma) => {
2✔
447
        const { deletedTime } = await prisma.trash.findFirstOrThrow({
2✔
448
          where: { resourceId: tableId, resourceType: ResourceType.Table },
2✔
449
        });
2✔
450

451
        if (!deletedTime) {
2✔
452
          throw new ForbiddenException(
×
453
            'Unable to restore this table because it is not in the trash'
×
454
          );
455
        }
×
456

457
        await this.tableService.restoreTable(baseId, tableId);
2✔
458

459
        await prisma.field.updateMany({
2✔
460
          where: { tableId, deletedTime },
2✔
461
          data: { deletedTime: null },
2✔
462
        });
2✔
463

464
        await prisma.view.updateMany({
2✔
465
          where: { tableId, deletedTime },
2✔
466
          data: { deletedTime: null },
2✔
467
        });
2✔
468

469
        await prisma.trash.deleteMany({
2✔
470
          where: { resourceId: tableId },
2✔
471
        });
2✔
472
      },
2✔
473
      {
2✔
474
        timeout: this.thresholdConfig.bigTransactionTimeout,
2✔
475
      }
2✔
476
    );
477
  }
2✔
478

479
  async sqlQuery(tableId: string, viewId: string, sql: string) {
133✔
480
    this.logger.log('sqlQuery:sql: ' + sql);
×
481
    const { queryBuilder } = await this.recordService.buildFilterSortQuery(tableId, {
×
482
      viewId,
×
483
    });
×
484

485
    const baseQuery = queryBuilder.toString();
×
486
    const { dbTableName } = await this.prismaService.tableMeta.findFirstOrThrow({
×
487
      where: { id: tableId, deletedTime: null },
×
488
      select: { dbTableName: true },
×
489
    });
×
490

491
    const combinedQuery = `
×
492
      WITH base AS (${baseQuery})
×
493
      ${sql.replace(dbTableName, 'base')};
×
494
    `;
×
495
    this.logger.log('sqlQuery:sql:combine: ' + combinedQuery);
×
496

497
    return this.prismaService.$queryRawUnsafe(combinedQuery);
×
498
  }
×
499

500
  async updateName(baseId: string, tableId: string, name: string) {
133✔
501
    await this.prismaService.$tx(async () => {
2✔
502
      await this.tableService.updateTable(baseId, tableId, { name });
2✔
503
    });
2✔
504
  }
2✔
505

506
  async updateIcon(baseId: string, tableId: string, icon: string) {
133✔
507
    await this.prismaService.$tx(async () => {
2✔
508
      await this.tableService.updateTable(baseId, tableId, { icon });
2✔
509
    });
2✔
510
  }
2✔
511

512
  async updateDescription(baseId: string, tableId: string, description: string | null) {
133✔
513
    await this.prismaService.$tx(async () => {
2✔
514
      await this.tableService.updateTable(baseId, tableId, { description });
2✔
515
    });
2✔
516
  }
2✔
517

518
  async updateDbTableName(baseId: string, tableId: string, dbTableNameRo: string) {
133✔
519
    const dbTableName = this.dbProvider.joinDbTableName(baseId, dbTableNameRo);
4✔
520
    const existDbTableName = await this.prismaService.tableMeta
4✔
521
      .findFirst({
4✔
522
        where: { baseId, dbTableName, deletedTime: null },
4✔
523
        select: { id: true },
4✔
524
      })
4✔
525
      .catch(() => {
4✔
526
        throw new NotFoundException(`table ${tableId} not found`);
×
527
      });
×
528

529
    if (existDbTableName) {
4✔
530
      throw new BadRequestException(`dbTableName ${dbTableNameRo} already exists`);
×
531
    }
×
532

533
    const { dbTableName: oldDbTableName } = await this.prismaService.tableMeta
4✔
534
      .findFirstOrThrow({
4✔
535
        where: { id: tableId, baseId, deletedTime: null },
4✔
536
        select: { dbTableName: true },
4✔
537
      })
4✔
538
      .catch(() => {
4✔
539
        throw new NotFoundException(`table ${tableId} not found`);
×
540
      });
×
541

542
    const linkFieldsQuery = this.dbProvider.optionsQuery(
4✔
543
      FieldType.Link,
4✔
544
      'fkHostTableName',
4✔
545
      oldDbTableName
4✔
546
    );
547
    const lookupFieldsQuery = this.dbProvider.lookupOptionsQuery('fkHostTableName', oldDbTableName);
4✔
548

549
    await this.prismaService.$tx(async (prisma) => {
4✔
550
      const linkFieldsRaw =
4✔
551
        await this.prismaService.$queryRawUnsafe<{ id: string; options: string }[]>(
4✔
552
          linkFieldsQuery
4✔
553
        );
554
      const lookupFieldsRaw =
4✔
555
        await this.prismaService.$queryRawUnsafe<{ id: string; lookupOptions: string }[]>(
4✔
556
          lookupFieldsQuery
4✔
557
        );
558

559
      for (const field of linkFieldsRaw) {
4✔
560
        const options = JSON.parse(field.options as string) as ILinkFieldOptions;
8✔
561
        await prisma.field.update({
8✔
562
          where: { id: field.id },
8✔
563
          data: { options: JSON.stringify({ ...options, fkHostTableName: dbTableName }) },
8✔
564
        });
8✔
565
      }
8✔
566

567
      for (const field of lookupFieldsRaw) {
4✔
568
        const lookupOptions = JSON.parse(field.lookupOptions as string) as ILookupOptionsVo;
4✔
569
        await prisma.field.update({
4✔
570
          where: { id: field.id },
4✔
571
          data: {
4✔
572
            lookupOptions: JSON.stringify({
4✔
573
              ...lookupOptions,
4✔
574
              fkHostTableName: dbTableName,
4✔
575
            }),
4✔
576
          },
4✔
577
        });
4✔
578
      }
4✔
579

580
      await this.tableService.updateTable(baseId, tableId, { dbTableName });
4✔
581
      const renameSql = this.dbProvider.renameTableName(oldDbTableName, dbTableName);
4✔
582
      for (const sql of renameSql) {
4✔
583
        await prisma.$executeRawUnsafe(sql);
4✔
584
      }
4✔
585
    });
4✔
586
  }
4✔
587

588
  async shuffle(baseId: string) {
133✔
589
    const tables = await this.prismaService.tableMeta.findMany({
×
590
      where: { baseId, deletedTime: null },
×
591
      select: { id: true },
×
592
      orderBy: { order: 'asc' },
×
593
    });
×
594

595
    this.logger.log(`lucky table shuffle! ${baseId}`, 'shuffle');
×
596

597
    await this.prismaService.$tx(async () => {
×
598
      for (let i = 0; i < tables.length; i++) {
×
599
        const table = tables[i];
×
600
        await this.tableService.updateTable(baseId, table.id, { order: i });
×
601
      }
×
602
    });
×
603
  }
×
604

605
  async updateOrder(baseId: string, tableId: string, orderRo: IUpdateOrderRo) {
133✔
606
    const { anchorId, position } = orderRo;
8✔
607

608
    const table = await this.prismaService.tableMeta
8✔
609
      .findFirstOrThrow({
8✔
610
        select: { order: true, id: true },
8✔
611
        where: { baseId, id: tableId, deletedTime: null },
8✔
612
      })
8✔
613
      .catch(() => {
8✔
614
        throw new NotFoundException(`Table ${tableId} not found`);
×
615
      });
×
616

617
    const anchorTable = await this.prismaService.tableMeta
8✔
618
      .findFirstOrThrow({
8✔
619
        select: { order: true, id: true },
8✔
620
        where: { baseId, id: anchorId, deletedTime: null },
8✔
621
      })
8✔
622
      .catch(() => {
8✔
623
        throw new NotFoundException(`Anchor ${anchorId} not found`);
×
624
      });
×
625

626
    const tablesOrder = await this.prismaService.txClient().tableMeta.findMany({
8✔
627
      where: {
8✔
628
        baseId,
8✔
629
        deletedTime: null,
8✔
630
      },
8✔
631
      select: {
8✔
632
        order: true,
8✔
633
      },
8✔
634
    });
8✔
635

636
    const uniqOrder = [...new Set(tablesOrder.map((t) => t.order))];
8✔
637

638
    // if the table order has the same order, should shuffle
8✔
639
    const shouldShuffle = uniqOrder.length !== tablesOrder.length;
8✔
640

641
    await updateOrder({
8✔
642
      query: baseId,
8✔
643
      position,
8✔
644
      item: table,
8✔
645
      anchorItem: anchorTable,
8✔
646
      getNextItem: async (whereOrder, align) => {
8✔
647
        return this.prismaService.tableMeta.findFirst({
8✔
648
          select: { order: true, id: true },
8✔
649
          where: {
8✔
650
            baseId,
8✔
651
            deletedTime: null,
8✔
652
            order: whereOrder,
8✔
653
          },
8✔
654
          orderBy: { order: align },
8✔
655
        });
8✔
656
      },
8✔
657
      update: async (
8✔
658
        parentId: string,
8✔
659
        id: string,
8✔
660
        data: { newOrder: number; oldOrder: number }
8✔
661
      ) => {
662
        await this.prismaService.$tx(async () => {
8✔
663
          await this.tableService.updateTable(parentId, id, { order: data.newOrder });
8✔
664
        });
8✔
665
      },
8✔
666
      shuffle: this.shuffle.bind(this),
8✔
667
      shouldShuffle,
8✔
668
    });
8✔
669
  }
8✔
670

671
  async getPermission(baseId: string, tableId: string): Promise<ITablePermissionVo> {
133✔
672
    let role: IRole | null = await this.permissionService.getRoleByBaseId(baseId);
×
673
    if (!role) {
×
674
      const { spaceId } = await this.permissionService.getUpperIdByBaseId(baseId);
×
675
      role = await this.permissionService.getRoleBySpaceId(spaceId);
×
676
    }
×
677
    if (!role) {
×
678
      throw new NotFoundException(`Role not found`);
×
679
    }
×
680
    return this.getPermissionByRole(tableId, role);
×
681
  }
×
682

683
  async getPermissionByRole(tableId: string, role: IRole) {
133✔
684
    const permissionMap = getBasePermission(role);
×
685
    const tablePermission = actionPrefixMap[ActionPrefix.Table].reduce(
×
686
      (acc, action) => {
×
687
        acc[action] = permissionMap[action];
×
688
        return acc;
×
689
      },
×
690
      {} as Record<TableAction, boolean>
×
691
    );
692
    const viewPermission = actionPrefixMap[ActionPrefix.View].reduce(
×
693
      (acc, action) => {
×
694
        acc[action] = permissionMap[action];
×
695
        return acc;
×
696
      },
×
697
      {} as Record<ViewAction, boolean>
×
698
    );
699

700
    const recordPermission = actionPrefixMap[ActionPrefix.Record].reduce(
×
701
      (acc, action) => {
×
702
        acc[action] = permissionMap[action];
×
703
        return acc;
×
704
      },
×
705
      {} as Record<RecordAction, boolean>
×
706
    );
707

708
    const fieldPermission = actionPrefixMap[ActionPrefix.Field].reduce(
×
709
      (acc, action) => {
×
710
        acc[action] = permissionMap[action];
×
711
        return acc;
×
712
      },
×
713
      {} as Record<FieldAction, boolean>
×
714
    );
715

716
    return {
×
717
      table: tablePermission,
×
718
      field: fieldPermission,
×
719
      record: recordPermission,
×
720
      view: viewPermission,
×
721
    };
×
722
  }
×
723
}
133✔
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