From 173a81ead8f8165b87cbf3c8cea05d880c989483 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 13 Apr 2026 16:22:22 -0700 Subject: [PATCH] Fixing up the crud for multiple sites --- .../siteResource/createSiteResource.ts | 3 +- .../siteResource/listAllSiteResourcesByOrg.ts | 58 +++++++++++++++---- .../siteResource/updateSiteResource.ts | 16 +++-- src/components/ClientResourcesTable.tsx | 6 +- .../CreateInternalResourceDialog.tsx | 2 +- src/components/EditInternalResourceDialog.tsx | 5 +- src/components/InternalResourceForm.tsx | 20 +++---- 7 files changed, 73 insertions(+), 37 deletions(-) diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index da5355c9e..9a7d632fd 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -222,8 +222,7 @@ export async function createSiteResource( const sitesToAssign = await db .select() .from(sites) - .where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId))) - .limit(1); + .where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId))); if (sitesToAssign.length !== siteIds.length) { return next( diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index aa1fe7043..8750e7516 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -1,4 +1,4 @@ -import { db, SiteResource, siteNetworks, siteResources, sites } from "@server/db"; +import { db, DB_TYPE, SiteResource, siteNetworks, siteResources, sites } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -81,6 +81,40 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{ })[]; }>; +/** + * Returns an aggregation expression compatible with both SQLite and PostgreSQL. + * - SQLite: json_group_array(col) → returns a JSON array string, parsed after fetch + * - PostgreSQL: array_agg(col) → returns a native array + */ +function aggCol(column: any) { + if (DB_TYPE === "sqlite") { + return sql`json_group_array(${column})`; + } + return sql`array_agg(${column})`; +} + +/** + * For SQLite the aggregated columns come back as JSON strings; parse them into + * proper arrays. For PostgreSQL the driver already returns native arrays, so + * the row is returned unchanged. + */ +function transformSiteResourceRow(row: any) { + if (DB_TYPE !== "sqlite") { + return row; + } + 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[] + }; +} + function querySiteResourcesBase() { return db .select({ @@ -107,19 +141,21 @@ function querySiteResourcesBase() { fullDomain: siteResources.fullDomain, networkId: siteResources.networkId, defaultNetworkId: siteResources.defaultNetworkId, - siteNames: sql`array_agg(${sites.name})`, - siteNiceIds: sql`array_agg(${sites.niceId})`, - siteIds: sql`array_agg(${sites.siteId})`, - siteAddresses: sql<(string | null)[]>`array_agg(${sites.address})`, - siteOnlines: sql`array_agg(${sites.online})` + siteNames: aggCol(sites.name), + siteNiceIds: aggCol(sites.niceId), + siteIds: aggCol(sites.siteId), + siteAddresses: aggCol<(string | null)[]>(sites.address), + siteOnlines: aggCol(sites.online) }) .from(siteResources) - .innerJoin(siteNetworks, eq(siteResources.networkId, siteNetworks.networkId)) + .innerJoin( + siteNetworks, + eq(siteResources.networkId, siteNetworks.networkId) + ) .innerJoin(sites, eq(siteNetworks.siteId, sites.siteId)) .groupBy(siteResources.siteResourceId); } - registry.registerPath({ method: "get", path: "/org/{orgId}/site-resources", @@ -210,7 +246,7 @@ export async function listAllSiteResourcesByOrg( .as("filtered_site_resources") ); - const [siteResourcesList, totalCount] = await Promise.all([ + const [siteResourcesRaw, totalCount] = await Promise.all([ baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) @@ -224,6 +260,8 @@ export async function listAllSiteResourcesByOrg( countQuery ]); + const siteResourcesList = siteResourcesRaw.map(transformSiteResourceRow); + return response(res, { data: { siteResources: siteResourcesList, @@ -247,4 +285,4 @@ export async function listAllSiteResourcesByOrg( ) ); } -} +} \ No newline at end of file diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 24b9f45b2..40e0feef9 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -280,8 +280,7 @@ export async function updateSiteResource( inArray(sites.siteId, siteIds), eq(sites.orgId, existingSiteResource.orgId) ) - ) - .limit(1); + ); if (sitesToAssign.length !== siteIds.length) { return next( @@ -727,7 +726,12 @@ export async function handleMessagingForUpdatedSiteResource( // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all - if (destinationChanged || aliasChanged || portRangesChanged || destinationPortChanged) { + if ( + destinationChanged || + aliasChanged || + portRangesChanged || + destinationPortChanged + ) { for (const site of sites) { const [newt] = await trx .select() @@ -742,7 +746,11 @@ export async function handleMessagingForUpdatedSiteResource( } // Only update targets on newt if destination changed - if (destinationChanged || portRangesChanged || destinationPortChanged) { + if ( + destinationChanged || + portRangesChanged || + destinationPortChanged + ) { const oldTarget = await generateSubnetProxyTargetV2( existingSiteResource, mergedAllClients diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 4fd7f44fe..c32208321 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -653,11 +653,7 @@ export default function ClientResourcesTable({ { // Delay refresh to allow modal to close smoothly diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index b90cae8a6..b9c978b3f 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -67,7 +67,7 @@ export default function CreateInternalResourceDialog({ `/org/${orgId}/site-resource`, { name: data.name, - siteId: data.siteIds[0], + siteIds: data.siteIds, mode: data.mode, destination: data.destination, enabled: true, diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 7d1c7e8aa..859981f7d 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -15,7 +15,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { resourceQueries } from "@app/lib/queries"; -import { ListSitesResponse } from "@server/routers/site"; import { useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import { useState, useTransition } from "react"; @@ -27,8 +26,6 @@ import { isHostname } from "./InternalResourceForm"; -type Site = ListSitesResponse["sites"][0]; - type EditInternalResourceDialogProps = { open: boolean; setOpen: (val: boolean) => void; @@ -69,7 +66,7 @@ export default function EditInternalResourceDialog({ await api.post(`/site-resource/${resource.id}`, { name: data.name, - siteId: data.siteIds[0], + siteIds: data.siteIds, mode: data.mode, niceId: data.niceId, destination: data.destination, diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index 11abd8919..13d24b6b0 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -136,9 +136,9 @@ export type InternalResourceData = { id: number; name: string; orgId: string; - siteName: string; + siteNames: string[]; mode: InternalResourceMode; - siteId: number; + siteIds: number[]; niceId: string; destination: string; alias?: string | null; @@ -160,13 +160,11 @@ const tagSchema = z.object({ id: z.string(), text: z.string() }); function buildSelectedSitesForResource( resource: InternalResourceData, ): Selectedsite[] { - return [ - { - name: resource.siteName, - siteId: resource.siteId, - type: "newt" - } - ]; + return resource.siteIds.map((siteId, idx) => ({ + name: resource.siteNames[idx] ?? "", + siteId, + type: "newt" as const + })); } export type InternalResourceFormValues = { @@ -483,7 +481,7 @@ export function InternalResourceForm({ variant === "edit" && resource ? { name: resource.name, - siteIds: [resource.siteId], + siteIds: resource.siteIds, mode: resource.mode ?? "host", destination: resource.destination ?? "", alias: resource.alias ?? null, @@ -594,7 +592,7 @@ export function InternalResourceForm({ if (resourceChanged) { form.reset({ name: resource.name, - siteIds: [resource.siteId], + siteIds: resource.siteIds, mode: resource.mode ?? "host", destination: resource.destination ?? "", alias: resource.alias ?? null,