Remove json_group_array from queries

Also improve handling tcp/udp resources in newt function so it does not
loop twice
This commit is contained in:
Owen 2025-03-08 11:58:48 -05:00
parent 212fff73c6
commit 1e78188c71
2 changed files with 148 additions and 105 deletions

View file

@ -7,7 +7,7 @@ import {
Target, Target,
targets targets
} from "@server/db/schema"; } from "@server/db/schema";
import { eq, and, sql } from "drizzle-orm"; import { eq, and, sql, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers"; import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger"; import logger from "@server/logger";
@ -75,68 +75,84 @@ export const handleRegisterMessage: MessageHandler = async (context) => {
allowedIps: [site.subnet] allowedIps: [site.subnet]
}); });
const allResources = await db // Improved version
.select({ const allResources = await db.transaction(async (tx) => {
// Resource fields // First get all resources for the site
resourceId: resources.resourceId, const resourcesList = await tx
subdomain: resources.subdomain, .select({
fullDomain: resources.fullDomain, resourceId: resources.resourceId,
ssl: resources.ssl, subdomain: resources.subdomain,
blockAccess: resources.blockAccess, fullDomain: resources.fullDomain,
sso: resources.sso, ssl: resources.ssl,
emailWhitelistEnabled: resources.emailWhitelistEnabled, blockAccess: resources.blockAccess,
http: resources.http, sso: resources.sso,
proxyPort: resources.proxyPort, emailWhitelistEnabled: resources.emailWhitelistEnabled,
protocol: resources.protocol, http: resources.http,
// Targets as a subquery proxyPort: resources.proxyPort,
targets: sql<string>`json_group_array(json_object( protocol: resources.protocol
'targetId', ${targets.targetId}, })
'ip', ${targets.ip}, .from(resources)
'method', ${targets.method}, .where(eq(resources.siteId, siteId));
'port', ${targets.port},
'internalPort', ${targets.internalPort}, // Get all enabled targets for these resources in a single query
'enabled', ${targets.enabled} const resourceIds = resourcesList.map((r) => r.resourceId);
))`.as("targets") const allTargets =
}) resourceIds.length > 0
.from(resources) ? await tx
.leftJoin( .select({
targets, resourceId: targets.resourceId,
and( targetId: targets.targetId,
eq(targets.resourceId, resources.resourceId), ip: targets.ip,
eq(targets.enabled, true) method: targets.method,
port: targets.port,
internalPort: targets.internalPort,
enabled: targets.enabled
})
.from(targets)
.where(
and(
inArray(targets.resourceId, resourceIds),
eq(targets.enabled, true)
)
)
: [];
// Combine the data in JS instead of using SQL for the JSON
return resourcesList.map((resource) => ({
...resource,
targets: allTargets.filter(
(target) => target.resourceId === resource.resourceId
) )
) }));
.where(eq(resources.siteId, siteId)) });
.groupBy(resources.resourceId);
let tcpTargets: string[] = []; const { tcpTargets, udpTargets } = allResources.reduce(
let udpTargets: string[] = []; (acc, resource) => {
// Skip resources with no targets
if (!resource.targets?.length) return acc;
for (const resource of allResources) { // Format valid targets into strings
const targets = JSON.parse(resource.targets); const formattedTargets = resource.targets
if (!targets || targets.length === 0) { .filter(
continue;
}
if (resource.protocol === "tcp") {
tcpTargets = tcpTargets.concat(
targets.map(
(target: Target) => (target: Target) =>
`${ target?.internalPort && target?.ip && target?.port
target.internalPort ? target.internalPort + ":" : ""
}${target.ip}:${target.port}`
) )
); .map(
} else {
udpTargets = tcpTargets.concat(
targets.map(
(target: Target) => (target: Target) =>
`${ `${target.internalPort}:${target.ip}:${target.port}`
target.internalPort ? target.internalPort + ":" : "" );
}${target.ip}:${target.port}`
) // Add to the appropriate protocol array
); if (resource.protocol === "tcp") {
} acc.tcpTargets.push(...formattedTargets);
} } else {
acc.udpTargets.push(...formattedTargets);
}
return acc;
},
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
);
return { return {
message: { message: {

View file

@ -1,6 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config"; import config from "@server/lib/config";
@ -12,52 +12,79 @@ export async function traefikConfigProvider(
res: Response res: Response
): Promise<any> { ): Promise<any> {
try { try {
const allResources = await db // Get all resources with related data
.select({ const allResources = await db.transaction(async (tx) => {
// Resource fields // First query to get resources with site and org info
resourceId: resources.resourceId, const resourcesWithRelations = await tx
subdomain: resources.subdomain, .select({
fullDomain: resources.fullDomain, // Resource fields
ssl: resources.ssl, resourceId: resources.resourceId,
blockAccess: resources.blockAccess, subdomain: resources.subdomain,
sso: resources.sso, fullDomain: resources.fullDomain,
emailWhitelistEnabled: resources.emailWhitelistEnabled, ssl: resources.ssl,
http: resources.http, blockAccess: resources.blockAccess,
proxyPort: resources.proxyPort, sso: resources.sso,
protocol: resources.protocol, emailWhitelistEnabled: resources.emailWhitelistEnabled,
isBaseDomain: resources.isBaseDomain, http: resources.http,
domainId: resources.domainId, proxyPort: resources.proxyPort,
// Site fields protocol: resources.protocol,
site: { isBaseDomain: resources.isBaseDomain,
siteId: sites.siteId, domainId: resources.domainId,
type: sites.type, // Site fields
subnet: sites.subnet site: {
}, siteId: sites.siteId,
// Org fields type: sites.type,
org: { subnet: sites.subnet
orgId: orgs.orgId },
}, // Org fields
// Targets as a subquery org: {
targets: sql<string>`json_group_array(json_object( orgId: orgs.orgId
'targetId', ${targets.targetId}, }
'ip', ${targets.ip}, })
'method', ${targets.method}, .from(resources)
'port', ${targets.port}, .innerJoin(sites, eq(sites.siteId, resources.siteId))
'internalPort', ${targets.internalPort}, .innerJoin(orgs, eq(resources.orgId, orgs.orgId));
'enabled', ${targets.enabled}
))`.as("targets") // Get all resource IDs from the first query
}) const resourceIds = resourcesWithRelations.map((r) => r.resourceId);
.from(resources)
.innerJoin(sites, eq(sites.siteId, resources.siteId)) // Second query to get all enabled targets for these resources
.innerJoin(orgs, eq(resources.orgId, orgs.orgId)) const allTargets =
.leftJoin( resourceIds.length > 0
targets, ? await tx
and( .select({
eq(targets.resourceId, resources.resourceId), resourceId: targets.resourceId,
eq(targets.enabled, true) targetId: targets.targetId,
) ip: targets.ip,
) method: targets.method,
.groupBy(resources.resourceId); port: targets.port,
internalPort: targets.internalPort,
enabled: targets.enabled
})
.from(targets)
.where(
and(
inArray(targets.resourceId, resourceIds),
eq(targets.enabled, true)
)
)
: [];
// Create a map for fast target lookup by resourceId
const targetsMap = allTargets.reduce((map, target) => {
if (!map.has(target.resourceId)) {
map.set(target.resourceId, []);
}
map.get(target.resourceId).push(target);
return map;
}, new Map());
// Combine the data
return resourcesWithRelations.map((resource) => ({
...resource,
targets: targetsMap.get(resource.resourceId) || []
}));
});
if (!allResources.length) { if (!allResources.length) {
return res.status(HttpCode.OK).json({}); return res.status(HttpCode.OK).json({});
@ -101,7 +128,7 @@ export async function traefikConfigProvider(
}; };
for (const resource of allResources) { for (const resource of allResources) {
const targets = JSON.parse(resource.targets); const targets = resource.targets as Target[];
const site = resource.site; const site = resource.site;
const org = resource.org; const org = resource.org;