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

teableio / teable / 11344146875

15 Oct 2024 10:25AM UTC coverage: 84.698%. First build
11344146875

Pull #986

github

web-flow
Merge f9f249562 into 7794225a9
Pull Request #986: feat: support excel form view

5741 of 6049 branches covered (94.91%)

54 of 55 new or added lines in 5 files covered. (98.18%)

37881 of 44725 relevant lines covered (84.7%)

1623.86 hits per line

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

70.52
/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
  IOtOperation,
4✔
10
  IViewRo,
4✔
11
  IViewVo,
4✔
12
  IColumnMetaRo,
4✔
13
  IViewPropertyKeys,
4✔
14
  IViewOptions,
4✔
15
  IGridColumnMeta,
4✔
16
  IFilter,
4✔
17
  IFilterItem,
4✔
18
  ILinkFieldOptions,
4✔
19
  IPluginViewOptions,
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
  generatePluginInstallId,
4✔
31
} from '@teable/core';
4✔
32
import { PrismaService } from '@teable/db-main-prisma';
4✔
33
import { PluginPosition, PluginStatus } from '@teable/openapi';
4✔
34
import type {
4✔
35
  IViewPluginUpdateStorageRo,
4✔
36
  IGetViewFilterLinkRecordsVo,
4✔
37
  IUpdateOrderRo,
4✔
38
  IUpdateRecordOrdersRo,
4✔
39
  IViewInstallPluginRo,
4✔
40
  IViewShareMetaRo,
4✔
41
} from '@teable/openapi';
4✔
42
import { Knex } from 'knex';
4✔
43
import { InjectModel } from 'nest-knexjs';
4✔
44
import { ClsService } from 'nestjs-cls';
4✔
45
import { InjectDbProvider } from '../../../db-provider/db.provider';
4✔
46
import { IDbProvider } from '../../../db-provider/db.provider.interface';
4✔
47
import { EventEmitterService } from '../../../event-emitter/event-emitter.service';
4✔
48
import { Events } from '../../../event-emitter/events';
4✔
49
import type { IClsStore } from '../../../types/cls';
4✔
50
import { Timing } from '../../../utils/timing';
4✔
51
import { updateMultipleOrders, updateOrder } from '../../../utils/update-order';
4✔
52
import { FieldService } from '../../field/field.service';
4✔
53
import type { IFieldInstance } from '../../field/model/factory';
4✔
54
import { createFieldInstanceByRaw, createFieldInstanceByVo } from '../../field/model/factory';
4✔
55
import { RecordService } from '../../record/record.service';
4✔
56
import { createViewInstanceByRaw } from '../model/factory';
4✔
57
import { ViewService } from '../view.service';
4✔
58

4✔
59
@Injectable()
4✔
60
export class ViewOpenApiService {
4✔
61
  private logger = new Logger(ViewOpenApiService.name);
181✔
62

181✔
63
  constructor(
181✔
64
    private readonly prismaService: PrismaService,
181✔
65
    private readonly recordService: RecordService,
181✔
66
    private readonly viewService: ViewService,
181✔
67
    private readonly fieldService: FieldService,
181✔
68
    private readonly eventEmitterService: EventEmitterService,
181✔
69
    private readonly cls: ClsService<IClsStore>,
181✔
70
    @InjectDbProvider() private readonly dbProvider: IDbProvider,
181✔
71
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex
181✔
72
  ) {}
181✔
73

181✔
74
  async createView(tableId: string, viewRo: IViewRo) {
181✔
75
    if (viewRo.type === ViewType.Plugin) {
1,373✔
76
      const res = await this.pluginInstall(tableId, {
×
77
        name: viewRo.name,
×
78
        pluginId: (viewRo.options as IPluginViewOptions).pluginId,
×
79
      });
×
80
      return this.viewService.getViewById(res.viewId);
×
81
    }
×
82
    return await this.prismaService.$tx(async () => {
1,373✔
83
      return this.createViewInner(tableId, viewRo);
1,373✔
84
    });
1,373✔
85
  }
1,373✔
86

181✔
87
  async deleteView(tableId: string, viewId: string) {
181✔
88
    return await this.prismaService.$tx(async () => {
6✔
89
      return await this.deleteViewInner(tableId, viewId);
6✔
90
    });
6✔
91
  }
6✔
92

181✔
93
  private async createViewInner(tableId: string, viewRo: IViewRo): Promise<IViewVo> {
181✔
94
    return await this.viewService.createView(tableId, viewRo);
1,373✔
95
  }
1,373✔
96

181✔
97
  private async deleteViewInner(tableId: string, viewId: string) {
181✔
98
    return await this.viewService.deleteView(tableId, viewId);
6✔
99
  }
6✔
100

181✔
101
  private updateRecordOrderSql(orderRawSql: string, dbTableName: string, indexField: string) {
181✔
102
    return this.knex
×
103
      .raw(
×
104
        `
×
105
        UPDATE :dbTableName:
×
106
        SET :indexField: = temp_order.new_order
×
107
        FROM (
×
108
          SELECT __id, ROW_NUMBER() OVER (ORDER BY ${orderRawSql}) AS new_order FROM :dbTableName:
×
109
        ) AS temp_order
×
110
        WHERE :dbTableName:.__id = temp_order.__id AND :dbTableName:.:indexField: != temp_order.new_order;
×
111
      `,
×
112
        {
×
113
          dbTableName,
×
114
          indexField,
×
115
        }
×
116
      )
×
117
      .toQuery();
×
118
  }
×
119

181✔
120
  @Timing()
181✔
121
  async manualSort(tableId: string, viewId: string, viewOrderRo: IManualSortRo) {
×
122
    const { sortObjs } = viewOrderRo;
×
123
    const dbTableName = await this.recordService.getDbTableName(tableId);
×
124
    const fields = await this.fieldService.getFieldsByQuery(tableId, { viewId });
×
125
    const indexField = await this.viewService.getOrCreateViewIndexField(dbTableName, viewId);
×
126

×
127
    const queryBuilder = this.knex(dbTableName);
×
128

×
129
    const fieldInsMap = fields.reduce(
×
130
      (map, field) => {
×
131
        map[field.id] = createFieldInstanceByVo(field);
×
132
        return map;
×
133
      },
×
134
      {} as Record<string, IFieldInstance>
×
135
    );
×
136

×
137
    const orderRawSql = this.dbProvider
×
138
      .sortQuery(queryBuilder, fieldInsMap, sortObjs)
×
139
      .getRawSortSQLText();
×
140

×
141
    // build ops
×
142
    const newSort = {
×
143
      sortObjs: sortObjs,
×
144
      manualSort: true,
×
145
    };
×
146

×
147
    await this.prismaService.$tx(async (prisma) => {
×
148
      await prisma.$executeRawUnsafe(
×
149
        this.updateRecordOrderSql(orderRawSql, dbTableName, indexField)
×
150
      );
×
151
      await this.viewService.updateViewSort(tableId, viewId, newSort);
×
152
    });
×
153
  }
×
154

181✔
155
  async updateViewColumnMeta(
181✔
156
    tableId: string,
38✔
157
    viewId: string,
38✔
158
    columnMetaRo: IColumnMetaRo,
38✔
159
    windowId?: string
38✔
160
  ) {
38✔
161
    const view = await this.prismaService.view
38✔
162
      .findFirstOrThrow({
38✔
163
        where: { tableId, id: viewId },
38✔
164
        select: {
38✔
165
          columnMeta: true,
38✔
166
          version: true,
38✔
167
          id: true,
38✔
168
          type: true,
38✔
169
        },
38✔
170
      })
38✔
171
      .catch(() => {
38✔
172
        throw new BadRequestException('view found column meta error');
×
173
      });
×
174

38✔
175
    // validate field legal
38✔
176
    const fields = await this.prismaService.field.findMany({
38✔
177
      where: { tableId, deletedTime: null },
38✔
178
      select: {
38✔
179
        id: true,
38✔
180
        isPrimary: true,
38✔
181
      },
38✔
182
    });
38✔
183
    const primaryFields = fields.filter((field) => field.isPrimary).map((field) => field.id);
38✔
184

38✔
185
    const isHiddenPrimaryField = columnMetaRo.some(
38✔
186
      (f) => primaryFields.includes(f.fieldId) && (f.columnMeta as IGridColumnMeta).hidden
38✔
187
    );
38✔
188
    const fieldIds = columnMetaRo.map(({ fieldId }) => fieldId);
38✔
189

38✔
190
    if (!fieldIds.every((id) => fields.map(({ id }) => id).includes(id))) {
38✔
191
      throw new BadRequestException('field is not found in table');
×
192
    }
×
193

38✔
194
    const allowHiddenPrimaryType = [ViewType.Calendar, ViewType.Form];
38✔
195
    /**
38✔
196
     * validate whether hidden primary field
38✔
197
     * only form view or list view(todo) can hidden primary field
38✔
198
     */
38✔
199
    if (isHiddenPrimaryField && !allowHiddenPrimaryType.includes(view.type as ViewType)) {
38✔
200
      throw new ForbiddenException('primary field can not be hidden');
2✔
201
    }
2✔
202

36✔
203
    const curColumnMeta = JSON.parse(view.columnMeta);
36✔
204
    const ops: IOtOperation[] = [];
36✔
205

36✔
206
    columnMetaRo.forEach(({ fieldId, columnMeta }) => {
36✔
207
      const obj = {
36✔
208
        fieldId,
36✔
209
        newColumnMeta: { ...curColumnMeta[fieldId], ...columnMeta },
36✔
210
        oldColumnMeta: curColumnMeta[fieldId] ? curColumnMeta[fieldId] : undefined,
36✔
211
      };
36✔
212
      ops.push(ViewOpBuilder.editor.updateViewColumnMeta.build(obj));
36✔
213
    });
36✔
214

36✔
215
    await this.updateViewByOps(tableId, viewId, ops);
36✔
216

36✔
217
    if (windowId) {
38✔
218
      this.eventEmitterService.emitAsync(Events.OPERATION_VIEW_UPDATE, {
2✔
219
        tableId,
2✔
220
        windowId,
2✔
221
        viewId,
2✔
222
        userId: this.cls.get('user.id'),
2✔
223
        byOps: ops,
2✔
224
      });
2✔
225
    }
2✔
226
  }
38✔
227

181✔
228
  async updateShareMeta(tableId: string, viewId: string, viewShareMetaRo: IViewShareMetaRo) {
181✔
229
    return this.setViewProperty(tableId, viewId, 'shareMeta', viewShareMetaRo);
14✔
230
  }
14✔
231

181✔
232
  async setViewProperty(
181✔
233
    tableId: string,
132✔
234
    viewId: string,
132✔
235
    key: IViewPropertyKeys,
132✔
236
    newValue: unknown,
132✔
237
    windowId?: string
132✔
238
  ) {
132✔
239
    const curView = await this.prismaService.view
132✔
240
      .findFirstOrThrow({
132✔
241
        select: { [key]: true },
132✔
242
        where: { tableId, id: viewId, deletedTime: null },
132✔
243
      })
132✔
244
      .catch(() => {
132✔
245
        throw new BadRequestException('View not found');
×
246
      });
×
247
    const oldValue =
132✔
248
      curView[key] != null && VIEW_JSON_KEYS.includes(key)
132✔
249
        ? JSON.parse(curView[key])
90✔
250
        : curView[key];
132✔
251
    const ops = ViewOpBuilder.editor.setViewProperty.build({
132✔
252
      key,
132✔
253
      newValue,
132✔
254
      oldValue,
132✔
255
    });
132✔
256

132✔
257
    await this.updateViewByOps(tableId, viewId, [ops]);
132✔
258

132✔
259
    if (windowId) {
132✔
260
      this.eventEmitterService.emitAsync(Events.OPERATION_VIEW_UPDATE, {
6✔
261
        tableId,
6✔
262
        windowId,
6✔
263
        viewId,
6✔
264
        userId: this.cls.get('user.id'),
6✔
265
        byKey: {
6✔
266
          key,
6✔
267
          newValue,
6✔
268
          oldValue,
6✔
269
        },
6✔
270
      });
6✔
271
    }
6✔
272
  }
132✔
273

181✔
274
  async updateViewByOps(tableId: string, viewId: string, ops: IOtOperation[]) {
181✔
275
    return await this.prismaService.$tx(async () => {
238✔
276
      return await this.viewService.updateViewByOps(tableId, viewId, ops);
238✔
277
    });
238✔
278
  }
238✔
279

181✔
280
  async patchViewOptions(
181✔
281
    tableId: string,
10✔
282
    viewId: string,
10✔
283
    viewOptions: IViewOptions,
10✔
284
    windowId?: string
10✔
285
  ) {
10✔
286
    const curView = await this.prismaService.view
10✔
287
      .findFirstOrThrow({
10✔
288
        select: { options: true, type: true },
10✔
289
        where: { tableId, id: viewId, deletedTime: null },
10✔
290
      })
10✔
291
      .catch(() => {
10✔
292
        throw new BadRequestException('View option not found');
×
293
      });
×
294
    const { options, type: viewType } = curView;
10✔
295

10✔
296
    // validate option type
10✔
297
    try {
10✔
298
      validateOptionsType(viewType as ViewType, viewOptions);
10✔
299
    } catch (err) {
10✔
300
      throw new BadRequestException(err);
2✔
301
    }
2✔
302

8✔
303
    const oldOptions = options ? JSON.parse(options) : options;
10✔
304
    const op = ViewOpBuilder.editor.setViewProperty.build({
10✔
305
      key: 'options',
10✔
306
      newValue: {
10✔
307
        ...oldOptions,
10✔
308
        ...viewOptions,
10✔
309
      },
10✔
310
      oldValue: oldOptions,
10✔
311
    });
10✔
312
    await this.updateViewByOps(tableId, viewId, [op]);
10✔
313

8✔
314
    if (windowId) {
10✔
315
      this.eventEmitterService.emitAsync(Events.OPERATION_VIEW_UPDATE, {
×
316
        tableId,
×
317
        windowId,
×
318
        viewId,
×
319
        userId: this.cls.get('user.id'),
×
320
        byOps: [op],
×
321
      });
×
322
    }
×
323
  }
10✔
324

181✔
325
  /**
181✔
326
   * shuffle view order
181✔
327
   */
181✔
328
  async shuffle(tableId: string) {
181✔
329
    const views = await this.prismaService.view.findMany({
×
330
      where: { tableId, deletedTime: null },
×
331
      select: { id: true, order: true },
×
332
      orderBy: { order: 'asc' },
×
333
    });
×
334

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

×
337
    await this.prismaService.$tx(async () => {
×
338
      for (let i = 0; i < views.length; i++) {
×
339
        const view = views[i];
×
340
        await this.viewService.updateViewByOps(tableId, view.id, [
×
341
          ViewOpBuilder.editor.setViewProperty.build({
×
342
            key: 'order',
×
343
            newValue: i,
×
344
            oldValue: view.order,
×
345
          }),
×
346
        ]);
×
347
      }
×
348
    });
×
349
  }
×
350

181✔
351
  async updateViewOrder(
181✔
352
    tableId: string,
10✔
353
    viewId: string,
10✔
354
    orderRo: IUpdateOrderRo,
10✔
355
    windowId?: string
10✔
356
  ) {
10✔
357
    const { anchorId, position } = orderRo;
10✔
358

10✔
359
    const view = await this.prismaService.view
10✔
360
      .findFirstOrThrow({
10✔
361
        select: { order: true, id: true },
10✔
362
        where: { tableId, id: viewId, deletedTime: null },
10✔
363
      })
10✔
364
      .catch(() => {
10✔
365
        throw new NotFoundException(`View ${viewId} not found in the table`);
×
366
      });
×
367

10✔
368
    const anchorView = await this.prismaService.view
10✔
369
      .findFirstOrThrow({
10✔
370
        select: { order: true, id: true },
10✔
371
        where: { tableId, id: anchorId, deletedTime: null },
10✔
372
      })
10✔
373
      .catch(() => {
10✔
374
        throw new NotFoundException(`Anchor ${anchorId} not found in the table`);
×
375
      });
×
376

10✔
377
    await updateOrder({
10✔
378
      query: tableId,
10✔
379
      position,
10✔
380
      item: view,
10✔
381
      anchorItem: anchorView,
10✔
382
      getNextItem: async (whereOrder, align) => {
10✔
383
        return this.prismaService.view.findFirst({
10✔
384
          select: { order: true, id: true },
10✔
385
          where: {
10✔
386
            tableId,
10✔
387
            deletedTime: null,
10✔
388
            order: whereOrder,
10✔
389
          },
10✔
390
          orderBy: { order: align },
10✔
391
        });
10✔
392
      },
10✔
393
      update: async (
10✔
394
        parentId: string,
10✔
395
        id: string,
10✔
396
        data: { newOrder: number; oldOrder: number }
10✔
397
      ) => {
10✔
398
        const op = ViewOpBuilder.editor.setViewProperty.build({
10✔
399
          key: 'order',
10✔
400
          newValue: data.newOrder,
10✔
401
          oldValue: data.oldOrder,
10✔
402
        });
10✔
403
        await this.updateViewByOps(parentId, id, [op]);
10✔
404

10✔
405
        if (windowId) {
10✔
406
          this.eventEmitterService.emitAsync(Events.OPERATION_VIEW_UPDATE, {
2✔
407
            tableId,
2✔
408
            windowId,
2✔
409
            viewId,
2✔
410
            userId: this.cls.get('user.id'),
2✔
411
            byOps: [op],
2✔
412
          });
2✔
413
        }
2✔
414
      },
10✔
415
      shuffle: this.shuffle.bind(this),
10✔
416
    });
10✔
417
  }
10✔
418

181✔
419
  /**
181✔
420
   * shuffle record order
181✔
421
   */
181✔
422
  async shuffleRecords(dbTableName: string, indexField: string) {
181✔
423
    const recordCount = await this.recordService.getAllRecordCount(dbTableName);
×
424
    if (recordCount > 100_000) {
×
425
      throw new BadRequestException('Not enough gap to move the row here');
×
426
    }
×
427

×
428
    const sql = this.updateRecordOrderSql(
×
429
      this.knex.raw(`?? ASC`, [indexField]).toQuery(),
×
430
      dbTableName,
×
431
      indexField
×
432
    );
×
433

×
434
    await this.prismaService.$executeRawUnsafe(sql);
×
435
  }
×
436

181✔
437
  async updateRecordOrdersInner(props: {
181✔
438
    tableId: string;
26✔
439
    dbTableName: string;
26✔
440
    itemLength: number;
26✔
441
    indexField: string;
26✔
442
    orderRo: {
26✔
443
      anchorId: string;
26✔
444
      position: 'before' | 'after';
26✔
445
    };
26✔
446
    update: (indexes: number[]) => Promise<void>;
26✔
447
  }) {
26✔
448
    const { tableId, itemLength, dbTableName, indexField, orderRo, update } = props;
26✔
449
    const { anchorId, position } = orderRo;
26✔
450

26✔
451
    const anchorRecordSql = this.knex(dbTableName)
26✔
452
      .select({
26✔
453
        id: '__id',
26✔
454
        order: indexField,
26✔
455
      })
26✔
456
      .where('__id', anchorId)
26✔
457
      .toQuery();
26✔
458

26✔
459
    const anchorRecord = await this.prismaService
26✔
460
      .txClient()
26✔
461
      .$queryRawUnsafe<{ id: string; order: number }[]>(anchorRecordSql)
26✔
462
      .then((res) => {
26✔
463
        return res[0];
26✔
464
      });
26✔
465

26✔
466
    if (!anchorRecord) {
26✔
467
      throw new NotFoundException(`Anchor ${anchorId} not found in the table`);
×
468
    }
×
469

26✔
470
    await updateMultipleOrders({
26✔
471
      parentId: tableId,
26✔
472
      position,
26✔
473
      itemLength,
26✔
474
      anchorItem: anchorRecord,
26✔
475
      getNextItem: async (whereOrder, align) => {
26✔
476
        const nextRecordSql = this.knex(dbTableName)
26✔
477
          .select({
26✔
478
            id: '__id',
26✔
479
            order: indexField,
26✔
480
          })
26✔
481
          .where(
26✔
482
            indexField,
26✔
483
            whereOrder.lt != null ? '<' : '>',
26✔
484
            (whereOrder.lt != null ? whereOrder.lt : whereOrder.gt) as number
26✔
485
          )
26✔
486
          .orderBy(indexField, align)
26✔
487
          .limit(1)
26✔
488
          .toQuery();
26✔
489
        return this.prismaService
26✔
490
          .txClient()
26✔
491
          .$queryRawUnsafe<{ id: string; order: number }[]>(nextRecordSql)
26✔
492
          .then((res) => {
26✔
493
            return res[0];
26✔
494
          });
26✔
495
      },
26✔
496
      update,
26✔
497
      shuffle: async () => {
26✔
498
        await this.shuffleRecords(dbTableName, indexField);
×
499
      },
×
500
    });
26✔
501
  }
26✔
502

181✔
503
  async updateRecordIndexes(
181✔
504
    tableId: string,
4✔
505
    viewId: string,
4✔
506
    recordsWithOrder: {
4✔
507
      id: string;
4✔
508
      order?: Record<string, number>;
4✔
509
    }[]
4✔
510
  ) {
4✔
511
    // for notify view update only
4✔
512
    await this.prismaService.$tx(async () => {
4✔
513
      const ops = ViewOpBuilder.editor.setViewProperty.build({
4✔
514
        key: 'lastModifiedTime',
4✔
515
        newValue: new Date().toISOString(),
4✔
516
      });
4✔
517
      await this.viewService.updateViewByOps(tableId, viewId, [ops]);
4✔
518
      await this.recordService.updateRecordIndexes(tableId, recordsWithOrder);
4✔
519
    });
4✔
520
  }
4✔
521

181✔
522
  async updateRecordOrders(
181✔
523
    tableId: string,
12✔
524
    viewId: string,
12✔
525
    orderRo: IUpdateRecordOrdersRo,
12✔
526
    windowId?: string
12✔
527
  ) {
12✔
528
    const recordIds = orderRo.recordIds;
12✔
529
    const dbTableName = await this.recordService.getDbTableName(tableId);
12✔
530
    const orderIndexesBefore = windowId
12✔
531
      ? await this.recordService.getRecordIndexes(tableId, recordIds, viewId)
2✔
532
      : undefined;
10✔
533

12✔
534
    const indexField = await this.viewService.getOrCreateViewIndexField(dbTableName, viewId);
12✔
535

12✔
536
    await this.updateRecordOrdersInner({
12✔
537
      tableId,
12✔
538
      dbTableName,
12✔
539
      itemLength: recordIds.length,
12✔
540
      indexField,
12✔
541
      orderRo,
12✔
542
      update: async (indexes) => {
12✔
543
        // for notify view update only
12✔
544
        const ops = ViewOpBuilder.editor.setViewProperty.build({
12✔
545
          key: 'lastModifiedTime',
12✔
546
          newValue: new Date().toISOString(),
12✔
547
        });
12✔
548

12✔
549
        await this.prismaService.$tx(async (prisma) => {
12✔
550
          await this.viewService.updateViewByOps(tableId, viewId, [ops]);
12✔
551
          for (let i = 0; i < recordIds.length; i++) {
12✔
552
            const recordId = recordIds[i];
18✔
553
            const updateRecordSql = this.knex(dbTableName)
18✔
554
              .update({
18✔
555
                [indexField]: indexes[i],
18✔
556
              })
18✔
557
              .where('__id', recordId)
18✔
558
              .toQuery();
18✔
559
            await prisma.$executeRawUnsafe(updateRecordSql);
18✔
560
          }
18✔
561
        });
12✔
562
      },
12✔
563
    });
12✔
564

12✔
565
    if (windowId) {
12✔
566
      const orderIndexesAfter = await this.recordService.getRecordIndexes(
2✔
567
        tableId,
2✔
568
        recordIds,
2✔
569
        viewId
2✔
570
      );
2✔
571
      this.eventEmitterService.emitAsync(Events.OPERATION_RECORDS_ORDER_UPDATE, {
2✔
572
        tableId,
2✔
573
        windowId,
2✔
574
        recordIds,
2✔
575
        viewId,
2✔
576
        userId: this.cls.get('user.id'),
2✔
577
        orderIndexesBefore,
2✔
578
        orderIndexesAfter,
2✔
579
      });
2✔
580
    }
2✔
581
  }
12✔
582

181✔
583
  async refreshShareId(tableId: string, viewId: string) {
181✔
584
    const view = await this.prismaService.view.findUnique({
×
585
      where: { id: viewId, tableId, deletedTime: null },
×
586
      select: { shareId: true, enableShare: true },
×
587
    });
×
588
    if (!view) {
×
589
      throw new NotFoundException(`View ${viewId} does not exist`);
×
590
    }
×
591
    const { enableShare } = view;
×
592
    if (!enableShare) {
×
593
      throw new BadRequestException(`View ${viewId} has not been enabled share`);
×
594
    }
×
595
    const newShareId = generateShareId();
×
596
    const setShareIdOp = ViewOpBuilder.editor.setViewProperty.build({
×
597
      key: 'shareId',
×
598
      newValue: newShareId,
×
599
      oldValue: view.shareId || undefined,
×
600
    });
×
601
    await this.updateViewByOps(tableId, viewId, [setShareIdOp]);
×
602
    return { shareId: newShareId };
×
603
  }
×
604

181✔
605
  async enableShare(tableId: string, viewId: string) {
181✔
606
    const view = await this.prismaService.view.findUnique({
44✔
607
      where: { id: viewId, tableId, deletedTime: null },
44✔
608
    });
44✔
609
    if (!view) {
44✔
610
      throw new NotFoundException(`View ${viewId} does not exist`);
×
611
    }
×
612
    const { enableShare, shareId } = view;
44✔
613
    if (enableShare) {
44✔
614
      throw new BadRequestException(`View ${viewId} has already been enabled share`);
×
615
    }
×
616
    const newShareId = generateShareId();
44✔
617
    const enableShareOp = ViewOpBuilder.editor.setViewProperty.build({
44✔
618
      key: 'enableShare',
44✔
619
      newValue: true,
44✔
620
      oldValue: enableShare || undefined,
44✔
621
    });
44✔
622
    const setShareIdOp = ViewOpBuilder.editor.setViewProperty.build({
44✔
623
      key: 'shareId',
44✔
624
      newValue: newShareId,
44✔
625
      oldValue: shareId || undefined,
44✔
626
    });
44✔
627

44✔
628
    const ops = [enableShareOp, setShareIdOp];
44✔
629

44✔
630
    const viewInstance = createViewInstanceByRaw(view);
44✔
631
    if (!view.shareMeta && viewInstance.defaultShareMeta) {
44✔
632
      const initShareMetaOp = ViewOpBuilder.editor.setViewProperty.build({
44✔
633
        key: 'shareMeta',
44✔
634
        newValue: viewInstance.defaultShareMeta,
44✔
635
      });
44✔
636
      ops.push(initShareMetaOp);
44✔
637
    }
44✔
638
    await this.updateViewByOps(tableId, viewId, ops);
44✔
639
    return { shareId: newShareId };
44✔
640
  }
44✔
641

181✔
642
  async disableShare(tableId: string, viewId: string) {
181✔
643
    const view = await this.prismaService.view.findUnique({
×
644
      where: { id: viewId, tableId, deletedTime: null },
×
645
      select: { shareId: true, enableShare: true, shareMeta: true },
×
646
    });
×
647
    if (!view) {
×
648
      throw new NotFoundException(`View ${viewId} does not exist`);
×
649
    }
×
650
    const { enableShare } = view;
×
651
    if (!enableShare) {
×
652
      throw new BadRequestException(`View ${viewId} has already been disable share`);
×
653
    }
×
654
    const enableShareOp = ViewOpBuilder.editor.setViewProperty.build({
×
655
      key: 'enableShare',
×
656
      newValue: false,
×
657
      oldValue: enableShare || undefined,
×
658
    });
×
659

×
660
    await this.updateViewByOps(tableId, viewId, [enableShareOp]);
×
661
  }
×
662

181✔
663
  /**
181✔
664
   * @param linkFields {fieldId: foreignTableId}
181✔
665
   * @returns {foreignTableId: Set<recordId>}
181✔
666
   */
181✔
667
  private async collectFilterLinkFieldRecords(
181✔
668
    linkFields: Record<string, string>,
6✔
669
    filter?: IFilter
6✔
670
  ) {
6✔
671
    if (!filter || !filter.filterSet) {
6✔
672
      return undefined;
×
673
    }
×
674

6✔
675
    const tableRecordMap: Record<string, Set<string>> = {};
6✔
676

6✔
677
    const mergeRecordMap = (source: Record<string, Set<string>> = {}) => {
6✔
678
      for (const [fieldId, recordSet] of Object.entries(source)) {
12✔
679
        tableRecordMap[fieldId] = tableRecordMap[fieldId] || new Set();
12✔
680
        recordSet.forEach((item) => tableRecordMap[fieldId].add(item));
12✔
681
      }
12✔
682
    };
12✔
683

6✔
684
    for (const filterItem of filter.filterSet) {
6✔
685
      if ('filterSet' in filterItem) {
12✔
686
        const groupTableRecordMap = await this.collectFilterLinkFieldRecords(
4✔
687
          linkFields,
4✔
688
          filterItem as IFilter
4✔
689
        );
4✔
690
        if (groupTableRecordMap) {
4✔
691
          mergeRecordMap(groupTableRecordMap);
4✔
692
        }
4✔
693
        continue;
4✔
694
      }
4✔
695

8✔
696
      const { value, fieldId } = filterItem as IFilterItem;
8✔
697

8✔
698
      const foreignTableId = linkFields[fieldId];
8✔
699
      if (!foreignTableId) {
12✔
700
        continue;
×
701
      }
✔
702

8✔
703
      if (Array.isArray(value)) {
12✔
704
        mergeRecordMap({ [foreignTableId]: new Set(value as string[]) });
4✔
705
      } else if (typeof value === 'string' && value.startsWith(IdPrefix.Record)) {
4✔
706
        mergeRecordMap({ [foreignTableId]: new Set([value]) });
4✔
707
      }
4✔
708
    }
12✔
709

6✔
710
    return tableRecordMap;
6✔
711
  }
6✔
712

181✔
713
  async getFilterLinkRecords(tableId: string, viewId: string) {
181✔
714
    const view = await this.viewService.getViewById(viewId);
2✔
715
    return this.getFilterLinkRecordsByTable(tableId, view.filter);
2✔
716
  }
2✔
717

181✔
718
  async getFilterLinkRecordsByTable(tableId: string, filter?: IFilter) {
181✔
719
    if (!filter) {
2✔
720
      return [];
×
721
    }
×
722
    const linkFields = await this.prismaService.field.findMany({
2✔
723
      where: { tableId, deletedTime: null, type: FieldType.Link },
2✔
724
    });
2✔
725
    const linkFieldTableMap = linkFields.reduce(
2✔
726
      (map, field) => {
2✔
727
        const { foreignTableId } = JSON.parse(field.options as string) as ILinkFieldOptions;
4✔
728
        map[field.id] = foreignTableId;
4✔
729
        return map;
4✔
730
      },
4✔
731
      {} as Record<string, string>
2✔
732
    );
2✔
733
    const tableRecordMap = await this.collectFilterLinkFieldRecords(linkFieldTableMap, filter);
2✔
734

2✔
735
    if (!tableRecordMap) {
2✔
736
      return [];
×
737
    }
×
738
    const res: IGetViewFilterLinkRecordsVo = [];
2✔
739
    for (const [foreignTableId, recordSet] of Object.entries(tableRecordMap)) {
2✔
740
      const dbTableName = await this.recordService.getDbTableName(foreignTableId);
4✔
741
      const primaryField = await this.prismaService.field.findFirst({
4✔
742
        where: { tableId: foreignTableId, isPrimary: true, deletedTime: null },
4✔
743
      });
4✔
744
      if (!primaryField) {
4✔
745
        continue;
×
746
      }
×
747

4✔
748
      const dbFieldName = primaryField.dbFieldName;
4✔
749

4✔
750
      const nativeQuery = this.knex(dbTableName)
4✔
751
        .select('__id as id', `${dbFieldName} as title`)
4✔
752
        .orderBy('__auto_number')
4✔
753
        .whereIn('__id', Array.from(recordSet))
4✔
754
        .toQuery();
4✔
755

4✔
756
      const list = await this.prismaService
4✔
757
        .txClient()
4✔
758
        .$queryRawUnsafe<{ id: string; title: string | null }[]>(nativeQuery);
4✔
759
      const fieldInstances = createFieldInstanceByRaw(primaryField);
4✔
760
      res.push({
4✔
761
        tableId: foreignTableId,
4✔
762
        records: list.map(({ id, title }) => ({
4✔
763
          id,
10✔
764
          title:
10✔
765
            fieldInstances.cellValue2String(fieldInstances.convertDBValue2CellValue(title)) ||
10✔
766
            undefined,
×
767
        })),
10✔
768
      });
4✔
769
    }
4✔
770
    return res;
2✔
771
  }
2✔
772

181✔
773
  async pluginInstall(tableId: string, ro: IViewInstallPluginRo) {
181✔
774
    const userId = this.cls.get('user.id');
×
775
    const { name, pluginId } = ro;
×
776
    const plugin = await this.prismaService.plugin.findUnique({
×
777
      where: { id: pluginId, status: PluginStatus.Published },
×
778
      select: { id: true, name: true, logo: true, positions: true },
×
779
    });
×
780
    if (!plugin) {
×
781
      throw new NotFoundException(`Plugin ${pluginId} not found`);
×
782
    }
×
783
    if (!plugin.positions.includes(PluginPosition.View)) {
×
784
      throw new BadRequestException(`Plugin ${pluginId} does not support install in view`);
×
785
    }
×
786
    const viewName = name || plugin.name;
×
787
    return this.prismaService.$tx(async (prisma) => {
×
788
      const pluginInstallId = generatePluginInstallId();
×
789
      const view = await this.createViewInner(tableId, {
×
790
        name: viewName,
×
791
        type: ViewType.Plugin,
×
792
        options: {
×
793
          pluginInstallId,
×
794
          pluginId,
×
795
          pluginLogo: plugin.logo,
×
796
        } as IPluginViewOptions,
×
797
      });
×
798
      const table = await prisma.tableMeta.findUniqueOrThrow({
×
799
        where: { id: tableId, deletedTime: null },
×
800
        select: { baseId: true },
×
801
      });
×
802
      const newPlugin = await prisma.pluginInstall.create({
×
803
        data: {
×
804
          id: pluginInstallId,
×
805
          baseId: table?.baseId,
×
806
          positionId: view.id,
×
807
          position: PluginPosition.View,
×
808
          name: viewName,
×
809
          pluginId: ro.pluginId,
×
810
          createdBy: userId,
×
811
        },
×
812
      });
×
813
      return {
×
814
        pluginId: newPlugin.pluginId,
×
815
        pluginInstallId: newPlugin.id,
×
816
        name: newPlugin.name,
×
817
        viewId: view.id,
×
818
      };
×
819
    });
×
820
  }
×
821

181✔
822
  async updatePluginStorage(viewId: string, storage: IViewPluginUpdateStorageRo['storage']) {
181✔
823
    const pluginInstall = await this.prismaService.pluginInstall.findFirst({
×
824
      where: { positionId: viewId, position: PluginPosition.View },
×
825
      select: { id: true },
×
826
    });
×
827
    if (!pluginInstall) {
×
828
      throw new NotFoundException(`Plugin install not found`);
×
829
    }
×
830
    return this.prismaService.pluginInstall.update({
×
831
      where: { id: pluginInstall.id },
×
NEW
832
      data: { storage: JSON.stringify(storage) },
×
833
    });
×
834
  }
×
835

181✔
836
  async getPluginInstall(tableId: string, viewId: string) {
181✔
837
    const table = await this.prismaService.tableMeta.findUniqueOrThrow({
×
838
      where: { id: tableId, deletedTime: null },
×
839
      select: { baseId: true },
×
840
    });
×
841
    const pluginInstall = await this.prismaService.pluginInstall.findFirst({
×
842
      where: { positionId: viewId, position: PluginPosition.View },
×
843
      select: {
×
844
        id: true,
×
845
        pluginId: true,
×
846
        name: true,
×
847
        storage: true,
×
848
        plugin: {
×
849
          select: { url: true },
×
850
        },
×
851
      },
×
852
    });
×
853
    if (!pluginInstall) {
×
854
      throw new NotFoundException(`Plugin install not found`);
×
855
    }
×
856
    return {
×
857
      name: pluginInstall.name,
×
858
      pluginId: pluginInstall.pluginId,
×
859
      pluginInstallId: pluginInstall.id,
×
860
      storage: pluginInstall.storage ? JSON.parse(pluginInstall.storage) : undefined,
×
861
      baseId: table.baseId,
×
862
      url: pluginInstall.plugin.url || undefined,
×
863
    };
×
864
  }
×
865
}
181✔
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