Fixing up the crud for multiple sites

This commit is contained in:
Owen
2026-04-13 16:22:22 -07:00
parent 676eacc9cf
commit 173a81ead8
7 changed files with 73 additions and 37 deletions

View File

@@ -222,8 +222,7 @@ export async function createSiteResource(
const sitesToAssign = await db const sitesToAssign = await db
.select() .select()
.from(sites) .from(sites)
.where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId))) .where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId)));
.limit(1);
if (sitesToAssign.length !== siteIds.length) { if (sitesToAssign.length !== siteIds.length) {
return next( return next(

View File

@@ -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 response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; 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<T>(column: any) {
if (DB_TYPE === "sqlite") {
return sql<T>`json_group_array(${column})`;
}
return sql<T>`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() { function querySiteResourcesBase() {
return db return db
.select({ .select({
@@ -107,19 +141,21 @@ function querySiteResourcesBase() {
fullDomain: siteResources.fullDomain, fullDomain: siteResources.fullDomain,
networkId: siteResources.networkId, networkId: siteResources.networkId,
defaultNetworkId: siteResources.defaultNetworkId, defaultNetworkId: siteResources.defaultNetworkId,
siteNames: sql<string[]>`array_agg(${sites.name})`, siteNames: aggCol<string[]>(sites.name),
siteNiceIds: sql<string[]>`array_agg(${sites.niceId})`, siteNiceIds: aggCol<string[]>(sites.niceId),
siteIds: sql<number[]>`array_agg(${sites.siteId})`, siteIds: aggCol<number[]>(sites.siteId),
siteAddresses: sql<(string | null)[]>`array_agg(${sites.address})`, siteAddresses: aggCol<(string | null)[]>(sites.address),
siteOnlines: sql<boolean[]>`array_agg(${sites.online})` siteOnlines: aggCol<boolean[]>(sites.online)
}) })
.from(siteResources) .from(siteResources)
.innerJoin(siteNetworks, eq(siteResources.networkId, siteNetworks.networkId)) .innerJoin(
siteNetworks,
eq(siteResources.networkId, siteNetworks.networkId)
)
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId)) .innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
.groupBy(siteResources.siteResourceId); .groupBy(siteResources.siteResourceId);
} }
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/site-resources", path: "/org/{orgId}/site-resources",
@@ -210,7 +246,7 @@ export async function listAllSiteResourcesByOrg(
.as("filtered_site_resources") .as("filtered_site_resources")
); );
const [siteResourcesList, totalCount] = await Promise.all([ const [siteResourcesRaw, totalCount] = await Promise.all([
baseQuery baseQuery
.limit(pageSize) .limit(pageSize)
.offset(pageSize * (page - 1)) .offset(pageSize * (page - 1))
@@ -224,6 +260,8 @@ export async function listAllSiteResourcesByOrg(
countQuery countQuery
]); ]);
const siteResourcesList = siteResourcesRaw.map(transformSiteResourceRow);
return response<ListAllSiteResourcesByOrgResponse>(res, { return response<ListAllSiteResourcesByOrgResponse>(res, {
data: { data: {
siteResources: siteResourcesList, siteResources: siteResourcesList,
@@ -247,4 +285,4 @@ export async function listAllSiteResourcesByOrg(
) )
); );
} }
} }

View File

@@ -280,8 +280,7 @@ export async function updateSiteResource(
inArray(sites.siteId, siteIds), inArray(sites.siteId, siteIds),
eq(sites.orgId, existingSiteResource.orgId) eq(sites.orgId, existingSiteResource.orgId)
) )
) );
.limit(1);
if (sitesToAssign.length !== siteIds.length) { if (sitesToAssign.length !== siteIds.length) {
return next( 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 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) { for (const site of sites) {
const [newt] = await trx const [newt] = await trx
.select() .select()
@@ -742,7 +746,11 @@ export async function handleMessagingForUpdatedSiteResource(
} }
// Only update targets on newt if destination changed // Only update targets on newt if destination changed
if (destinationChanged || portRangesChanged || destinationPortChanged) { if (
destinationChanged ||
portRangesChanged ||
destinationPortChanged
) {
const oldTarget = await generateSubnetProxyTargetV2( const oldTarget = await generateSubnetProxyTargetV2(
existingSiteResource, existingSiteResource,
mergedAllClients mergedAllClients

View File

@@ -653,11 +653,7 @@ export default function ClientResourcesTable({
<EditInternalResourceDialog <EditInternalResourceDialog
open={isEditDialogOpen} open={isEditDialogOpen}
setOpen={setIsEditDialogOpen} setOpen={setIsEditDialogOpen}
resource={{ resource={editingResource}
...editingResource,
siteName: editingResource.siteNames[0] ?? "",
siteId: editingResource.siteIds[0]
}}
orgId={orgId} orgId={orgId}
onSuccess={() => { onSuccess={() => {
// Delay refresh to allow modal to close smoothly // Delay refresh to allow modal to close smoothly

View File

@@ -67,7 +67,7 @@ export default function CreateInternalResourceDialog({
`/org/${orgId}/site-resource`, `/org/${orgId}/site-resource`,
{ {
name: data.name, name: data.name,
siteId: data.siteIds[0], siteIds: data.siteIds,
mode: data.mode, mode: data.mode,
destination: data.destination, destination: data.destination,
enabled: true, enabled: true,

View File

@@ -15,7 +15,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { resourceQueries } from "@app/lib/queries"; import { resourceQueries } from "@app/lib/queries";
import { ListSitesResponse } from "@server/routers/site";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
@@ -27,8 +26,6 @@ import {
isHostname isHostname
} from "./InternalResourceForm"; } from "./InternalResourceForm";
type Site = ListSitesResponse["sites"][0];
type EditInternalResourceDialogProps = { type EditInternalResourceDialogProps = {
open: boolean; open: boolean;
setOpen: (val: boolean) => void; setOpen: (val: boolean) => void;
@@ -69,7 +66,7 @@ export default function EditInternalResourceDialog({
await api.post(`/site-resource/${resource.id}`, { await api.post(`/site-resource/${resource.id}`, {
name: data.name, name: data.name,
siteId: data.siteIds[0], siteIds: data.siteIds,
mode: data.mode, mode: data.mode,
niceId: data.niceId, niceId: data.niceId,
destination: data.destination, destination: data.destination,

View File

@@ -136,9 +136,9 @@ export type InternalResourceData = {
id: number; id: number;
name: string; name: string;
orgId: string; orgId: string;
siteName: string; siteNames: string[];
mode: InternalResourceMode; mode: InternalResourceMode;
siteId: number; siteIds: number[];
niceId: string; niceId: string;
destination: string; destination: string;
alias?: string | null; alias?: string | null;
@@ -160,13 +160,11 @@ const tagSchema = z.object({ id: z.string(), text: z.string() });
function buildSelectedSitesForResource( function buildSelectedSitesForResource(
resource: InternalResourceData, resource: InternalResourceData,
): Selectedsite[] { ): Selectedsite[] {
return [ return resource.siteIds.map((siteId, idx) => ({
{ name: resource.siteNames[idx] ?? "",
name: resource.siteName, siteId,
siteId: resource.siteId, type: "newt" as const
type: "newt" }));
}
];
} }
export type InternalResourceFormValues = { export type InternalResourceFormValues = {
@@ -483,7 +481,7 @@ export function InternalResourceForm({
variant === "edit" && resource variant === "edit" && resource
? { ? {
name: resource.name, name: resource.name,
siteIds: [resource.siteId], siteIds: resource.siteIds,
mode: resource.mode ?? "host", mode: resource.mode ?? "host",
destination: resource.destination ?? "", destination: resource.destination ?? "",
alias: resource.alias ?? null, alias: resource.alias ?? null,
@@ -594,7 +592,7 @@ export function InternalResourceForm({
if (resourceChanged) { if (resourceChanged) {
form.reset({ form.reset({
name: resource.name, name: resource.name,
siteIds: [resource.siteId], siteIds: resource.siteIds,
mode: resource.mode ?? "host", mode: resource.mode ?? "host",
destination: resource.destination ?? "", destination: resource.destination ?? "",
alias: resource.alias ?? null, alias: resource.alias ?? null,