Merge pull request #57 from Hexastack/refactor/api-populate-queries

refactor: populate queries
This commit is contained in:
Mohamed Marrouchi 2024-09-21 19:55:57 +01:00 committed by GitHub
commit 86673b3b59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 744 additions and 1084 deletions

View File

@ -16,7 +16,7 @@ import { BaseRepository } from '@/utils/generics/base-repository';
import { BotStats, BotStatsType } from '../schemas/bot-stats.schema';
@Injectable()
export class BotStatsRepository extends BaseRepository<BotStats, never> {
export class BotStatsRepository extends BaseRepository<BotStats> {
constructor(@InjectModel(BotStats.name) readonly model: Model<BotStats>) {
super(model, BotStats);
}

View File

@ -36,14 +36,24 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { BlockCreateDto, BlockUpdateDto } from '../dto/block.dto';
import { Block, BlockFull, BlockStub } from '../schemas/block.schema';
import {
Block,
BlockFull,
BlockPopulate,
BlockStub,
} from '../schemas/block.schema';
import { BlockService } from '../services/block.service';
import { CategoryService } from '../services/category.service';
import { LabelService } from '../services/label.service';
@UseInterceptors(CsrfInterceptor)
@Controller('Block')
export class BlockController extends BaseController<Block, BlockStub> {
export class BlockController extends BaseController<
Block,
BlockStub,
BlockPopulate,
BlockFull
> {
constructor(
private readonly blockService: BlockService,
private readonly logger: LoggerService,
@ -68,15 +78,7 @@ export class BlockController extends BaseController<Block, BlockStub> {
@Query(new SearchFilterPipe<Block>({ allowedFields: ['category'] }))
filters: TFilterQuery<Block>,
): Promise<Block[] | BlockFull[]> {
return this.canPopulate(populate, [
'trigger_labels',
'assign_labels',
'nextBlocks',
'attachedBlock',
'category',
'previousBlocks',
'attachedToBlock',
])
return this.canPopulate(populate)
? await this.blockService.findAndPopulate(filters)
: await this.blockService.find(filters);
}
@ -189,15 +191,7 @@ export class BlockController extends BaseController<Block, BlockStub> {
@Query(PopulatePipe)
populate: string[],
): Promise<Block | BlockFull> {
const doc = this.canPopulate(populate, [
'trigger_labels',
'assign_labels',
'nextBlocks',
'attachedBlock',
'category',
'previousBlocks',
'attachedToBlock',
])
const doc = this.canPopulate(populate)
? await this.blockService.findOneAndPopulate(id)
: await this.blockService.findOne(id);
if (!doc) {

View File

@ -33,7 +33,6 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { CategoryCreateDto, CategoryUpdateDto } from '../dto/category.dto';
import { Category } from '../schemas/category.schema';
import { BlockService } from '../services/block.service';
import { CategoryService } from '../services/category.service';
@UseInterceptors(CsrfInterceptor)
@ -41,7 +40,6 @@ import { CategoryService } from '../services/category.service';
export class CategoryController extends BaseController<Category> {
constructor(
private readonly categoryService: CategoryService,
private readonly blockService: BlockService,
private readonly logger: LoggerService,
) {
super(categoryService);

View File

@ -32,12 +32,22 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { LabelCreateDto, LabelUpdateDto } from '../dto/label.dto';
import { Label, LabelStub } from '../schemas/label.schema';
import {
Label,
LabelFull,
LabelPopulate,
LabelStub,
} from '../schemas/label.schema';
import { LabelService } from '../services/label.service';
@UseInterceptors(CsrfInterceptor)
@Controller('label')
export class LabelController extends BaseController<Label, LabelStub> {
export class LabelController extends BaseController<
Label,
LabelStub,
LabelPopulate,
LabelFull
> {
constructor(
private readonly labelService: LabelService,
private readonly logger: LoggerService,
@ -53,7 +63,7 @@ export class LabelController extends BaseController<Label, LabelStub> {
@Query(new SearchFilterPipe<Label>({ allowedFields: ['name', 'title'] }))
filters: TFilterQuery<Label>,
) {
return this.canPopulate(populate, ['users'])
return this.canPopulate(populate)
? await this.labelService.findPageAndPopulate(filters, pageQuery)
: await this.labelService.findPage(filters, pageQuery);
}
@ -80,7 +90,7 @@ export class LabelController extends BaseController<Label, LabelStub> {
@Query(PopulatePipe)
populate: string[],
) {
const doc = this.canPopulate(populate, ['users'])
const doc = this.canPopulate(populate)
? await this.labelService.findOneAndPopulate(id)
: await this.labelService.findOne(id);
if (!doc) {

View File

@ -36,7 +36,12 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { MessageCreateDto } from '../dto/message.dto';
import { Message, MessageStub } from '../schemas/message.schema';
import {
Message,
MessageFull,
MessagePopulate,
MessageStub,
} from '../schemas/message.schema';
import {
OutgoingMessage,
OutgoingMessageFormat,
@ -49,7 +54,12 @@ import { SubscriberService } from '../services/subscriber.service';
@UseInterceptors(CsrfInterceptor)
@Controller('message')
export class MessageController extends BaseController<Message, MessageStub> {
export class MessageController extends BaseController<
Message,
MessageStub,
MessagePopulate,
MessageFull
> {
constructor(
private readonly messageService: MessageService,
private readonly subscriberService: SubscriberService,
@ -70,7 +80,7 @@ export class MessageController extends BaseController<Message, MessageStub> {
)
filters: TFilterQuery<Message>,
) {
return this.canPopulate(populate, ['recipient', 'sender', 'sentBy'])
return this.canPopulate(populate)
? await this.messageService.findPageAndPopulate(filters, pageQuery)
: await this.messageService.findPage(filters, pageQuery);
}
@ -97,7 +107,7 @@ export class MessageController extends BaseController<Message, MessageStub> {
@Query(PopulatePipe)
populate: string[],
) {
const doc = this.canPopulate(populate, ['recipient', 'sender', 'sentBy'])
const doc = this.canPopulate(populate)
? await this.messageService.findOneAndPopulate(id)
: await this.messageService.findOne(id);
if (!doc) {

View File

@ -32,14 +32,21 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { SubscriberUpdateDto } from '../dto/subscriber.dto';
import { Subscriber, SubscriberStub } from '../schemas/subscriber.schema';
import {
Subscriber,
SubscriberFull,
SubscriberPopulate,
SubscriberStub,
} from '../schemas/subscriber.schema';
import { SubscriberService } from '../services/subscriber.service';
@UseInterceptors(CsrfInterceptor)
@Controller('subscriber')
export class SubscriberController extends BaseController<
Subscriber,
SubscriberStub
SubscriberStub,
SubscriberPopulate,
SubscriberFull
> {
constructor(
private readonly subscriberService: SubscriberService,
@ -67,7 +74,7 @@ export class SubscriberController extends BaseController<
)
filters: TFilterQuery<Subscriber>,
) {
return this.canPopulate(populate, ['labels', 'assignedTo', 'avatar'])
return this.canPopulate(populate)
? await this.subscriberService.findPageAndPopulate(filters, pageQuery)
: await this.subscriberService.findPage(filters, pageQuery);
}
@ -100,7 +107,7 @@ export class SubscriberController extends BaseController<
@Query(PopulatePipe)
populate: string[],
) {
const doc = this.canPopulate(populate, ['labels', 'assignedTo', 'avatar'])
const doc = this.canPopulate(populate)
? await this.subscriberService.findOneAndPopulate(id)
: await this.subscriberService.findOne(id);
if (!doc) {

View File

@ -10,11 +10,11 @@
import { Injectable, Optional } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
TFilterQuery,
Model,
Document,
Types,
Model,
Query,
TFilterQuery,
Types,
UpdateQuery,
UpdateWithAggregationPipeline,
} from 'mongoose';
@ -23,27 +23,24 @@ import { LoggerService } from '@/logger/logger.service';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BlockCreateDto, BlockUpdateDto } from '../dto/block.dto';
import { Block, BlockFull } from '../schemas/block.schema';
import {
Block,
BLOCK_POPULATE,
BlockFull,
BlockPopulate,
} from '../schemas/block.schema';
@Injectable()
export class BlockRepository extends BaseRepository<
Block,
| 'trigger_labels'
| 'assign_labels'
| 'nextBlocks'
| 'attachedBlock'
| 'category'
| 'previousBlocks'
| 'attachedToBlock'
BlockPopulate,
BlockFull
> {
private readonly logger: LoggerService;
constructor(
@InjectModel(Block.name) readonly model: Model<Block>,
@Optional() logger?: LoggerService,
@Optional() private readonly logger?: LoggerService,
) {
super(model, Block);
this.logger = logger;
super(model, Block, BLOCK_POPULATE, BlockFull);
}
/**
@ -160,44 +157,4 @@ export class BlockRepository extends BaseRepository<
);
}
}
/**
* Finds blocks and populates related fields (e.g., labels, attached blocks).
*
* @param filters - The filter criteria for finding blocks.
*
* @returns The populated block results.
*/
async findAndPopulate(filters: TFilterQuery<Block>) {
const query = this.findQuery(filters).populate([
'trigger_labels',
'assign_labels',
'nextBlocks',
'attachedBlock',
'category',
'previousBlocks',
'attachedToBlock',
]);
return await this.execute(query, BlockFull);
}
/**
* Finds a single block by ID and populates related fields (e.g., labels, attached blocks).
*
* @param id - The ID of the block to find.
*
* @returns The populated block result or null if not found.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate([
'trigger_labels',
'assign_labels',
'nextBlocks',
'attachedBlock',
'category',
'previousBlocks',
'attachedToBlock',
]);
return await this.executeOne(query, BlockFull);
}
}

View File

@ -11,7 +11,6 @@ import { ForbiddenException, Injectable, Optional } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { LoggerService } from '@/logger/logger.service';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { Category } from '../schemas/category.schema';
@ -19,17 +18,13 @@ import { BlockService } from '../services/block.service';
@Injectable()
export class CategoryRepository extends BaseRepository<Category> {
private readonly logger: LoggerService;
private readonly blockService: BlockService;
constructor(
@InjectModel(Category.name) readonly model: Model<Category>,
@Optional() blockService?: BlockService,
@Optional() logger?: LoggerService,
) {
super(model, Category);
this.logger = logger;
this.blockService = blockService;
}

View File

@ -16,20 +16,23 @@ import { BaseRepository } from '@/utils/generics/base-repository';
import {
Conversation,
CONVERSATION_POPULATE,
ConversationDocument,
ConversationFull,
ConversationPopulate,
} from '../schemas/conversation.schema';
@Injectable()
export class ConversationRepository extends BaseRepository<
Conversation,
'sender' | 'current' | 'next'
ConversationPopulate,
ConversationFull
> {
constructor(
@InjectModel(Conversation.name) readonly model: Model<Conversation>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Conversation);
super(model, Conversation, CONVERSATION_POPULATE, ConversationFull);
}
/**

View File

@ -10,22 +10,29 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { TFilterQuery, Model, Document, Query } from 'mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { LoggerService } from '@/logger/logger.service';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { Label, LabelDocument, LabelFull } from '../schemas/label.schema';
import {
Label,
LABEL_POPULATE,
LabelDocument,
LabelFull,
LabelPopulate,
} from '../schemas/label.schema';
@Injectable()
export class LabelRepository extends BaseRepository<Label, 'users'> {
export class LabelRepository extends BaseRepository<
Label,
LabelPopulate,
LabelFull
> {
constructor(
@InjectModel(Label.name) readonly model: Model<Label>,
private readonly eventEmitter: EventEmitter2,
private readonly logger: LoggerService,
) {
super(model, Label);
super(model, Label, LABEL_POPULATE, LabelFull);
}
/**
@ -78,42 +85,4 @@ export class LabelRepository extends BaseRepository<Label, 'users'> {
);
this.eventEmitter.emit('hook:chatbot:label:delete', labels);
}
/**
* Fetches all label documents and populates the `users` field which references the subscribers.
*
* @returns A promise that resolves with an array of fully populated `LabelFull` documents.
*/
async findAllAndPopulate() {
const query = this.findAllQuery().populate(['users']);
return await this.execute(query, LabelFull);
}
/**
* Fetches a paginated list of label documents based on filters and populates the `users` (subscribers) field.
*
* @param filters - The filter criteria for querying the labels.
* @param pageQuery - The pagination query options.
*
* @returns A promise that resolves with a paginated array of fully populated `LabelFull` documents.
*/
async findPageAndPopulate(
filters: TFilterQuery<Label>,
pageQuery: PageQueryDto<Label>,
) {
const query = this.findPageQuery(filters, pageQuery).populate(['users']);
return await this.execute(query, LabelFull);
}
/**
* Fetches a single label document by its ID and populates the `users` (subscribers) field.
*
* @param id - The ID of the label to be fetched.
*
* @returns A promise that resolves with a fully populated label.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate(['users']);
return await this.executeOne(query, LabelFull);
}
}

View File

@ -9,23 +9,28 @@
import { Injectable, Optional } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { TFilterQuery, Model, Query } from 'mongoose';
import { Model } from 'mongoose';
import { LoggerService } from '@/logger/logger.service';
import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
import { NlpSampleState } from '@/nlp/schemas/types';
import { NlpSampleService } from '@/nlp/services/nlp-sample.service';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { Message, MessageFull } from '../schemas/message.schema';
import {
Message,
MESSAGE_POPULATE,
MessageFull,
MessagePopulate,
} from '../schemas/message.schema';
import { Subscriber } from '../schemas/subscriber.schema';
import { AnyMessage } from '../schemas/types/message';
@Injectable()
export class MessageRepository extends BaseRepository<
AnyMessage,
'sender' | 'recipient'
MessagePopulate,
MessageFull
> {
private readonly nlpSampleService: NlpSampleService;
@ -36,7 +41,12 @@ export class MessageRepository extends BaseRepository<
@Optional() nlpSampleService?: NlpSampleService,
@Optional() logger?: LoggerService,
) {
super(model, Message as new () => AnyMessage);
super(
model,
Message as new () => AnyMessage,
MESSAGE_POPULATE,
MessageFull,
);
this.logger = logger;
this.nlpSampleService = nlpSampleService;
}
@ -81,42 +91,6 @@ export class MessageRepository extends BaseRepository<
}
}
/**
* Retrieves a paginated list of messages with sender and recipient populated.
* Uses filter criteria and pagination settings for the query.
*
* @param filters - Filter criteria for querying messages.
* @param pageQuery - Pagination settings, including skip, limit, and sort order.
*
* @returns A paginated list of messages with sender and recipient details populated.
*/
async findPageAndPopulate(
filters: TFilterQuery<AnyMessage>,
pageQuery: PageQueryDto<AnyMessage>,
) {
const query = this.findPageQuery(filters, pageQuery).populate([
'sender',
'recipient',
]);
return await this.execute(
query as Query<AnyMessage[], AnyMessage, object, AnyMessage, 'find'>,
MessageFull,
);
}
/**
* Retrieves a single message by its ID, populating the sender and recipient fields.
*
* @param id - The ID of the message to retrieve.
*
* @returns The message with sender and recipient details populated.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate(['sender', 'recipient']);
return await this.executeOne(query, MessageFull);
}
/**
* Retrieves the message history for a given subscriber, with messages sent or received
* before the specified date. Results are limited and sorted by creation date.

View File

@ -20,25 +20,27 @@ import {
} from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { SubscriberUpdateDto } from '../dto/subscriber.dto';
import {
Subscriber,
SUBSCRIBER_POPULATE,
SubscriberDocument,
SubscriberFull,
SubscriberPopulate,
} from '../schemas/subscriber.schema';
@Injectable()
export class SubscriberRepository extends BaseRepository<
Subscriber,
'labels' | 'assignedTo' | 'avatar'
SubscriberPopulate,
SubscriberFull
> {
constructor(
@InjectModel(Subscriber.name) readonly model: Model<Subscriber>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Subscriber);
super(model, Subscriber, SUBSCRIBER_POPULATE, SubscriberFull);
}
/**
@ -159,11 +161,8 @@ export class SubscriberRepository extends BaseRepository<
* @returns The found subscriber entity with populated fields.
*/
async findOneByForeignIdAndPopulate(id: string): Promise<SubscriberFull> {
const query = this.findByForeignIdQuery(id).populate([
'labels',
'assignedTo',
]);
const [result] = await this.execute(query, SubscriberFull);
const query = this.findByForeignIdQuery(id).populate(this.populate);
const [result] = await this.execute(query, this.clsPopulate);
return result;
}
@ -223,56 +222,4 @@ export class SubscriberRepository extends BaseRepository<
},
);
}
/**
* Finds all subscribers and populates related fields such as `labels`, `assignedTo`, and `avatar`.
*
* @returns A list of all subscribers with populated fields.
*/
async findAllAndPopulate(): Promise<SubscriberFull[]> {
const query = this.findAllQuery().populate([
'labels',
'assignedTo',
'avatar',
]);
return await this.execute(query, SubscriberFull);
}
/**
* Finds subscribers using pagination and populates related fields such as `labels`, `assignedTo`, and `avatar`.
*
* @param filters - The filter criteria to apply when finding subscribers.
* @param pageQuery - The pagination query.
*
* @returns A paginated list of subscribers with populated fields.
*/
async findPageAndPopulate(
filters: TFilterQuery<Subscriber>,
pageQuery: PageQueryDto<Subscriber>,
): Promise<SubscriberFull[]> {
const query = this.findPageQuery(filters, pageQuery).populate([
'labels',
'assignedTo',
'avatar',
]);
return await this.execute(query, SubscriberFull);
}
/**
* Finds a single subscriber by criteria and populates related fields such as `labels`, `assignedTo`, and `avatar`.
*
* @param criteria - The filter criteria to apply when finding a subscriber.
*
* @returns The found subscriber entity with populated fields.
*/
async findOneAndPopulate(
criteria: string | TFilterQuery<Subscriber>,
): Promise<SubscriberFull> {
const query = this.findOneQuery(criteria).populate([
'labels',
'assignedTo',
'avatar',
]);
return await this.executeOne(query, SubscriberFull);
}
}

View File

@ -13,6 +13,7 @@ import { Schema as MongooseSchema, THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Category } from './category.schema';
import { Label } from './label.schema';
@ -194,3 +195,15 @@ BlockModel.schema.virtual('attachedToBlock', {
});
export default BlockModel.schema;
export type BlockPopulate = keyof TFilterPopulateFields<Block, BlockStub>;
export const BLOCK_POPULATE: BlockPopulate[] = [
'trigger_labels',
'assign_labels',
'nextBlocks',
'attachedBlock',
'category',
'previousBlocks',
'attachedToBlock',
];

View File

@ -12,6 +12,7 @@ import { Transform, Type } from 'class-transformer';
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Block } from './block.schema';
import { Subscriber } from './subscriber.schema';
@ -103,3 +104,14 @@ export const ConversationModel: ModelDefinition = {
};
export default ConversationModel.schema;
export type ConversationPopulate = keyof TFilterPopulateFields<
Conversation,
ConversationStub
>;
export const CONVERSATION_POPULATE: ConversationPopulate[] = [
'sender',
'current',
'next',
];

View File

@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Subscriber } from './subscriber.schema';
@ -77,3 +78,7 @@ LabelModel.schema.virtual('users', {
});
export default LabelModel.schema;
export type LabelPopulate = keyof TFilterPopulateFields<Label, LabelStub>;
export const LABEL_POPULATE: LabelPopulate[] = ['users'];

View File

@ -13,6 +13,7 @@ import { Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Subscriber } from './subscriber.schema';
import { StdIncomingMessage, StdOutgoingMessage } from './types/message';
@ -102,3 +103,7 @@ export const MessageModel: ModelDefinition = LifecycleHookManager.attach({
});
export default MessageModel.schema;
export type MessagePopulate = keyof TFilterPopulateFields<Message, MessageStub>;
export const MESSAGE_POPULATE: MessagePopulate[] = ['sender', 'recipient'];

View File

@ -15,6 +15,7 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
import { User } from '@/user/schemas/user.schema';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Label } from './label.schema';
import { ChannelData } from './types/channel';
@ -140,3 +141,14 @@ export const SubscriberModel: ModelDefinition = LifecycleHookManager.attach({
});
export default SubscriberModel.schema;
export type SubscriberPopulate = keyof TFilterPopulateFields<
Subscriber,
SubscriberStub
>;
export const SUBSCRIBER_POPULATE: SubscriberPopulate[] = [
'labels',
'assignedTo',
'avatar',
];

View File

@ -177,7 +177,10 @@ describe('BlockService', () => {
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
}));
expect(blockRepository.findAndPopulate).toHaveBeenCalledWith({});
expect(blockRepository.findAndPopulate).toHaveBeenCalledWith(
{},
undefined,
);
expect(result).toEqualPayload(blocksWithCategory);
});
});

View File

@ -8,7 +8,6 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
@ -24,7 +23,7 @@ import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service';
import { BlockRepository } from '../repositories/block.repository';
import { Block, BlockFull } from '../schemas/block.schema';
import { Block, BlockFull, BlockPopulate } from '../schemas/block.schema';
import { WithUrl } from '../schemas/types/attachment';
import { Context } from '../schemas/types/context';
import {
@ -36,7 +35,7 @@ import { NlpPattern, Pattern, PayloadPattern } from '../schemas/types/pattern';
import { Payload, StdQuickReply } from '../schemas/types/quick-reply';
@Injectable()
export class BlockService extends BaseService<Block> {
export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
constructor(
readonly repository: BlockRepository,
private readonly contentService: ContentService,
@ -49,28 +48,6 @@ export class BlockService extends BaseService<Block> {
super(repository);
}
/**
* Finds and populates blocks based on the specified filters.
*
* @param filters - Query filters used to specify search criteria for finding blocks.
*
* @returns A promise that resolves to the populated blocks matching the filters.
*/
async findAndPopulate(filters: TFilterQuery<Block>) {
return await this.repository.findAndPopulate(filters);
}
/**
* Finds and populates a block by ID.
*
* @param id - The block ID.
*
* @returns A promise that resolves to the populated block.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Find a block whose patterns matches the received event
*

View File

@ -17,12 +17,20 @@ import { BaseService } from '@/utils/generics/base-service';
import { VIEW_MORE_PAYLOAD } from '../helpers/constants';
import { ConversationRepository } from '../repositories/conversation.repository';
import { Block, BlockFull } from '../schemas/block.schema';
import { Conversation, ConversationFull } from '../schemas/conversation.schema';
import {
Conversation,
ConversationFull,
ConversationPopulate,
} from '../schemas/conversation.schema';
import { OutgoingMessageFormat } from '../schemas/types/message';
import { Payload } from '../schemas/types/quick-reply';
@Injectable()
export class ConversationService extends BaseService<Conversation> {
export class ConversationService extends BaseService<
Conversation,
ConversationPopulate,
ConversationFull
> {
constructor(
readonly repository: ConversationRepository,
private readonly logger: LoggerService,

View File

@ -14,10 +14,10 @@ import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { LabelRepository } from '../repositories/label.repository';
import { Label } from '../schemas/label.schema';
import { Label, LabelFull, LabelPopulate } from '../schemas/label.schema';
@Injectable()
export class LabelService extends BaseService<Label> {
export class LabelService extends BaseService<Label, LabelPopulate, LabelFull> {
constructor(readonly repository: LabelRepository) {
super(repository);
}

View File

@ -29,11 +29,16 @@ import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import { MessageRepository } from '../repositories/message.repository';
import { MessageFull, MessagePopulate } from '../schemas/message.schema';
import { Subscriber } from '../schemas/subscriber.schema';
import { AnyMessage } from '../schemas/types/message';
@Injectable()
export class MessageService extends BaseService<AnyMessage> {
export class MessageService extends BaseService<
AnyMessage,
MessagePopulate,
MessageFull
> {
private readonly logger: LoggerService;
private readonly gateway: WebsocketGateway;

View File

@ -35,10 +35,18 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
import { SubscriberUpdateDto } from '../dto/subscriber.dto';
import { SubscriberRepository } from '../repositories/subscriber.repository';
import { Subscriber } from '../schemas/subscriber.schema';
import {
Subscriber,
SubscriberFull,
SubscriberPopulate,
} from '../schemas/subscriber.schema';
@Injectable()
export class SubscriberService extends BaseService<Subscriber> {
export class SubscriberService extends BaseService<
Subscriber,
SubscriberPopulate,
SubscriberFull
> {
private readonly gateway: WebsocketGateway;
constructor(

View File

@ -13,15 +13,15 @@ import path from 'path';
import {
Body,
Controller,
Param,
Post,
Get,
Delete,
Get,
HttpCode,
UseInterceptors,
Patch,
NotFoundException,
Param,
Patch,
Post,
Query,
UseInterceptors,
} from '@nestjs/common';
import { BadRequestException } from '@nestjs/common/exceptions';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
@ -43,12 +43,22 @@ import { ContentService } from './../services/content.service';
import { ContentCreateDto } from '../dto/content.dto';
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
import { ContentType } from '../schemas/content-type.schema';
import { Content, ContentStub } from '../schemas/content.schema';
import {
Content,
ContentFull,
ContentPopulate,
ContentStub,
} from '../schemas/content.schema';
import { preprocessDynamicFields } from '../utilities';
@UseInterceptors(ContentTransformInterceptor, CsrfInterceptor)
@Controller('content')
export class ContentController extends BaseController<Content, ContentStub> {
export class ContentController extends BaseController<
Content,
ContentStub,
ContentPopulate,
ContentFull
> {
constructor(
private readonly contentService: ContentService,
private readonly contentTypeService: ContentTypeService,
@ -206,7 +216,7 @@ export class ContentController extends BaseController<Content, ContentStub> {
)
filters: TFilterQuery<Content>,
) {
return this.canPopulate(populate, ['entity'])
return this.canPopulate(populate)
? await this.contentService.findPageAndPopulate(filters, pageQuery)
: await this.contentService.findPage(filters, pageQuery);
}
@ -241,7 +251,7 @@ export class ContentController extends BaseController<Content, ContentStub> {
@Param('id') id: string,
@Query(PopulatePipe) populate: string[],
) {
const doc = this.canPopulate(populate, ['entity'])
const doc = this.canPopulate(populate)
? await this.contentService.findOneAndPopulate(id)
: await this.contentService.findOne(id);

View File

@ -31,12 +31,17 @@ import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { MenuCreateDto, MenuQueryDto } from '../dto/menu.dto';
import { Menu, MenuStub } from '../schemas/menu.schema';
import { Menu, MenuFull, MenuPopulate, MenuStub } from '../schemas/menu.schema';
import { MenuService } from '../services/menu.service';
@UseInterceptors(CsrfInterceptor)
@Controller('menu')
export class MenuController extends BaseController<Menu, MenuStub> {
export class MenuController extends BaseController<
Menu,
MenuStub,
MenuPopulate,
MenuFull
> {
constructor(
private readonly menuService: MenuService,
private readonly logger: LoggerService,

View File

@ -12,7 +12,6 @@ import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { BlockService } from '@/chat/services/block.service';
import { LoggerService } from '@/logger/logger.service';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { ContentType } from '../schemas/content-type.schema';
@ -24,11 +23,8 @@ export class ContentTypeRepository extends BaseRepository<ContentType> {
@InjectModel(ContentType.name) readonly model: Model<ContentType>,
@InjectModel(Content.name) private readonly contentModel: Model<Content>,
@Optional() private readonly blockService?: BlockService,
@Optional() private readonly logger?: LoggerService,
) {
super(model, ContentType);
this.logger = logger;
this.blockService = blockService;
}
/**

View File

@ -10,53 +10,32 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
TFilterQuery,
Model,
Document,
HydratedDocument,
Model,
Query,
TFilterQuery,
UpdateQuery,
UpdateWithAggregationPipeline,
HydratedDocument,
} from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { Content, ContentFull } from '../schemas/content.schema';
import {
Content,
CONTENT_POPULATE,
ContentFull,
ContentPopulate,
} from '../schemas/content.schema';
@Injectable()
export class ContentRepository extends BaseRepository<Content, 'entity'> {
export class ContentRepository extends BaseRepository<
Content,
ContentPopulate,
ContentFull
> {
constructor(@InjectModel(Content.name) readonly model: Model<Content>) {
super(model, Content);
}
/**
* Retrieves a paginated list of content documents based on the provided filter
* and pagination query, and populates the `entity` field.
*
* @param filter - A filter query for the content documents.
* @param pageQuery - Pagination and sorting options for the query.
*
* @returns A promise that resolves to an array of fully populated `ContentFull` documents.
*/
async findPageAndPopulate(
filter: TFilterQuery<Content>,
pageQuery: PageQueryDto<Content>,
): Promise<ContentFull[]> {
const query = this.findPageQuery(filter, pageQuery).populate('entity');
return await this.execute(query, ContentFull);
}
/**
* Finds a single content document by its ID and populates the `entity` field.
*
* @param id - The ID of the content document to retrieve.
*
* @returns A promise that resolves to the populated `ContentFull` document.
*/
async findOneAndPopulate(id: string): Promise<ContentFull> {
const query = this.findOneQuery(id).populate('entity');
return await this.executeOne(query, ContentFull);
super(model, Content, CONTENT_POPULATE, ContentFull);
}
/**

View File

@ -14,16 +14,26 @@ import { Document, Model, Query } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { Menu, MenuDocument, MenuFull } from '../schemas/menu.schema';
import {
Menu,
MENU_POPULATE,
MenuDocument,
MenuFull,
MenuPopulate,
} from '../schemas/menu.schema';
import { MenuType } from '../schemas/types/menu';
@Injectable()
export class MenuRepository extends BaseRepository<Menu, 'parent'> {
export class MenuRepository extends BaseRepository<
Menu,
MenuPopulate,
MenuFull
> {
constructor(
@InjectModel(Menu.name) readonly model: Model<Menu>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Menu);
super(model, Menu, MENU_POPULATE, MenuFull);
}
/**
@ -107,16 +117,4 @@ export class MenuRepository extends BaseRepository<Menu, 'parent'> {
): Promise<void> {
this.eventEmitter.emit('hook:menu:delete', result);
}
/**
* Finds a `Menu` document by its ID and populates the `parent` field.
*
* @param id - The ID of the `Menu` document to be found and populated.
*
* @returns A promise that resolves to the populated `MenuFull` document.
*/
async findOneAndPopulate(id: string): Promise<MenuFull> {
const query = this.findOneQuery(id).populate('parent');
return await this.executeOne(query, MenuFull);
}
}

View File

@ -14,6 +14,7 @@ import mongoose, { Document } from 'mongoose';
import { config } from '@/config';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { ContentType } from './content-type.schema';
@ -104,3 +105,7 @@ export const ContentModel: ModelDefinition = LifecycleHookManager.attach({
});
export default ContentModel.schema;
export type ContentPopulate = keyof TFilterPopulateFields<Content, ContentStub>;
export const CONTENT_POPULATE: ContentPopulate[] = ['entity'];

View File

@ -13,6 +13,7 @@ import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { MenuType } from './types/menu';
@ -78,3 +79,7 @@ export const MenuModel: ModelDefinition = LifecycleHookManager.attach({
});
export default MenuModel.schema;
export type MenuPopulate = keyof TFilterPopulateFields<Menu, MenuStub>;
export const MENU_POPULATE: MenuPopulate[] = ['parent'];

View File

@ -17,13 +17,20 @@ import { StdOutgoingListMessage } from '@/chat/schemas/types/message';
import { ContentOptions } from '@/chat/schemas/types/options';
import { LoggerService } from '@/logger/logger.service';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { ContentRepository } from '../repositories/content.repository';
import { Content } from '../schemas/content.schema';
import {
Content,
ContentFull,
ContentPopulate,
} from '../schemas/content.schema';
@Injectable()
export class ContentService extends BaseService<Content> {
export class ContentService extends BaseService<
Content,
ContentPopulate,
ContentFull
> {
constructor(
readonly repository: ContentRepository,
private readonly attachmentService: AttachmentService,
@ -32,33 +39,6 @@ export class ContentService extends BaseService<Content> {
super(repository);
}
/**
* Finds a content item by its ID and populates its related fields.
*
* @param id - The ID of the content to retrieve.
*
* @return The populated content entity.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Finds a page of content items based on filters and pagination options,
* and populates their related fields.
*
* @param filters - The query filters to apply.
* @param pageQuery - The pagination and sorting options.
*
* @return A list of populated content entities.
*/
async findPageAndPopulate(
filters: TFilterQuery<Content>,
pageQuery: PageQueryDto<Content>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Performs a text search on the content repository.
*

View File

@ -17,24 +17,22 @@ import {
import { OnEvent } from '@nestjs/event-emitter';
import { Cache } from 'cache-manager';
import { LoggerService } from '@/logger/logger.service';
import { MENU_CACHE_KEY } from '@/utils/constants/cache';
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
import { BaseService } from '@/utils/generics/base-service';
import { MenuCreateDto } from '../dto/menu.dto';
import { MenuRepository } from '../repositories/menu.repository';
import { Menu } from '../schemas/menu.schema';
import { Menu, MenuFull, MenuPopulate } from '../schemas/menu.schema';
import { AnyMenu, MenuTree, MenuType } from '../schemas/types/menu';
@Injectable()
export class MenuService extends BaseService<Menu> {
export class MenuService extends BaseService<Menu, MenuPopulate, MenuFull> {
private RootSymbol: symbol = Symbol('RootMenu');
constructor(
readonly repository: MenuRepository,
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly logger: LoggerService,
) {
super(repository);
}

View File

@ -8,19 +8,19 @@
*/
import {
Controller,
Get,
Post,
Body,
Patch,
Controller,
Delete,
Param,
Query,
Get,
HttpCode,
NotFoundException,
UseInterceptors,
MethodNotAllowedException,
InternalServerErrorException,
MethodNotAllowedException,
NotFoundException,
Param,
Patch,
Post,
Query,
UseInterceptors,
} from '@nestjs/common';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
import { TFilterQuery } from 'mongoose';
@ -34,14 +34,21 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
import { NlpEntity, NlpEntityStub } from '../schemas/nlp-entity.schema';
import {
NlpEntity,
NlpEntityFull,
NlpEntityPopulate,
NlpEntityStub,
} from '../schemas/nlp-entity.schema';
import { NlpEntityService } from '../services/nlp-entity.service';
@UseInterceptors(CsrfInterceptor)
@Controller('nlpentity')
export class NlpEntityController extends BaseController<
NlpEntity,
NlpEntityStub
NlpEntityStub,
NlpEntityPopulate,
NlpEntityFull
> {
constructor(
private readonly nlpEntityService: NlpEntityService,
@ -99,7 +106,7 @@ export class NlpEntityController extends BaseController<
@Param('id') id: string,
@Query(PopulatePipe) populate: string[],
) {
const doc = this.canPopulate(populate, ['values'])
const doc = this.canPopulate(populate)
? await this.nlpEntityService.findOneAndPopulate(id)
: await this.nlpEntityService.findOne(id);
if (!doc) {
@ -127,7 +134,7 @@ export class NlpEntityController extends BaseController<
@Query(new SearchFilterPipe<NlpEntity>({ allowedFields: ['name', 'doc'] }))
filters: TFilterQuery<NlpEntity>,
) {
return this.canPopulate(populate, ['values'])
return this.canPopulate(populate)
? await this.nlpEntityService.findPageAndPopulate(filters, pageQuery)
: await this.nlpEntityService.findPage(filters, pageQuery);
}

View File

@ -46,6 +46,7 @@ import { NlpSampleCreateDto, NlpSampleDto } from '../dto/nlp-sample.dto';
import {
NlpSample,
NlpSampleFull,
NlpSamplePopulate,
NlpSampleStub,
} from '../schemas/nlp-sample.schema';
import { NlpSampleEntityValue, NlpSampleState } from '../schemas/types';
@ -58,7 +59,9 @@ import { NlpService } from '../services/nlp.service';
@Controller('nlpsample')
export class NlpSampleController extends BaseController<
NlpSample,
NlpSampleStub
NlpSampleStub,
NlpSamplePopulate,
NlpSampleFull
> {
constructor(
private readonly nlpSampleService: NlpSampleService,
@ -217,7 +220,7 @@ export class NlpSampleController extends BaseController<
@Param('id') id: string,
@Query(PopulatePipe) populate: string[],
) {
const doc = this.canPopulate(populate, ['entities'])
const doc = this.canPopulate(populate)
? await this.nlpSampleService.findOneAndPopulate(id)
: await this.nlpSampleService.findOne(id);
if (!doc) {
@ -243,7 +246,7 @@ export class NlpSampleController extends BaseController<
@Query(new SearchFilterPipe<NlpSample>({ allowedFields: ['text', 'type'] }))
filters: TFilterQuery<NlpSample>,
) {
return this.canPopulate(populate, ['entities'])
return this.canPopulate(populate)
? await this.nlpSampleService.findPageAndPopulate(filters, pageQuery)
: await this.nlpSampleService.findPage(filters, pageQuery);
}

View File

@ -32,13 +32,23 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto';
import { NlpValue, NlpValueStub } from '../schemas/nlp-value.schema';
import {
NlpValue,
NlpValueFull,
NlpValuePopulate,
NlpValueStub,
} from '../schemas/nlp-value.schema';
import { NlpEntityService } from '../services/nlp-entity.service';
import { NlpValueService } from '../services/nlp-value.service';
@UseInterceptors(CsrfInterceptor)
@Controller('nlpvalue')
export class NlpValueController extends BaseController<NlpValue, NlpValueStub> {
export class NlpValueController extends BaseController<
NlpValue,
NlpValueStub,
NlpValuePopulate,
NlpValueFull
> {
constructor(
private readonly nlpValueService: NlpValueService,
private readonly nlpEntityService: NlpEntityService,
@ -103,7 +113,7 @@ export class NlpValueController extends BaseController<NlpValue, NlpValueStub> {
@Param('id') id: string,
@Query(PopulatePipe) populate: string[],
) {
const doc = this.canPopulate(populate, ['entity'])
const doc = this.canPopulate(populate)
? await this.nlpValueService.findOneAndPopulate(id)
: await this.nlpValueService.findOne(id);
if (!doc) {
@ -135,7 +145,7 @@ export class NlpValueController extends BaseController<NlpValue, NlpValueStub> {
)
filters: TFilterQuery<NlpValue>,
) {
return this.canPopulate(populate, ['entity'])
return this.canPopulate(populate)
? await this.nlpValueService.findPageAndPopulate(filters, pageQuery)
: await this.nlpValueService.findPage(filters, pageQuery);
}

View File

@ -13,21 +13,29 @@ import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery, Types } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
import { NlpValueRepository } from './nlp-value.repository';
import { NlpEntity, NlpEntityFull } from '../schemas/nlp-entity.schema';
import {
NLP_ENTITY_POPULATE,
NlpEntity,
NlpEntityFull,
NlpEntityPopulate,
} from '../schemas/nlp-entity.schema';
@Injectable()
export class NlpEntityRepository extends BaseRepository<NlpEntity, 'values'> {
export class NlpEntityRepository extends BaseRepository<
NlpEntity,
NlpEntityPopulate,
NlpEntityFull
> {
constructor(
@InjectModel(NlpEntity.name) readonly model: Model<NlpEntity>,
private readonly nlpValueRepository: NlpValueRepository,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
private readonly eventEmitter: EventEmitter2,
) {
super(model, NlpEntity);
super(model, NlpEntity, NLP_ENTITY_POPULATE, NlpEntityFull);
}
/**
@ -106,43 +114,4 @@ export class NlpEntityRepository extends BaseRepository<NlpEntity, 'values'> {
throw new Error('Attempted to delete NLP entity using unknown criteria');
}
}
/**
* Retrieves all NLP entities and populates related `values`.
*
* @returns Promise containing an array of fully populated NLP entities.
*/
async findAllAndPopulate() {
const query = this.findAllQuery().populate(['values']);
return await this.execute(query, NlpEntityFull);
}
/**
* Retrieves a paginated list of NLP entities based on filter criteria,
* and populates related `values`.
*
* @param filter Filter criteria for NLP entities.
* @param pageQuery Pagination query.
*
* @returns Promise containing the paginated result of fully populated NLP entities.
*/
async findPageAndPopulate(
filter: TFilterQuery<NlpEntity>,
pageQuery: PageQueryDto<NlpEntity>,
) {
const query = this.findPageQuery(filter, pageQuery).populate(['values']);
return await this.execute(query, NlpEntityFull);
}
/**
* Retrieves a single NLP entity by its ID and populates related `values`.
*
* @param id The ID of the NLP entity.
*
* @returns Promise containing the fully populated NLP entity.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate(['values']);
return await this.executeOne(query, NlpEntityFull);
}
}

View File

@ -9,55 +9,31 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { TFilterQuery, Model } from 'mongoose';
import { Model } from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import {
NLP_SAMPLE_ENTITY_POPULATE,
NlpSampleEntity,
NlpSampleEntityFull,
NlpSampleEntityPopulate,
} from '../schemas/nlp-sample-entity.schema';
@Injectable()
export class NlpSampleEntityRepository extends BaseRepository<
NlpSampleEntity,
'entity' | 'value' | 'sample'
NlpSampleEntityPopulate,
NlpSampleEntityFull
> {
constructor(
@InjectModel(NlpSampleEntity.name) readonly model: Model<NlpSampleEntity>,
) {
super(model, NlpSampleEntity);
}
/**
* Finds a paginated set of `NlpSampleEntity` documents based on the provided filter
* and page query. Also populates related fields such as `entity`, `value`, and `sample`.
*
* @param filter - The query filter for retrieving documents.
* @param pageQuery - The pagination options.
* @returns The paginated results with populated fields.
*/
async findPageAndPopulate(
filter: TFilterQuery<NlpSampleEntity>,
pageQuery: PageQueryDto<NlpSampleEntity>,
) {
const query = this.findPageQuery(filter, pageQuery).populate([
'entity',
'value',
'sample',
]);
return await this.execute(query, NlpSampleEntityFull);
}
/**
* Finds a single `NlpSampleEntity` document by ID and populates the related fields `entity`, `value`, and `sample`.
*
* @param id - The ID of the document to retrieve.
* @returns The document with populated fields.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate(['entity', 'value', 'sample']);
return await this.executeOne(query, NlpSampleEntityFull);
super(
model,
NlpSampleEntity,
NLP_SAMPLE_ENTITY_POPULATE,
NlpSampleEntityFull,
);
}
}

View File

@ -12,18 +12,26 @@ import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
import { NlpSample, NlpSampleFull } from '../schemas/nlp-sample.schema';
import {
NLP_SAMPLE_POPULATE,
NlpSample,
NlpSampleFull,
NlpSamplePopulate,
} from '../schemas/nlp-sample.schema';
@Injectable()
export class NlpSampleRepository extends BaseRepository<NlpSample, 'entities'> {
export class NlpSampleRepository extends BaseRepository<
NlpSample,
NlpSamplePopulate,
NlpSampleFull
> {
constructor(
@InjectModel(NlpSample.name) readonly model: Model<NlpSample>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
) {
super(model, NlpSample);
super(model, NlpSample, NLP_SAMPLE_POPULATE, NlpSampleFull);
}
/**
@ -52,56 +60,4 @@ export class NlpSampleRepository extends BaseRepository<NlpSample, 'entities'> {
);
}
}
/**
* Retrieves a paginated list of NLP samples and populates the related entities.
*
* @param filter Query filter used to retrieve NLP samples.
* @param pageQuery Pagination details for the query.
*
* @returns A promise that resolves to a paginated list of `NlpSampleFull` objects.
*/
async findPageAndPopulate(
filter: TFilterQuery<NlpSample>,
pageQuery: PageQueryDto<NlpSample>,
): Promise<NlpSampleFull[]> {
const query = this.findPageQuery(filter, pageQuery).populate(['entities']);
return await this.execute(query, NlpSampleFull);
}
/**
* Finds all NLP samples that match the filter and populates related entities.
*
* @param filter Query filter used to retrieve NLP samples.
*
* @returns A promise that resolves to a list of `NlpSampleFull` objects.
*/
async findAndPopulate(
filter: TFilterQuery<NlpSample>,
): Promise<NlpSampleFull[]> {
const query = this.findQuery(filter).populate(['entities']);
return await this.execute(query, NlpSampleFull);
}
/**
* Finds an NLP sample by its ID and populates related entities.
*
* @param id The ID of the NLP sample to retrieve.
*
* @returns A promise that resolves to the `NlpSampleFull` object.
*/
async findOneAndPopulate(id: string): Promise<NlpSampleFull> {
const query = this.findOneQuery(id).populate(['entities']);
return await this.executeOne(query, NlpSampleFull);
}
/**
* Retrieves all NLP samples and populates related entities.
*
* @returns A promise that resolves to a list of all `NlpSampleFull` objects.
*/
async findAllAndPopulate() {
const query = this.findAllQuery().populate(['entities']);
return await this.execute(query, NlpSampleFull);
}
}

View File

@ -13,23 +13,28 @@ import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
import {
NLP_VALUE_POPULATE,
NlpValue,
NlpValueDocument,
NlpValueFull,
NlpValuePopulate,
} from '../schemas/nlp-value.schema';
@Injectable()
export class NlpValueRepository extends BaseRepository<NlpValue, 'entity'> {
export class NlpValueRepository extends BaseRepository<
NlpValue,
NlpValuePopulate,
NlpValueFull
> {
constructor(
@InjectModel(NlpValue.name) readonly model: Model<NlpValue>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
private readonly eventEmitter: EventEmitter2,
) {
super(model, NlpValue);
super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull);
}
/**
@ -100,33 +105,4 @@ export class NlpValueRepository extends BaseRepository<NlpValue, 'entity'> {
throw new Error('Attempted to delete a NLP value using unknown criteria');
}
}
/**
* Finds and paginates NLP values based on the provided filter and page query,
* populating related entities.
*
* @param filter - The filter query used to search for NLP values.
* @param pageQuery - The pagination query details.
*
* @returns A list of populated NLP values for the requested page.
*/
async findPageAndPopulate(
filter: TFilterQuery<NlpValue>,
pageQuery: PageQueryDto<NlpValue>,
): Promise<NlpValueFull[]> {
const query = this.findPageQuery(filter, pageQuery).populate(['entity']);
return await this.execute(query, NlpValueFull);
}
/**
* Finds and populates a single NLP value by its ID.
*
* @param id - The ID of the NLP value to find.
*
* @returns The populated NLP value document.
*/
async findOneAndPopulate(id: string): Promise<NlpValueFull> {
const query = this.findOneQuery(id).populate(['entity']);
return await this.executeOne(query, NlpValueFull);
}
}

View File

@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { NlpValue } from './nlp-value.schema';
import { NlpEntityMap } from './types';
@ -95,3 +96,10 @@ NlpEntityModel.schema.virtual('values', {
});
export default NlpEntityModel.schema;
export type NlpEntityPopulate = keyof TFilterPopulateFields<
NlpEntity,
NlpEntityStub
>;
export const NLP_ENTITY_POPULATE: NlpEntityPopulate[] = ['values'];

View File

@ -12,6 +12,7 @@ import { Transform, Type } from 'class-transformer';
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { NlpEntity } from './nlp-entity.schema';
import { NlpSample } from './nlp-sample.schema';
@ -106,3 +107,14 @@ export const NlpSampleEntityModel: ModelDefinition = {
};
export default NlpSampleEntityModel.schema;
export type NlpSampleEntityPopulate = keyof TFilterPopulateFields<
NlpSampleEntity,
NlpSampleEntityStub
>;
export const NLP_SAMPLE_ENTITY_POPULATE: NlpSampleEntityPopulate[] = [
'entity',
'value',
'sample',
];

View File

@ -7,12 +7,13 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Exclude, Type } from 'class-transformer';
import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { NlpSampleEntity } from './nlp-sample-entity.schema';
import { NlpSampleState } from './types';
@ -68,3 +69,10 @@ NlpSampleModel.schema.virtual('entities', {
});
export default NlpSampleModel.schema;
export type NlpSamplePopulate = keyof TFilterPopulateFields<
NlpSample,
NlpSampleStub
>;
export const NLP_SAMPLE_POPULATE: NlpSamplePopulate[] = ['entities'];

View File

@ -13,6 +13,7 @@ import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { NlpEntity, NlpEntityFull } from './nlp-entity.schema';
import { NlpValueMap } from './types';
@ -105,3 +106,10 @@ export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({
});
export default NlpValueModel.schema;
export type NlpValuePopulate = keyof TFilterPopulateFields<
NlpValue,
NlpValueStub
>;
export const NLP_VALUE_POPULATE: NlpValuePopulate[] = ['entity'];

View File

@ -12,10 +12,18 @@ import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
import { NlpEntity } from '../schemas/nlp-entity.schema';
import {
NlpEntity,
NlpEntityFull,
NlpEntityPopulate,
} from '../schemas/nlp-entity.schema';
@Injectable()
export class NlpEntitySeeder extends BaseSeeder<NlpEntity> {
export class NlpEntitySeeder extends BaseSeeder<
NlpEntity,
NlpEntityPopulate,
NlpEntityFull
> {
constructor(nlpEntityRepository: NlpEntityRepository) {
super(nlpEntityRepository);
}

View File

@ -14,10 +14,18 @@ import { BaseSeeder } from '@/utils/generics/base-seeder';
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
import { NlpValueRepository } from '../repositories/nlp-value.repository';
import { NlpValue } from '../schemas/nlp-value.schema';
import {
NlpValue,
NlpValueFull,
NlpValuePopulate,
} from '../schemas/nlp-value.schema';
@Injectable()
export class NlpValueSeeder extends BaseSeeder<NlpValue> {
export class NlpValueSeeder extends BaseSeeder<
NlpValue,
NlpValuePopulate,
NlpValueFull
> {
constructor(
nlpValueRepository: NlpValueRepository,
private readonly nlpEntityRepository: NlpEntityRepository,

View File

@ -8,19 +8,25 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpValueService } from './nlp-value.service';
import { Lookup } from '../dto/nlp-entity.dto';
import { NlpEntityRepository } from '../repositories/nlp-entity.repository';
import { NlpEntity } from '../schemas/nlp-entity.schema';
import {
NlpEntity,
NlpEntityFull,
NlpEntityPopulate,
} from '../schemas/nlp-entity.schema';
import { NlpSampleEntityValue } from '../schemas/types';
@Injectable()
export class NlpEntityService extends BaseService<NlpEntity> {
export class NlpEntityService extends BaseService<
NlpEntity,
NlpEntityPopulate,
NlpEntityFull
> {
constructor(
readonly repository: NlpEntityRepository,
private readonly nlpValueService: NlpValueService,
@ -39,41 +45,6 @@ export class NlpEntityService extends BaseService<NlpEntity> {
return await this.repository.deleteOne(id);
}
/**
* Finds an entity by its ID and populates related data.
*
* @param id - The ID of the entity to find.
*
* @returns A promise that resolves with the populated entity.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Finds all entities and populates related data.
*
* @returns A promise that resolves with the populated list of entities.
*/
async findAllAndPopulate() {
return await this.repository.findAllAndPopulate();
}
/**
* Finds entities based on the specified filters and pagination, and populates related data.
*
* @param filters - The filters to apply to the query.
* @param pageQuery - The pagination and sorting options for the query.
*
* @returns A promise that resolves with the populated page of entities.
*/
async findPageAndPopulate(
filters: TFilterQuery<NlpEntity>,
pageQuery: PageQueryDto<NlpEntity>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Stores new entities based on the sample text and sample entities.
* Deletes all values relative to this entity before deleting the entity itself.

View File

@ -8,20 +8,26 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpEntityService } from './nlp-entity.service';
import { NlpValueService } from './nlp-value.service';
import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.repository';
import { NlpSampleEntity } from '../schemas/nlp-sample-entity.schema';
import {
NlpSampleEntity,
NlpSampleEntityFull,
NlpSampleEntityPopulate,
} from '../schemas/nlp-sample-entity.schema';
import { NlpSample } from '../schemas/nlp-sample.schema';
import { NlpSampleEntityValue } from '../schemas/types';
@Injectable()
export class NlpSampleEntityService extends BaseService<NlpSampleEntity> {
export class NlpSampleEntityService extends BaseService<
NlpSampleEntity,
NlpSampleEntityPopulate,
NlpSampleEntityFull
> {
constructor(
readonly repository: NlpSampleEntityRepository,
private readonly nlpEntityService: NlpEntityService,
@ -30,34 +36,6 @@ export class NlpSampleEntityService extends BaseService<NlpSampleEntity> {
super(repository);
}
/**
* Retrieves a single NLP sample entity by its ID and populates related
* entities for the retrieved sample.
*
* @param id The ID of the NLP sample entity to find and populate.
*
* @returns The populated NLP sample entity.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Retrieves a paginated list of NLP sample entities based on filters, and
* populates related entities for each retrieved sample entity.
*
* @param filters Filters to apply when searching for the entities.
* @param pageQuery Query parameters for pagination.
*
* @returns A paginated list of populated NLP sample entities.
*/
async findPageAndPopulate(
filters: TFilterQuery<NlpSampleEntity>,
pageQuery: PageQueryDto<NlpSampleEntity>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Adds new sample entities to the corresponding training sample and returns
* the created sample entities. It handles the storage of new entities and

View File

@ -8,7 +8,6 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import {
CommonExample,
@ -18,15 +17,22 @@ import {
LookupTable,
} from '@/extensions/helpers/nlp/default/types';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
import { NlpEntity, NlpEntityFull } from '../schemas/nlp-entity.schema';
import { NlpSample, NlpSampleFull } from '../schemas/nlp-sample.schema';
import {
NlpSample,
NlpSampleFull,
NlpSamplePopulate,
} from '../schemas/nlp-sample.schema';
import { NlpValue } from '../schemas/nlp-value.schema';
@Injectable()
export class NlpSampleService extends BaseService<NlpSample> {
export class NlpSampleService extends BaseService<
NlpSample,
NlpSamplePopulate,
NlpSampleFull
> {
constructor(readonly repository: NlpSampleRepository) {
super(repository);
}
@ -42,52 +48,6 @@ export class NlpSampleService extends BaseService<NlpSample> {
return await this.repository.deleteOne(id);
}
/**
* Finds a single NLP sample by its ID and populates related data.
*
* @param id - The unique identifier of the NLP sample to find.
*
* @returns A promise resolving to the found sample with populated fields.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Finds a page of NLP samples based on filters and populates related data.
*
* @param filters - Query filters to apply when searching for samples.
* @param pageQuery - Pagination and sorting options for the query.
*
* @returns A promise resolving to the paginated results with populated fields.
*/
async findPageAndPopulate(
filters: TFilterQuery<NlpSample>,
pageQuery: PageQueryDto<NlpSample>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Finds multiple NLP samples based on filters and populates related data.
*
* @param filters - Query filters to apply when searching for samples.
*
* @returns A promise resolving to the found samples with populated fields.
*/
async findAndPopulate(filters: TFilterQuery<NlpSample>) {
return await this.repository.findAndPopulate(filters);
}
/**
* Retrieves all NLP samples and populates related data.
*
* @returns A promise resolving to all samples with populated fields.
*/
async findAllAndPopulate() {
return await this.repository.findAllAndPopulate();
}
/**
* Formats a set of NLP samples into the Rasa NLU-compatible training dataset format.
*

View File

@ -8,20 +8,26 @@
*/
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { NlpEntityService } from './nlp-entity.service';
import { NlpValueCreateDto, NlpValueUpdateDto } from '../dto/nlp-value.dto';
import { NlpValueRepository } from '../repositories/nlp-value.repository';
import { NlpEntity } from '../schemas/nlp-entity.schema';
import { NlpValue } from '../schemas/nlp-value.schema';
import {
NlpValue,
NlpValueFull,
NlpValuePopulate,
} from '../schemas/nlp-value.schema';
import { NlpSampleEntityValue } from '../schemas/types';
@Injectable()
export class NlpValueService extends BaseService<NlpValue> {
export class NlpValueService extends BaseService<
NlpValue,
NlpValuePopulate,
NlpValueFull
> {
constructor(
readonly repository: NlpValueRepository,
@Inject(forwardRef(() => NlpEntityService))
@ -41,32 +47,6 @@ export class NlpValueService extends BaseService<NlpValue> {
return await this.repository.deleteOne(id);
}
/**
* Finds an NLP value by its ID and populates related entities.
*
* @param id The ID of the NLP value to find.
*
* @returns A promise that resolves with the populated NLP value.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Finds a page of NLP values based on filters, and populates related entities.
*
* @param filters The filters to apply when searching for NLP values.
* @param pageQuery Pagination information such as page number and size.
*
* @returns A promise that resolves with a page of populated NLP values.
*/
async findPageAndPopulate(
filters: TFilterQuery<NlpValue>,
pageQuery: PageQueryDto<NlpValue>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Adds new NLP values or updates existing ones based on the provided training sample.
* This method handles both the creation of new values and the addition of synonyms.

View File

@ -92,10 +92,8 @@ describe('ModelController', () => {
describe('find', () => {
it('should find models', async () => {
jest.spyOn(modelService, 'findAndPopulate');
const result = await modelController.find([], {});
expect(modelService.findAndPopulate).toHaveBeenCalledWith({}, [
'permissions',
]);
const result = await modelController.find(['permissions'], {});
expect(modelService.findAndPopulate).toHaveBeenCalledWith({});
expect(result).toEqualPayload(
modelFixtures.map((modelFixture) => ({
...modelFixture,
@ -122,9 +120,7 @@ describe('ModelController', () => {
return acc;
}, []);
expect(modelService.findAndPopulate).toHaveBeenCalledWith({}, [
'permissions',
]);
expect(modelService.findAndPopulate).toHaveBeenCalledWith({});
expect(result).toEqualPayload(modelsWithPermissionsAndUsers);
});
});

View File

@ -13,11 +13,21 @@ import { TFilterQuery } from 'mongoose';
import { BaseController } from '@/utils/generics/base-controller';
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { Model, ModelStub } from '../schemas/model.schema';
import {
Model,
ModelFull,
ModelPopulate,
ModelStub,
} from '../schemas/model.schema';
import { ModelService } from '../services/model.service';
@Controller('model')
export class ModelController extends BaseController<Model, ModelStub> {
export class ModelController extends BaseController<
Model,
ModelStub,
ModelPopulate,
ModelFull
> {
constructor(private readonly modelService: ModelService) {
super(modelService);
}
@ -39,9 +49,8 @@ export class ModelController extends BaseController<Model, ModelStub> {
populate: string[],
filters: TFilterQuery<Model>,
) {
return await this.modelService.findAndPopulate(
filters,
this.canPopulate(populate, ['permissions']) ? populate : ['permissions'],
);
return this.canPopulate(populate)
? await this.modelService.findAndPopulate(filters)
: await this.modelService.find(filters);
}
}

View File

@ -29,7 +29,12 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { PermissionCreateDto } from '../dto/permission.dto';
import { Permission, PermissionStub } from '../schemas/permission.schema';
import {
Permission,
PermissionFull,
PermissionPopulate,
PermissionStub,
} from '../schemas/permission.schema';
import { ModelService } from '../services/model.service';
import { PermissionService } from '../services/permission.service';
import { RoleService } from '../services/role.service';
@ -38,7 +43,9 @@ import { RoleService } from '../services/role.service';
@Controller('permission')
export class PermissionController extends BaseController<
Permission,
PermissionStub
PermissionStub,
PermissionPopulate,
PermissionFull
> {
constructor(
private readonly permissionService: PermissionService,
@ -68,7 +75,7 @@ export class PermissionController extends BaseController<
)
filters: TFilterQuery<Permission>,
) {
return this.canPopulate(populate, ['model', 'role'])
return this.canPopulate(populate)
? await this.permissionService.findAndPopulate(filters)
: await this.permissionService.find(filters);
}

View File

@ -32,12 +32,17 @@ import { PopulatePipe } from '@/utils/pipes/populate.pipe';
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { RoleCreateDto, RoleUpdateDto } from '../dto/role.dto';
import { Role, RoleStub } from '../schemas/role.schema';
import { Role, RoleFull, RolePopulate, RoleStub } from '../schemas/role.schema';
import { RoleService } from '../services/role.service';
@UseInterceptors(CsrfInterceptor)
@Controller('role')
export class RoleController extends BaseController<Role, RoleStub> {
export class RoleController extends BaseController<
Role,
RoleStub,
RolePopulate,
RoleFull
> {
constructor(
private readonly roleService: RoleService,
private readonly logger: LoggerService,
@ -58,7 +63,7 @@ export class RoleController extends BaseController<Role, RoleStub> {
@Query(new SearchFilterPipe<Role>({ allowedFields: ['name'] }))
filters: TFilterQuery<Role>,
) {
return this.canPopulate(populate, ['permissions', 'users'])
return this.canPopulate(populate)
? await this.roleService.findPageAndPopulate(filters, pageQuery)
: await this.roleService.findPage(filters, pageQuery);
}
@ -89,7 +94,7 @@ export class RoleController extends BaseController<Role, RoleStub> {
@Query(PopulatePipe)
populate: string[],
) {
const doc = this.canPopulate(populate, ['permissions', 'users'])
const doc = this.canPopulate(populate)
? await this.roleService.findOneAndPopulate(id)
: await this.roleService.findOne(id);

View File

@ -148,9 +148,7 @@ describe('UserController', () => {
it('should find one user and populate its roles', async () => {
jest.spyOn(userService, 'findOneAndPopulate');
const result = await userController.findOne(user.id, ['roles']);
expect(userService.findOneAndPopulate).toHaveBeenCalledWith(user.id, [
'roles',
]);
expect(userService.findOneAndPopulate).toHaveBeenCalledWith(user.id);
expect(result).toEqualPayload(
{
...userFixtures.find(({ username }) => username === 'admin'),
@ -166,9 +164,7 @@ describe('UserController', () => {
it('should find users, and for each user populate the corresponding roles', async () => {
jest.spyOn(userService, 'findPageAndPopulate');
const result = await userService.findPageAndPopulate({}, pageQuery, [
'roles',
]);
const result = await userService.findPageAndPopulate({}, pageQuery);
const usersWithRoles = userFixtures.reduce((acc, currUser) => {
acc.push({
@ -181,7 +177,6 @@ describe('UserController', () => {
expect(userService.findPageAndPopulate).toHaveBeenCalledWith(
{},
pageQuery,
['roles'],
);
expect(result).toEqualPayload(usersWithRoles, [
...IGNORED_FIELDS,

View File

@ -48,7 +48,7 @@ import {
UserResetPasswordDto,
UserUpdateStateAndRolesDto,
} from '../dto/user.dto';
import { User, UserStub } from '../schemas/user.schema';
import { User, UserFull, UserPopulate, UserStub } from '../schemas/user.schema';
import { InvitationService } from '../services/invitation.service';
import { PasswordResetService } from '../services/passwordReset.service';
import { PermissionService } from '../services/permission.service';
@ -58,7 +58,12 @@ import { ValidateAccountService } from '../services/validate-account.service';
@UseInterceptors(CsrfInterceptor)
@Controller('user')
export class ReadOnlyUserController extends BaseController<User, UserStub> {
export class ReadOnlyUserController extends BaseController<
User,
UserStub,
UserPopulate,
UserFull
> {
constructor(
protected readonly userService: UserService,
protected readonly roleService: RoleService,
@ -122,7 +127,6 @@ export class ReadOnlyUserController extends BaseController<User, UserStub> {
const currentUser = await this.userService.findOneAndPopulate(
req.user.id as string,
['roles'],
);
const currentPermissions = await this.permissionService.findAndPopulate({
role: {
@ -165,8 +169,8 @@ export class ReadOnlyUserController extends BaseController<User, UserStub> {
)
filters: TFilterQuery<User>,
) {
return this.canPopulate(populate, ['roles', 'avatar'])
? await this.userService.findPageAndPopulate(filters, pageQuery, populate)
return this.canPopulate(populate)
? await this.userService.findPageAndPopulate(filters, pageQuery)
: await this.userService.find(filters);
}
@ -201,10 +205,9 @@ export class ReadOnlyUserController extends BaseController<User, UserStub> {
@Query(PopulatePipe)
populate: string[],
) {
const doc = await this.userService.findOneAndPopulate(
id,
this.canPopulate(populate, ['roles', 'avatar']) ? populate : ['roles'],
);
const doc = this.canPopulate(populate)
? await this.userService.findOneAndPopulate(id)
: await this.userService.findOne(id);
if (!doc) {
this.logger.warn(`Unable to find User by id ${id}`);

View File

@ -8,21 +8,26 @@
*/
import { InjectModel } from '@nestjs/mongoose';
import { TFilterQuery, Model } from 'mongoose';
import { Model, TFilterQuery } from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import {
Invitation,
INVITATION_POPULATE,
InvitationDocument,
InvitationFull,
InvitationPopulate,
} from '../schemas/invitation.schema';
import { hash } from '../utilities/hash';
export class InvitationRepository extends BaseRepository<Invitation, 'roles'> {
export class InvitationRepository extends BaseRepository<
Invitation,
InvitationPopulate,
InvitationFull
> {
constructor(@InjectModel(Invitation.name) readonly model: Model<Invitation>) {
super(model, Invitation);
super(model, Invitation, INVITATION_POPULATE, InvitationFull);
}
/**
@ -52,32 +57,4 @@ export class InvitationRepository extends BaseRepository<Invitation, 'roles'> {
: filter;
return super.findQuery(filterWithHashedToken);
}
/**
* Finds a paginated list of invitations based on the filter and page query and populates the `roles` field.
*
* @param filter - The filter object for querying invitations.
* @param pageQuery - The pagination query parameters.
*
* @returns A promise that resolves to a list of populated `InvitationFull` objects.
*/
async findPageAndPopulate(
filter: TFilterQuery<Invitation>,
pageQuery: PageQueryDto<Invitation>,
): Promise<InvitationFull[]> {
const query = this.findPageQuery(filter, pageQuery).populate('roles');
return this.execute(query, InvitationFull);
}
/**
* Finds a single invitation by its ID and populates the `roles` field.
*
* @param {string} id - The ID of the invitation to be retrieved.
*
* @returns {Promise<InvitationFull>} - A promise that resolves to the populated `InvitationFull` object.
*/
async findOneAndPopulate(id: string): Promise<InvitationFull> {
const query = this.findOneQuery(id).populate('roles');
return this.executeOne(query, InvitationFull);
}
}

View File

@ -23,7 +23,7 @@ import { Model as ModelType } from './../schemas/model.schema';
import { ModelRepository } from '../repositories/model.repository';
import { PermissionRepository } from '../repositories/permission.repository';
import { ModelModel } from '../schemas/model.schema';
import { PermissionModel, Permission } from '../schemas/permission.schema';
import { Permission, PermissionModel } from '../schemas/permission.schema';
describe('ModelRepository', () => {
let modelRepository: ModelRepository;
@ -69,7 +69,7 @@ describe('ModelRepository', () => {
jest.spyOn(modelModel, 'find');
const allModels = await modelRepository.findAll();
const allPermissions = await permissionRepository.findAll();
const result = await modelRepository.findAndPopulate({}, ['permissions']);
const result = await modelRepository.findAndPopulate({});
const modelsWithPermissions = allModels.reduce((acc, currModel) => {
acc.push({
...currModel,

View File

@ -9,21 +9,30 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { TFilterQuery, Model as MongooseModel } from 'mongoose';
import { Model as MongooseModel } from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { ModelFull, Model } from '../schemas/model.schema';
import {
Model,
MODEL_POPULATE,
ModelFull,
ModelPopulate,
} from '../schemas/model.schema';
import { Permission } from '../schemas/permission.schema';
@Injectable()
export class ModelRepository extends BaseRepository<Model, 'permissions'> {
export class ModelRepository extends BaseRepository<
Model,
ModelPopulate,
ModelFull
> {
constructor(
@InjectModel(Model.name) readonly model: MongooseModel<Model>,
@InjectModel(Permission.name)
private readonly permissionModel: MongooseModel<Permission>,
) {
super(model, Model);
super(model, Model, MODEL_POPULATE, ModelFull);
}
/**
@ -40,32 +49,4 @@ export class ModelRepository extends BaseRepository<Model, 'permissions'> {
}
return result;
}
/**
* Finds `Model` documents that match the provided filter and populates specified fields.
*
* @param filter - The filter query to apply.
* @param populate - The fields to populate.
*
* @returns The populated `Model` documents.
*/
async findAndPopulate(
filter: TFilterQuery<Model>,
populate: string[],
): Promise<ModelFull[]> {
const query = this.findQuery(filter).populate(populate);
return await this.execute(query, ModelFull);
}
/**
* Finds a single `Model` document by its ID and populates its `permissions` field.
*
* @param id - The ID of the `Model` document to find.
*
* @returns The populated `Model` document.
*/
async findOneAndPopulate(id: string) {
const query = this.findOneQuery(id).populate('permissions');
return await this.executeOne(query, ModelFull);
}
}

View File

@ -14,18 +14,24 @@ import { Document, Model, Query, TFilterQuery, Types } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { Permission, PermissionFull } from '../schemas/permission.schema';
import {
Permission,
PERMISSION_POPULATE,
PermissionFull,
PermissionPopulate,
} from '../schemas/permission.schema';
@Injectable()
export class PermissionRepository extends BaseRepository<
Permission,
'model' | 'role'
PermissionPopulate,
PermissionFull
> {
constructor(
@InjectModel(Permission.name) readonly model: Model<Permission>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Permission);
super(model, Permission, PERMISSION_POPULATE, PermissionFull);
}
/**

View File

@ -16,12 +16,18 @@ import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { Permission } from '../schemas/permission.schema';
import { Role, RoleFull } from '../schemas/role.schema';
import {
Role,
ROLE_POPULATE,
RoleFull,
RolePopulate,
} from '../schemas/role.schema';
@Injectable()
export class RoleRepository extends BaseRepository<
Role,
'permissions' | 'users'
RolePopulate,
RoleFull
> {
constructor(
@InjectModel(Role.name) readonly model: Model<Role>,
@ -29,7 +35,7 @@ export class RoleRepository extends BaseRepository<
private readonly permissionModel: Model<Permission>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Role);
super(model, Role, ROLE_POPULATE, RoleFull);
}
/**

View File

@ -13,6 +13,7 @@ import { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Model } from 'mongoose';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { LoggerService } from '@/logger/logger.service';
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
import { installPermissionFixtures } from '@/utils/test/fixtures/permission';
@ -53,7 +54,12 @@ describe('UserRepository', () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
rootMongooseTestModule(installPermissionFixtures),
MongooseModule.forFeature([UserModel, PermissionModel, RoleModel]),
MongooseModule.forFeature([
UserModel,
PermissionModel,
RoleModel,
AttachmentModel,
]),
],
providers: [
LoggerService,
@ -85,9 +91,7 @@ describe('UserRepository', () => {
describe('findOneAndPopulate', () => {
it('should find one user and populate its role', async () => {
jest.spyOn(userModel, 'findById');
const result = await userRepository.findOneAndPopulate(user.id, [
'roles',
]);
const result = await userRepository.findOneAndPopulate(user.id);
expect(userModel.findById).toHaveBeenCalledWith(user.id);
expect(result).toEqualPayload(
{
@ -106,9 +110,7 @@ describe('UserRepository', () => {
jest.spyOn(userRepository, 'findPageAndPopulate');
const allUsers = await userRepository.findAll();
const allRoles = await roleRepository.findAll();
const result = await userRepository.findPageAndPopulate({}, pageQuery, [
'roles',
]);
const result = await userRepository.findPageAndPopulate({}, pageQuery);
const usersWithRoles = allUsers.reduce((acc, currUser) => {
acc.push({
...currUser,

View File

@ -10,25 +10,34 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
TFilterQuery,
Document,
Model,
Query,
TFilterQuery,
UpdateQuery,
UpdateWithAggregationPipeline,
Document,
} from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { UserEditProfileDto } from '../dto/user.dto';
import { User, UserDocument, UserFull } from '../schemas/user.schema';
import {
User,
USER_POPULATE,
UserDocument,
UserFull,
UserPopulate,
} from '../schemas/user.schema';
import { hash } from '../utilities/bcryptjs';
@Injectable()
export class UserRepository extends BaseRepository<User, 'roles' | 'avatar'> {
export class UserRepository extends BaseRepository<
User,
UserPopulate,
UserFull
> {
constructor(@InjectModel(User.name) readonly model: Model<User>) {
super(model, User);
super(model, User, USER_POPULATE, UserFull);
}
/**
@ -87,35 +96,4 @@ export class UserRepository extends BaseRepository<User, 'roles' | 'avatar'> {
});
}
}
/**
* Finds a page of user documents with population based on the given filter and page query.
*
* @param filter The filter criteria for finding users.
* @param pageQuery The pagination and sorting information.
* @param populate The fields to populate in the user documents.
*
* @returns A promise that resolves to an array of populated `UserFull` documents.
*/
async findPageAndPopulate(
filter: TFilterQuery<User>,
pageQuery: PageQueryDto<User>,
populate: string[],
): Promise<UserFull[]> {
const query = this.findPageQuery(filter, pageQuery).populate(populate);
return await this.execute(query, UserFull);
}
/**
* Finds a single user document by ID with population of related fields.
*
* @param id The ID of the user to find.
* @param populate The fields to populate in the user document.
*
* @returns A promise that resolves to a populated `UserFull` document.
*/
async findOneAndPopulate(id: string, populate: string[]): Promise<UserFull> {
const query = this.findOneQuery(id).populate(populate);
return await this.executeOne(query, UserFull);
}
}

View File

@ -13,6 +13,7 @@ import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { isEmail } from '@/utils/validation-rules/is-email';
import { Role } from './role.schema';
@ -70,3 +71,10 @@ export const InvitationModel: ModelDefinition = LifecycleHookManager.attach({
});
export default InvitationModel.schema;
export type InvitationPopulate = keyof TFilterPopulateFields<
Invitation,
InvitationStub
>;
export const INVITATION_POPULATE: InvitationPopulate[] = ['roles'];

View File

@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Permission } from './permission.schema';
import { TRelation } from '../types/index.type';
@ -72,3 +73,7 @@ ModelModel.schema.virtual('permissions', {
});
export default ModelModel.schema;
export type ModelPopulate = keyof TFilterPopulateFields<Model, ModelStub>;
export const MODEL_POPULATE: ModelPopulate[] = ['permissions'];

View File

@ -13,6 +13,7 @@ import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Model } from './model.schema';
import { Role } from './role.schema';
@ -69,3 +70,10 @@ export const PermissionModel: ModelDefinition = LifecycleHookManager.attach({
});
export default PermissionModel.schema;
export type PermissionPopulate = keyof TFilterPopulateFields<
Permission,
PermissionStub
>;
export const PERMISSION_POPULATE: PermissionPopulate[] = ['model', 'role'];

View File

@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Permission } from './permission.schema';
import { User } from './user.schema';
@ -70,3 +71,7 @@ RoleModel.schema.virtual('users', {
});
export default RoleModel.schema;
export type RolePopulate = keyof TFilterPopulateFields<Role, RoleStub>;
export const ROLE_POPULATE: RolePopulate[] = ['permissions', 'users'];

View File

@ -7,13 +7,14 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Transform, Type } from 'class-transformer';
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
import { Schema as MongooseSchema, THydratedDocument } from 'mongoose';
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';
import { Role } from './role.schema';
import { UserProvider } from '../types/user-provider.type';
@ -129,3 +130,7 @@ export const UserModel: ModelDefinition = LifecycleHookManager.attach({
});
export default UserModel.schema;
export type UserPopulate = keyof TFilterPopulateFields<User, UserStub>;
export const USER_POPULATE: UserPopulate[] = ['roles', 'avatar'];

View File

@ -12,10 +12,10 @@ import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { ModelRepository } from '../repositories/model.repository';
import { Model } from '../schemas/model.schema';
import { Model, ModelFull, ModelPopulate } from '../schemas/model.schema';
@Injectable()
export class ModelSeeder extends BaseSeeder<Model> {
export class ModelSeeder extends BaseSeeder<Model, ModelPopulate, ModelFull> {
constructor(private readonly modelRepository: ModelRepository) {
super(modelRepository);
}

View File

@ -12,10 +12,18 @@ import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { PermissionRepository } from '../repositories/permission.repository';
import { Permission } from '../schemas/permission.schema';
import {
Permission,
PermissionFull,
PermissionPopulate,
} from '../schemas/permission.schema';
@Injectable()
export class PermissionSeeder extends BaseSeeder<Permission> {
export class PermissionSeeder extends BaseSeeder<
Permission,
PermissionPopulate,
PermissionFull
> {
constructor(private readonly permissionRepository: PermissionRepository) {
super(permissionRepository);
}

View File

@ -12,10 +12,10 @@ import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { RoleRepository } from '../repositories/role.repository';
import { Role } from '../schemas/role.schema';
import { Role, RoleFull, RolePopulate } from '../schemas/role.schema';
@Injectable()
export class RoleSeeder extends BaseSeeder<Role> {
export class RoleSeeder extends BaseSeeder<Role, RolePopulate, RoleFull> {
constructor(private readonly roleRepository: RoleRepository) {
super(roleRepository);
}

View File

@ -12,10 +12,10 @@ import { Injectable } from '@nestjs/common';
import { BaseSeeder } from '@/utils/generics/base-seeder';
import { UserRepository } from '../repositories/user.repository';
import { User } from '../schemas/user.schema';
import { User, UserFull, UserPopulate } from '../schemas/user.schema';
@Injectable()
export class UserSeeder extends BaseSeeder<User> {
export class UserSeeder extends BaseSeeder<User, UserPopulate, UserFull> {
constructor(private readonly userRepository: UserRepository) {
super(userRepository);
}

View File

@ -15,20 +15,26 @@ import {
} from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { MailerService } from '@nestjs-modules/mailer';
import { TFilterQuery } from 'mongoose';
import { config } from '@/config';
import { ExtendedI18nService } from '@/extended-i18n.service';
import { LoggerService } from '@/logger/logger.service';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { InvitationCreateDto } from '../dto/invitation.dto';
import { InvitationRepository } from '../repositories/invitation.repository';
import { Invitation } from '../schemas/invitation.schema';
import {
Invitation,
InvitationFull,
InvitationPopulate,
} from '../schemas/invitation.schema';
@Injectable()
export class InvitationService extends BaseService<Invitation> {
export class InvitationService extends BaseService<
Invitation,
InvitationPopulate,
InvitationFull
> {
constructor(
@Inject(InvitationRepository)
readonly repository: InvitationRepository,
@ -112,30 +118,4 @@ export class InvitationService extends BaseService<Invitation> {
async updateOne(..._: any): Promise<Invitation> {
throw new Error('Illegal Update');
}
/**
* Finds a single invitation by ID and populates related data.
*
* @param id - The ID of the invitation to find.
*
* @returns The invitation with populated fields.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Finds and paginates invitations based on the provided filters and pagination query, with populated related data.
*
* @param filters - The filters to apply when finding invitations.
* @param pageQuery - The pagination query to apply.
*
* @returns A list of paginated invitations with populated fields.
*/
async findPageAndPopulate(
filters: TFilterQuery<Invitation>,
pageQuery: PageQueryDto<Invitation>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
}

View File

@ -70,7 +70,7 @@ describe('ModelService', () => {
jest.spyOn(modelRepository, 'findAndPopulate');
const models = await modelRepository.findAll();
const permissions = await permissionRepository.findAll();
const result = await modelService.findAndPopulate({}, ['permissions']);
const result = await modelService.findAndPopulate({});
const modelsWithPermissions = models.reduce((acc, currModel) => {
acc.push({
...currModel,
@ -80,9 +80,10 @@ describe('ModelService', () => {
});
return acc;
}, []);
expect(modelRepository.findAndPopulate).toHaveBeenCalledWith({}, [
'permissions',
]);
expect(modelRepository.findAndPopulate).toHaveBeenCalledWith(
{},
undefined,
);
expect(result).toEqualPayload(modelsWithPermissions);
});
});

View File

@ -8,15 +8,14 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { BaseService } from '@/utils/generics/base-service';
import { ModelRepository } from '../repositories/model.repository';
import { Model } from '../schemas/model.schema';
import { Model, ModelFull, ModelPopulate } from '../schemas/model.schema';
@Injectable()
export class ModelService extends BaseService<Model> {
export class ModelService extends BaseService<Model, ModelPopulate, ModelFull> {
constructor(readonly repository: ModelRepository) {
super(repository);
}
@ -31,26 +30,4 @@ export class ModelService extends BaseService<Model> {
async deleteOne(id: string) {
return await this.repository.deleteOne(id);
}
/**
* Finds multiple Model entities based on provided filters and populates related entities.
*
* @param filters - The filters used to query the Model entities.
* @param populate - Optional array of related entity fields to populate in the result.
*
* @returns A promise that resolves to the list of found Model entities with populated fields.
*/
async findAndPopulate(filters: TFilterQuery<Model>, populate?: string[]) {
return await this.repository.findAndPopulate(filters, populate);
}
/**
* Finds a single Model entity by its unique identifier and populates related entities.
*
* @param id - The unique identifier of the Model entity to find.
* @returns A promise that resolves to the found Model entity with populated fields.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
}

View File

@ -11,58 +11,32 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Cache } from 'cache-manager';
import { TFilterQuery } from 'mongoose';
import { LoggerService } from '@/logger/logger.service';
import { PERMISSION_CACHE_KEY } from '@/utils/constants/cache';
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
import { BaseService } from '@/utils/generics/base-service';
import { PermissionRepository } from '../repositories/permission.repository';
import { Permission, PermissionFull } from '../schemas/permission.schema';
import {
Permission,
PermissionFull,
PermissionPopulate,
} from '../schemas/permission.schema';
import { PermissionsTree } from '../types/permission.type';
@Injectable()
export class PermissionService extends BaseService<Permission> {
export class PermissionService extends BaseService<
Permission,
PermissionPopulate,
PermissionFull
> {
constructor(
readonly repository: PermissionRepository,
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly logger: LoggerService,
) {
super(repository);
}
/**
* Retrieves all permissions and populates related fields such as roles and models.
*
* @returns A promise that resolves with the populated permissions.
*/
async findAllAndPopulate() {
return await this.repository.findAllAndPopulate();
}
/**
* Retrieves permissions based on the provided filter and populates related fields.
*
* @param filter - Filter criteria to apply when searching for permissions.
*
* @returns A promise that resolves with the filtered and populated permissions.
*/
async findAndPopulate(filter: TFilterQuery<Permission>) {
return await this.repository.findAndPopulate(filter);
}
/**
* Retrieves a single permission by its identifier and populates related fields.
*
* @param id - Identifier of the permission to retrieve.
*
* @returns A promise that resolves with the populated permission.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
/**
* Handles permission update events by clearing the permissions cache.
*

View File

@ -8,43 +8,15 @@
*/
import { Injectable } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { RoleRepository } from '../repositories/role.repository';
import { Role } from '../schemas/role.schema';
import { Role, RoleFull, RolePopulate } from '../schemas/role.schema';
@Injectable()
export class RoleService extends BaseService<Role> {
export class RoleService extends BaseService<Role, RolePopulate, RoleFull> {
constructor(readonly repository: RoleRepository) {
super(repository);
}
/**
* Retrieves a paginated list of roles with related fields populated, based on the provided filters and pagination options.
*
* @param filters - Criteria used to filter the roles.
* @param pageQuery - Pagination options, including page size and number.
*
* @returns A paginated result set of roles with related fields populated.
*/
async findPageAndPopulate(
filters: TFilterQuery<Role>,
pageQuery: PageQueryDto<Role>,
) {
return await this.repository.findPageAndPopulate(filters, pageQuery);
}
/**
* Retrieves a single role by its ID and populates its related fields.
*
* @param id - The unique identifier of the role to retrieve.
*
* @returns The role with related fields populated, or null if no role is found.
*/
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
}

View File

@ -99,10 +99,8 @@ describe('UserService', () => {
describe('findOneAndPopulate', () => {
it('should find one user and populate its role', async () => {
jest.spyOn(userRepository, 'findOneAndPopulate');
const result = await userService.findOneAndPopulate(user.id, ['roles']);
expect(userRepository.findOneAndPopulate).toHaveBeenCalledWith(user.id, [
'roles',
]);
const result = await userService.findOneAndPopulate(user.id);
expect(userRepository.findOneAndPopulate).toHaveBeenCalledWith(user.id);
expect(result).toEqualPayload(
{
...userFixtures.find(({ username }) => username === 'admin'),
@ -118,9 +116,7 @@ describe('UserService', () => {
const pageQuery = getPageQuery<User>({ sort: ['_id', 'asc'] });
jest.spyOn(userRepository, 'findPageAndPopulate');
const allUsers = await userRepository.findAll();
const result = await userService.findPageAndPopulate({}, pageQuery, [
'roles',
]);
const result = await userService.findPageAndPopulate({}, pageQuery);
const usersWithRoles = allUsers.reduce((acc, currUser) => {
acc.push({
...currUser,
@ -132,7 +128,6 @@ describe('UserService', () => {
expect(userRepository.findPageAndPopulate).toHaveBeenCalledWith(
{},
pageQuery,
['roles'],
);
expect(result).toEqualPayload(usersWithRoles);
});

View File

@ -10,40 +10,20 @@
import { join } from 'path';
import { Injectable, NotFoundException, StreamableFile } from '@nestjs/common';
import { TFilterQuery } from 'mongoose';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { getStreamableFile } from '@/attachment/utilities';
import { config } from '@/config';
import { BaseService } from '@/utils/generics/base-service';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { RoleService } from './role.service';
import { UserRepository } from '../repositories/user.repository';
import { User } from '../schemas/user.schema';
import { User, UserFull, UserPopulate } from '../schemas/user.schema';
@Injectable()
export class UserService extends BaseService<User> {
constructor(
readonly repository: UserRepository,
private readonly roleService: RoleService,
private readonly attachmentService: AttachmentService,
) {
export class UserService extends BaseService<User, UserPopulate, UserFull> {
constructor(readonly repository: UserRepository) {
super(repository);
}
/**
* Finds a user by ID and populates the specified related fields.
*
* @param id - The ID of the user to find.
* @param populate - (Optional) Array of related fields to populate in the result.
*
* @returns A promise that resolves with the populated user record.
*/
async findOneAndPopulate(id: string, populate?: string[]) {
return await this.repository.findOneAndPopulate(id, populate);
}
/**
* Retrieves the user's profile picture as a streamable file.
*
@ -52,7 +32,7 @@ export class UserService extends BaseService<User> {
* @returns A promise that resolves with the streamable file of the user's profile picture.
*/
async userProfilePic(id: string): Promise<StreamableFile> {
const user = await this.findOneAndPopulate(id, ['avatar']);
const user = await this.findOneAndPopulate(id);
if (user) {
const attachment = user.avatar;
const path = join(config.parameters.uploadDir, attachment.location);
@ -72,25 +52,4 @@ export class UserService extends BaseService<User> {
throw new NotFoundException('Profile Not found');
}
}
/**
* Finds a paginated list of users based on filters and populates the specified related fields.
*
* @param filters - Filters to apply to the user search.
* @param pageQuery - Pagination and sorting information for the query.
* @param populate - (Optional) Array of related fields to populate in the result.
*
* @returns A promise that resolves with a paginated list of users.
*/
async findPageAndPopulate(
filters: TFilterQuery<User>,
pageQuery: PageQueryDto<User>,
populate?: string[],
) {
return await this.repository.findPageAndPopulate(
filters,
pageQuery,
populate,
);
}
}

View File

@ -7,7 +7,6 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/
import { LoggerService } from '@nestjs/common';
import { Cache } from 'cache-manager';
export function Cacheable(cacheKey: string) {
@ -20,7 +19,6 @@ export function Cacheable(cacheKey: string) {
descriptor.value = async function (...args: any[]) {
const cache: Cache = this.cacheManager;
const logger: LoggerService = this.logger; // Access the logger from the instance
if (!cache) {
throw new Error(
@ -28,19 +26,15 @@ export function Cacheable(cacheKey: string) {
);
}
if (!logger) {
throw new Error('Cacheable() requires the the logger service.');
}
// Try to get cached data
try {
const cachedResult = await cache.get(cacheKey);
if (cachedResult) {
logger.debug(`Cache hit for key: ${cacheKey}`);
return cachedResult;
}
} catch (error) {
logger.error(`Cache get error for key: ${cacheKey}:`, error);
// eslint-disable-next-line no-console
console.error(`Cache get error for key: ${cacheKey}:`, error);
}
// Call the original method if cache miss
@ -49,9 +43,9 @@ export function Cacheable(cacheKey: string) {
// Set the new result in cache
try {
await cache.set(cacheKey, result);
logger.debug(`Cache set for key: ${cacheKey}`);
} catch (error) {
logger.error(`Cache set error for key: ${cacheKey}:`, error);
// eslint-disable-next-line no-console
console.error(`Cache set error for key: ${cacheKey}:`, error);
}
return result;

View File

@ -12,10 +12,15 @@ import { TFilterQuery } from 'mongoose';
import { BaseSchema } from './base-schema';
import { BaseService } from './base-service';
import { TValidateProps, TFilterPopulateFields } from '../types/filter.types';
import { TValidateProps } from '../types/filter.types';
export abstract class BaseController<T extends BaseSchema, TStub = never> {
constructor(private readonly service: BaseService<T>) {}
export abstract class BaseController<
T extends BaseSchema,
TStub = never,
P extends string = never,
TFull extends Omit<T, P> = never,
> {
constructor(protected readonly service: BaseService<T, P, TFull>) {}
/**
* Checks if the given populate fields are allowed based on the allowed fields list.
@ -23,12 +28,12 @@ export abstract class BaseController<T extends BaseSchema, TStub = never> {
* @param allowedFields - The list of allowed populate fields.
* @return - True if all populate fields are allowed, otherwise false.
*/
protected canPopulate(
populate: string[],
allowedFields: (keyof TFilterPopulateFields<T, TStub>)[],
): boolean {
return (populate as typeof allowedFields).some((p) =>
allowedFields.includes(p),
protected canPopulate(populate: string[]): boolean {
return populate.some((p) =>
this.service
.getRepository()
.getPopulate()
.includes(p as P),
);
}

View File

@ -32,6 +32,7 @@ export type DeleteResult = {
export abstract class BaseRepository<
T extends FlattenMaps<unknown>,
P extends string = never,
TFull extends Omit<T, P> = never,
U = Omit<T, keyof BaseSchema>,
D = Document<T>,
> {
@ -42,10 +43,16 @@ export abstract class BaseRepository<
constructor(
readonly model: Model<T>,
private readonly cls: new () => T,
protected readonly populate: P[] = [],
protected readonly clsPopulate: new () => TFull = undefined,
) {
this.registerLifeCycleHooks();
}
getPopulate() {
return this.populate;
}
private registerLifeCycleHooks() {
const repository = this;
const hooks = LifecycleHookManager.getHooks(this.cls.name);
@ -153,6 +160,12 @@ export abstract class BaseRepository<
return await this.executeOne(query, this.cls, options);
}
async findOneAndPopulate(criteria: string | TFilterQuery<T>) {
this.ensureCanPopulate();
const query = this.findOneQuery(criteria).populate(this.populate);
return await this.executeOne(query, this.clsPopulate);
}
protected findQuery(filter: TFilterQuery<T>, sort?: QuerySortDto<T>) {
const query = this.model.find<T>(filter);
if (sort) {
@ -166,14 +179,32 @@ export abstract class BaseRepository<
return await this.execute(query, this.cls);
}
protected findAllQuery() {
return this.findQuery({});
private ensureCanPopulate() {
if (!this.populate || !this.clsPopulate) {
throw new Error('Cannot populate query');
}
}
async findAndPopulate(filters: TFilterQuery<T>, sort?: QuerySortDto<T>) {
this.ensureCanPopulate();
const query = this.findQuery(filters, sort).populate(this.populate);
return await this.execute(query, this.clsPopulate);
}
protected findAllQuery(sort?: QuerySortDto<T>) {
return this.findQuery({}, sort);
}
async findAll(sort?: QuerySortDto<T>) {
return await this.find({}, sort);
}
async findAllAndPopulate(sort?: QuerySortDto<T>) {
this.ensureCanPopulate();
const query = this.findAllQuery(sort).populate(this.populate);
return await this.execute(query, this.clsPopulate);
}
protected findPageQuery(
filters: TFilterQuery<T>,
{ skip, limit, sort }: PageQueryDto<T>,
@ -192,6 +223,17 @@ export abstract class BaseRepository<
return await this.execute(query, this.cls);
}
async findPageAndPopulate(
filters: TFilterQuery<T>,
pageQuery: PageQueryDto<T>,
) {
this.ensureCanPopulate();
const query = this.findPageQuery(filters, pageQuery).populate(
this.populate,
);
return await this.execute(query, this.clsPopulate);
}
async countAll(): Promise<number> {
return await this.model.estimatedDocumentCount().exec();
}

View File

@ -10,8 +10,12 @@
import { BaseRepository } from './base-repository';
import { BaseSchema } from './base-schema';
export abstract class BaseSeeder<T, P extends string = never> {
constructor(protected readonly repository: BaseRepository<T, P>) {}
export abstract class BaseSeeder<
T,
P extends string = never,
TFull extends Omit<T, P> = never,
> {
constructor(protected readonly repository: BaseRepository<T, P, TFull>) {}
async findAll(): Promise<T[]> {
return await this.repository.findAll();

View File

@ -16,8 +16,16 @@ import { BaseRepository } from './base-repository';
import { BaseSchema } from './base-schema';
import { PageQueryDto, QuerySortDto } from '../pagination/pagination-query.dto';
export abstract class BaseService<T extends BaseSchema> {
constructor(readonly repository: BaseRepository<T, never>) {}
export abstract class BaseService<
T extends BaseSchema,
P extends string = never,
TFull extends Omit<T, P> = never,
> {
constructor(protected readonly repository: BaseRepository<T, P, TFull>) {}
getRepository() {
return this.repository;
}
async findOne(
criteria: string | TFilterQuery<T>,
@ -26,14 +34,26 @@ export abstract class BaseService<T extends BaseSchema> {
return await this.repository.findOne(criteria, options);
}
async findOneAndPopulate(id: string) {
return await this.repository.findOneAndPopulate(id);
}
async find(filter: TFilterQuery<T>, sort?: QuerySortDto<T>): Promise<T[]> {
return await this.repository.find(filter, sort);
}
async findAndPopulate(filters: TFilterQuery<T>, sort?: QuerySortDto<T>) {
return await this.repository.findAndPopulate(filters, sort);
}
async findAll(sort?: QuerySortDto<T>): Promise<T[]> {
return await this.repository.findAll(sort);
}
async findAllAndPopulate(sort?: QuerySortDto<T>): Promise<TFull[]> {
return await this.repository.findAllAndPopulate(sort);
}
async findPage(
filters: TFilterQuery<T>,
pageQueryDto: PageQueryDto<T>,
@ -41,6 +61,13 @@ export abstract class BaseService<T extends BaseSchema> {
return await this.repository.findPage(filters, pageQueryDto);
}
async findPageAndPopulate(
filters: TFilterQuery<T>,
pageQueryDto: PageQueryDto<T>,
): Promise<TFull[]> {
return await this.repository.findPageAndPopulate(filters, pageQueryDto);
}
async countAll(): Promise<number> {
return await this.repository.countAll();
}