Allow storefront provision when editing a domain git URL.

Edit Domain can wire deploy_slug the same way as Add Domain so existing businesses get a Deploy button without recreating the domain.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 14:18:31 +03:30
co-authored by Cursor
parent d933aef40b
commit 7d123502f8
2 changed files with 70 additions and 5 deletions
+60 -4
View File
@@ -55,6 +55,8 @@ type BusinessRow = {
domainId: bigint | null; domainId: bigint | null;
domain: string | null; domain: string | null;
sslEnabled: boolean | null; sslEnabled: boolean | null;
gitRepoUrl: string | null;
deploySlug: string | null;
ownerUserId: bigint | null; ownerUserId: bigint | null;
ownerName: string | null; ownerName: string | null;
ownerNameEn: string | null; ownerNameEn: string | null;
@@ -134,6 +136,8 @@ export class BusinessAdminService {
dom.id AS "domainId", dom.id AS "domainId",
dom.host AS "domain", dom.host AS "domain",
dom.ssl_enabled AS "sslEnabled", dom.ssl_enabled AS "sslEnabled",
dom.git_repo_url AS "gitRepoUrl",
dom.deploy_slug AS "deploySlug",
own."ownerUserId" AS "ownerUserId", own."ownerUserId" AS "ownerUserId",
own."ownerName" AS "ownerName", own."ownerName" AS "ownerName",
own."ownerNameEn" AS "ownerNameEn", own."ownerNameEn" AS "ownerNameEn",
@@ -144,7 +148,7 @@ export class BusinessAdminService {
b.settings->'modules'->'charts' AS "homeCharts" b.settings->'modules'->'charts' AS "homeCharts"
FROM businesses b FROM businesses b
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT d.id, d.host, d.ssl_enabled SELECT d.id, d.host, d.ssl_enabled, d.git_repo_url, d.deploy_slug
FROM domains d FROM domains d
WHERE d.business_id = b.id WHERE d.business_id = b.id
ORDER BY d.is_primary DESC, d.created_at DESC ORDER BY d.is_primary DESC, d.created_at DESC
@@ -548,12 +552,19 @@ export class BusinessAdminService {
const businessId = BigInt(businessIdRaw); const businessId = BigInt(businessIdRaw);
const domainId = BigInt(domainIdRaw); const domainId = BigInt(domainIdRaw);
const host = dto.host.trim(); const host = dto.host.trim().toLowerCase();
const gitRepoUrl = dto.gitRepoUrl?.trim() || null;
if (!host) { if (!host) {
throw new BadRequestException('host is required'); throw new BadRequestException('host is required');
} }
if (gitRepoUrl && !isValidGitRepoUrl(gitRepoUrl)) {
throw new BadRequestException(
'gitRepoUrl must be an SSH git URL (e.g. git@git.meshkee.com:Meshkee-Websites/oaktasty.git)',
);
}
const domain = await this.prisma.domain.findFirst({ const domain = await this.prisma.domain.findFirst({
where: { id: domainId, businessId }, where: { id: domainId, businessId },
}); });
@@ -567,10 +578,55 @@ export class BusinessAdminService {
throw new ConflictException('Domain host is already taken'); throw new ConflictException('Domain host is already taken');
} }
return this.prisma.domain.update({ let deploySlug = domain.deploySlug;
let provisionError: string | null = null;
let nextGitRepoUrl = domain.gitRepoUrl;
if (gitRepoUrl) {
const slug = domain.deploySlug?.trim() || deploySlugFromHost(host);
if (!slug) {
throw new BadRequestException('Could not derive deploy slug from host');
}
const slugTaken = await this.prisma.domain.findFirst({
where: { deploySlug: slug, NOT: { id: domainId } },
select: { id: true },
});
if (slugTaken) {
throw new ConflictException(`Deploy slug "${slug}" is already in use`);
}
try {
await this.websiteDeployAgent.provision({
slug,
host,
gitRepoUrl,
});
deploySlug = slug;
nextGitRepoUrl = gitRepoUrl;
} catch (err) {
if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
provisionError = err.message;
} else {
provisionError = 'Storefront provision failed on websites server';
}
}
}
const updated = await this.prisma.domain.update({
where: { id: domainId }, where: { id: domainId },
data: { host }, data: {
host,
...(gitRepoUrl && !provisionError
? { deploySlug, gitRepoUrl: nextGitRepoUrl }
: {}),
},
}); });
return {
...updated,
provisionError,
};
} }
async disable(businessIdRaw: string, dto: DisableBusinessDto, actor: AuthUser) { async disable(businessIdRaw: string, dto: DisableBusinessDto, actor: AuthUser) {
+10 -1
View File
@@ -1,7 +1,16 @@
import { IsString, MinLength } from 'class-validator'; import { IsOptional, IsString, Matches, MinLength } from 'class-validator';
export class UpdateDomainDto { export class UpdateDomainDto {
@IsString() @IsString()
@MinLength(1) @MinLength(1)
host!: string; host!: string;
/** SSH git URL — when set, provisions storefront deploy on the websites VM. */
@IsOptional()
@IsString()
@Matches(/^(?:git@[\w.-]+:[\w./-]+\.git|ssh:\/\/git@[\w.-]+(?::\d+)?\/[\w./-]+\.git)$/i, {
message:
'gitRepoUrl must be an SSH git URL (e.g. git@git.meshkee.com:Meshkee-Websites/oaktasty.git)',
})
gitRepoUrl?: string;
} }