feat: implement strict null check

This commit is contained in:
yassinedorbozgithub 2025-01-13 20:10:19 +01:00
parent 11ef58d048
commit d2223d6bfd
19 changed files with 84 additions and 83 deletions

View File

@ -27,7 +27,7 @@ import {
import { attachment, attachmentFile } from '../mocks/attachment.mock'; import { attachment, attachmentFile } from '../mocks/attachment.mock';
import { AttachmentRepository } from '../repositories/attachment.repository'; import { AttachmentRepository } from '../repositories/attachment.repository';
import { AttachmentModel, Attachment } from '../schemas/attachment.schema'; import { Attachment, AttachmentModel } from '../schemas/attachment.schema';
import { AttachmentService } from '../services/attachment.service'; import { AttachmentService } from '../services/attachment.service';
import { AttachmentController } from './attachment.controller'; import { AttachmentController } from './attachment.controller';
@ -35,7 +35,7 @@ import { AttachmentController } from './attachment.controller';
describe('AttachmentController', () => { describe('AttachmentController', () => {
let attachmentController: AttachmentController; let attachmentController: AttachmentController;
let attachmentService: AttachmentService; let attachmentService: AttachmentService;
let attachmentToDelete: Attachment; let attachmentToDelete: Attachment | null;
beforeAll(async () => { beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -79,7 +79,7 @@ describe('AttachmentController', () => {
describe('Upload', () => { describe('Upload', () => {
it('should throw BadRequestException if no file is selected to be uploaded', async () => { it('should throw BadRequestException if no file is selected to be uploaded', async () => {
const promiseResult = attachmentController.uploadFile({ const promiseResult = attachmentController.uploadFile({
file: undefined, file: [],
}); });
await expect(promiseResult).rejects.toThrow( await expect(promiseResult).rejects.toThrow(
new BadRequestException('No file was selected'), new BadRequestException('No file was selected'),
@ -121,17 +121,17 @@ describe('AttachmentController', () => {
name: 'store1.jpg', name: 'store1.jpg',
}); });
const result = await attachmentController.download({ const result = await attachmentController.download({
id: storedAttachment.id, id: storedAttachment!.id,
}); });
expect(attachmentService.findOne).toHaveBeenCalledWith( expect(attachmentService.findOne).toHaveBeenCalledWith(
storedAttachment.id, storedAttachment!.id,
); );
expect(result.options).toEqual({ expect(result?.options).toEqual({
type: storedAttachment.type, type: storedAttachment!.type,
length: storedAttachment.size, length: storedAttachment!.size,
disposition: `attachment; filename="${encodeURIComponent( disposition: `attachment; filename="${encodeURIComponent(
storedAttachment.name, storedAttachment!.name,
)}"`, )}"`,
}); });
}); });
@ -141,11 +141,11 @@ describe('AttachmentController', () => {
it('should delete an attachment by id', async () => { it('should delete an attachment by id', async () => {
jest.spyOn(attachmentService, 'deleteOne'); jest.spyOn(attachmentService, 'deleteOne');
const result = await attachmentController.deleteOne( const result = await attachmentController.deleteOne(
attachmentToDelete.id, attachmentToDelete!.id,
); );
expect(attachmentService.deleteOne).toHaveBeenCalledWith( expect(attachmentService.deleteOne).toHaveBeenCalledWith(
attachmentToDelete.id, attachmentToDelete!.id,
); );
expect(result).toEqual({ expect(result).toEqual({
acknowledged: true, acknowledged: true,
@ -155,10 +155,10 @@ describe('AttachmentController', () => {
it('should throw a NotFoundException when attempting to delete an attachment by id', async () => { it('should throw a NotFoundException when attempting to delete an attachment by id', async () => {
await expect( await expect(
attachmentController.deleteOne(attachmentToDelete.id), attachmentController.deleteOne(attachmentToDelete!.id),
).rejects.toThrow( ).rejects.toThrow(
new NotFoundException( new NotFoundException(
`Attachment with ID ${attachmentToDelete.id} not found`, `Attachment with ID ${attachmentToDelete!.id} not found`,
), ),
); );
}); });

View File

@ -146,7 +146,7 @@ export class AttachmentController extends BaseController<Attachment> {
@Get('download/:id/:filename?') @Get('download/:id/:filename?')
async download( async download(
@Param() params: AttachmentDownloadDto, @Param() params: AttachmentDownloadDto,
): Promise<StreamableFile> { ): Promise<StreamableFile | undefined> {
const attachment = await this.attachmentService.findOne(params.id); const attachment = await this.attachmentService.findOne(params.id);
if (!attachment) { if (!attachment) {

View File

@ -100,32 +100,31 @@ export default class LlmNluHelper
* *
* @returns An array of objects representing the found entities, with their `value`, `start`, and `end` positions. * @returns An array of objects representing the found entities, with their `value`, `start`, and `end` positions.
*/ */
private findKeywordEntities( private findKeywordEntities(text: string, entity: NlpEntityFull) {
text: string, return (
entity: NlpEntityFull, entity.values
): NLU.ParseEntity[] { .flatMap(({ value, expressions }) => {
return entity.values const allValues = [value, ...expressions];
.flatMap(({ value, expressions }) => {
const allValues = [value, ...expressions];
// Filter the terms that are found in the text // Filter the terms that are found in the text
return allValues return allValues
.flatMap((term) => { .flatMap((term) => {
const regex = new RegExp(`\\b${term}\\b`, 'g'); const regex = new RegExp(`\\b${term}\\b`, 'g');
const matches = [...text.matchAll(regex)]; const matches = [...text.matchAll(regex)];
// Map matches to FoundEntity format // Map matches to FoundEntity format
return matches.map((match) => ({ return matches.map((match) => ({
entity: entity.name, entity: entity.name,
value: term, value: term,
start: match.index!, start: match.index!,
end: match.index! + term.length, end: match.index! + term.length,
confidence: 1, confidence: 1,
})); }));
}) })
.shift(); .shift();
}) })
.filter((v) => !!v); .filter((v) => !!v) || []
);
} }
async predict(text: string): Promise<NLU.ParseEntities> { async predict(text: string): Promise<NLU.ParseEntities> {
@ -133,7 +132,7 @@ export default class LlmNluHelper
const helper = await this.helperService.getDefaultLlmHelper(); const helper = await this.helperService.getDefaultLlmHelper();
const defaultLanguage = await this.languageService.getDefaultLanguage(); const defaultLanguage = await this.languageService.getDefaultLanguage();
// Detect language // Detect language
const language = await helper.generateStructuredResponse<string>( const language = await helper.generateStructuredResponse<string>?.(
`input text: ${text}`, `input text: ${text}`,
settings.model, settings.model,
this.languageClassifierPrompt, this.languageClassifierPrompt,
@ -147,13 +146,13 @@ export default class LlmNluHelper
{ {
entity: 'language', entity: 'language',
value: language || defaultLanguage.code, value: language || defaultLanguage.code,
confidence: undefined, confidence: 0.0,
}, },
]; ];
for await (const { name, doc, prompt, values } of this for await (const { name, doc, prompt, values } of this
.traitClassifierPrompts) { .traitClassifierPrompts) {
const allowedValues = values.map(({ value }) => value); const allowedValues = values.map(({ value }) => value);
const result = await helper.generateStructuredResponse<string>( const result = await helper.generateStructuredResponse<string>?.(
`input text: ${text}`, `input text: ${text}`,
settings.model, settings.model,
prompt, prompt,
@ -163,12 +162,13 @@ export default class LlmNluHelper
enum: allowedValues.concat('unknown'), enum: allowedValues.concat('unknown'),
}, },
); );
const safeValue = result.toLowerCase().trim(); const safeValue = result?.toLowerCase().trim();
const value = allowedValues.includes(safeValue) ? safeValue : ''; const value =
safeValue && allowedValues.includes(safeValue) ? safeValue : '';
traits.push({ traits.push({
entity: name, entity: name,
value, value,
confidence: undefined, confidence: 0.0,
}); });
} }
@ -179,7 +179,7 @@ export default class LlmNluHelper
}); });
const entities = keywordEntities.flatMap((keywordEntity) => const entities = keywordEntities.flatMap((keywordEntity) =>
this.findKeywordEntities(text, keywordEntity), this.findKeywordEntities(text, keywordEntity),
); ) as NLU.ParseEntity[];
return { entities: traits.concat(entities) }; return { entities: traits.concat(entities) };
} }

View File

@ -186,7 +186,7 @@ describe('MigrationService', () => {
await service.run({ await service.run({
action: MigrationAction.UP, action: MigrationAction.UP,
version: null, version: undefined,
isAutoMigrate: false, isAutoMigrate: false,
}); });

View File

@ -28,6 +28,7 @@ import { Migration, MigrationDocument } from './migration.schema';
import { import {
MigrationAction, MigrationAction,
MigrationName, MigrationName,
MigrationRunOneParams,
MigrationRunParams, MigrationRunParams,
MigrationSuccessCallback, MigrationSuccessCallback,
MigrationVersion, MigrationVersion,
@ -237,7 +238,7 @@ module.exports = {
* *
* @returns Resolves when the migration action is successfully executed or stops if the migration already exists. * @returns Resolves when the migration action is successfully executed or stops if the migration already exists.
*/ */
private async runOne({ version, action }: MigrationRunParams) { private async runOne({ version, action }: MigrationRunOneParams) {
// Verify DB status // Verify DB status
const { exist, migrationDocument } = await this.verifyStatus({ const { exist, migrationDocument } = await this.verifyStatus({
version, version,
@ -255,7 +256,7 @@ module.exports = {
http: this.httpService, http: this.httpService,
}); });
if (result) { if (result && migrationDocument) {
await this.successCallback({ await this.successCallback({
version, version,
action, action,

View File

@ -27,6 +27,10 @@ export interface MigrationRunParams {
isAutoMigrate?: boolean; isAutoMigrate?: boolean;
} }
export interface MigrationRunOneParams extends MigrationRunParams {
version: MigrationVersion;
}
export interface MigrationSuccessCallback extends MigrationRunParams { export interface MigrationSuccessCallback extends MigrationRunParams {
migrationDocument: MigrationDocument; migrationDocument: MigrationDocument;
} }

View File

@ -109,7 +109,7 @@ describe('NlpEntityController', () => {
acc.push({ acc.push({
...curr, ...curr,
values: nlpValueFixtures.filter( values: nlpValueFixtures.filter(
({ entity }) => parseInt(entity) === index, ({ entity }) => parseInt(entity!) === index,
) as NlpEntityFull['values'], ) as NlpEntityFull['values'],
lookups: curr.lookups!, lookups: curr.lookups!,
builtin: curr.builtin!, builtin: curr.builtin!,

View File

@ -98,7 +98,7 @@ describe('NlpValueController', () => {
acc.push({ acc.push({
...curr, ...curr,
entity: nlpEntityFixtures[ entity: nlpEntityFixtures[
parseInt(curr.entity) parseInt(curr.entity!)
] as NlpValueFull['entity'], ] as NlpValueFull['entity'],
builtin: curr.builtin!, builtin: curr.builtin!,
expressions: curr.expressions!, expressions: curr.expressions!,
@ -125,7 +125,7 @@ describe('NlpValueController', () => {
(acc, curr) => { (acc, curr) => {
const ValueWithEntities = { const ValueWithEntities = {
...curr, ...curr,
entity: nlpEntities[parseInt(curr.entity)].id, entity: nlpEntities[parseInt(curr.entity!)].id,
expressions: curr.expressions!, expressions: curr.expressions!,
metadata: curr.metadata!, metadata: curr.metadata!,
builtin: curr.builtin!, builtin: curr.builtin!,

View File

@ -75,8 +75,9 @@ export class NlpValueController extends BaseController<
this.validate({ this.validate({
dto: createNlpValueDto, dto: createNlpValueDto,
allowedIds: { allowedIds: {
entity: (await this.nlpEntityService.findOne(createNlpValueDto.entity)) entity: createNlpValueDto.entity
?.id, ? (await this.nlpEntityService.findOne(createNlpValueDto.entity))?.id
: null,
}, },
}); });
return await this.nlpValueService.create(createNlpValueDto); return await this.nlpValueService.create(createNlpValueDto);
@ -167,7 +168,10 @@ export class NlpValueController extends BaseController<
@Param('id') id: string, @Param('id') id: string,
@Body() updateNlpValueDto: NlpValueUpdateDto, @Body() updateNlpValueDto: NlpValueUpdateDto,
): Promise<NlpValue> { ): Promise<NlpValue> {
const result = await this.nlpValueService.updateOne(id, updateNlpValueDto); const result = await this.nlpValueService.updateOne(id, {
...updateNlpValueDto,
entity: updateNlpValueDto.entity || undefined,
});
if (!result) { if (!result) {
this.logger.warn(`Unable to update NLP Value by id ${id}`); this.logger.warn(`Unable to update NLP Value by id ${id}`);
throw new NotFoundException(`NLP Value with ID ${id} not found`); throw new NotFoundException(`NLP Value with ID ${id} not found`);

View File

@ -49,7 +49,7 @@ export class NlpValueCreateDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsObjectId({ message: 'Entity must be a valid ObjectId' }) @IsObjectId({ message: 'Entity must be a valid ObjectId' })
entity: string; entity: string | null;
} }
export class NlpValueUpdateDto extends PartialType(NlpValueCreateDto) {} export class NlpValueUpdateDto extends PartialType(NlpValueCreateDto) {}

View File

@ -87,7 +87,7 @@ describe('NlpValueRepository', () => {
const ValueWithEntities = { const ValueWithEntities = {
...curr, ...curr,
entity: nlpEntityFixtures[ entity: nlpEntityFixtures[
parseInt(curr.entity) parseInt(curr.entity!)
] as NlpValueFull['entity'], ] as NlpValueFull['entity'],
builtin: curr.builtin!, builtin: curr.builtin!,
expressions: curr.expressions!, expressions: curr.expressions!,

View File

@ -38,7 +38,7 @@ export class NlpValueSeeder extends BaseSeeder<
const entities = await this.nlpEntityRepository.findAll(); const entities = await this.nlpEntityRepository.findAll();
const modelDtos = models.map((v) => ({ const modelDtos = models.map((v) => ({
...v, ...v,
entity: entities.find(({ name }) => name === v.entity)?.id, entity: entities.find(({ name }) => name === v.entity)?.id || null,
})); }));
await this.repository.createMany(modelDtos); await this.repository.createMany(modelDtos);
return true; return true;

View File

@ -69,9 +69,7 @@ describe('NlpValueService', () => {
nlpValues = await nlpValueRepository.findAll(); nlpValues = await nlpValueRepository.findAll();
}); });
afterAll(async () => { afterAll(closeInMongodConnection);
await closeInMongodConnection();
});
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
@ -94,9 +92,7 @@ describe('NlpValueService', () => {
(acc, curr) => { (acc, curr) => {
const ValueWithEntities = { const ValueWithEntities = {
...curr, ...curr,
entity: nlpEntityFixtures[ entity: nlpEntityFixtures[parseInt(curr.entity!)] as NlpEntity,
parseInt(curr.entity)
] as NlpValueFull['entity'],
expressions: curr.expressions!, expressions: curr.expressions!,
metadata: curr.metadata!, metadata: curr.metadata!,
builtin: curr.builtin!, builtin: curr.builtin!,

View File

@ -11,11 +11,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { DeleteResult } from '@/utils/generics/base-repository'; import { DeleteResult } from '@/utils/generics/base-repository';
import { BaseService } from '@/utils/generics/base-service'; import { BaseService } from '@/utils/generics/base-service';
import { import { NlpValueCreateDto, NlpValueDto } from '../dto/nlp-value.dto';
NlpValueCreateDto,
NlpValueDto,
NlpValueUpdateDto,
} from '../dto/nlp-value.dto';
import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpValueRepository } from '../repositories/nlp-value.repository';
import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NlpEntity } from '../schemas/nlp-entity.schema';
import { import {
@ -139,7 +135,7 @@ export class NlpValueService extends BaseService<
expressions: vMap[e.value].expressions?.concat([ expressions: vMap[e.value].expressions?.concat([
sampleText.slice(e.start, e.end), sampleText.slice(e.start, e.end),
]), ]),
} as NlpValueUpdateDto); });
}); });
await Promise.all(synonymsToAdd); await Promise.all(synonymsToAdd);

View File

@ -48,7 +48,7 @@ export async function moveFiles(
const files = await fs.promises.readdir(sourceFolder); const files = await fs.promises.readdir(sourceFolder);
// Filter only files (skip directories) // Filter only files (skip directories)
const filePaths = []; const filePaths: string[] = [];
for (const file of files) { for (const file of files) {
const filePath = join(sourceFolder, file); const filePath = join(sourceFolder, file);
const stat = await fs.promises.stat(filePath); const stat = await fs.promises.stat(filePath);

View File

@ -7,10 +7,10 @@
*/ */
import { import {
Injectable,
PipeTransform,
ArgumentMetadata, ArgumentMetadata,
BadRequestException, BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common'; } from '@nestjs/common';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
@ -33,7 +33,7 @@ export class ObjectIdPipe implements PipeTransform<string, Promise<string>> {
async transform(value: string, { type, data }: ArgumentMetadata) { async transform(value: string, { type, data }: ArgumentMetadata) {
if (typeof value === 'string' && data === 'id' && type === 'param') { if (typeof value === 'string' && data === 'id' && type === 'param') {
const errors = await this.getErrors(value); const errors = await this.getErrors(value);
if (errors) if (errors.constraints)
throw new BadRequestException(Object.values(errors.constraints)[0]); throw new BadRequestException(Object.values(errors.constraints)[0]);
} else if ( } else if (
typeof value === 'object' && typeof value === 'object' &&
@ -45,7 +45,7 @@ export class ObjectIdPipe implements PipeTransform<string, Promise<string>> {
if (param.startsWith('id')) { if (param.startsWith('id')) {
const errors = await this.getErrors(String(paramValue)); const errors = await this.getErrors(String(paramValue));
if (errors) if (errors.constraints)
throw new BadRequestException( throw new BadRequestException(
Object.values(errors.constraints)[0], Object.values(errors.constraints)[0],
); );

View File

@ -136,8 +136,10 @@ export const installConversationTypeFixtures = async () => {
conversationFixtures.map((conversationFixture) => ({ conversationFixtures.map((conversationFixture) => ({
...conversationFixture, ...conversationFixture,
sender: subscribers[parseInt(conversationFixture.sender)].id, sender: subscribers[parseInt(conversationFixture.sender)].id,
current: blocks[parseInt(conversationFixture.current)].id, current: conversationFixture?.current
next: conversationFixture.next.map((n) => blocks[parseInt(n)].id), ? blocks[parseInt(conversationFixture.current)].id
: null,
next: conversationFixture.next?.map((n) => blocks[parseInt(n)].id),
})), })),
); );
}; };

View File

@ -51,12 +51,10 @@ export const installNlpValueFixtures = async () => {
const NlpValue = mongoose.model(NlpValueModel.name, NlpValueModel.schema); const NlpValue = mongoose.model(NlpValueModel.name, NlpValueModel.schema);
const nlpValues = await NlpValue.insertMany( const nlpValues = await NlpValue.insertMany(
nlpValueFixtures.map((v) => { nlpValueFixtures.map((v) => ({
return { ...v,
...v, entity: v?.entity ? nlpEntities[parseInt(v.entity)].id : null,
entity: nlpEntities[parseInt(v.entity)].id, })),
};
}),
); );
return { nlpEntities, nlpValues }; return { nlpEntities, nlpValues };
}; };

View File

@ -13,7 +13,7 @@
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,