Skip to content

Commit

Permalink
fix: auto update name in user-related cell (#823)
Browse files Browse the repository at this point in the history
  • Loading branch information
tea-artist authored Aug 15, 2024
1 parent 9f8ffb0 commit 13081fb
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 22 deletions.
16 changes: 16 additions & 0 deletions apps/nestjs-backend/src/db-provider/db.provider.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ export interface IDbProvider {

dropColumn(tableName: string, columnName: string): string[];

updateJsonColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string;

updateJsonArrayColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string;

// sql response format: { name: string }[], name for columnName.
columnInfo(tableName: string): string;

Expand Down
52 changes: 52 additions & 0 deletions apps/nestjs-backend/src/db-provider/postgres.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,58 @@ export class PostgresProvider implements IDbProvider {
.toQuery();
}

updateJsonColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string {
return this.knex(tableName)
.where(this.knex.raw(`"${columnName}"->>'id' = ?`, [id]))
.update({
[columnName]: this.knex.raw(
`
jsonb_set(
"${columnName}",
'{${key}}',
to_jsonb(?::text)
)
`,
[value]
),
})
.toQuery();
}

updateJsonArrayColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string {
return this.knex(tableName)
.update({
[columnName]: this.knex.raw(
`
(
SELECT jsonb_agg(
CASE
WHEN elem->>'id' = ?
THEN jsonb_set(elem, '{${key}}', to_jsonb(?::text))
ELSE elem
END
)
FROM jsonb_array_elements("${columnName}") AS elem
)
`,
[id, value]
),
})
.toQuery();
}

modifyColumnSchema(tableName: string, columnName: string, schemaType: SchemaType): string[] {
return [
this.knex.schema
Expand Down
48 changes: 48 additions & 0 deletions apps/nestjs-backend/src/db-provider/sqlite.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,54 @@ export class SqliteProvider implements IDbProvider {
return this.knex.raw(`PRAGMA table_info(??)`, [tableName]).toQuery();
}

updateJsonColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string {
return this.knex(tableName)
.where(this.knex.raw(`json_extract(${columnName}, '$.id') = ?`, [id]))
.update({
[columnName]: this.knex.raw(
`
json_patch(${columnName}, json_object(?, ?))
`,
[key, value]
),
})
.toQuery();
}

updateJsonArrayColumn(
tableName: string,
columnName: string,
id: string,
key: string,
value: string
): string {
return this.knex(tableName)
.update({
[columnName]: this.knex.raw(
`
(
SELECT json_group_array(
CASE
WHEN json_extract(value, '$.id') = ?
THEN json_patch(value, json_object(?, ?))
ELSE value
END
)
FROM json_each(${columnName})
)
`,
[id, key, value]
),
})
.toQuery();
}

duplicateTable(
fromSchema: string,
toSchema: string,
Expand Down
4 changes: 3 additions & 1 deletion apps/nestjs-backend/src/event-emitter/events/event.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ export enum Events {
TABLE_VIEW_DELETE = 'table.view.delete',
TABLE_VIEW_UPDATE = 'table.view.update',

TABLE_USER_RENAME_COMPLETE = 'table.user.rename.complete',

SHARED_VIEW_CREATE = 'shared.view.create',
SHARED_VIEW_DELETE = 'shared.view.delete',
SHARED_VIEW_UPDATE = 'shared.view.update',

USER_SIGNIN = 'user.signin',
USER_SIGNUP = 'user.signup',
USER_RENAME = 'user.rename',
USER_SIGNOUT = 'user.signout',
USER_UPDATE = 'user.update',
USER_DELETE = 'user.delete',

// USER_PASSWORD_RESET = 'user.password.reset',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,15 @@ export class InvitationService {
throw new BadRequestException('Space not found');
}

const invitationEmails = data.emails.map((email) => email.toLowerCase());
const sendUsers = await this.prismaService.user.findMany({
select: { id: true, name: true, email: true },
where: { email: { in: data.emails } },
where: { email: { in: invitationEmails } },
});

const noExistEmails = data.emails.filter((email) => !sendUsers.find((u) => u.email === email));
const noExistEmails = invitationEmails.filter(
(email) => !sendUsers.find((u) => u.email.toLowerCase() === email.toLowerCase())
);

return await this.prismaService.$tx(async () => {
// create user if not exist
Expand Down
3 changes: 2 additions & 1 deletion apps/nestjs-backend/src/features/record/record.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { DbProvider } from '../../db-provider/db.provider';
import { AttachmentsStorageModule } from '../attachments/attachments-storage.module';
import { CalculationModule } from '../calculation/calculation.module';
import { RecordService } from './record.service';
import { UserNameListener } from './user-name.listener.service';

@Module({
imports: [CalculationModule, AttachmentsStorageModule],
providers: [RecordService, DbProvider],
providers: [UserNameListener, RecordService, DbProvider],
exports: [RecordService],
})
export class RecordModule {}
39 changes: 37 additions & 2 deletions apps/nestjs-backend/src/features/record/typecast.validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ interface IServices {
collaboratorService: CollaboratorService;
}

interface IObjectType {
id?: string;
title?: string;
name?: string;
email?: string;
}

const convertUser = (input: unknown): string | undefined => {
if (typeof input === 'string') return input;

if (Array.isArray(input)) {
if (input.every((item) => typeof item === 'string')) {
return input.join();
}
if (input.every((item) => typeof item === 'object' && item !== null)) {
return (
input
.map((item) => convertUser(item as IObjectType))
.filter(Boolean)
.join() || undefined
);
}
return undefined;
}

if (typeof input === 'object' && input !== null) {
const obj = input as IObjectType;
return obj.id ?? obj.email ?? obj.title ?? obj.name ?? undefined;
}

return undefined;
};

/**
* Cell type conversion:
* Because there are some merge operations, we choose column-by-column conversion here.
Expand Down Expand Up @@ -216,9 +249,11 @@ export class TypeCastAndValidate {

private async castToUser(cellValues: unknown[]): Promise<unknown[]> {
const ctx = await this.services.collaboratorService.getBaseCollabsWithPrimary(this.tableId);

return this.mapFieldsCellValuesWithValidate(cellValues, (cellValue: unknown) => {
if (typeof cellValue === 'string') {
const cv = (this.field as UserFieldCore).convertStringToCellValue(cellValue, {
const strValue = convertUser(cellValue);
if (strValue) {
const cv = (this.field as UserFieldCore).convertStringToCellValue(strValue, {
userSets: ctx,
});
if (Array.isArray(cv)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Field, PrismaService } from '@teable/db-main-prisma';
import { IUserInfoVo } from '@teable/openapi';
import { Knex } from 'knex';
import { InjectModel } from 'nest-knexjs';
import { InjectDbProvider } from '../../db-provider/db.provider';
import { IDbProvider } from '../../db-provider/db.provider.interface';
import { EventEmitterService } from '../../event-emitter/event-emitter.service';
import { Events } from '../../event-emitter/events';
import { Timing } from '../../utils/timing';

@Injectable()
export class UserNameListener {
private readonly logger = new Logger(UserNameListener.name);

constructor(
private readonly prismaService: PrismaService,
private readonly eventEmitterService: EventEmitterService,
@InjectDbProvider() private readonly dbProvider: IDbProvider,
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex
) {}

private async getFieldsForUser(userId: string) {
const query = this.knex('collaborator')
.join('space', 'collaborator.space_id', 'space.id')
.join('base', 'space.id', 'base.space_id')
.join('table_meta', 'base.id', 'table_meta.base_id')
.join('field', 'table_meta.id', 'field.table_id')
.where('collaborator.user_id', userId)
.whereIn('field.type', ['user', 'createdBy', 'lastModifiedBy'])
.whereNull('collaborator.deleted_time')
.whereNull('space.deleted_time')
.whereNull('base.deleted_time')
.whereNull('table_meta.deleted_time')
.whereNull('field.deleted_time')
.select({
id: 'field.id',
tableId: 'field.table_id',
type: 'field.type',
dbFieldName: 'field.db_field_name',
isMultipleCellValue: 'field.is_multiple_cell_value',
})
.toQuery();

return this.prismaService.$queryRawUnsafe<Field[]>(query);
}

@Timing()
private async updateUserFieldName(field: Field, id: string, name: string) {
const tableId = field.tableId;
const { dbTableName } = await this.prismaService.tableMeta.findUniqueOrThrow({
where: { id: tableId },
select: { dbTableName: true },
});

const sql = field.isMultipleCellValue
? this.dbProvider.updateJsonArrayColumn(dbTableName, field.dbFieldName, id, 'title', name)
: this.dbProvider.updateJsonColumn(dbTableName, field.dbFieldName, id, 'title', name);

return await this.prismaService.$executeRawUnsafe(sql);
}

@OnEvent(Events.USER_RENAME, { async: true })
async updateUserName(user: IUserInfoVo) {
const fields = await this.getFieldsForUser(user.id);

this.logger.log(`Updating user name for ${fields.length} fields`);

for (const field of fields) {
try {
await this.updateUserFieldName(field, user.id, user.name);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
this.logger.error(e.message, e.stack);
}
}

this.eventEmitterService.emit(Events.TABLE_USER_RENAME_COMPLETE, user);
}
}
12 changes: 10 additions & 2 deletions apps/nestjs-backend/src/features/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from '@teable/core';
import type { Prisma } from '@teable/db-main-prisma';
import { PrismaService } from '@teable/db-main-prisma';
import { type ICreateSpaceRo, type IUserNotifyMeta, UploadType } from '@teable/openapi';
import { UploadType } from '@teable/openapi';
import type { IUserInfoVo, ICreateSpaceRo, IUserNotifyMeta } from '@teable/openapi';
import { ClsService } from 'nestjs-cls';
import sharp from 'sharp';
import { BaseConfig, IBaseConfig } from '../../configs/base.config';
Expand Down Expand Up @@ -150,12 +151,19 @@ export class UserService {
}

async updateUserName(id: string, name: string) {
await this.prismaService.txClient().user.update({
const user: IUserInfoVo = await this.prismaService.txClient().user.update({
data: {
name,
},
where: { id, deletedTime: null },
select: {
id: true,
name: true,
email: true,
avatar: true,
},
});
this.eventEmitterService.emitAsync(Events.USER_RENAME, user);
}

async updateAvatar(id: string, avatarFile: { path: string; mimetype: string; size: number }) {
Expand Down
15 changes: 8 additions & 7 deletions apps/nestjs-backend/src/utils/timing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export function Timing(customLoggerKey?: string): MethodDecorator {

const printLog = () => {
const end = process.hrtime.bigint();
logger.verbose(
`${className} - ${String(customLoggerKey || propertyKey)} Execution Time: ${
(end - start) / BigInt(1000000)
} ms; Heap Usage: ${
Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
} MB`
);
const gap = (end - start) / BigInt(1000000);
if (gap > 100) {
logger.log(
`${className} - ${String(customLoggerKey || propertyKey)} Execution Time: ${gap} ms; Heap Usage: ${
Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
} MB`
);
}
};

if (result instanceof Promise) {
Expand Down
Loading

0 comments on commit 13081fb

Please sign in to comment.