Add Smart Notes AI analysis and Mento Memory with user approval.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 14:29:34 +03:30
co-authored by Cursor
parent 4d9f3c04cd
commit d152d1c19f
20 changed files with 1577 additions and 9 deletions
+45
View File
@@ -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;
}
+57
View File
@@ -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 };
}
}
+10
View File
@@ -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 {}
+167
View File
@@ -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(),
};
}
}