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:
parent
1df5854825
commit
92ee490a57
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TenantContextService } from '@iconsulting/shared';
|
||||
import { IAdminRepository } from '../../../domain/repositories/admin.repository.interface';
|
||||
import { AdminEntity } from '../../../domain/entities/admin.entity';
|
||||
import { AdminRole } from '../../../domain/value-objects/admin-role.enum';
|
||||
|
|
@ -11,20 +12,32 @@ export class AdminPostgresRepository implements IAdminRepository {
|
|||
constructor(
|
||||
@InjectRepository(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> {
|
||||
const orm = this.toORM(admin);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.adminRepo.save(orm);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +47,8 @@ export class AdminPostgresRepository implements IAdminRepository {
|
|||
limit?: number;
|
||||
offset?: number;
|
||||
}): 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) {
|
||||
query.andWhere('admin.role = :role', { role: options.role });
|
||||
|
|
@ -62,7 +76,8 @@ export class AdminPostgresRepository implements IAdminRepository {
|
|||
role?: AdminRole;
|
||||
isActive?: boolean;
|
||||
}): 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) {
|
||||
query.andWhere('admin.role = :role', { role: options.role });
|
||||
|
|
@ -77,16 +92,18 @@ export class AdminPostgresRepository implements IAdminRepository {
|
|||
|
||||
async update(admin: AdminEntity): Promise<void> {
|
||||
const orm = this.toORM(admin);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.adminRepo.save(orm);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.adminRepo.delete(id);
|
||||
await this.adminRepo.delete({ id, tenantId: this.getTenantId() });
|
||||
}
|
||||
|
||||
private toORM(entity: AdminEntity): AdminORM {
|
||||
const orm = new AdminORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.username = entity.username;
|
||||
orm.passwordHash = entity.passwordHash;
|
||||
orm.name = entity.name;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, ILike } from 'typeorm';
|
||||
import { TenantContextService } from '@iconsulting/shared';
|
||||
import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface';
|
||||
import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity';
|
||||
import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity';
|
||||
|
|
@ -14,17 +15,27 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
private articleRepo: Repository<KnowledgeArticleORM>,
|
||||
@InjectRepository(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> {
|
||||
const orm = this.toArticleORM(article);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.articleRepo.save(orm);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +44,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
options?: { publishedOnly?: boolean; limit?: number; offset?: number },
|
||||
): Promise<KnowledgeArticleEntity[]> {
|
||||
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) {
|
||||
query.andWhere('article.isPublished = true');
|
||||
|
|
@ -58,7 +70,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
options?: { category?: string; publishedOnly?: boolean; limit?: number },
|
||||
): Promise<KnowledgeArticleEntity[]> {
|
||||
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}%`,
|
||||
});
|
||||
|
||||
|
|
@ -86,6 +99,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
minSimilarity?: number;
|
||||
},
|
||||
): Promise<Array<{ article: KnowledgeArticleEntity; similarity: number }>> {
|
||||
const tenantId = this.getTenantId();
|
||||
const embeddingStr = `[${embedding.join(',')}]`;
|
||||
const limit = options?.limit || 5;
|
||||
const minSimilarity = options?.minSimilarity || 0.7;
|
||||
|
|
@ -94,7 +108,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
SELECT *,
|
||||
1 - (embedding <=> '${embeddingStr}'::vector) as similarity
|
||||
FROM knowledge_articles
|
||||
WHERE embedding IS NOT NULL
|
||||
WHERE tenant_id = $1
|
||||
AND embedding IS NOT NULL
|
||||
`;
|
||||
|
||||
if (options?.category) {
|
||||
|
|
@ -106,13 +121,12 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
}
|
||||
|
||||
sql += `
|
||||
HAVING 1 - (embedding <=> '${embeddingStr}'::vector) >= ${minSimilarity}
|
||||
AND 1 - (embedding <=> '${embeddingStr}'::vector) >= ${minSimilarity}
|
||||
ORDER BY similarity DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
// 使用原生查询以利用pgvector
|
||||
const results = await this.articleRepo.query(sql);
|
||||
const results = await this.articleRepo.query(sql, [tenantId]);
|
||||
|
||||
return results.map((row: any) => ({
|
||||
article: this.toArticleEntityFromRaw(row),
|
||||
|
|
@ -122,15 +136,17 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
|
||||
async updateArticle(article: KnowledgeArticleEntity): Promise<void> {
|
||||
const orm = this.toArticleORM(article);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.articleRepo.save(orm);
|
||||
}
|
||||
|
||||
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> {
|
||||
const query = this.articleRepo.createQueryBuilder('article');
|
||||
const query = this.articleRepo.createQueryBuilder('article')
|
||||
.where('article.tenant_id = :tenantId', { tenantId: this.getTenantId() });
|
||||
|
||||
if (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> {
|
||||
const orm = this.toChunkORM(chunk);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.chunkRepo.save(orm);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async findChunksByArticleId(articleId: string): Promise<KnowledgeChunkEntity[]> {
|
||||
const orms = await this.chunkRepo.find({
|
||||
where: { articleId },
|
||||
where: { articleId, tenantId: this.getTenantId() },
|
||||
order: { chunkIndex: 'ASC' },
|
||||
});
|
||||
return orms.map(orm => this.toChunkEntity(orm));
|
||||
|
|
@ -171,6 +193,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
minSimilarity?: number;
|
||||
},
|
||||
): Promise<Array<{ chunk: KnowledgeChunkEntity; similarity: number }>> {
|
||||
const tenantId = this.getTenantId();
|
||||
const embeddingStr = `[${embedding.join(',')}]`;
|
||||
const limit = options?.limit || 5;
|
||||
const minSimilarity = options?.minSimilarity || 0.7;
|
||||
|
|
@ -180,7 +203,8 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
1 - (c.embedding <=> '${embeddingStr}'::vector) as similarity
|
||||
FROM knowledge_chunks c
|
||||
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
|
||||
`;
|
||||
|
||||
|
|
@ -194,7 +218,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const results = await this.chunkRepo.query(sql);
|
||||
const results = await this.chunkRepo.query(sql, [tenantId]);
|
||||
|
||||
return results.map((row: any) => ({
|
||||
chunk: this.toChunkEntityFromRaw(row),
|
||||
|
|
@ -203,7 +227,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
}
|
||||
|
||||
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 {
|
||||
const orm = new KnowledgeArticleORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.title = entity.title;
|
||||
orm.content = entity.content;
|
||||
orm.summary = entity.summary;
|
||||
|
|
@ -280,6 +305,7 @@ export class KnowledgePostgresRepository implements IKnowledgeRepository {
|
|||
private toChunkORM(entity: KnowledgeChunkEntity): KnowledgeChunkORM {
|
||||
const orm = new KnowledgeChunkORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.articleId = entity.articleId;
|
||||
orm.content = entity.content;
|
||||
orm.chunkIndex = entity.chunkIndex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, LessThan, MoreThan } from 'typeorm';
|
||||
import { TenantContextService } from '@iconsulting/shared';
|
||||
import {
|
||||
IUserMemoryRepository,
|
||||
ISystemExperienceRepository,
|
||||
|
|
@ -19,15 +20,25 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
constructor(
|
||||
@InjectRepository(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> {
|
||||
const orm = this.toORM(memory);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.memoryRepo.save(orm);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +47,8 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
options?: { memoryType?: MemoryType; includeExpired?: boolean; limit?: number },
|
||||
): Promise<UserMemoryEntity[]> {
|
||||
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) {
|
||||
query.andWhere('memory.memoryType = :type', { type: options.memoryType });
|
||||
|
|
@ -62,6 +74,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
embedding: number[],
|
||||
options?: { memoryType?: MemoryType; limit?: number; minSimilarity?: number },
|
||||
): Promise<Array<{ memory: UserMemoryEntity; similarity: number }>> {
|
||||
const tenantId = this.getTenantId();
|
||||
const embeddingStr = `[${embedding.join(',')}]`;
|
||||
const limit = options?.limit || 5;
|
||||
const minSimilarity = options?.minSimilarity || 0.7;
|
||||
|
|
@ -70,7 +83,8 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
SELECT *,
|
||||
1 - (embedding <=> '${embeddingStr}'::vector) as similarity
|
||||
FROM user_memories
|
||||
WHERE user_id = '${userId}'
|
||||
WHERE tenant_id = $1
|
||||
AND user_id = $2
|
||||
AND embedding IS NOT NULL
|
||||
AND is_expired = false
|
||||
`;
|
||||
|
|
@ -85,7 +99,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const results = await this.memoryRepo.query(sql);
|
||||
const results = await this.memoryRepo.query(sql, [tenantId, userId]);
|
||||
|
||||
return results.map((row: any) => ({
|
||||
memory: this.toEntityFromRaw(row),
|
||||
|
|
@ -95,7 +109,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
|
||||
async findTopMemories(userId: string, limit: number): Promise<UserMemoryEntity[]> {
|
||||
const orms = await this.memoryRepo.find({
|
||||
where: { userId, isExpired: false },
|
||||
where: { userId, tenantId: this.getTenantId(), isExpired: false },
|
||||
order: { importance: 'DESC', accessCount: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
|
|
@ -104,15 +118,16 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
|
||||
async update(memory: UserMemoryEntity): Promise<void> {
|
||||
const orm = this.toORM(memory);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.memoryRepo.save(orm);
|
||||
}
|
||||
|
||||
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> {
|
||||
await this.memoryRepo.delete({ userId });
|
||||
await this.memoryRepo.delete({ userId, tenantId: this.getTenantId() });
|
||||
}
|
||||
|
||||
async markExpiredMemories(userId: string, olderThanDays: number): Promise<number> {
|
||||
|
|
@ -122,6 +137,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
const result = await this.memoryRepo.update(
|
||||
{
|
||||
userId,
|
||||
tenantId: this.getTenantId(),
|
||||
isExpired: false,
|
||||
updatedAt: LessThan(cutoffDate),
|
||||
},
|
||||
|
|
@ -134,6 +150,7 @@ export class UserMemoryPostgresRepository implements IUserMemoryRepository {
|
|||
private toORM(entity: UserMemoryEntity): UserMemoryORM {
|
||||
const orm = new UserMemoryORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.userId = entity.userId;
|
||||
orm.memoryType = entity.memoryType;
|
||||
orm.content = entity.content;
|
||||
|
|
@ -191,15 +208,25 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
constructor(
|
||||
@InjectRepository(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> {
|
||||
const orm = this.toORM(experience);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.experienceRepo.save(orm);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +236,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
offset?: number;
|
||||
}): Promise<SystemExperienceEntity[]> {
|
||||
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) {
|
||||
query.andWhere('exp.experienceType = :type', { type: options.experienceType });
|
||||
|
|
@ -232,7 +260,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
limit?: number;
|
||||
}): Promise<SystemExperienceEntity[]> {
|
||||
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) {
|
||||
query.andWhere('exp.experienceType = :type', { type: options.experienceType });
|
||||
|
|
@ -263,6 +292,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
minSimilarity?: number;
|
||||
},
|
||||
): Promise<Array<{ experience: SystemExperienceEntity; similarity: number }>> {
|
||||
const tenantId = this.getTenantId();
|
||||
const embeddingStr = `[${embedding.join(',')}]`;
|
||||
const limit = options?.limit || 5;
|
||||
const minSimilarity = options?.minSimilarity || 0.7;
|
||||
|
|
@ -271,7 +301,8 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
SELECT *,
|
||||
1 - (embedding <=> '${embeddingStr}'::vector) as similarity
|
||||
FROM system_experiences
|
||||
WHERE embedding IS NOT NULL
|
||||
WHERE tenant_id = $1
|
||||
AND embedding IS NOT NULL
|
||||
`;
|
||||
|
||||
if (options?.activeOnly !== false) {
|
||||
|
|
@ -288,7 +319,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const results = await this.experienceRepo.query(sql);
|
||||
const results = await this.experienceRepo.query(sql, [tenantId]);
|
||||
|
||||
return results.map((row: any) => ({
|
||||
experience: this.toEntityFromRaw(row),
|
||||
|
|
@ -300,28 +331,31 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
embedding: number[],
|
||||
threshold: number,
|
||||
): Promise<SystemExperienceEntity[]> {
|
||||
const tenantId = this.getTenantId();
|
||||
const embeddingStr = `[${embedding.join(',')}]`;
|
||||
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM system_experiences
|
||||
WHERE embedding IS NOT NULL
|
||||
WHERE tenant_id = $1
|
||||
AND embedding IS NOT NULL
|
||||
AND 1 - (embedding <=> '${embeddingStr}'::vector) >= ${threshold}
|
||||
ORDER BY 1 - (embedding <=> '${embeddingStr}'::vector) DESC
|
||||
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));
|
||||
}
|
||||
|
||||
async update(experience: SystemExperienceEntity): Promise<void> {
|
||||
const orm = this.toORM(experience);
|
||||
orm.tenantId = this.getTenantId();
|
||||
await this.experienceRepo.save(orm);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.experienceRepo.delete(id);
|
||||
await this.experienceRepo.delete({ id, tenantId: this.getTenantId() });
|
||||
}
|
||||
|
||||
async getStatistics(): Promise<{
|
||||
|
|
@ -329,12 +363,17 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
byStatus: Record<VerificationStatus, 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
|
||||
.createQueryBuilder('exp')
|
||||
.select('exp.verificationStatus', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('exp.tenant_id = :tenantId', { tenantId })
|
||||
.groupBy('exp.verificationStatus')
|
||||
.getRawMany();
|
||||
|
||||
|
|
@ -342,6 +381,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
.createQueryBuilder('exp')
|
||||
.select('exp.experienceType', 'type')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('exp.tenant_id = :tenantId', { tenantId })
|
||||
.groupBy('exp.experienceType')
|
||||
.getRawMany();
|
||||
|
||||
|
|
@ -361,6 +401,7 @@ export class SystemExperiencePostgresRepository implements ISystemExperienceRepo
|
|||
private toORM(entity: SystemExperienceEntity): SystemExperienceORM {
|
||||
const orm = new SystemExperienceORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.experienceType = entity.experienceType;
|
||||
orm.content = entity.content;
|
||||
orm.confidence = entity.confidence;
|
||||
|
|
|
|||
Loading…
Reference in New Issue