Handle deleting client and orphaning resources

This commit is contained in:
Owen
2026-04-28 22:19:03 -07:00
parent 2203ebf723
commit a44100c2bd
5 changed files with 77 additions and 28 deletions

View File

@@ -47,6 +47,7 @@ const createSiteResourceSchema = z
ssl: z.boolean().optional(), // only used for http mode
scheme: z.enum(["http", "https"]).optional(),
siteIds: z.array(z.int()),
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
// proxyPort: z.int().positive().optional(),
destinationPort: z.int().positive().optional(),
destination: z.string().min(1),
@@ -187,7 +188,8 @@ export async function createSiteResource(
const {
name,
niceId,
siteIds,
siteIds: siteIdsInput,
siteId,
mode,
scheme,
// proxyPort,
@@ -208,6 +210,12 @@ export async function createSiteResource(
subdomain
} = parsedBody.data;
// Backward compatibility: merge deprecated siteId into siteIds array
const siteIds = [...siteIdsInput];
if (siteId !== undefined && !siteIds.includes(siteId)) {
siteIds.push(siteId);
}
if (mode == "http") {
const hasHttpFeature = await isLicensedOrSubscribed(
orgId,

View File

@@ -98,9 +98,11 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
*/
function aggCol<T>(column: any) {
if (DB_TYPE === "sqlite") {
// json_group_array will include NULLs for left-joined missing rows;
// we filter them out in transformSiteResourceRow keeping arrays aligned.
return sql<T>`json_group_array(${column})`;
}
return sql<T>`array_agg(${column})`;
return sql<T>`COALESCE(array_agg(${column}) FILTER (WHERE ${sites.siteId} IS NOT NULL), '{}')`;
}
/**
@@ -112,16 +114,36 @@ function transformSiteResourceRow(row: any) {
if (DB_TYPE !== "sqlite") {
return row;
}
const siteIdsRaw = JSON.parse(row.siteIds) as (number | null)[];
const siteNamesRaw = JSON.parse(row.siteNames) as (string | null)[];
const siteNiceIdsRaw = JSON.parse(row.siteNiceIds) as (string | null)[];
const siteAddressesRaw = JSON.parse(row.siteAddresses) as (string | null)[];
const siteOnlinesRaw = JSON.parse(row.siteOnlines) as (0 | 1 | null)[];
// When a site resource has no associated sites (left join produced no
// matches), the aggregated arrays will contain a single NULL entry. Strip
// those out, keeping the parallel arrays aligned by siteId presence.
const siteIds: number[] = [];
const siteNames: string[] = [];
const siteNiceIds: string[] = [];
const siteAddresses: (string | null)[] = [];
const siteOnlines: boolean[] = [];
for (let i = 0; i < siteIdsRaw.length; i++) {
if (siteIdsRaw[i] == null) continue;
siteIds.push(siteIdsRaw[i] as number);
siteNames.push((siteNamesRaw[i] ?? "") as string);
siteNiceIds.push((siteNiceIdsRaw[i] ?? "") as string);
siteAddresses.push(siteAddressesRaw[i] ?? null);
siteOnlines.push(siteOnlinesRaw[i] === 1);
}
return {
...row,
siteNames: JSON.parse(row.siteNames) as string[],
siteNiceIds: JSON.parse(row.siteNiceIds) as string[],
siteIds: JSON.parse(row.siteIds) as number[],
siteAddresses: JSON.parse(row.siteAddresses) as (string | null)[],
// SQLite stores booleans as 0/1 integers
siteOnlines: (JSON.parse(row.siteOnlines) as (0 | 1)[]).map(
(v) => v === 1
) as boolean[]
siteNames,
siteNiceIds,
siteIds,
siteAddresses,
siteOnlines
};
}
@@ -158,11 +180,11 @@ function querySiteResourcesBase() {
siteOnlines: aggCol<boolean[]>(sites.online)
})
.from(siteResources)
.innerJoin(
.leftJoin(
siteNetworks,
eq(siteResources.networkId, siteNetworks.networkId)
)
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
.leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
.groupBy(siteResources.siteResourceId);
}
@@ -215,6 +237,8 @@ export async function listAllSiteResourcesByOrg(
const conditions = [and(eq(siteResources.orgId, orgId))];
if (siteId != null) {
// Keep inner joins here: filtering by a specific site implies the
// resource must have at least one matching site.
const resourcesForSite = db
.select({ id: siteResources.siteResourceId })
.from(siteResources)

View File

@@ -44,6 +44,7 @@ const updateSiteResourceSchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
siteIds: z.array(z.int()),
siteId: z.int().positive().optional(),
// niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
niceId: z
.string()
@@ -196,7 +197,8 @@ export async function updateSiteResource(
const { siteResourceId } = parsedParams.data;
const {
name,
siteIds, // because it can change
siteIds: siteIdsInput, // because it can change
siteId,
niceId,
mode,
scheme,
@@ -217,6 +219,12 @@ export async function updateSiteResource(
subdomain
} = parsedBody.data;
// Backward compatibility: merge deprecated siteId into siteIds array
const siteIds = [...siteIdsInput];
if (siteId !== undefined && !siteIds.includes(siteId)) {
siteIds.push(siteId);
}
// Check if site resource exists
const [existingSiteResource] = await db
.select()