support labels in list alises

This commit is contained in:
miloschwartz
2026-07-01 16:17:27 -04:00
parent 023110b341
commit 663244fa3a

View File

@@ -5,8 +5,12 @@ import {
userSiteResources,
roleSiteResources,
userOrgRoles,
userOrgs
userOrgs,
labels,
siteResourceLabels
} from "@server/db";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { and, eq, inArray, asc, isNotNull, ne, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -19,13 +23,33 @@ import { regionalCache as cache } from "#dynamic/lib/cache";
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
const labelFilterQuerySchema = z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
if (typeof val === "string") {
return val.split(",");
}
return undefined;
}, z.array(z.string()))
.optional()
.catch([]);
function userResourceAliasesCacheKey(
orgId: string,
userId: string,
page: number,
pageSize: number
pageSize: number,
includeLabels: boolean,
labelFilter: string[]
) {
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}`;
const labelsKey =
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
}
const listUserResourceAliasesParamsSchema = z.strictObject({
@@ -56,43 +80,35 @@ const listUserResourceAliasesQuerySchema = z.strictObject({
type: "integer",
default: 1,
description: "Page number to retrieve"
})
}),
includeLabels: z
.enum(["true", "false"])
.optional()
.default("false")
.transform((val) => val === "true")
.openapi({
type: "boolean",
default: false,
description:
"When true, include label names for each alias in the items field"
}),
labels: labelFilterQuerySchema.openapi({
type: "array",
description:
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
})
});
export type UserResourceAliasItem = {
alias: string;
labels: string[];
};
export type ListUserResourceAliasesResponse = PaginatedResponse<{
aliases: string[];
items?: UserResourceAliasItem[];
}>;
// registry.registerPath({
// method: "get",
// path: "/org/{orgId}/user-resource-aliases",
// description:
// "List private (host-mode) site resource aliases the authenticated user can access in the organization, paginated.",
// tags: [OpenAPITags.PrivateResource],
// request: {
// params: z.object({
// orgId: z.string()
// }),
// query: listUserResourceAliasesQuerySchema
// },
// responses: {
// 200: {
// description: "Successful response",
// content: {
// "application/json": {
// schema: z.object({
// data: z.record(z.string(), z.any()).nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
// status: z.number()
// })
// }
// }
// }
// }
// });
export async function listUserResourceAliases(
req: Request,
res: Response,
@@ -110,7 +126,12 @@ export async function listUserResourceAliases(
)
);
}
const { page, pageSize } = parsedQuery.data;
const {
page,
pageSize,
includeLabels,
labels: labelFilter
} = parsedQuery.data;
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
req.params
@@ -149,7 +170,9 @@ export async function listUserResourceAliases(
orgId,
userId,
page,
pageSize
pageSize,
includeLabels,
labelFilter ?? []
);
const cachedData: ListUserResourceAliasesResponse | undefined =
await cache.get(cacheKey);
@@ -204,6 +227,7 @@ export async function listUserResourceAliases(
if (accessibleSiteResourceIds.length === 0) {
const data: ListUserResourceAliasesResponse = {
aliases: [],
...(includeLabels ? { items: [] } : {}),
pagination: {
total: 0,
pageSize,
@@ -224,18 +248,44 @@ export async function listUserResourceAliases(
});
}
const whereClause = and(
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
orgId,
tierMatrix.labels
);
const whereConditions = [
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
or(eq(siteResources.mode, "host"), eq(siteResources.mode, "ssh")),
isNotNull(siteResources.alias),
ne(siteResources.alias, ""),
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
);
];
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
whereConditions.push(
inArray(
siteResources.siteResourceId,
db
.select({ id: siteResourceLabels.siteResourceId })
.from(siteResourceLabels)
.innerJoin(
labels,
eq(labels.labelId, siteResourceLabels.labelId)
)
.where(inArray(labels.name, labelFilter))
)
);
}
const whereClause = and(...whereConditions);
const baseSelect = () =>
db
.select({ alias: siteResources.alias })
.select({
alias: siteResources.alias,
siteResourceId: siteResources.siteResourceId
})
.from(siteResources)
.where(whereClause);
@@ -251,8 +301,46 @@ export async function listUserResourceAliases(
const aliases = rows.map((r) => r.alias as string);
let items: UserResourceAliasItem[] | undefined;
if (includeLabels) {
const siteResourceIdList = rows.map((r) => r.siteResourceId);
let labelsForSiteResources: Array<{
name: string;
siteResourceId: number;
}> = [];
if (isLabelFeatureEnabled && siteResourceIdList.length > 0) {
labelsForSiteResources = await db
.select({
name: labels.name,
siteResourceId: siteResourceLabels.siteResourceId
})
.from(labels)
.innerJoin(
siteResourceLabels,
eq(siteResourceLabels.labelId, labels.labelId)
)
.where(
inArray(
siteResourceLabels.siteResourceId,
siteResourceIdList
)
)
.orderBy(asc(siteResourceLabels.siteResourceLabelId));
}
items = rows.map((row) => ({
alias: row.alias as string,
labels: labelsForSiteResources
.filter((l) => l.siteResourceId === row.siteResourceId)
.map((l) => l.name)
}));
}
const data: ListUserResourceAliasesResponse = {
aliases,
...(items !== undefined ? { items } : {}),
pagination: {
total: totalCount,
pageSize,