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:
@@ -69,16 +69,23 @@ export class TenantsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateTenantDto, userId?: string) {
|
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 {
|
try {
|
||||||
const tenant = await this.prisma.$transaction(async (tx: any) => {
|
const tenant = await this.prisma.$transaction(async (tx: any) => {
|
||||||
const created = await tx.tenant.create({
|
const created = await tx.tenant.create({
|
||||||
data: {
|
data: {
|
||||||
product: dto.product,
|
product: dto.product,
|
||||||
slug: dto.slug.toLowerCase(),
|
slug,
|
||||||
nameFa: dto.nameFa,
|
nameFa: dto.nameFa,
|
||||||
nameEn: dto.nameEn,
|
nameEn: dto.nameEn,
|
||||||
domain: this.normalizeDomain(dto.domain),
|
domain,
|
||||||
domains: aliases as unknown as Prisma.InputJsonValue,
|
domains: aliases as unknown as Prisma.InputJsonValue,
|
||||||
avatarUrl: dto.avatarUrl,
|
avatarUrl: dto.avatarUrl,
|
||||||
primaryColor: dto.primaryColor,
|
primaryColor: dto.primaryColor,
|
||||||
@@ -140,8 +147,23 @@ export class TenantsService {
|
|||||||
throw new NotFoundException('نسخه یافت نشد');
|
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;
|
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 =
|
const aliasesChanged =
|
||||||
nextAliases !== undefined &&
|
nextAliases !== undefined &&
|
||||||
JSON.stringify(this.uniqueNormalizedHosts(this.readDomains(before.domains))) !==
|
JSON.stringify(this.uniqueNormalizedHosts(this.readDomains(before.domains))) !==
|
||||||
@@ -381,11 +403,52 @@ export class TenantsService {
|
|||||||
return `${Date.now()}`;
|
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) {
|
private handleUnique(error: unknown) {
|
||||||
if (
|
if (
|
||||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
error.code === 'P2002'
|
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 تکراری است');
|
throw new ConflictException('slug یا domain تکراری است');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user