fix: nlp sample inconsistency when language is deleted

This commit is contained in:
Mohamed Marrouchi 2024-09-24 16:21:00 +01:00
parent 82dd888f2f
commit 6652629995
8 changed files with 78 additions and 17 deletions

View File

@ -9,6 +9,7 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
@ -63,6 +64,7 @@ describe('LanguageController', () => {
},
},
LoggerService,
EventEmitter2,
],
}).compile();
languageService = module.get<LanguageService>(LanguageService);

View File

@ -8,16 +8,46 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { Language } from '../schemas/language.schema';
@Injectable()
export class LanguageRepository extends BaseRepository<Language> {
constructor(@InjectModel(Language.name) readonly model: Model<Language>) {
constructor(
@InjectModel(Language.name) readonly model: Model<Language>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Language);
}
/**
* Pre-delete hook that triggers before an language is deleted.
*
* @param query The query used to delete the language.
* @param criteria The filter criteria used to find the language for deletion.
*/
async preDelete(
_query: Query<
DeleteResult,
Document<Language, any, any>,
unknown,
Language,
'deleteOne' | 'deleteMany'
>,
criteria: TFilterQuery<Language>,
): Promise<void> {
if (criteria._id) {
const language = await this.findOne(
typeof criteria === 'string' ? { _id: criteria } : criteria,
);
this.eventEmitter.emit('hook:language:delete', language);
} else {
throw new Error('Attempted to delete language using unknown criteria');
}
}
}

View File

@ -11,6 +11,7 @@ import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
@Schema({ timestamps: true })
export class Language extends BaseSchema {
@ -41,10 +42,10 @@ export class Language extends BaseSchema {
isRTL?: boolean;
}
export const LanguageModel: ModelDefinition = {
export const LanguageModel: ModelDefinition = LifecycleHookManager.attach({
name: Language.name,
schema: SchemaFactory.createForClass(Language),
};
});
export type LanguageDocument = THydratedDocument<Language>;

View File

@ -49,15 +49,15 @@ export class NlpSampleStub extends BaseSchema {
@Prop({
type: MongooseSchema.Types.ObjectId,
ref: 'Language',
required: true,
required: false,
})
language: unknown;
language: unknown | null;
}
@Schema({ timestamps: true })
export class NlpSample extends NlpSampleStub {
@Transform(({ obj }) => obj.language.toString())
language: string;
language: string | null;
@Exclude()
entities?: never;
@ -66,7 +66,7 @@ export class NlpSample extends NlpSampleStub {
@Schema({ timestamps: true })
export class NlpSampleFull extends NlpSampleStub {
@Type(() => Language)
language: Language;
language: Language | null;
@Type(() => NlpSampleEntity)
entities: NlpSampleEntity[];

View File

@ -8,6 +8,7 @@
*/
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
CommonExample,
@ -16,6 +17,7 @@ import {
ExampleEntity,
LookupTable,
} from '@/extensions/helpers/nlp/default/types';
import { Language } from '@/i18n/schemas/language.schema';
import { LanguageService } from '@/i18n/services/language.service';
import { BaseService } from '@/utils/generics/base-service';
@ -140,4 +142,21 @@ export class NlpSampleService extends BaseService<
entity_synonyms,
};
}
/**
* When a language gets deleted, we need to set related samples to null
*
* @param language The language that has been deleted.
*/
@OnEvent('hook:language:delete')
async handleLanguageDelete(language: Language) {
await this.updateMany(
{
language: language.id,
},
{
language: null,
},
);
}
}

View File

@ -12,6 +12,7 @@ import AddIcon from "@mui/icons-material/Add";
import { Button, Grid, Paper } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "react-query";
import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog";
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
@ -21,6 +22,7 @@ import {
} from "@/app-components/tables/columns/getColumns";
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
import { DataGrid } from "@/app-components/tables/DataGrid";
import { isSameEntity } from "@/hooks/crud/helpers";
import { useDelete } from "@/hooks/crud/useDelete";
import { useFind } from "@/hooks/crud/useFind";
import { useUpdate } from "@/hooks/crud/useUpdate";
@ -42,6 +44,7 @@ export const Languages = () => {
const addDialogCtl = useDialog<ILanguage>(false);
const editDialogCtl = useDialog<ILanguage>(false);
const deleteDialogCtl = useDialog<string>(false);
const queryClient = useQueryClient();
const hasPermission = useHasPermission();
const { onSearch, searchPayload } = useSearch<ILanguage>({
$or: ["title", "code"],
@ -66,6 +69,13 @@ export const Languages = () => {
toast.error(t("message.internal_server_error"));
},
onSuccess() {
queryClient.removeQueries({
predicate: ({ queryKey }) => {
const [_qType, qEntity] = queryKey;
return isSameEntity(qEntity, EntityType.NLP_SAMPLE);
},
});
deleteDialogCtl.closeDialog();
toast.success(t("message.item_delete_success"));
},

View File

@ -104,7 +104,6 @@ export default function NlpSample() {
{
label: ActionColumnLabel.Edit,
action: ({ entities, language, ...rest }) => {
const lang = getLanguageFromCache(language) as ILanguage;
const data: INlpDatasetSample = {
...rest,
entities: entities?.map((e) => {
@ -119,7 +118,9 @@ export default function NlpSample() {
entity: getNlpEntityFromCache(entity)?.name || "",
};
}),
language: lang.code,
language: language
? (getLanguageFromCache(language) as ILanguage).code
: null,
};
editDialogCtl.openDialog(data);
@ -186,9 +187,7 @@ export default function NlpSample() {
maxWidth: 90,
field: "language",
renderCell: ({ row }) => {
const language = getLanguageFromCache(row.language);
return language?.title;
return row.language ? getLanguageFromCache(row.language)?.title : "";
},
headerName: t("label.language"),
sortable: true,

View File

@ -24,7 +24,7 @@ export interface INlpSampleAttributes {
trained?: boolean;
type?: NlpSampleType;
entities: string[];
language: string;
language: string | null;
}
export interface INlpSampleStub
@ -33,12 +33,12 @@ export interface INlpSampleStub
export interface INlpSample extends INlpSampleStub, IFormat<Format.BASIC> {
entities: string[];
language: string;
language: string | null;
}
export interface INlpSampleFull extends INlpSampleStub, IFormat<Format.FULL> {
entities: INlpSampleEntity[];
language: ILanguage;
language: ILanguage | null;
}
// Dataset Trainer