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

teableio / teable / 20135492339

11 Dec 2025 01:55PM UTC coverage: 71.773% (-0.02%) from 71.792%
20135492339

Pull #2236

github

web-flow
Merge 085ca6f8f into 600755edc
Pull Request #2236: feat: space layout

23046 of 25710 branches covered (89.64%)

54 of 92 new or added lines in 5 files covered. (58.7%)

3 existing lines in 1 file now uncovered.

58134 of 80997 relevant lines covered (71.77%)

4253.25 hits per line

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

74.0
/apps/nestjs-backend/src/features/user/last-visit/last-visit.service.ts
1
/* eslint-disable @typescript-eslint/naming-convention */
8✔
2
/* eslint-disable sonarjs/no-duplicate-string */
8✔
3
import { Injectable } from '@nestjs/common';
4
import { OnEvent } from '@nestjs/event-emitter';
5
import { HttpErrorCode, type IRole } from '@teable/core';
6
import { PrismaService } from '@teable/db-main-prisma';
7
import type {
8
  IGetUserLastVisitRo,
9
  IGetUserLastVisitBaseNodeRo,
10
  IUpdateUserLastVisitRo,
11
  IUserLastVisitListBaseVo,
12
  IUserLastVisitMapVo,
13
  IUserLastVisitVo,
14
  IUserLastVisitBaseNodeVo,
15
} from '@teable/openapi';
16
import { LastVisitResourceType } from '@teable/openapi';
17
import { Knex } from 'knex';
18
import { keyBy } from 'lodash';
19
import { InjectModel } from 'nest-knexjs';
20
import { ClsService } from 'nestjs-cls';
21
import { CustomHttpException } from '../../../custom.exception';
22
import { EventEmitterService } from '../../../event-emitter/event-emitter.service';
23
import type {
24
  BaseDeleteEvent,
25
  SpaceDeleteEvent,
26
  DashboardDeleteEvent,
27
  WorkflowDeleteEvent,
28
  AppDeleteEvent,
29
  TableDeleteEvent,
30
  ViewDeleteEvent,
31
} from '../../../event-emitter/events';
32
import { Events } from '../../../event-emitter/events';
33
import { LastVisitUpdateEvent } from '../../../event-emitter/events/last-visit/last-visit.event';
34
import type { IClsStore } from '../../../types/cls';
35

36
@Injectable()
37
export class LastVisitService {
8✔
38
  constructor(
305✔
39
    private readonly prismaService: PrismaService,
305✔
40
    @InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
305✔
41
    private readonly cls: ClsService<IClsStore>,
305✔
42
    private readonly eventEmitterService: EventEmitterService
305✔
43
  ) {}
305✔
44

45
  async getUserLastVisitBaseNode(
305✔
46
    userId: string,
4✔
47
    params: IGetUserLastVisitBaseNodeRo
4✔
48
  ): Promise<IUserLastVisitBaseNodeVo> {
4✔
49
    const lastVisit = await this.prismaService.userLastVisit.findFirst({
4✔
50
      where: {
4✔
51
        userId,
4✔
52
        parentResourceId: params.parentResourceId,
4✔
53
        resourceType: {
4✔
54
          in: [
4✔
55
            LastVisitResourceType.Table,
4✔
56
            LastVisitResourceType.Dashboard,
4✔
57
            LastVisitResourceType.Automation,
4✔
58
            LastVisitResourceType.App,
4✔
59
          ],
60
        },
4✔
61
      },
4✔
62
      orderBy: {
4✔
63
        lastVisitTime: 'desc',
4✔
64
      },
4✔
65
      take: 1,
4✔
66
      select: {
4✔
67
        resourceId: true,
4✔
68
        resourceType: true,
4✔
69
      },
4✔
70
    });
4✔
71

72
    if (!lastVisit) {
4✔
73
      return;
2✔
74
    }
2✔
75

76
    return {
2✔
77
      resourceId: lastVisit.resourceId,
2✔
78
      resourceType: lastVisit.resourceType as LastVisitResourceType,
2✔
79
    };
2✔
80
  }
2✔
81

82
  async spaceVisit(userId: string, parentResourceId: string) {
305✔
NEW
83
    const lastVisit = await this.prismaService.userLastVisit.findFirst({
×
NEW
84
      where: {
×
NEW
85
        userId,
×
NEW
86
        parentResourceId,
×
NEW
87
        resourceType: LastVisitResourceType.Space,
×
NEW
88
      },
×
NEW
89
      orderBy: {
×
NEW
90
        lastVisitTime: 'desc',
×
NEW
91
      },
×
NEW
92
      take: 1,
×
NEW
93
      select: {
×
NEW
94
        resourceId: true,
×
NEW
95
        resourceType: true,
×
NEW
96
      },
×
NEW
97
    });
×
98

NEW
99
    if (lastVisit) {
×
NEW
100
      return {
×
NEW
101
        resourceId: lastVisit.resourceId,
×
NEW
102
        resourceType: lastVisit.resourceType as LastVisitResourceType,
×
NEW
103
      };
×
NEW
104
    }
×
105

NEW
106
    return undefined;
×
NEW
107
  }
×
108

109
  async tableVisit(userId: string, baseId: string): Promise<IUserLastVisitVo | undefined> {
305✔
110
    const knex = this.knex;
6✔
111

112
    const query = this.knex
6✔
113
      .with('table_visit', (qb) => {
6✔
114
        qb.select({
6✔
115
          resourceId: 'ulv.resource_id',
6✔
116
        })
6✔
117
          .from('user_last_visit as ulv')
6✔
118
          .leftJoin('table_meta as t', function () {
6✔
119
            this.on('t.id', '=', 'ulv.resource_id').andOnNull('t.deleted_time');
6✔
120
          })
6✔
121
          .where('ulv.user_id', userId)
6✔
122
          .where('ulv.resource_type', LastVisitResourceType.Table)
6✔
123
          .where('ulv.parent_resource_id', baseId)
6✔
124
          .limit(1);
6✔
125
      })
6✔
126
      .select({
6✔
127
        tableId: 'table_visit.resourceId',
6✔
128
        viewId: 'ulv.resource_id',
6✔
129
      })
6✔
130
      .from('table_visit')
6✔
131
      .leftJoin('user_last_visit as ulv', function () {
6✔
132
        this.on('ulv.parent_resource_id', '=', 'table_visit.resourceId')
6✔
133
          .andOn('ulv.resource_type', knex.raw('?', LastVisitResourceType.View))
6✔
134
          .andOn('ulv.user_id', knex.raw('?', userId));
6✔
135
      })
6✔
136
      .leftJoin('view as v', function () {
6✔
137
        this.on('v.id', '=', 'ulv.resource_id').andOnNull('v.deleted_time');
6✔
138
      })
6✔
139
      .whereRaw('(ulv.resource_id IS NULL OR v.id IS NOT NULL)')
6✔
140
      .limit(1)
6✔
141
      .toQuery();
6✔
142

143
    const results = await this.prismaService.$queryRawUnsafe<
6✔
144
      {
145
        tableId: string;
146
        tableLastVisitTime: Date;
147
        viewId: string;
148
        viewLastVisitTime: Date;
149
      }[]
150
    >(query);
6✔
151

152
    const result = results[0];
6✔
153

154
    if (result && result.tableId && result.viewId) {
6✔
155
      return {
1✔
156
        resourceId: result.tableId,
1✔
157
        childResourceId: result.viewId,
1✔
158
        resourceType: LastVisitResourceType.Table,
1✔
159
      };
1✔
160
    }
1✔
161

162
    if (result && result.tableId) {
6✔
163
      const table = await this.prismaService.tableMeta.findFirst({
4✔
164
        select: {
4✔
165
          id: true,
4✔
166
          views: {
4✔
167
            select: {
4✔
168
              id: true,
4✔
169
            },
4✔
170
            take: 1,
4✔
171
            orderBy: {
4✔
172
              order: 'asc',
4✔
173
            },
4✔
174
            where: {
4✔
175
              deletedTime: null,
4✔
176
            },
4✔
177
          },
4✔
178
        },
4✔
179
        where: {
4✔
180
          id: result.tableId,
4✔
181
          deletedTime: null,
4✔
182
        },
4✔
183
      });
4✔
184

185
      if (!table) {
4✔
186
        return;
×
187
      }
×
188

189
      return {
4✔
190
        resourceId: table.id,
4✔
191
        childResourceId: table.views[0].id,
4✔
192
        resourceType: LastVisitResourceType.Table,
4✔
193
      };
4✔
194
    }
4✔
195

196
    const table = await this.prismaService.tableMeta.findFirst({
1✔
197
      select: {
1✔
198
        id: true,
1✔
199
        views: {
1✔
200
          select: {
1✔
201
            id: true,
1✔
202
          },
1✔
203
          take: 1,
1✔
204
          orderBy: {
1✔
205
            order: 'asc',
1✔
206
          },
1✔
207
          where: {
1✔
208
            deletedTime: null,
1✔
209
          },
1✔
210
        },
1✔
211
      },
1✔
212
      where: {
1✔
213
        baseId,
1✔
214
        deletedTime: null,
1✔
215
      },
1✔
216
      orderBy: {
1✔
217
        order: 'asc',
1✔
218
      },
1✔
219
    });
1✔
220

221
    if (!table) {
6✔
222
      return;
×
223
    }
✔
224

225
    return {
1✔
226
      resourceId: table.id,
1✔
227
      childResourceId: table.views[0].id,
1✔
228
      resourceType: LastVisitResourceType.Table,
1✔
229
    };
1✔
230
  }
1✔
231

232
  async viewVisit(userId: string, parentResourceId: string) {
305✔
233
    const query = this.knex
2✔
234
      .select({
2✔
235
        resourceId: 'ulv.resource_id',
2✔
236
      })
2✔
237
      .from('user_last_visit as ulv')
2✔
238
      .leftJoin('view as v', function () {
2✔
239
        this.on('v.id', '=', 'ulv.resource_id').andOnNull('v.deleted_time');
2✔
240
      })
2✔
241
      .where('ulv.user_id', userId)
2✔
242
      .where('ulv.resource_type', LastVisitResourceType.View)
2✔
243
      .where('ulv.parent_resource_id', parentResourceId)
2✔
244
      .whereNotNull('v.id')
2✔
245
      .limit(1);
2✔
246

247
    const sql = query.toQuery();
2✔
248

249
    const results = await this.prismaService.$queryRawUnsafe<IUserLastVisitVo[]>(sql);
2✔
250
    const lastVisit = results[0];
2✔
251

252
    if (lastVisit) {
2✔
253
      return {
1✔
254
        resourceId: lastVisit.resourceId,
1✔
255
        resourceType: LastVisitResourceType.View,
1✔
256
      };
1✔
257
    }
1✔
258

259
    const view = await this.prismaService.view.findFirst({
1✔
260
      select: {
1✔
261
        id: true,
1✔
262
      },
1✔
263
      where: {
1✔
264
        tableId: parentResourceId,
1✔
265
        deletedTime: null,
1✔
266
      },
1✔
267
      orderBy: {
1✔
268
        order: 'asc',
1✔
269
      },
1✔
270
    });
1✔
271

272
    if (view) {
1✔
273
      return {
1✔
274
        resourceId: view.id,
1✔
275
        resourceType: LastVisitResourceType.View,
1✔
276
      };
1✔
277
    }
1✔
278
  }
2✔
279

280
  async dashboardVisit(userId: string, parentResourceId: string) {
305✔
281
    const query = this.knex
×
282
      .select({
×
283
        resourceId: 'ulv.resource_id',
×
284
      })
×
285
      .from('user_last_visit as ulv')
×
286
      .leftJoin('dashboard as v', function () {
×
287
        this.on('v.id', '=', 'ulv.resource_id');
×
288
      })
×
289
      .where('ulv.user_id', userId)
×
290
      .where('ulv.resource_type', LastVisitResourceType.Dashboard)
×
291
      .where('ulv.parent_resource_id', parentResourceId)
×
292
      .whereNotNull('v.id')
×
293
      .limit(1);
×
294

295
    const sql = query.toQuery();
×
296

297
    const results = await this.prismaService.$queryRawUnsafe<IUserLastVisitVo[]>(sql);
×
298
    const lastVisit = results[0];
×
299

300
    if (lastVisit) {
×
301
      return {
×
302
        resourceId: lastVisit.resourceId,
×
303
        resourceType: LastVisitResourceType.Dashboard,
×
304
      };
×
305
    }
×
306

307
    const dashboard = await this.prismaService.dashboard.findFirst({
×
308
      select: {
×
309
        id: true,
×
310
      },
×
311
      where: {
×
312
        baseId: parentResourceId,
×
313
      },
×
314
    });
×
315

316
    if (dashboard) {
×
317
      return {
×
318
        resourceId: dashboard.id,
×
319
        resourceType: LastVisitResourceType.Dashboard,
×
320
      };
×
321
    }
×
322
  }
×
323

324
  async automationVisit(userId: string, parentResourceId: string) {
305✔
325
    const query = this.knex
×
326
      .select({
×
327
        resourceId: 'ulv.resource_id',
×
328
      })
×
329
      .from('user_last_visit as ulv')
×
330
      .leftJoin('workflow as v', function () {
×
331
        this.on('v.id', '=', 'ulv.resource_id').andOnNull('v.deleted_time');
×
332
      })
×
333
      .where('ulv.user_id', userId)
×
334
      .where('ulv.resource_type', LastVisitResourceType.Automation)
×
335
      .where('ulv.parent_resource_id', parentResourceId)
×
336
      .whereNotNull('v.id')
×
337
      .limit(1)
×
338
      .toQuery();
×
339

340
    const results = await this.prismaService.$queryRawUnsafe<IUserLastVisitVo[]>(query);
×
341
    const lastVisit = results[0];
×
342

343
    if (lastVisit) {
×
344
      return {
×
345
        resourceId: lastVisit.resourceId,
×
346
        resourceType: LastVisitResourceType.Automation,
×
347
      };
×
348
    }
×
349

350
    const workflowQuery = this.knex('workflow')
×
351
      .select({
×
352
        id: 'id',
×
353
      })
×
354
      .where('base_id', parentResourceId)
×
355
      .whereNull('deleted_time')
×
356
      .orderBy('order', 'asc')
×
357
      .limit(1)
×
358
      .toQuery();
×
359

360
    const workflowResults =
×
361
      await this.prismaService.$queryRawUnsafe<{ id: string }[]>(workflowQuery);
×
362
    const workflow = workflowResults[0];
×
363

364
    if (workflow) {
×
365
      return {
×
366
        resourceId: workflow.id,
×
367
        resourceType: LastVisitResourceType.Automation,
×
368
      };
×
369
    }
×
370
  }
×
371

372
  async appVisit(userId: string, parentResourceId: string) {
305✔
373
    const query = this.knex
×
374
      .select({
×
375
        resourceId: 'ulv.resource_id',
×
376
      })
×
377
      .from('user_last_visit as ulv')
×
378
      .leftJoin('app as a', function () {
×
379
        this.on('a.id', '=', 'ulv.resource_id').andOnNull('a.deleted_time');
×
380
      })
×
381
      .where('ulv.user_id', userId)
×
382
      .where('ulv.resource_type', LastVisitResourceType.App)
×
383
      .where('ulv.parent_resource_id', parentResourceId)
×
384
      .whereNotNull('a.id')
×
385
      .limit(1)
×
386
      .toQuery();
×
387

388
    const results = await this.prismaService.$queryRawUnsafe<IUserLastVisitVo[]>(query);
×
389
    const lastVisit = results[0];
×
390

391
    if (lastVisit) {
×
392
      return {
×
393
        resourceId: lastVisit.resourceId,
×
394
        resourceType: LastVisitResourceType.App,
×
395
      };
×
396
    }
×
397

398
    const appQuery = this.knex('app')
×
399
      .select({
×
400
        id: 'id',
×
401
      })
×
402
      .where('base_id', parentResourceId)
×
403
      .whereNull('deleted_time')
×
404
      .orderBy('last_modified_time', 'desc')
×
405
      .limit(1)
×
406
      .toQuery();
×
407

408
    const appResults = await this.prismaService.$queryRawUnsafe<{ id: string }[]>(appQuery);
×
409
    const app = appResults[0];
×
410

411
    if (app) {
×
412
      return {
×
413
        resourceId: app.id,
×
414
        resourceType: LastVisitResourceType.App,
×
415
      };
×
416
    }
×
417

418
    return undefined;
×
419
  }
×
420

421
  async baseVisit(): Promise<IUserLastVisitListBaseVo> {
305✔
422
    const userId = this.cls.get('user.id');
2✔
423
    const departmentIds = this.cls.get('organization.departments')?.map((d) => d.id);
2✔
424
    const query = this.knex
2✔
425
      .distinct(['ulv.resource_id'])
2✔
426
      .select({
2✔
427
        resourceId: 'ulv.resource_id',
2✔
428
        resourceType: 'ulv.resource_type',
2✔
429
        lastVisitTime: 'ulv.last_visit_time',
2✔
430
        resourceName: 'b.name',
2✔
431
        resourceIcon: 'b.icon',
2✔
432
        resourceRole: 'c.role_name',
2✔
433
        spaceId: 's.id',
2✔
434
        createBy: 'b.created_by',
2✔
435
      })
2✔
436
      .from('user_last_visit as ulv')
2✔
437
      .join('base as b', function () {
2✔
438
        this.on('b.id', '=', 'ulv.resource_id').andOnNull('b.deleted_time');
2✔
439
      })
2✔
440
      .join('space as s', function () {
2✔
441
        this.on('s.id', '=', 'ulv.parent_resource_id').andOnNull('s.deleted_time');
2✔
442
      })
2✔
443
      .join('collaborator as c', function () {
2✔
444
        this.onIn('c.principal_id', [...(departmentIds ?? []), userId]).andOn(function () {
2✔
445
          this.on('c.resource_id', '=', 'ulv.parent_resource_id').orOn(
2✔
446
            'c.resource_id',
2✔
447
            '=',
2✔
448
            'ulv.resource_id'
2✔
449
          );
450
        });
2✔
451
      })
2✔
452
      .where('ulv.user_id', userId)
2✔
453
      .where('ulv.resource_type', LastVisitResourceType.Base)
2✔
454
      .whereNotNull('b.id')
2✔
455
      .whereNotNull('c.id')
2✔
456
      .orderBy('ulv.last_visit_time', 'desc');
2✔
457

458
    const results = await this.prismaService.$queryRawUnsafe<
2✔
459
      {
460
        resourceId: string;
461
        resourceType: LastVisitResourceType;
462
        lastVisitTime: Date;
463
        resourceName: string;
464
        resourceIcon: string;
465
        resourceRole: IRole;
466
        spaceId: string;
467
        createBy: string;
468
      }[]
469
    >(query.toQuery());
2✔
470

471
    const list = results.map((result) => ({
2✔
472
      resourceId: result.resourceId,
10✔
473
      resourceType: result.resourceType,
10✔
474
      lastVisitTime: result.lastVisitTime.toISOString(),
10✔
475
      resource: {
10✔
476
        id: result.resourceId,
10✔
477
        name: result.resourceName,
10✔
478
        icon: result.resourceIcon,
10✔
479
        role: result.resourceRole,
10✔
480
        spaceId: result.spaceId,
10✔
481
        createdBy: result.createBy,
10✔
482
      },
10✔
483
    }));
10✔
484

485
    return {
2✔
486
      total: results.length,
2✔
487
      list,
2✔
488
    };
2✔
489
  }
2✔
490

491
  async getUserLastVisit(
305✔
492
    userId: string,
8✔
493
    params: IGetUserLastVisitRo
8✔
494
  ): Promise<IUserLastVisitVo | undefined> {
8✔
495
    switch (params.resourceType) {
8✔
496
      case LastVisitResourceType.Space:
8✔
NEW
497
        return this.spaceVisit(userId, params.parentResourceId);
×
498
      case LastVisitResourceType.Table:
8✔
499
        return this.tableVisit(userId, params.parentResourceId);
6✔
500
      case LastVisitResourceType.View:
8✔
501
        return this.viewVisit(userId, params.parentResourceId);
2✔
502
      case LastVisitResourceType.Dashboard:
8✔
503
        return this.dashboardVisit(userId, params.parentResourceId);
×
504
      case LastVisitResourceType.Automation:
8✔
505
        return this.automationVisit(userId, params.parentResourceId);
×
506
      case LastVisitResourceType.App:
8✔
507
        return this.appVisit(userId, params.parentResourceId);
×
508
      default:
8✔
509
        throw new CustomHttpException('Invalid resource type', HttpErrorCode.VALIDATION_ERROR, {
×
510
          localization: {
×
511
            i18nKey: 'httpErrors.lastVisit.invalidResourceType',
×
512
          },
×
513
        });
×
514
    }
8✔
515
  }
8✔
516

517
  async updateUserLastVisit(userId: string, updateData: IUpdateUserLastVisitRo) {
305✔
518
    this.eventEmitterService.emitAsync(
29✔
519
      Events.LAST_VISIT_UPDATE,
29✔
520
      new LastVisitUpdateEvent(updateData)
29✔
521
    );
522
    const { resourceType, resourceId, parentResourceId, childResourceId } = updateData;
29✔
523

524
    if (resourceType === LastVisitResourceType.Base) {
29✔
525
      await this.updateUserLastVisitRecord({
21✔
526
        userId,
21✔
527
        resourceType: LastVisitResourceType.Base,
21✔
528
        resourceId,
21✔
529
        parentResourceId,
21✔
530
      });
21✔
531
      return;
21✔
532
    }
21✔
533

534
    await this.updateUserLastVisitRecord({
8✔
535
      userId,
8✔
536
      resourceType,
8✔
537
      resourceId,
8✔
538
      parentResourceId,
8✔
539
      maxRecords: 1,
8✔
540
      maxKeys: ['parentResourceId'],
8✔
541
    });
8✔
542

543
    if (childResourceId) {
29✔
544
      await this.updateUserLastVisitRecord({
2✔
545
        userId,
2✔
546
        resourceType: LastVisitResourceType.View,
2✔
547
        resourceId: childResourceId,
2✔
548
        parentResourceId: resourceId,
2✔
549
        maxRecords: 1,
2✔
550
        maxKeys: ['parentResourceId'],
2✔
551
      });
2✔
552
    }
2✔
553
  }
29✔
554

555
  async updateUserLastVisitRecord({
305✔
556
    userId,
31✔
557
    resourceType,
31✔
558
    resourceId,
31✔
559
    maxRecords = 10,
31✔
560
    parentResourceId,
31✔
561
    maxKeys,
31✔
562
  }: {
563
    userId: string;
564
    resourceType: string;
565
    resourceId: string;
566
    parentResourceId: string;
567
    maxRecords?: number;
568
    maxKeys?: 'parentResourceId'[];
569
  }) {
31✔
570
    await this.prismaService.$transaction(async (prisma) => {
31✔
571
      await prisma.userLastVisit.upsert({
31✔
572
        where: {
31✔
573
          userId_resourceType_resourceId: {
31✔
574
            userId,
31✔
575
            resourceType,
31✔
576
            resourceId,
31✔
577
          },
31✔
578
        },
31✔
579
        update: {
31✔
580
          lastVisitTime: new Date().toISOString(),
31✔
581
        },
31✔
582
        create: {
31✔
583
          userId,
31✔
584
          resourceType,
31✔
585
          resourceId,
31✔
586
          parentResourceId,
31✔
587
        },
31✔
588
      });
31✔
589

590
      const oldRecords = await prisma.userLastVisit.findMany({
31✔
591
        where: {
31✔
592
          userId,
31✔
593
          resourceType,
31✔
594
          ...(maxKeys?.includes('parentResourceId') ? { parentResourceId } : {}),
31✔
595
        },
31✔
596
        orderBy: {
31✔
597
          lastVisitTime: 'desc',
31✔
598
        },
31✔
599
        skip: maxRecords,
31✔
600
        select: {
31✔
601
          id: true,
31✔
602
        },
31✔
603
      });
31✔
604

605
      if (oldRecords.length > 0) {
31✔
606
        await prisma.userLastVisit.deleteMany({
13✔
607
          where: {
13✔
608
            id: {
13✔
609
              in: oldRecords.map((record) => record.id),
13✔
610
            },
13✔
611
          },
13✔
612
        });
13✔
613
      }
13✔
614
    });
31✔
615
  }
31✔
616

617
  async getUserLastVisitMap(
305✔
618
    userId: string,
1✔
619
    params: IGetUserLastVisitRo
1✔
620
  ): Promise<IUserLastVisitMapVo> {
1✔
621
    const tables = await this.prismaService.tableMeta.findMany({
1✔
622
      select: {
1✔
623
        id: true,
1✔
624
      },
1✔
625
      where: {
1✔
626
        baseId: params.parentResourceId,
1✔
627
        deletedTime: null,
1✔
628
      },
1✔
629
    });
1✔
630

631
    const query = this.knex
1✔
632
      .select({
1✔
633
        resourceId: 'ulv.resource_id',
1✔
634
        parentResourceId: 'ulv.parent_resource_id',
1✔
635
      })
1✔
636
      .from('user_last_visit as ulv')
1✔
637
      .leftJoin('view as v', function () {
1✔
638
        this.on('v.id', '=', 'ulv.resource_id').andOnNull('v.deleted_time');
1✔
639
      })
1✔
640
      .where('ulv.user_id', userId)
1✔
641
      .where('ulv.resource_type', LastVisitResourceType.View)
1✔
642
      .whereIn(
1✔
643
        'ulv.parent_resource_id',
1✔
644
        tables.map((table) => table.id)
1✔
645
      )
646
      .whereNotNull('v.id');
1✔
647

648
    const sql = query.toQuery();
1✔
649
    const results =
1✔
650
      await this.prismaService.$queryRawUnsafe<(IUserLastVisitVo & { parentResourceId: string })[]>(
1✔
651
        sql
1✔
652
      );
653

654
    // If some tables don't have a last visited view, find their first view
1✔
655
    const tablesWithVisit = new Set(results.map((result) => result.parentResourceId));
1✔
656
    const tablesWithoutVisit = tables.filter((table) => !tablesWithVisit.has(table.id));
1✔
657

658
    if (tablesWithoutVisit.length > 0) {
1✔
659
      const defaultViews = await this.prismaService.view.findMany({
1✔
660
        select: {
1✔
661
          id: true,
1✔
662
          tableId: true,
1✔
663
        },
1✔
664
        where: {
1✔
665
          tableId: {
1✔
666
            in: tablesWithoutVisit.map((t) => t.id),
1✔
667
          },
1✔
668
          deletedTime: null,
1✔
669
        },
1✔
670
        orderBy: {
1✔
671
          order: 'asc',
1✔
672
        },
1✔
673
        distinct: ['tableId'],
1✔
674
      });
1✔
675

676
      // Add default views to results
1✔
677
      for (const view of defaultViews) {
1✔
678
        results.push({
2✔
679
          resourceId: view.id,
2✔
680
          parentResourceId: view.tableId,
2✔
681
          resourceType: LastVisitResourceType.View,
2✔
682
        });
2✔
683
      }
2✔
684
    }
1✔
685

686
    return keyBy(results, 'parentResourceId');
1✔
687
  }
1✔
688

689
  @OnEvent(Events.BASE_DELETE, { async: true })
305✔
690
  @OnEvent(Events.SPACE_DELETE, { async: true })
2,650✔
691
  @OnEvent(Events.TABLE_DELETE, { async: true })
2,650✔
692
  @OnEvent(Events.TABLE_VIEW_DELETE, { async: true })
2,650✔
693
  @OnEvent(Events.DASHBOARD_DELETE, { async: true })
2,650✔
694
  @OnEvent(Events.WORKFLOW_DELETE, { async: true })
2,650✔
695
  @OnEvent(Events.APP_DELETE, { async: true })
2,650✔
696
  protected async resourceDeleteListener(
2,650✔
697
    listenerEvent:
2,650✔
698
      | BaseDeleteEvent
699
      | SpaceDeleteEvent
700
      | TableDeleteEvent
701
      | ViewDeleteEvent
702
      | DashboardDeleteEvent
703
      | WorkflowDeleteEvent
704
      | AppDeleteEvent
2,650✔
705
  ) {
2,650✔
706
    switch (listenerEvent.name) {
2,650✔
707
      case Events.BASE_DELETE:
2,650✔
708
        await this.prismaService.userLastVisit.deleteMany({
169✔
709
          where: {
169✔
710
            OR: [
169✔
711
              {
169✔
712
                resourceId: listenerEvent.payload.baseId,
169✔
713
                resourceType: LastVisitResourceType.Base,
169✔
714
              },
169✔
715
              {
169✔
716
                parentResourceId: listenerEvent.payload.baseId,
169✔
717
                resourceType: LastVisitResourceType.Table,
169✔
718
              },
169✔
719
            ],
720
          },
169✔
721
        });
169✔
722
        break;
169✔
723
      case Events.SPACE_DELETE:
2,650✔
724
        await this.prismaService.userLastVisit.deleteMany({
84✔
725
          where: {
84✔
726
            parentResourceId: listenerEvent.payload.spaceId,
84✔
727
            resourceType: LastVisitResourceType.Base,
84✔
728
          },
84✔
729
        });
84✔
730
        break;
84✔
731
      case Events.TABLE_DELETE:
2,650✔
732
        await this.prismaService.userLastVisit.deleteMany({
2,355✔
733
          where: {
2,355✔
734
            OR: [
2,355✔
735
              {
2,355✔
736
                resourceId: listenerEvent.payload.tableId,
2,355✔
737
                resourceType: LastVisitResourceType.Table,
2,355✔
738
              },
2,355✔
739
              {
2,355✔
740
                parentResourceId: listenerEvent.payload.tableId,
2,355✔
741
                resourceType: LastVisitResourceType.View,
2,355✔
742
              },
2,355✔
743
            ],
744
          },
2,355✔
745
        });
2,355✔
746
        break;
2,355✔
747
      case Events.TABLE_VIEW_DELETE:
2,650✔
748
        await this.prismaService.userLastVisit.deleteMany({
18✔
749
          where: {
18✔
750
            resourceId: listenerEvent.payload.viewId,
18✔
751
            resourceType: LastVisitResourceType.View,
18✔
752
          },
18✔
753
        });
18✔
754
        break;
18✔
755
      case Events.DASHBOARD_DELETE:
2,650✔
756
        await this.prismaService.userLastVisit.deleteMany({
24✔
757
          where: {
24✔
758
            resourceId: listenerEvent.payload.dashboardId,
24✔
759
            resourceType: LastVisitResourceType.Dashboard,
24✔
760
          },
24✔
761
        });
24✔
762
        break;
24✔
763
      case Events.WORKFLOW_DELETE:
2,650✔
764
        await this.prismaService.userLastVisit.deleteMany({
×
765
          where: {
×
766
            resourceId: listenerEvent.payload.workflowId,
×
767
            resourceType: LastVisitResourceType.Automation,
×
768
          },
×
769
        });
×
770
        break;
×
771
      case Events.APP_DELETE:
2,650✔
772
        await this.prismaService.userLastVisit.deleteMany({
×
773
          where: {
×
774
            resourceId: listenerEvent.payload.appId,
×
775
            resourceType: LastVisitResourceType.App,
×
776
          },
×
777
        });
×
778
        break;
×
779
    }
2,650✔
780

781
    this.eventEmitterService.emitAsync(Events.LAST_VISIT_CLEAR, {});
2,650✔
782
  }
2,650✔
783
}
305✔
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