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

teableio / teable / 8537011228

03 Apr 2024 10:15AM CUT coverage: 82.825% (+61.3%) from 21.535%
8537011228

Pull #514

github

web-flow
Merge fe52186b9 into 45ee7ebb3
Pull Request #514: refactor: user and link selector

4033 of 4226 branches covered (95.43%)

343 of 372 new or added lines in 8 files covered. (92.2%)

26958 of 32548 relevant lines covered (82.83%)

1213.2 hits per line

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

72.38
/apps/nestjs-backend/src/features/view/open-api/view-open-api.service.ts
1
import {
4✔
2
  BadRequestException,
4✔
3
  Injectable,
4✔
4
  Logger,
4✔
5
  NotFoundException,
4✔
6
  ForbiddenException,
4✔
7
} from '@nestjs/common';
4✔
8
import type {
4✔
9
  IFieldVo,
4✔
10
  IOtOperation,
4✔
11
  IViewRo,
4✔
12
  IViewVo,
4✔
13
  IColumnMetaRo,
4✔
14
  IViewPropertyKeys,
4✔
15
  IViewOptions,
4✔
16
  IGridColumnMeta,
4✔
17
  IFilter,
4✔
18
  IFilterItem,
4✔
19
  ILinkFieldOptions,
4✔
20
} from '@teable/core';
4✔
21
import {
4✔
22
  ViewType,
4✔
23
  IManualSortRo,
4✔
24
  ViewOpBuilder,
4✔
25
  generateShareId,
4✔
26
  VIEW_JSON_KEYS,
4✔
27
  validateOptionsType,
4✔
28
  FieldType,
4✔
29
  IdPrefix,
4✔
30
} from '@teable/core';
4✔
31
import { PrismaService } from '@teable/db-main-prisma';
4✔
32
import type {
4✔
33
  IGetViewFilterLinkRecordsVo,
4✔
34
  IUpdateOrderRo,
4✔
35
  IUpdateRecordOrdersRo,
4✔
36
} from '@teable/openapi';
4✔
37
import { Knex } from 'knex';
4✔
38
import { InjectModel } from 'nest-knexjs';
4✔
39
import { Timing } from '../../../utils/timing';
4✔
40
import { updateMultipleOrders, updateOrder } from '../../../utils/update-order';
4✔
41
import { FieldService } from '../../field/field.service';
4✔
42
import { createFieldInstanceByRaw } from '../../field/model/factory';
4✔
43
import { RecordService } from '../../record/record.service';
4✔
44
import { ViewService } from '../view.service';
4✔
45

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

148✔
50
  constructor(
148✔
51
    private readonly prismaService: PrismaService,
148✔
52
    private readonly recordService: RecordService,
148✔
53
    private readonly viewService: ViewService,
148✔
54
    private readonly fieldService: FieldService,
148✔
55
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
148✔
56
  ) {}
148✔
57

148✔
58
  async createView(tableId: string, viewRo: IViewRo) {
148✔
59
    return await this.prismaService.$tx(async () => {
1,080✔
60
      return await this.createViewInner(tableId, viewRo);
1,080✔
61
    });
1,080✔
62
  }
1,080✔
63

148✔
64
  async deleteView(tableId: string, viewId: string) {
148✔
65
    return await this.prismaService.$tx(async () => {
×
66
      return await this.deleteViewInner(tableId, viewId);
×
67
    });
×
68
  }
×
69

148✔
70
  private async createViewInner(tableId: string, viewRo: IViewRo): Promise<IViewVo> {
148✔
71
    return await this.viewService.createView(tableId, viewRo);
1,080✔
72
  }
1,080✔
73

148✔
74
  private async deleteViewInner(tableId: string, viewId: string) {
148✔
75
    await this.viewService.deleteView(tableId, viewId);
×
76
  }
×
77

148✔
78
  private updateRecordOrderSql(orderRawSql: string, dbTableName: string, indexField: string) {
148✔
79
    return this.knex
×
80
      .raw(
×
81
        `
×
82
        UPDATE :dbTableName:
×
83
        SET :indexField: = temp_order.new_order
×
84
        FROM (
×
85
          SELECT __id, ROW_NUMBER() OVER (ORDER BY ${orderRawSql}) AS new_order FROM :dbTableName:
×
86
        ) AS temp_order
×
87
        WHERE :dbTableName:.__id = temp_order.__id AND :dbTableName:.:indexField: != temp_order.new_order;
×
88
      `,
×
89
        {
×
90
          dbTableName,
×
91
          indexField,
×
92
        }
×
93
      )
×
94
      .toQuery();
×
95
  }
×
96

148✔
97
  @Timing()
148✔
98
  async manualSort(tableId: string, viewId: string, viewOrderRo: IManualSortRo) {
×
99
    const { sortObjs } = viewOrderRo;
×
100
    const dbTableName = await this.recordService.getDbTableName(tableId);
×
101
    const fields = await this.fieldService.getFieldsByQuery(tableId, { viewId });
×
102
    const indexField = await this.viewService.getOrCreateViewIndexField(dbTableName, viewId);
×
103

×
104
    const fieldMap = fields.reduce(
×
105
      (map, field) => {
×
106
        map[field.id] = field;
×
107
        return map;
×
108
      },
×
109
      {} as Record<string, IFieldVo>
×
110
    );
×
111

×
112
    let orderRawSql = sortObjs
×
113
      .map((sort) => {
×
114
        const { fieldId, order } = sort;
×
115

×
116
        const field = fieldMap[fieldId];
×
117
        if (!field) {
×
118
          return;
×
119
        }
×
120

×
121
        const column =
×
122
          field.dbFieldType === 'JSON'
×
123
            ? this.knex.raw(`CAST(?? as text)`, [field.dbFieldName]).toQuery()
×
124
            : this.knex.ref(field.dbFieldName).toQuery();
×
125

×
126
        const nulls = order.toUpperCase() === 'ASC' ? 'FIRST' : 'LAST';
×
127

×
128
        return `${column} ${order} NULLS ${nulls}`;
×
129
      })
×
130
      .join();
×
131

×
132
    // ensure order stable
×
133
    orderRawSql += this.knex.raw(`, ?? ASC`, ['__auto_number']).toQuery();
×
134

×
135
    // build ops
×
136
    const newSort = {
×
137
      sortObjs: sortObjs,
×
138
      manualSort: true,
×
139
    };
×
140

×
141
    await this.prismaService.$tx(async (prisma) => {
×
142
      await prisma.$executeRawUnsafe(
×
143
        this.updateRecordOrderSql(orderRawSql, dbTableName, indexField)
×
144
      );
×
145
      await this.viewService.updateViewSort(tableId, viewId, newSort);
×
146
    });
×
147
  }
×
148

148✔
149
  async updateViewColumnMeta(tableId: string, viewId: string, columnMetaRo: IColumnMetaRo) {
148✔
150
    const view = await this.prismaService.view
26✔
151
      .findFirstOrThrow({
26✔
152
        where: { tableId, id: viewId },
26✔
153
        select: {
26✔
154
          columnMeta: true,
26✔
155
          version: true,
26✔
156
          id: true,
26✔
157
          type: true,
26✔
158
        },
26✔
159
      })
26✔
160
      .catch(() => {
26✔
161
        throw new BadRequestException('view found column meta error');
×
162
      });
×
163

26✔
164
    // validate field legal
26✔
165
    const fields = await this.prismaService.field.findMany({
26✔
166
      where: { tableId, deletedTime: null },
26✔
167
      select: {
26✔
168
        id: true,
26✔
169
        isPrimary: true,
26✔
170
      },
26✔
171
    });
26✔
172
    const primaryFields = fields.filter((field) => field.isPrimary).map((field) => field.id);
26✔
173

26✔
174
    const isHiddenPrimaryField = columnMetaRo.some(
26✔
175
      (f) => primaryFields.includes(f.fieldId) && (f.columnMeta as IGridColumnMeta).hidden
26✔
176
    );
26✔
177
    const fieldIds = columnMetaRo.map(({ fieldId }) => fieldId);
26✔
178

26✔
179
    if (!fieldIds.every((id) => fields.map(({ id }) => id).includes(id))) {
26✔
180
      throw new BadRequestException('field is not found in table');
×
181
    }
×
182

26✔
183
    const allowHiddenPrimaryType = [ViewType.Calendar, ViewType.Form];
26✔
184
    /**
26✔
185
     * validate whether hidden primary field
26✔
186
     * only form view or list view(todo) can hidden primary field
26✔
187
     */
26✔
188
    if (isHiddenPrimaryField && !allowHiddenPrimaryType.includes(view.type as ViewType)) {
26✔
189
      throw new ForbiddenException('primary field can not be hidden');
2✔
190
    }
2✔
191

24✔
192
    const curColumnMeta = JSON.parse(view.columnMeta);
24✔
193
    const ops: IOtOperation[] = [];
24✔
194

24✔
195
    columnMetaRo.forEach(({ fieldId, columnMeta }) => {
24✔
196
      const obj = {
24✔
197
        fieldId,
24✔
198
        newColumnMeta: { ...curColumnMeta[fieldId], ...columnMeta },
24✔
199
        oldColumnMeta: curColumnMeta[fieldId] ? curColumnMeta[fieldId] : undefined,
24✔
200
      };
24✔
201
      ops.push(ViewOpBuilder.editor.updateViewColumnMeta.build(obj));
24✔
202
    });
24✔
203
    await this.prismaService.$tx(async () => {
24✔
204
      await this.viewService.updateViewByOps(tableId, viewId, ops);
24✔
205
    });
24✔
206
  }
24✔
207

148✔
208
  async setViewProperty(
148✔
209
    tableId: string,
92✔
210
    viewId: string,
92✔
211
    key: IViewPropertyKeys,
92✔
212
    newValue: unknown
92✔
213
  ) {
92✔
214
    const curView = await this.prismaService.view
92✔
215
      .findFirstOrThrow({
92✔
216
        select: { [key]: true },
92✔
217
        where: { tableId, id: viewId, deletedTime: null },
92✔
218
      })
92✔
219
      .catch(() => {
92✔
220
        throw new BadRequestException('View not found');
×
221
      });
×
222
    const oldValue =
92✔
223
      curView[key] != null && VIEW_JSON_KEYS.includes(key)
92✔
224
        ? JSON.parse(curView[key])
74✔
225
        : curView[key];
92✔
226
    const ops = ViewOpBuilder.editor.setViewProperty.build({
92✔
227
      key,
92✔
228
      newValue,
92✔
229
      oldValue,
92✔
230
    });
92✔
231
    await this.prismaService.$tx(async () => {
92✔
232
      await this.viewService.updateViewByOps(tableId, viewId, [ops]);
92✔
233
    });
92✔
234
  }
92✔
235

148✔
236
  async patchViewOptions(tableId: string, viewId: string, viewOptions: IViewOptions) {
148✔
237
    const curView = await this.prismaService.view
10✔
238
      .findFirstOrThrow({
10✔
239
        select: { options: true, type: true },
10✔
240
        where: { tableId, id: viewId, deletedTime: null },
10✔
241
      })
10✔
242
      .catch(() => {
10✔
243
        throw new BadRequestException('View option not found');
×
244
      });
×
245
    const { options, type: viewType } = curView;
10✔
246

10✔
247
    // validate option type
10✔
248
    try {
10✔
249
      validateOptionsType(viewType as ViewType, viewOptions);
10✔
250
    } catch (err) {
10✔
251
      throw new BadRequestException(err);
2✔
252
    }
2✔
253

8✔
254
    const oldOptions = options ? JSON.parse(options) : options;
10✔
255
    const ops = ViewOpBuilder.editor.setViewProperty.build({
10✔
256
      key: 'options',
10✔
257
      newValue: {
10✔
258
        ...oldOptions,
10✔
259
        ...viewOptions,
10✔
260
      },
10✔
261
      oldValue: oldOptions,
10✔
262
    });
10✔
263
    await this.prismaService.$tx(async () => {
10✔
264
      await this.viewService.updateViewByOps(tableId, viewId, [ops]);
8✔
265
    });
8✔
266
  }
8✔
267

148✔
268
  /**
148✔
269
   * shuffle view order
148✔
270
   */
148✔
271
  async shuffle(tableId: string) {
148✔
272
    const views = await this.prismaService.view.findMany({
×
273
      where: { tableId, deletedTime: null },
×
274
      select: { id: true, order: true },
×
275
      orderBy: { order: 'asc' },
×
276
    });
×
277

×
278
    this.logger.log(`lucky view shuffle! ${tableId}`, 'shuffle');
×
279

×
280
    await this.prismaService.$tx(async () => {
×
281
      for (let i = 0; i < views.length; i++) {
×
282
        const view = views[i];
×
283
        await this.viewService.updateViewByOps(tableId, view.id, [
×
284
          ViewOpBuilder.editor.setViewProperty.build({
×
285
            key: 'order',
×
286
            newValue: i,
×
287
            oldValue: view.order,
×
288
          }),
×
289
        ]);
×
290
      }
×
291
    });
×
292
  }
×
293

148✔
294
  async updateViewOrder(tableId: string, viewId: string, orderRo: IUpdateOrderRo) {
148✔
295
    const { anchorId, position } = orderRo;
8✔
296

8✔
297
    const view = await this.prismaService.view
8✔
298
      .findFirstOrThrow({
8✔
299
        select: { order: true, id: true },
8✔
300
        where: { tableId, id: viewId, deletedTime: null },
8✔
301
      })
8✔
302
      .catch(() => {
8✔
303
        throw new NotFoundException(`View ${viewId} not found in the table`);
×
304
      });
×
305

8✔
306
    const anchorView = await this.prismaService.view
8✔
307
      .findFirstOrThrow({
8✔
308
        select: { order: true, id: true },
8✔
309
        where: { tableId, id: anchorId, deletedTime: null },
8✔
310
      })
8✔
311
      .catch(() => {
8✔
312
        throw new NotFoundException(`Anchor ${anchorId} not found in the table`);
×
313
      });
×
314

8✔
315
    await updateOrder({
8✔
316
      parentId: tableId,
8✔
317
      position,
8✔
318
      item: view,
8✔
319
      anchorItem: anchorView,
8✔
320
      getNextItem: async (whereOrder, align) => {
8✔
321
        return this.prismaService.view.findFirst({
8✔
322
          select: { order: true, id: true },
8✔
323
          where: {
8✔
324
            tableId,
8✔
325
            deletedTime: null,
8✔
326
            order: whereOrder,
8✔
327
          },
8✔
328
          orderBy: { order: align },
8✔
329
        });
8✔
330
      },
8✔
331
      update: async (
8✔
332
        parentId: string,
8✔
333
        id: string,
8✔
334
        data: { newOrder: number; oldOrder: number }
8✔
335
      ) => {
8✔
336
        const ops = ViewOpBuilder.editor.setViewProperty.build({
8✔
337
          key: 'order',
8✔
338
          newValue: data.newOrder,
8✔
339
          oldValue: data.oldOrder,
8✔
340
        });
8✔
341

8✔
342
        await this.prismaService.$tx(async () => {
8✔
343
          await this.viewService.updateViewByOps(parentId, id, [ops]);
8✔
344
        });
8✔
345
      },
8✔
346
      shuffle: this.shuffle.bind(this),
8✔
347
    });
8✔
348
  }
8✔
349

148✔
350
  /**
148✔
351
   * shuffle record order
148✔
352
   */
148✔
353
  async shuffleRecords(dbTableName: string, indexField: string) {
148✔
354
    const recordCount = await this.recordService.getAllRecordCount(dbTableName);
×
355
    if (recordCount > 100_000) {
×
356
      throw new BadRequestException('Not enough gap to move the row here');
×
357
    }
×
358

×
359
    const sql = this.updateRecordOrderSql(
×
360
      this.knex.raw(`?? ASC`, [indexField]).toQuery(),
×
361
      dbTableName,
×
362
      indexField
×
363
    );
×
364

×
365
    await this.prismaService.$executeRawUnsafe(sql);
×
366
  }
×
367

148✔
368
  async updateRecordOrdersInner(props: {
148✔
369
    tableId: string;
14✔
370
    dbTableName: string;
14✔
371
    itemLength: number;
14✔
372
    indexField: string;
14✔
373
    orderRo: {
14✔
374
      anchorId: string;
14✔
375
      position: 'before' | 'after';
14✔
376
    };
14✔
377
    update: (indexes: number[]) => Promise<void>;
14✔
378
  }) {
14✔
379
    const { tableId, itemLength, dbTableName, indexField, orderRo, update } = props;
14✔
380
    const { anchorId, position } = orderRo;
14✔
381

14✔
382
    const anchorRecordSql = this.knex(dbTableName)
14✔
383
      .select({
14✔
384
        id: '__id',
14✔
385
        order: indexField,
14✔
386
      })
14✔
387
      .where('__id', anchorId)
14✔
388
      .toQuery();
14✔
389

14✔
390
    const anchorRecord = await this.prismaService
14✔
391
      .txClient()
14✔
392
      .$queryRawUnsafe<{ id: string; order: number }[]>(anchorRecordSql)
14✔
393
      .then((res) => {
14✔
394
        return res[0];
14✔
395
      });
14✔
396

14✔
397
    if (!anchorRecord) {
14✔
398
      throw new NotFoundException(`Anchor ${anchorId} not found in the table`);
×
399
    }
×
400

14✔
401
    await updateMultipleOrders({
14✔
402
      parentId: tableId,
14✔
403
      position,
14✔
404
      itemLength,
14✔
405
      anchorItem: anchorRecord,
14✔
406
      getNextItem: async (whereOrder, align) => {
14✔
407
        const nextRecordSql = this.knex(dbTableName)
14✔
408
          .select({
14✔
409
            id: '__id',
14✔
410
            order: indexField,
14✔
411
          })
14✔
412
          .where(
14✔
413
            indexField,
14✔
414
            whereOrder.lt != null ? '<' : '>',
14✔
415
            (whereOrder.lt != null ? whereOrder.lt : whereOrder.gt) as number
14✔
416
          )
14✔
417
          .orderBy(indexField, align)
14✔
418
          .limit(1)
14✔
419
          .toQuery();
14✔
420
        return this.prismaService
14✔
421
          .txClient()
14✔
422
          .$queryRawUnsafe<{ id: string; order: number }[]>(nextRecordSql)
14✔
423
          .then((res) => {
14✔
424
            return res[0];
14✔
425
          });
14✔
426
      },
14✔
427
      update,
14✔
428
      shuffle: async () => {
14✔
429
        await this.shuffleRecords(dbTableName, indexField);
×
430
      },
×
431
    });
14✔
432
  }
14✔
433

148✔
434
  async updateRecordOrders(tableId: string, viewId: string, orderRo: IUpdateRecordOrdersRo) {
148✔
435
    const dbTableName = await this.recordService.getDbTableName(tableId);
8✔
436

8✔
437
    const indexField = await this.viewService.getOrCreateViewIndexField(dbTableName, viewId);
8✔
438
    const recordIds = orderRo.recordIds;
8✔
439

8✔
440
    await this.updateRecordOrdersInner({
8✔
441
      tableId,
8✔
442
      dbTableName,
8✔
443
      itemLength: recordIds.length,
8✔
444
      indexField,
8✔
445
      orderRo,
8✔
446
      update: async (indexes) => {
8✔
447
        // for notify view update only
8✔
448
        const ops = ViewOpBuilder.editor.setViewProperty.build({
8✔
449
          key: 'lastModifiedTime',
8✔
450
          newValue: new Date().toISOString(),
8✔
451
        });
8✔
452

8✔
453
        await this.prismaService.$tx(async (prisma) => {
8✔
454
          await this.viewService.updateViewByOps(tableId, viewId, [ops]);
8✔
455
          for (let i = 0; i < recordIds.length; i++) {
8✔
456
            const recordId = recordIds[i];
14✔
457
            const updateRecordSql = this.knex(dbTableName)
14✔
458
              .update({
14✔
459
                [indexField]: indexes[i],
14✔
460
              })
14✔
461
              .where('__id', recordId)
14✔
462
              .toQuery();
14✔
463
            await prisma.$executeRawUnsafe(updateRecordSql);
14✔
464
          }
14✔
465
        });
8✔
466
      },
8✔
467
    });
8✔
468
  }
8✔
469

148✔
470
  async refreshShareId(tableId: string, viewId: string) {
148✔
471
    const view = await this.prismaService.view.findUnique({
×
472
      where: { id: viewId, tableId, deletedTime: null },
×
473
      select: { shareId: true, enableShare: true },
×
474
    });
×
475
    if (!view) {
×
476
      throw new NotFoundException(`View ${viewId} does not exist`);
×
477
    }
×
478
    const { enableShare } = view;
×
479
    if (!enableShare) {
×
480
      throw new BadRequestException(`View ${viewId} has not been enabled share`);
×
481
    }
×
482
    const newShareId = generateShareId();
×
483
    const setShareIdOp = ViewOpBuilder.editor.setViewProperty.build({
×
484
      key: 'shareId',
×
485
      newValue: newShareId,
×
486
      oldValue: view.shareId || undefined,
×
487
    });
×
488
    await this.prismaService.$tx(async () => {
×
489
      await this.viewService.updateViewByOps(tableId, viewId, [setShareIdOp]);
×
490
    });
×
491
    return { shareId: newShareId };
×
492
  }
×
493

148✔
494
  async enableShare(tableId: string, viewId: string) {
148✔
495
    const view = await this.prismaService.view.findUnique({
14✔
496
      where: { id: viewId, tableId, deletedTime: null },
14✔
497
      select: { shareId: true, enableShare: true },
14✔
498
    });
14✔
499
    if (!view) {
14✔
500
      throw new NotFoundException(`View ${viewId} does not exist`);
×
501
    }
×
502
    const { enableShare, shareId } = view;
14✔
503
    if (enableShare) {
14✔
504
      throw new BadRequestException(`View ${viewId} has already been enabled share`);
×
505
    }
×
506
    const newShareId = generateShareId();
14✔
507
    const enableShareOp = ViewOpBuilder.editor.setViewProperty.build({
14✔
508
      key: 'enableShare',
14✔
509
      newValue: true,
14✔
510
      oldValue: enableShare || undefined,
14✔
511
    });
14✔
512
    const setShareIdOp = ViewOpBuilder.editor.setViewProperty.build({
14✔
513
      key: 'shareId',
14✔
514
      newValue: newShareId,
14✔
515
      oldValue: shareId || undefined,
14✔
516
    });
14✔
517
    await this.prismaService.$tx(async () => {
14✔
518
      await this.viewService.updateViewByOps(tableId, viewId, [enableShareOp, setShareIdOp]);
14✔
519
    });
14✔
520
    return { shareId: newShareId };
14✔
521
  }
14✔
522

148✔
523
  async disableShare(tableId: string, viewId: string) {
148✔
524
    const view = await this.prismaService.view.findUnique({
×
525
      where: { id: viewId, tableId, deletedTime: null },
×
526
      select: { shareId: true, enableShare: true, shareMeta: true },
×
527
    });
×
528
    if (!view) {
×
529
      throw new NotFoundException(`View ${viewId} does not exist`);
×
530
    }
×
531
    const { enableShare } = view;
×
532
    if (!enableShare) {
×
533
      throw new BadRequestException(`View ${viewId} has already been disable share`);
×
534
    }
×
535
    const enableShareOp = ViewOpBuilder.editor.setViewProperty.build({
×
536
      key: 'enableShare',
×
537
      newValue: false,
×
538
      oldValue: enableShare || undefined,
×
539
    });
×
540

×
541
    await this.prismaService.$tx(async () => {
×
542
      await this.viewService.updateViewByOps(tableId, viewId, [enableShareOp]);
×
543
    });
×
544
  }
×
545

148✔
546
  /**
148✔
547
   * @param linkFields {fieldId: foreignTableId}
148✔
548
   * @returns {foreignTableId: Set<recordId>}
148✔
549
   */
148✔
550
  private async collectFilterLinkFieldRecords(
148✔
551
    linkFields: Record<string, string>,
6✔
552
    filter?: IFilter
6✔
553
  ) {
6✔
554
    if (!filter || !filter.filterSet) {
6✔
NEW
555
      return undefined;
×
NEW
556
    }
×
557

6✔
558
    const tableRecordMap: Record<string, Set<string>> = {};
6✔
559

6✔
560
    const mergeRecordMap = (source: Record<string, Set<string>> = {}) => {
6✔
561
      for (const [fieldId, recordSet] of Object.entries(source)) {
12✔
562
        tableRecordMap[fieldId] = tableRecordMap[fieldId] || new Set();
12✔
563
        recordSet.forEach((item) => tableRecordMap[fieldId].add(item));
12✔
564
      }
12✔
565
    };
12✔
566

6✔
567
    for (const filterItem of filter.filterSet) {
6✔
568
      if ('filterSet' in filterItem) {
12✔
569
        const groupTableRecordMap = await this.collectFilterLinkFieldRecords(
4✔
570
          linkFields,
4✔
571
          filterItem as IFilter
4✔
572
        );
4✔
573
        if (groupTableRecordMap) {
4✔
574
          mergeRecordMap(groupTableRecordMap);
4✔
575
        }
4✔
576
        continue;
4✔
577
      }
4✔
578

8✔
579
      const { value, fieldId } = filterItem as IFilterItem;
8✔
580

8✔
581
      const foreignTableId = linkFields[fieldId];
8✔
582
      if (!foreignTableId) {
12✔
NEW
583
        continue;
×
NEW
584
      }
✔
585

8✔
586
      if (Array.isArray(value)) {
12✔
587
        mergeRecordMap({ [foreignTableId]: new Set(value as string[]) });
4✔
588
      } else if (typeof value === 'string' && value.startsWith(IdPrefix.Record)) {
4✔
589
        mergeRecordMap({ [foreignTableId]: new Set([value]) });
4✔
590
      }
4✔
591
    }
12✔
592

6✔
593
    return tableRecordMap;
6✔
594
  }
6✔
595

148✔
596
  async getFilterLinkRecords(tableId: string, viewId: string) {
148✔
597
    const view = await this.viewService.getViewById(viewId);
2✔
598
    const linkFields = await this.prismaService.field.findMany({
2✔
599
      where: { tableId, deletedTime: null, type: FieldType.Link, options: { not: null } },
2✔
600
    });
2✔
601
    const linkFieldTableMap = linkFields.reduce(
2✔
602
      (map, field) => {
2✔
603
        const { foreignTableId } = JSON.parse(field.options as string) as ILinkFieldOptions;
4✔
604
        map[field.id] = foreignTableId;
4✔
605
        return map;
4✔
606
      },
4✔
607
      {} as Record<string, string>
2✔
608
    );
2✔
609
    const tableRecordMap = await this.collectFilterLinkFieldRecords(linkFieldTableMap, view.filter);
2✔
610

2✔
611
    if (!tableRecordMap) {
2✔
NEW
612
      return [];
×
NEW
613
    }
×
614
    const res: IGetViewFilterLinkRecordsVo = [];
2✔
615
    for (const [foreignTableId, recordSet] of Object.entries(tableRecordMap)) {
2✔
616
      const dbTableName = await this.recordService.getDbTableName(foreignTableId);
4✔
617
      const primaryField = await this.prismaService.field.findFirst({
4✔
618
        where: { tableId: foreignTableId, isPrimary: true, deletedTime: null },
4✔
619
      });
4✔
620
      if (!primaryField) {
4✔
NEW
621
        continue;
×
NEW
622
      }
×
623

4✔
624
      const dbFieldName = primaryField.dbFieldName;
4✔
625

4✔
626
      const nativeQuery = this.knex(dbTableName)
4✔
627
        .select('__id as id', `${dbFieldName} as title`)
4✔
628
        .orderBy('__auto_number')
4✔
629
        .whereIn('__id', Array.from(recordSet))
4✔
630
        .toQuery();
4✔
631

4✔
632
      const list = await this.prismaService
4✔
633
        .txClient()
4✔
634
        .$queryRawUnsafe<{ id: string; title: string | null }[]>(nativeQuery);
4✔
635
      const fieldInstances = createFieldInstanceByRaw(primaryField);
4✔
636
      res.push({
4✔
637
        tableId: foreignTableId,
4✔
638
        records: list.map(({ id, title }) => ({
4✔
639
          id,
10✔
640
          title:
10✔
641
            fieldInstances.cellValue2String(fieldInstances.convertDBValue2CellValue(title)) ||
10✔
NEW
642
            undefined,
×
643
        })),
10✔
644
      });
4✔
645
    }
4✔
646
    return res;
2✔
647
  }
2✔
648
}
148✔
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