Add Smart Notes AI analysis and Mento Memory with user approval.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
Min,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class ListMemoryQueryDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
personId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class CreateMemoryItemDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
personId!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(500)
|
||||
content!: string;
|
||||
}
|
||||
|
||||
export class UpdateMemoryItemDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(500)
|
||||
content!: string;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
import {
|
||||
CreateMemoryItemDto,
|
||||
ListMemoryQueryDto,
|
||||
UpdateMemoryItemDto,
|
||||
} from './dto';
|
||||
import { MemoryService } from './memory.service';
|
||||
|
||||
@Controller('memory')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class MemoryController {
|
||||
constructor(private readonly memory: MemoryService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: Actor, @Query() query: ListMemoryQueryDto) {
|
||||
return this.memory.list(
|
||||
user.organizationId,
|
||||
user,
|
||||
query.personId,
|
||||
query.page ?? 1,
|
||||
query.limit ?? 50,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: Actor, @Body() dto: CreateMemoryItemDto) {
|
||||
return this.memory.create(user.organizationId, user.userId, user, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@CurrentUser() user: Actor,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateMemoryItemDto,
|
||||
) {
|
||||
return this.memory.update(user.organizationId, id, user, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@CurrentUser() user: Actor, @Param('id') id: string) {
|
||||
await this.memory.remove(user.organizationId, id, user);
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MemoryController } from './memory.controller';
|
||||
import { MemoryService } from './memory.service';
|
||||
|
||||
@Module({
|
||||
controllers: [MemoryController],
|
||||
providers: [MemoryService],
|
||||
exports: [MemoryService],
|
||||
})
|
||||
export class MemoryModule {}
|
||||
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateMemoryItemRequest,
|
||||
MemoryItemDto,
|
||||
PaginatedResponse,
|
||||
UpdateMemoryItemRequest,
|
||||
} from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { type Actor, ownerWhere } from '../common/record-scope';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(
|
||||
organizationId: string,
|
||||
actor: Actor,
|
||||
personId: string,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
): Promise<PaginatedResponse<MemoryItemDto>> {
|
||||
await this.assertPersonAccess(organizationId, actor, personId);
|
||||
|
||||
const where = { organizationId, personId };
|
||||
const [rows, total] = await this.prisma.$transaction([
|
||||
this.prisma.memoryItem.findMany({
|
||||
where,
|
||||
include: {
|
||||
confirmedBy: { select: { id: true, fullName: true } },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.memoryItem.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: rows.map((row) => this.toDto(row)),
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
async create(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
actor: Actor,
|
||||
dto: CreateMemoryItemRequest,
|
||||
): Promise<MemoryItemDto> {
|
||||
await this.assertPersonAccess(organizationId, actor, dto.personId);
|
||||
|
||||
const row = await this.prisma.memoryItem.create({
|
||||
data: {
|
||||
organizationId,
|
||||
personId: dto.personId,
|
||||
content: dto.content.trim(),
|
||||
sourceType: 'manual',
|
||||
confirmedById: userId,
|
||||
},
|
||||
include: {
|
||||
confirmedBy: { select: { id: true, fullName: true } },
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
actor: Actor,
|
||||
dto: UpdateMemoryItemRequest,
|
||||
): Promise<MemoryItemDto> {
|
||||
const existing = await this.findAccessible(organizationId, id, actor);
|
||||
const row = await this.prisma.memoryItem.update({
|
||||
where: { id: existing.id },
|
||||
data: { content: dto.content.trim() },
|
||||
include: {
|
||||
confirmedBy: { select: { id: true, fullName: true } },
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async remove(organizationId: string, id: string, actor: Actor): Promise<void> {
|
||||
const existing = await this.findAccessible(organizationId, id, actor);
|
||||
await this.prisma.memoryItem.delete({ where: { id: existing.id } });
|
||||
}
|
||||
|
||||
async createFromNote(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
personId: string,
|
||||
noteId: string,
|
||||
content: string,
|
||||
): Promise<MemoryItemDto> {
|
||||
const row = await this.prisma.memoryItem.create({
|
||||
data: {
|
||||
organizationId,
|
||||
personId,
|
||||
content: content.trim(),
|
||||
sourceType: 'note',
|
||||
sourceId: noteId,
|
||||
confirmedById: userId,
|
||||
},
|
||||
include: {
|
||||
confirmedBy: { select: { id: true, fullName: true } },
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
private async findAccessible(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
actor: Actor,
|
||||
) {
|
||||
const row = await this.prisma.memoryItem.findFirst({
|
||||
where: { id, organizationId },
|
||||
include: { person: { select: { id: true, ownerId: true } } },
|
||||
});
|
||||
if (!row) throw new NotFoundException('مورد حافظه پیدا نشد');
|
||||
await this.assertPersonAccess(organizationId, actor, row.personId);
|
||||
return row;
|
||||
}
|
||||
|
||||
private async assertPersonAccess(
|
||||
organizationId: string,
|
||||
actor: Actor,
|
||||
personId: string,
|
||||
) {
|
||||
const person = await this.prisma.person.findFirst({
|
||||
where: { id: personId, organizationId, ...ownerWhere(actor) },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!person) {
|
||||
throw new BadRequestException('شخص انتخابشده معتبر نیست');
|
||||
}
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
personId: string;
|
||||
content: string;
|
||||
sourceType: string;
|
||||
sourceId: string | null;
|
||||
confirmedBy: { id: string; fullName: string } | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): MemoryItemDto {
|
||||
return {
|
||||
id: row.id,
|
||||
personId: row.personId,
|
||||
content: row.content,
|
||||
sourceType: row.sourceType as MemoryItemDto['sourceType'],
|
||||
sourceId: row.sourceId,
|
||||
confirmedBy: row.confirmedBy,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user