feat(multi-tenant): complete repository tenant filtering for remaining services

- knowledge-postgres.repository: add tenant_id to all queries and raw SQL
- memory-postgres.repository: add tenant_id filtering for UserMemory and SystemExperience
- admin-postgres.repository: add tenant_id filtering (direct injection for nullable tenantId)
- All 11 repositories now have proper tenant isolation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-25 19:12:04 -08:00
parent 1df5854825
commit 92ee490a57
3 changed files with 119 additions and 35 deletions

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { TenantContextService } from '@iconsulting/shared';
import { IAdminRepository } from '../../../domain/repositories/admin.repository.interface'; import { IAdminRepository } from '../../../domain/repositories/admin.repository.interface';
import { AdminEntity } from '../../../domain/entities/admin.entity'; import { AdminEntity } from '../../../domain/entities/admin.entity';
import { AdminRole } from '../../../domain/value-objects/admin-role.enum'; import { AdminRole } from '../../../domain/value-objects/admin-role.enum';
@ -11,20 +12,32 @@ export class AdminPostgresRepository implements IAdminRepository {
constructor( constructor(
@InjectRepository(AdminORM) @InjectRepository(AdminORM)
private adminRepo: Repository<AdminORM>, private adminRepo: Repository<AdminORM>,
private readonly tenantContext: TenantContextService,
) {} ) {}
private getTenantId(): string {
const id = this.tenantContext.getCurrentTenantId();
if (!id) throw new Error('Tenant context not set');
return id;
}
async save(admin: AdminEntity): Promise<void> { async save(admin: AdminEntity): Promise<void> {
const orm = this.toORM(admin); const orm = this.toORM(admin);
orm.tenantId = this.getTenantId();
await this.adminRepo.save(orm); await this.adminRepo.save(orm);
} }
async findById(id: string): Promise<AdminEntity | null> { async findById(id: string): Promise<AdminEntity | null> {
const orm = await this.adminRepo.findOne({ where: { id } }); const orm = await this.adminRepo.findOne({
where: { id, tenantId: this.getTenantId() },
});
return orm ? this.toEntity(orm) : null; return orm ? this.toEntity(orm) : null;
} }
async findByUsername(username: string): Promise<AdminEntity | null> { async findByUsername(username: string): Promise<AdminEntity | null> {
const orm = await this.adminRepo.findOne({ where: { username } }); const orm = await this.adminRepo.findOne({
where: { username, tenantId: this.getTenantId() },
});
return orm ? this.toEntity(orm) : null; return orm ? this.toEntity(orm) : null;
} }
@ -34,7 +47,8 @@ export class AdminPostgresRepository implements IAdminRepository {
limit?: number; limit?: number;
offset?: number; offset?: number;
}): Promise<AdminEntity[]> { }): Promise<AdminEntity[]> {
const query = this.adminRepo.createQueryBuilder('admin'); const query = this.adminRepo.createQueryBuilder('admin')
.where('admin.tenant_id = :tenantId', { tenantId: this.getTenantId() });
if (options?.role) { if (options?.role) {
query.andWhere('admin.role = :role', { role: options.role }); query.andWhere('admin.role = :role', { role: options.role });
@ -62,7 +76,8 @@ export class AdminPostgresRepository implements IAdminRepository {
role?: AdminRole; role?: AdminRole;
isActive?: boolean; isActive?: boolean;
}): Promise<number> { }): Promise<number> {
const query = this.adminRepo.createQueryBuilder('admin'); const query = this.adminRepo.createQueryBuilder('admin')
.where('admin.tenant_id = :tenantId', { tenantId: this.getTenantId() });
if (options?.role) { if (options?.role) {
query.andWhere('admin.role = :role', { role: options.role }); query.andWhere('admin.role = :role', { role: options.role });
@ -77,16 +92,18 @@ export class AdminPostgresRepository implements IAdminRepository {
async update(admin: AdminEntity): Promise<void> { async update(admin: AdminEntity): Promise<void> {
const orm = this.toORM(admin); const orm = this.toORM(admin);
orm.tenantId = this.getTenantId();
await this.adminRepo.save(orm); await this.adminRepo.save(orm);
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.adminRepo.delete(id); await this.adminRepo.delete({ id, tenantId: this.getTenantId() });
} }
private toORM(entity: AdminEntity): AdminORM { private toORM(entity: AdminEntity): AdminORM {
const orm = new AdminORM(); const orm = new AdminORM();
orm.id = entity.id; orm.id = entity.id;
orm.tenantId = this.getTenantId();
orm.username = entity.username; orm.username = entity.username;
orm.passwordHash = entity.passwordHash; orm.passwordHash = entity.passwordHash;
orm.name = entity.name; orm.name = entity.name;

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, ILike } from 'typeorm'; import { Repository, ILike } from 'typeorm';
import { TenantContextService } from '@iconsulting/shared';
import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface'; import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface';
import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity'; import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity';
import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity'; import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity';
@ -14,17 +15,27 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
private articleRepo: Repository<KnowledgeArticleORM>, private articleRepo: Repository<KnowledgeArticleORM>,
@InjectRepository(KnowledgeChunkORM) @InjectRepository(KnowledgeChunkORM)
private chunkRepo: Repository<KnowledgeChunkORM>, private chunkRepo: Repository<KnowledgeChunkORM>,
private readonly tenantContext: TenantContextService,
) {} ) {}
private getTenantId(): string {
const id = this.tenantContext.getCurrentTenantId();
if (!id) throw new Error('Tenant context not set');
return id;
}
// ========== 文章操作 ========== // ========== 文章操作 ==========
async saveArticle(article: KnowledgeArticleEntity): Promise<void> { async saveArticle(article: KnowledgeArticleEntity): Promise<void> {
const orm = this.toArticleORM(article); const orm = this.toArticleORM(article);
orm.tenantId = this.getTenantId();
await this.articleRepo.save(orm); await this.articleRepo.save(orm);
} }
async findArticleById(id: string): Promise<KnowledgeArticleEntity | null> { async findArticleById(id: string): Promise<KnowledgeArticleEntity | null> {
const orm = await this.articleRepo.findOne({ where: { id } }); const orm = await this.articleRepo.findOne({
where: { id, tenantId: this.getTenantId() },
});
return orm ? this.toArticleEntity(orm) : null; return orm ? this.toArticleEntity(orm) : null;
} }
@ -33,7 +44,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
options?: { publishedOnly?: boolean; limit?: number; offset?: number }, options?: { publishedOnly?: boolean; limit?: number; offset?: number },
): Promise<KnowledgeArticleEntity[]> { ): Promise<KnowledgeArticleEntity[]> {
const query = this.articleRepo.createQueryBuilder('article') const query = this.articleRepo.createQueryBuilder('article')
.where('article.category = :category', { category }); .where('article.tenant_id = :tenantId', { tenantId: this.getTenantId() })
.andWhere('article.category = :category', { category });
if (options?.publishedOnly) { if (options?.publishedOnly) {
query.andWhere('article.isPublished = true'); query.andWhere('article.isPublished = true');
@ -58,7 +70,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
options?: { category?: string; publishedOnly?: boolean; limit?: number }, options?: { category?: string; publishedOnly?: boolean; limit?: number },
): Promise<KnowledgeArticleEntity[]> { ): Promise<KnowledgeArticleEntity[]> {
const query = this.articleRepo.createQueryBuilder('article') const query = this.articleRepo.createQueryBuilder('article')
.where('(article.title ILIKE :search OR article.content ILIKE :search)', { .where('article.tenant_id = :tenantId', { tenantId: this.getTenantId() })
.andWhere('(article.title ILIKE :search OR article.content ILIKE :search)', {
search: `%${queryStr}%`, search: `%${queryStr}%`,
}); });
@ -86,6 +99,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
minSimilarity?: number; minSimilarity?: number;
}, },
): Promise<Array<{ article: KnowledgeArticleEntity; similarity: number }>> { ): Promise<Array<{ article: KnowledgeArticleEntity; similarity: number }>> {
const tenantId = this.getTenantId();
const embeddingStr = `[${embedding.join(',')}]`; const embeddingStr = `[${embedding.join(',')}]`;
const limit = options?.limit || 5; const limit = options?.limit || 5;
const minSimilarity = options?.minSimilarity || 0.7; const minSimilarity = options?.minSimilarity || 0.7;
@ -94,7 +108,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
SELECT *, SELECT *,
1 - (embedding <=> '${embeddingStr}'::vector) as similarity 1 - (embedding <=> '${embeddingStr}'::vector) as similarity
FROM knowledge_articles FROM knowledge_articles
WHERE embedding IS NOT NULL WHERE tenant_id = $1
AND embedding IS NOT NULL
`; `;
if (options?.category) { if (options?.category) {
@ -106,13 +121,12 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
} }
sql += ` sql += `
HAVING 1 - (embedding <=> '${embeddingStr}'::vector) >= ${minSimilarity} AND 1 - (embedding <=> '${embeddingStr}'::vector) >= ${minSimilarity}
ORDER BY similarity DESC ORDER BY similarity DESC
LIMIT ${limit} LIMIT ${limit}
`; `;
// 使用原生查询以利用pgvector const results = await this.articleRepo.query(sql, [tenantId]);
const results = await this.articleRepo.query(sql);
return results.map((row: any) => ({ return results.map((row: any) => ({
article: this.toArticleEntityFromRaw(row), article: this.toArticleEntityFromRaw(row),
@ -122,15 +136,17 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
async updateArticle(article: KnowledgeArticleEntity): Promise<void> { async updateArticle(article: KnowledgeArticleEntity): Promise<void> {
const orm = this.toArticleORM(article); const orm = this.toArticleORM(article);
orm.tenantId = this.getTenantId();
await this.articleRepo.save(orm); await this.articleRepo.save(orm);
} }
async deleteArticle(id: string): Promise<void> { async deleteArticle(id: string): Promise<void> {
await this.articleRepo.delete(id); await this.articleRepo.delete({ id, tenantId: this.getTenantId() });
} }
async countArticles(options?: { category?: string; publishedOnly?: boolean }): Promise<number> { async countArticles(options?: { category?: string; publishedOnly?: boolean }): Promise<number> {
const query = this.articleRepo.createQueryBuilder('article'); const query = this.articleRepo.createQueryBuilder('article')
.where('article.tenant_id = :tenantId', { tenantId: this.getTenantId() });
if (options?.category) { if (options?.category) {
query.andWhere('article.category = :category', { category: options.category }); query.andWhere('article.category = :category', { category: options.category });
@ -147,17 +163,23 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
async saveChunk(chunk: KnowledgeChunkEntity): Promise<void> { async saveChunk(chunk: KnowledgeChunkEntity): Promise<void> {
const orm = this.toChunkORM(chunk); const orm = this.toChunkORM(chunk);
orm.tenantId = this.getTenantId();
await this.chunkRepo.save(orm); await this.chunkRepo.save(orm);
} }
async saveChunks(chunks: KnowledgeChunkEntity[]): Promise<void> { async saveChunks(chunks: KnowledgeChunkEntity[]): Promise<void> {
const orms = chunks.map(chunk => this.toChunkORM(chunk)); const tenantId = this.getTenantId();
const orms = chunks.map(chunk => {
const orm = this.toChunkORM(chunk);
orm.tenantId = tenantId;
return orm;
});
await this.chunkRepo.save(orms); await this.chunkRepo.save(orms);
} }
async findChunksByArticleId(articleId: string): Promise<KnowledgeChunkEntity[]> { async findChunksByArticleId(articleId: string): Promise<KnowledgeChunkEntity[]> {
const orms = await this.chunkRepo.find({ const orms = await this.chunkRepo.find({
where: { articleId }, where: { articleId, tenantId: this.getTenantId() },
order: { chunkIndex: 'ASC' }, order: { chunkIndex: 'ASC' },
}); });
return orms.map(orm => this.toChunkEntity(orm)); return orms.map(orm => this.toChunkEntity(orm));
@ -171,6 +193,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
minSimilarity?: number; minSimilarity?: number;
}, },
): Promise<Array<{ chunk: KnowledgeChunkEntity; similarity: number }>> { ): Promise<Array<{ chunk: KnowledgeChunkEntity; similarity: number }>> {
const tenantId = this.getTenantId();
const embeddingStr = `[${embedding.join(',')}]`; const embeddingStr = `[${embedding.join(',')}]`;
const limit = options?.limit || 5; const limit = options?.limit || 5;
const minSimilarity = options?.minSimilarity || 0.7; const minSimilarity = options?.minSimilarity || 0.7;
@ -180,7 +203,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
1 - (c.embedding <=> '${embeddingStr}'::vector) as similarity 1 - (c.embedding <=> '${embeddingStr}'::vector) as similarity
FROM knowledge_chunks c FROM knowledge_chunks c
JOIN knowledge_articles a ON c.article_id = a.id JOIN knowledge_articles a ON c.article_id = a.id
WHERE c.embedding IS NOT NULL WHERE c.tenant_id = $1
AND c.embedding IS NOT NULL
AND a.is_published = true AND a.is_published = true
`; `;
@ -194,7 +218,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
LIMIT ${limit} LIMIT ${limit}
`; `;
const results = await this.chunkRepo.query(sql); const results = await this.chunkRepo.query(sql, [tenantId]);
return results.map((row: any) => ({ return results.map((row: any) => ({
chunk: this.toChunkEntityFromRaw(row), chunk: this.toChunkEntityFromRaw(row),
@ -203,7 +227,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
} }
async deleteChunksByArticleId(articleId: string): Promise<void> { async deleteChunksByArticleId(articleId: string): Promise<void> {
await this.chunkRepo.delete({ articleId }); await this.chunkRepo.delete({ articleId, tenantId: this.getTenantId() });
} }
// ========== 转换方法 ========== // ========== 转换方法 ==========
@ -211,6 +235,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
private toArticleORM(entity: KnowledgeArticleEntity): KnowledgeArticleORM { private toArticleORM(entity: KnowledgeArticleEntity): KnowledgeArticleORM {
const orm = new KnowledgeArticleORM(); const orm = new KnowledgeArticleORM();
orm.id = entity.id; orm.id = entity.id;
orm.tenantId = this.getTenantId();
orm.title = entity.title; orm.title = entity.title;
orm.content = entity.content; orm.content = entity.content;
orm.summary = entity.summary; orm.summary = entity.summary;
@ -280,6 +305,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
private toChunkORM(entity: KnowledgeChunkEntity): KnowledgeChunkORM { private toChunkORM(entity: KnowledgeChunkEntity): KnowledgeChunkORM {
const orm = new KnowledgeChunkORM(); const orm = new KnowledgeChunkORM();
orm.id = entity.id; orm.id = entity.id;
orm.tenantId = this.getTenantId();
orm.articleId = entity.articleId; orm.articleId = entity.articleId;
orm.content = entity.content; orm.content = entity.content;
orm.chunkIndex = entity.chunkIndex; orm.chunkIndex = entity.chunkIndex;

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThan, MoreThan } from 'typeorm'; import { Repository, LessThan, MoreThan } from 'typeorm';
import { TenantContextService } from '@iconsulting/shared';
import { import {
IUserMemoryRepository, IUserMemoryRepository,
ISystemExperienceRepository, ISystemExperienceRepository,
@ -19,15 +20,25 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
constructor( constructor(
@InjectRepository(UserMemoryORM) @InjectRepository(UserMemoryORM)
private memoryRepo: Repository<UserMemoryORM>, private memoryRepo: Repository<UserMemoryORM>,
private readonly tenantContext: TenantContextService,
) {} ) {}
private getTenantId(): string {
const id = this.tenantContext.getCurrentTenantId();
if (!id) throw new Error('Tenant context not set');
return id;
}
async save(memory: UserMemoryEntity): Promise<void> { async save(memory: UserMemoryEntity): Promise<void> {
const orm = this.toORM(memory); const orm = this.toORM(memory);
orm.tenantId = this.getTenantId();
await this.memoryRepo.save(orm); await this.memoryRepo.save(orm);
} }
async findById(id: string): Promise<UserMemoryEntity | null> { async findById(id: string): Promise<UserMemoryEntity | null> {
const orm = await this.memoryRepo.findOne({ where: { id } }); const orm = await this.memoryRepo.findOne({
where: { id, tenantId: this.getTenantId() },
});
return orm ? this.toEntity(orm) : null; return orm ? this.toEntity(orm) : null;
} }
@ -36,7 +47,8 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
options?: { memoryType?: MemoryType; includeExpired?: boolean; limit?: number }, options?: { memoryType?: MemoryType; includeExpired?: boolean; limit?: number },
): Promise<UserMemoryEntity[]> { ): Promise<UserMemoryEntity[]> {
const query = this.memoryRepo.createQueryBuilder('memory') const query = this.memoryRepo.createQueryBuilder('memory')
.where('memory.userId = :userId', { userId }); .where('memory.tenant_id = :tenantId', { tenantId: this.getTenantId() })
.andWhere('memory.userId = :userId', { userId });
if (options?.memoryType) { if (options?.memoryType) {
query.andWhere('memory.memoryType = :type', { type: options.memoryType }); query.andWhere('memory.memoryType = :type', { type: options.memoryType });
@ -62,6 +74,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
embedding: number[], embedding: number[],
options?: { memoryType?: MemoryType; limit?: number; minSimilarity?: number }, options?: { memoryType?: MemoryType; limit?: number; minSimilarity?: number },
): Promise<Array<{ memory: UserMemoryEntity; similarity: number }>> { ): Promise<Array<{ memory: UserMemoryEntity; similarity: number }>> {
const tenantId = this.getTenantId();
const embeddingStr = `[${embedding.join(',')}]`; const embeddingStr = `[${embedding.join(',')}]`;
const limit = options?.limit || 5; const limit = options?.limit || 5;
const minSimilarity = options?.minSimilarity || 0.7; const minSimilarity = options?.minSimilarity || 0.7;
@ -70,7 +83,8 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
SELECT *, SELECT *,
1 - (embedding <=> '${embeddingStr}'::vector) as similarity 1 - (embedding <=> '${embeddingStr}'::vector) as similarity
FROM user_memories FROM user_memories
WHERE user_id = '${userId}' WHERE tenant_id = $1
AND user_id = $2
AND embedding IS NOT NULL AND embedding IS NOT NULL
AND is_expired = false AND is_expired = false
`; `;
@ -85,7 +99,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
LIMIT ${limit} LIMIT ${limit}
`; `;
const results = await this.memoryRepo.query(sql); const results = await this.memoryRepo.query(sql, [tenantId, userId]);
return results.map((row: any) => ({ return results.map((row: any) => ({
memory: this.toEntityFromRaw(row), memory: this.toEntityFromRaw(row),
@ -95,7 +109,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
async findTopMemories(userId: string, limit: number): Promise<UserMemoryEntity[]> { async findTopMemories(userId: string, limit: number): Promise<UserMemoryEntity[]> {
const orms = await this.memoryRepo.find({ const orms = await this.memoryRepo.find({
where: { userId, isExpired: false }, where: { userId, tenantId: this.getTenantId(), isExpired: false },
order: { importance: 'DESC', accessCount: 'DESC' }, order: { importance: 'DESC', accessCount: 'DESC' },
take: limit, take: limit,
}); });
@ -104,15 +118,16 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
async update(memory: UserMemoryEntity): Promise<void> { async update(memory: UserMemoryEntity): Promise<void> {
const orm = this.toORM(memory); const orm = this.toORM(memory);
orm.tenantId = this.getTenantId();
await this.memoryRepo.save(orm); await this.memoryRepo.save(orm);
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.memoryRepo.delete(id); await this.memoryRepo.delete({ id, tenantId: this.getTenantId() });
} }
async deleteByUserId(userId: string): Promise<void> { async deleteByUserId(userId: string): Promise<void> {
await this.memoryRepo.delete({ userId }); await this.memoryRepo.delete({ userId, tenantId: this.getTenantId() });
} }
async markExpiredMemories(userId: string, olderThanDays: number): Promise<number> { async markExpiredMemories(userId: string, olderThanDays: number): Promise<number> {
@ -122,6 +137,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
const result = await this.memoryRepo.update( const result = await this.memoryRepo.update(
{ {
userId, userId,
tenantId: this.getTenantId(),
isExpired: false, isExpired: false,
updatedAt: LessThan(cutoffDate), updatedAt: LessThan(cutoffDate),
}, },
@ -134,6 +150,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
private toORM(entity: UserMemoryEntity): UserMemoryORM { private toORM(entity: UserMemoryEntity): UserMemoryORM {
const orm = new UserMemoryORM(); const orm = new UserMemoryORM();
orm.id = entity.id; orm.id = entity.id;
orm.tenantId = this.getTenantId();
orm.userId = entity.userId; orm.userId = entity.userId;
orm.memoryType = entity.memoryType; orm.memoryType = entity.memoryType;
orm.content = entity.content; orm.content = entity.content;
@ -191,15 +208,25 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
constructor( constructor(
@InjectRepository(SystemExperienceORM) @InjectRepository(SystemExperienceORM)
private experienceRepo: Repository<SystemExperienceORM>, private experienceRepo: Repository<SystemExperienceORM>,
private readonly tenantContext: TenantContextService,
) {} ) {}
private getTenantId(): string {
const id = this.tenantContext.getCurrentTenantId();
if (!id) throw new Error('Tenant context not set');
return id;
}
async save(experience: SystemExperienceEntity): Promise<void> { async save(experience: SystemExperienceEntity): Promise<void> {
const orm = this.toORM(experience); const orm = this.toORM(experience);
orm.tenantId = this.getTenantId();
await this.experienceRepo.save(orm); await this.experienceRepo.save(orm);
} }
async findById(id: string): Promise<SystemExperienceEntity | null> { async findById(id: string): Promise<SystemExperienceEntity | null> {
const orm = await this.experienceRepo.findOne({ where: { id } }); const orm = await this.experienceRepo.findOne({
where: { id, tenantId: this.getTenantId() },
});
return orm ? this.toEntity(orm) : null; return orm ? this.toEntity(orm) : null;
} }
@ -209,7 +236,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
offset?: number; offset?: number;
}): Promise<SystemExperienceEntity[]> { }): Promise<SystemExperienceEntity[]> {
const query = this.experienceRepo.createQueryBuilder('exp') const query = this.experienceRepo.createQueryBuilder('exp')
.where('exp.verificationStatus = :status', { status: VerificationStatus.PENDING }); .where('exp.tenant_id = :tenantId', { tenantId: this.getTenantId() })
.andWhere('exp.verificationStatus = :status', { status: VerificationStatus.PENDING });
if (options?.experienceType) { if (options?.experienceType) {
query.andWhere('exp.experienceType = :type', { type: options.experienceType }); query.andWhere('exp.experienceType = :type', { type: options.experienceType });
@ -232,7 +260,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
limit?: number; limit?: number;
}): Promise<SystemExperienceEntity[]> { }): Promise<SystemExperienceEntity[]> {
const query = this.experienceRepo.createQueryBuilder('exp') const query = this.experienceRepo.createQueryBuilder('exp')
.where('exp.isActive = true'); .where('exp.tenant_id = :tenantId', { tenantId: this.getTenantId() })
.andWhere('exp.isActive = true');
if (options?.experienceType) { if (options?.experienceType) {
query.andWhere('exp.experienceType = :type', { type: options.experienceType }); query.andWhere('exp.experienceType = :type', { type: options.experienceType });
@ -263,6 +292,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
minSimilarity?: number; minSimilarity?: number;
}, },
): Promise<Array<{ experience: SystemExperienceEntity; similarity: number }>> { ): Promise<Array<{ experience: SystemExperienceEntity; similarity: number }>> {
const tenantId = this.getTenantId();
const embeddingStr = `[${embedding.join(',')}]`; const embeddingStr = `[${embedding.join(',')}]`;
const limit = options?.limit || 5; const limit = options?.limit || 5;
const minSimilarity = options?.minSimilarity || 0.7; const minSimilarity = options?.minSimilarity || 0.7;
@ -271,7 +301,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
SELECT *, SELECT *,
1 - (embedding <=> '${embeddingStr}'::vector) as similarity 1 - (embedding <=> '${embeddingStr}'::vector) as similarity
FROM system_experiences FROM system_experiences
WHERE embedding IS NOT NULL WHERE tenant_id = $1
AND embedding IS NOT NULL
`; `;
if (options?.activeOnly !== false) { if (options?.activeOnly !== false) {
@ -288,7 +319,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
LIMIT ${limit} LIMIT ${limit}
`; `;
const results = await this.experienceRepo.query(sql); const results = await this.experienceRepo.query(sql, [tenantId]);
return results.map((row: any) => ({ return results.map((row: any) => ({
experience: this.toEntityFromRaw(row), experience: this.toEntityFromRaw(row),
@ -300,28 +331,31 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
embedding: number[], embedding: number[],
threshold: number, threshold: number,
): Promise<SystemExperienceEntity[]> { ): Promise<SystemExperienceEntity[]> {
const tenantId = this.getTenantId();
const embeddingStr = `[${embedding.join(',')}]`; const embeddingStr = `[${embedding.join(',')}]`;
const sql = ` const sql = `
SELECT * SELECT *
FROM system_experiences FROM system_experiences
WHERE embedding IS NOT NULL WHERE tenant_id = $1
AND embedding IS NOT NULL
AND 1 - (embedding <=> '${embeddingStr}'::vector) >= ${threshold} AND 1 - (embedding <=> '${embeddingStr}'::vector) >= ${threshold}
ORDER BY 1 - (embedding <=> '${embeddingStr}'::vector) DESC ORDER BY 1 - (embedding <=> '${embeddingStr}'::vector) DESC
LIMIT 10 LIMIT 10
`; `;
const results = await this.experienceRepo.query(sql); const results = await this.experienceRepo.query(sql, [tenantId]);
return results.map((row: any) => this.toEntityFromRaw(row)); return results.map((row: any) => this.toEntityFromRaw(row));
} }
async update(experience: SystemExperienceEntity): Promise<void> { async update(experience: SystemExperienceEntity): Promise<void> {
const orm = this.toORM(experience); const orm = this.toORM(experience);
orm.tenantId = this.getTenantId();
await this.experienceRepo.save(orm); await this.experienceRepo.save(orm);
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.experienceRepo.delete(id); await this.experienceRepo.delete({ id, tenantId: this.getTenantId() });
} }
async getStatistics(): Promise<{ async getStatistics(): Promise<{
@ -329,12 +363,17 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
byStatus: Record<VerificationStatus, number>; byStatus: Record<VerificationStatus, number>;
byType: Record<ExperienceType, number>; byType: Record<ExperienceType, number>;
}> { }> {
const total = await this.experienceRepo.count(); const tenantId = this.getTenantId();
const total = await this.experienceRepo.count({
where: { tenantId },
});
const statusCounts = await this.experienceRepo const statusCounts = await this.experienceRepo
.createQueryBuilder('exp') .createQueryBuilder('exp')
.select('exp.verificationStatus', 'status') .select('exp.verificationStatus', 'status')
.addSelect('COUNT(*)', 'count') .addSelect('COUNT(*)', 'count')
.where('exp.tenant_id = :tenantId', { tenantId })
.groupBy('exp.verificationStatus') .groupBy('exp.verificationStatus')
.getRawMany(); .getRawMany();
@ -342,6 +381,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
.createQueryBuilder('exp') .createQueryBuilder('exp')
.select('exp.experienceType', 'type') .select('exp.experienceType', 'type')
.addSelect('COUNT(*)', 'count') .addSelect('COUNT(*)', 'count')
.where('exp.tenant_id = :tenantId', { tenantId })
.groupBy('exp.experienceType') .groupBy('exp.experienceType')
.getRawMany(); .getRawMany();
@ -361,6 +401,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
private toORM(entity: SystemExperienceEntity): SystemExperienceORM { private toORM(entity: SystemExperienceEntity): SystemExperienceORM {
const orm = new SystemExperienceORM(); const orm = new SystemExperienceORM();
orm.id = entity.id; orm.id = entity.id;
orm.tenantId = this.getTenantId();
orm.experienceType = entity.experienceType; orm.experienceType = entity.experienceType;
orm.content = entity.content; orm.content = entity.content;
orm.confidence = entity.confidence; orm.confidence = entity.confidence;