fix(tenants): clearer duplicate slug/domain errors on create

Pre-check uniqueness and report which field conflicts with which existing version; strip primary domain from aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 18:47:38 +03:30
co-authored by Cursor
parent ff7e24b2c1
commit edfbdb3469
+67 -4
View File
@@ -69,16 +69,23 @@ export class TenantsService {
}
async create(dto: CreateTenantDto, userId?: string) {
const aliases = this.uniqueNormalizedHosts(dto.domains);
const slug = dto.slug.toLowerCase().trim();
const domain = this.normalizeDomain(dto.domain);
const aliases = this.uniqueNormalizedHosts(dto.domains).filter(
(host) => this.hostKey(host) !== this.hostKey(domain),
);
await this.assertSlugDomainAvailable({ slug, domain });
try {
const tenant = await this.prisma.$transaction(async (tx: any) => {
const created = await tx.tenant.create({
data: {
product: dto.product,
slug: dto.slug.toLowerCase(),
slug,
nameFa: dto.nameFa,
nameEn: dto.nameEn,
domain: this.normalizeDomain(dto.domain),
domain,
domains: aliases as unknown as Prisma.InputJsonValue,
avatarUrl: dto.avatarUrl,
primaryColor: dto.primaryColor,
@@ -140,8 +147,23 @@ export class TenantsService {
throw new NotFoundException('نسخه یافت نشد');
}
const nextAliases =
const nextDomain =
dto.domain !== undefined ? this.normalizeDomain(dto.domain) : before.domain;
const nextAliasesRaw =
dto.domains !== undefined ? this.uniqueNormalizedHosts(dto.domains) : undefined;
const nextAliases =
nextAliasesRaw !== undefined
? nextAliasesRaw.filter((host) => this.hostKey(host) !== this.hostKey(nextDomain))
: undefined;
if (dto.slug !== undefined || dto.domain !== undefined) {
await this.assertSlugDomainAvailable({
slug: dto.slug !== undefined ? dto.slug.toLowerCase().trim() : undefined,
domain: dto.domain !== undefined ? nextDomain : undefined,
excludeId: id,
});
}
const aliasesChanged =
nextAliases !== undefined &&
JSON.stringify(this.uniqueNormalizedHosts(this.readDomains(before.domains))) !==
@@ -381,11 +403,52 @@ export class TenantsService {
return `${Date.now()}`;
}
private async assertSlugDomainAvailable(opts: {
slug?: string;
domain?: string;
excludeId?: string;
}) {
if (opts.slug) {
const bySlug = await this.prisma.tenant.findUnique({
where: { slug: opts.slug },
select: { id: true, nameFa: true, domain: true, slug: true },
});
if (bySlug && bySlug.id !== opts.excludeId) {
throw new ConflictException(
`slug «${opts.slug}» قبلاً برای نسخه «${bySlug.nameFa}» (${bySlug.domain}) ثبت شده است`,
);
}
}
if (opts.domain) {
const byDomain = await this.prisma.tenant.findUnique({
where: { domain: opts.domain },
select: { id: true, nameFa: true, domain: true, slug: true },
});
if (byDomain && byDomain.id !== opts.excludeId) {
throw new ConflictException(
`دامنه «${opts.domain}» قبلاً دامنهٔ اصلی نسخه «${byDomain.nameFa}» (slug: ${byDomain.slug}) است`,
);
}
}
}
private handleUnique(error: unknown) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
const targets = Array.isArray(error.meta?.target)
? (error.meta?.target as string[])
: [];
if (targets.includes('slug')) {
throw new ConflictException('slug تکراری است — این slug قبلاً برای نسخه دیگری ثبت شده');
}
if (targets.includes('domain')) {
throw new ConflictException(
'دامنه تکراری است — این دامنه قبلاً دامنهٔ اصلی نسخه دیگری است',
);
}
throw new ConflictException('slug یا domain تکراری است');
}
}