Merge pull request #563 from Hexastack/562-issue-strict-null-check
Some checks failed
Build and Push Docker API Image / build-and-push (push) Has been cancelled
Build and Push Docker Base Image / build-and-push (push) Has been cancelled
Build and Push Docker UI Image / build-and-push (push) Has been cancelled

feat: implement strict null checks
This commit is contained in:
Med Marrouchi 2025-01-15 18:03:15 +01:00 committed by GitHub
commit 47e8056a15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 128 additions and 87 deletions

View File

@ -27,7 +27,7 @@ import {
import { attachment, attachmentFile } from '../mocks/attachment.mock';
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 { AttachmentController } from './attachment.controller';
@ -55,14 +55,12 @@ describe('AttachmentController', () => {
attachmentController =
module.get<AttachmentController>(AttachmentController);
attachmentService = module.get<AttachmentService>(AttachmentService);
attachmentToDelete = await attachmentService.findOne({
attachmentToDelete = (await attachmentService.findOne({
name: 'store1.jpg',
});
}))!;
});
afterAll(async () => {
await closeInMongodConnection();
});
afterAll(closeInMongodConnection);
afterEach(jest.clearAllMocks);
@ -79,7 +77,7 @@ describe('AttachmentController', () => {
describe('Upload', () => {
it('should throw BadRequestException if no file is selected to be uploaded', async () => {
const promiseResult = attachmentController.uploadFile({
file: undefined,
file: [],
});
await expect(promiseResult).rejects.toThrow(
new BadRequestException('No file was selected'),
@ -117,9 +115,9 @@ describe('AttachmentController', () => {
it('should download the attachment by id', async () => {
jest.spyOn(attachmentService, 'findOne');
const storedAttachment = await attachmentService.findOne({
const storedAttachment = (await attachmentService.findOne({
name: 'store1.jpg',
});
}))!;
const result = await attachmentController.download({
id: storedAttachment.id,
});
@ -127,7 +125,7 @@ describe('AttachmentController', () => {
expect(attachmentService.findOne).toHaveBeenCalledWith(
storedAttachment.id,
);
expect(result.options).toEqual({
expect(result?.options).toEqual({
type: storedAttachment.type,
length: storedAttachment.size,
disposition: `attachment; filename="${encodeURIComponent(

View File

@ -256,9 +256,15 @@ export class AttachmentService extends BaseService<Attachment> {
async download(
attachment: Attachment,
rootDir = config.parameters.uploadDir,
) {
): Promise<StreamableFile> {
if (this.getStoragePlugin()) {
return await this.getStoragePlugin()?.download(attachment);
const streamableFile =
await this.getStoragePlugin()?.download(attachment);
if (!streamableFile) {
throw new NotFoundException('No file was found');
}
return streamableFile;
} else {
const path = resolve(join(rootDir, attachment.location));

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

View File

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

View File

@ -29,6 +29,7 @@ import { Migration, MigrationDocument } from './migration.schema';
import {
MigrationAction,
MigrationName,
MigrationRunOneParams,
MigrationRunParams,
MigrationSuccessCallback,
MigrationVersion,
@ -239,7 +240,7 @@ module.exports = {
*
* @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
const { exist, migrationDocument } = await this.verifyStatus({
version,
@ -258,7 +259,7 @@ module.exports = {
attachmentService: this.attachmentService,
});
if (result) {
if (result && migrationDocument) {
await this.successCallback({
version,
action,

View File

@ -442,7 +442,10 @@ const migrateAttachmentMessages = async ({
type: response.headers['content-type'],
channel: {},
});
await updateAttachmentId(msg._id, attachment.id);
if (attachment) {
await updateAttachmentId(msg._id, attachment.id);
}
}
} else {
logger.warn(

View File

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

View File

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

View File

@ -98,7 +98,7 @@ describe('NlpValueController', () => {
acc.push({
...curr,
entity: nlpEntityFixtures[
parseInt(curr.entity)
parseInt(curr.entity!)
] as NlpValueFull['entity'],
builtin: curr.builtin!,
expressions: curr.expressions!,
@ -125,7 +125,7 @@ describe('NlpValueController', () => {
(acc, curr) => {
const ValueWithEntities = {
...curr,
entity: nlpEntities[parseInt(curr.entity)].id,
entity: curr.entity ? nlpEntities[parseInt(curr.entity!)].id : null,
expressions: curr.expressions!,
metadata: curr.metadata!,
builtin: curr.builtin!,
@ -133,7 +133,7 @@ describe('NlpValueController', () => {
acc.push(ValueWithEntities);
return acc;
},
[] as TFixtures<NlpValue>[],
[] as TFixtures<NlpValueCreateDto>[],
);
expect(result).toEqualPayload(nlpValueFixturesWithEntities);
});

View File

@ -75,8 +75,9 @@ export class NlpValueController extends BaseController<
this.validate({
dto: createNlpValueDto,
allowedIds: {
entity: (await this.nlpEntityService.findOne(createNlpValueDto.entity))
?.id,
entity: createNlpValueDto.entity
? (await this.nlpEntityService.findOne(createNlpValueDto.entity))?.id
: null,
},
});
return await this.nlpValueService.create(createNlpValueDto);

View File

@ -6,7 +6,6 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { PartialType } from '@nestjs/mapped-types';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsArray,
@ -49,11 +48,42 @@ export class NlpValueCreateDto {
@IsString()
@IsNotEmpty()
@IsObjectId({ message: 'Entity must be a valid ObjectId' })
entity: string;
entity: string | null;
}
export class NlpValueUpdateDto extends PartialType(NlpValueCreateDto) {}
export class NlpValueUpdateDto {
@ApiPropertyOptional({ description: 'Foreign ID', type: String })
@IsOptional()
@IsString()
foreign_id?: string;
@ApiPropertyOptional({ description: 'Nlp value', type: String })
@IsOptional()
@IsString()
value?: string;
@ApiPropertyOptional({
description: 'Nlp value expressions',
isArray: true,
type: Array,
})
@IsOptional()
@IsArray()
expressions?: string[];
@ApiPropertyOptional({ description: 'Nlp value entity', type: String })
@IsOptional()
@IsString()
@IsObjectId({ message: 'Entity must be a valid ObjectId' })
entity?: string | null;
@ApiPropertyOptional({ description: 'Nlp value is builtin', type: Boolean })
@IsOptional()
@IsBoolean()
builtin?: boolean;
}
export type NlpValueDto = DtoConfig<{
create: NlpValueCreateDto;
update: NlpValueUpdateDto;
}>;

View File

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

View File

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

View File

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

View File

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

View File

@ -478,7 +478,7 @@ export abstract class BaseRepository<
async updateOne<D extends Partial<U>>(
criteria: string | TFilterQuery<T>,
dto: UpdateQuery<D>,
dto: UpdateQuery<DtoInfer<DtoAction.Update, Dto, D>>,
options: QueryOptions<D> | null = {
new: true,
},

View File

@ -177,7 +177,7 @@ export abstract class BaseService<
async updateOne(
criteria: string | TFilterQuery<T>,
dto: Partial<U>,
dto: DtoInfer<DtoAction.Update, Dto, Partial<U>>,
options?: QueryOptions<Partial<U>> | null,
): Promise<T | null> {
return await this.repository.updateOne(criteria, dto, options);

View File

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

View File

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

View File

@ -142,7 +142,7 @@ export const installConversationTypeFixtures = async () => {
conversationFixtures.map((conversationFixture) => ({
...conversationFixture,
sender: subscribers[parseInt(conversationFixture.sender)].id,
current: conversationFixture?.current
current: conversationFixture.current
? blocks[parseInt(conversationFixture.current)]?.id
: undefined,
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 nlpValues = await NlpValue.insertMany(
nlpValueFixtures.map((v) => {
return {
...v,
entity: nlpEntities[parseInt(v.entity)].id,
};
}),
nlpValueFixtures.map((v) => ({
...v,
entity: v?.entity ? nlpEntities[parseInt(v.entity)].id : null,
})),
);
return { nlpEntities, nlpValues };
};

View File

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