Skip to content
Open
25 changes: 18 additions & 7 deletions api/application.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { queryClient } from "@/app/_layout";
import { filterResourceByTeam, filterResourcesByTeam } from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
Expand Down Expand Up @@ -98,18 +99,26 @@ export const ApplicationKeys = {
// Fetch functions
export const getApplications = async () => {
const data = await coolifyFetch<Application[]>("/applications");
data.forEach((app) =>
const filtered = await filterResourcesByTeam(
data,
(app) => app.destination.server.team_id,
);
filtered.forEach((app) =>
optimisticUpdateOne(ApplicationKeys.queries.single(app.uuid), app),
);
return data;
return filtered;
};

export const getApplication = async (uuid: string) => {
queryClient.cancelQueries({
queryKey: ApplicationKeys.queries.all(),
exact: true,
});
return coolifyFetch<Application>(`/applications/${uuid}`);
const application = await coolifyFetch<Application>(`/applications/${uuid}`);
return filterResourceByTeam(
application,
(app) => app.destination.server.team_id,
);
};

export const getApplicationLogs = async (uuid: string, lines = 100) => {
Expand Down Expand Up @@ -217,7 +226,7 @@ export const useApplications = (

export const useApplication = (
uuid: string,
options?: Omit<UseQueryOptions<Application, Error>, "queryKey">,
options?: Omit<UseQueryOptions<Application | null, Error>, "queryKey">,
) => {
return useQuery({
queryKey: ApplicationKeys.queries.single(uuid),
Expand Down Expand Up @@ -272,7 +281,7 @@ export const useCreateApplicationEnv = (
return update;
},
onError: onOptimisticUpdateError,
onSettled: onOptimisticUpdateSettled(),
onSettled: onOptimisticUpdateSettled(ApplicationKeys.queries.envs(uuid)),
});
};

Expand Down Expand Up @@ -339,8 +348,10 @@ export const useUpdateApplication: UseUpdateApplication = (
return { update, insert };
},
onError: (error, variables, context) => {
onOptimisticUpdateError(error, variables, context?.update);
onOptimisticUpdateError(error, variables, context?.insert);
if (context) {
onOptimisticUpdateError(error, variables, context.update);
onOptimisticUpdateError(error, variables, context.insert);
}
},
onSettled: () => onOptimisticUpdateSettled(ApplicationKeys.queries.all())(),
});
Expand Down
7 changes: 3 additions & 4 deletions api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,9 @@ export async function optimisticUpdateOne<T>(
return { previousData, queryKey };
}

export const onOptimisticUpdateError = (
data: unknown,
error: unknown,
variables: unknown,
export const onOptimisticUpdateError = <TVariables = unknown>(
error: Error,
variables: TVariables,
context?: {
queryKey: (string | number)[];
previousData: unknown;
Expand Down
34 changes: 25 additions & 9 deletions api/databases.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
import {
filterResourceByTeam,
filterResourcesByTeam,
} from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
Expand Down Expand Up @@ -85,8 +89,12 @@ export const DatabaseKeys = {
*/
export const getDatabases = async () => {
const data = await coolifyFetch<Database[]>("/databases");
const filtered = await filterResourcesByTeam(
data,
(db) => db.destination.server.team_id,
);
// Set individual database cache entries
data.forEach((database) => {
filtered.forEach((database) => {
optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), database);
optimisticUpdateOne(DatabaseKeys.queries.single(database.uuid), database);
});
Expand All @@ -97,9 +105,15 @@ export const getDatabases = async () => {

export const getDatabase = async (uuid: string) => {
const data = await coolifyFetch<Database>(`/databases/${uuid}`);
// Update the databases list cache with the new database
optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), data);
return data;
const filtered = await filterResourceByTeam(
data,
(db) => db.destination.server.team_id,
);
if (filtered) {
// Update the databases list cache with the new database
optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), filtered);
}
return filtered;
};

export const getDatabaseLogs = async (uuid: string, lines = 100) => {
Expand Down Expand Up @@ -166,7 +180,7 @@ export const useDatabases = (

export const useDatabase = (
uuid: string,
options?: Omit<UseQueryOptions<Database, Error>, "queryKey">
options?: Omit<UseQueryOptions<Database | null, Error>, "queryKey">
) => {
return useQuery({
queryKey: DatabaseKeys.queries.single(uuid),
Expand Down Expand Up @@ -236,10 +250,12 @@ export const useUpdateDatabase: UseUpdateDatabase = (uuid: string, options) => {
return { update, insert };
},
onError: (error, variables, context) => {
onOptimisticUpdateError(error, variables, context?.update);
onOptimisticUpdateError(error, variables, context?.insert);
if (context) {
onOptimisticUpdateError(error, variables, context.update);
onOptimisticUpdateError(error, variables, context.insert);
}
},
onSettled: () => onOptimisticUpdateSettled(DatabaseKeys.queries.all())(),
onSettled: onOptimisticUpdateSettled(DatabaseKeys.queries.all()),
});
};

Expand All @@ -260,6 +276,6 @@ export const useCreateDatabase = (
type: CoolifyDatabases;
}) => createDatabase(body, type),
...options,
onSettled: () => onOptimisticUpdateSettled(DatabaseKeys.queries.all())(),
onSettled: onOptimisticUpdateSettled(DatabaseKeys.queries.all()),
});
};
17 changes: 13 additions & 4 deletions api/private-keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
filterResourceByTeam,
filterResourcesByTeam,
} from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { coolifyFetch, optimisticUpdateOne } from "./client";
import { PrivateKey } from "./types/private-keys.types";
Expand All @@ -14,14 +18,19 @@ export const PrivateKeyKeys = {
// Fetch functions
export const getPrivateKeys = async () => {
const res = await coolifyFetch<PrivateKey[]>("/security/keys");
res.forEach((key) =>
const filtered = await filterResourcesByTeam(
res,
(key) => key.team_id,
);
filtered.forEach((key) =>
optimisticUpdateOne(PrivateKeyKeys.queries.single(key.uuid), key)
);
return res;
return filtered;
};

export const getPrivateKey = async (uuid: string) => {
return coolifyFetch<PrivateKey>(`/security/keys/${uuid}`);
const key = await coolifyFetch<PrivateKey>(`/security/keys/${uuid}`);
return filterResourceByTeam(key, (k) => k.team_id);
};

// Query hooks
Expand All @@ -37,7 +46,7 @@ export const usePrivateKeys = (

export const usePrivateKey = (
uuid: string,
options?: Omit<UseQueryOptions<PrivateKey, Error>, "queryKey">
options?: Omit<UseQueryOptions<PrivateKey | null, Error>, "queryKey">
) => {
return useQuery({
queryKey: PrivateKeyKeys.queries.single(uuid),
Expand Down
46 changes: 29 additions & 17 deletions api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { queryClient } from "@/app/_layout";
import { filterResourceByTeam } from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query";
import { coolifyFetch, onOptimisticUpdateError } from "./client";
import {
PartialProject,
Project,
ProjectCreateBody,
} from "./types/project.types";
import { Project, ProjectBase, ProjectCreateBody } from "./types/project.types";
import {
ResourceActionResponse,
ResourceCreateResponse,
Expand All @@ -31,18 +28,29 @@ export const ProjectKeys = {

// Fetch functions
export const getProjects = async () => {
const data = await coolifyFetch<PartialProject[]>("/projects");
data.forEach((project) => {
queryClient.prefetchQuery({
queryKey: ProjectKeys.queries.single(project.uuid),
queryFn: () => getProject(project.uuid),
});
const data = await coolifyFetch<ProjectBase[]>("/projects");

const projectsWithTeam = await Promise.all(
data.map(async (project) => {
try {
return await getProject(project.uuid);
} catch {
return null;
}
}),
);
const validProjects = projectsWithTeam.filter((p) => p !== null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking N+1 queries cause slow project list loading

High Severity

The getProjects function now uses await Promise.all() to fetch each project individually via getProject(), blocking until all N requests complete. Previously, prefetchQuery initiated background fetches without blocking the list return. This N+1 query pattern causes the project list to load significantly slower as the number of projects increases.

Fix in Cursor Fix in Web


validProjects.forEach((project) => {
queryClient.setQueryData(ProjectKeys.queries.single(project.uuid), project);
});
return data;

return validProjects;
};

export const getProject = async (uuid: string) => {
return coolifyFetch<Project>(`/projects/${uuid}`);
const project = await coolifyFetch<Project>(`/projects/${uuid}`);
return filterResourceByTeam(project, (p) => p.team_id);
};

export const createProject = async (data: ProjectCreateBody) => {
Expand All @@ -66,7 +74,7 @@ export const deleteProject = async (uuid: string) => {

// Query hooks
export const useProjects = (
options?: Omit<UseQueryOptions<PartialProject[], Error>, "queryKey">
options?: Omit<UseQueryOptions<ProjectBase[], Error>, "queryKey">,
) => {
return useQuery({
queryKey: ProjectKeys.queries.all(),
Expand All @@ -77,7 +85,7 @@ export const useProjects = (

export const useProject = (
uuid: string,
options?: Omit<UseQueryOptions<Project, Error>, "queryKey">
options?: Omit<UseQueryOptions<Project | null, Error>, "queryKey">,
) => {
return useQuery({
queryKey: ProjectKeys.queries.single(uuid),
Expand All @@ -88,7 +96,11 @@ export const useProject = (

// Mutation hooks
export const useCreateProject = (
options?: UseMutationOptions<ResourceCreateResponse, Error, ProjectCreateBody>
options?: UseMutationOptions<
ResourceCreateResponse,
Error,
ProjectCreateBody
>,
) => {
return useMutation({
mutationKey: ProjectKeys.mutations.create(),
Expand All @@ -105,7 +117,7 @@ export const useCreateProject = (

export const useDeleteProject = (
uuid: string,
options?: UseMutationOptions<ResourceActionResponse, Error, void>
options?: UseMutationOptions<ResourceActionResponse, Error, void>,
) => {
return useMutation({
mutationKey: ProjectKeys.mutations.delete(uuid),
Expand Down
12 changes: 9 additions & 3 deletions api/resources.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { filterResourcesByTeam } from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { ApplicationKeys, useApplication } from "./application";
import {
Expand Down Expand Up @@ -39,20 +40,25 @@ const getQueryKeyFromType = (type: ResourceFromListType) => {
// Fetch functions
export const getResources = async () => {
const data = await coolifyFetch<Resource[]>("/resources");
data.forEach((resource) => {
// Resources have team_id in destination.server.team_id (similar to applications/databases)
const filtered = await filterResourcesByTeam(
data,
(resource) => resource.destination?.server?.team_id,
);
filtered.forEach((resource) => {
const key = getQueryKeyFromType(resource.type);

optimisticUpdateInsertOneToMany(key, resource);
optimisticUpdateOne([...key, resource.uuid], resource);
});
return data;
return filtered;
};

// Query hooks
export const useResource = <T extends Resource>(
uuid: string,
type: ResourceType,
options?: Omit<UseQueryOptions<T, Error>, "queryKey">
options?: Omit<UseQueryOptions<T | null, Error>, "queryKey">
) => {
if (!type) {
throw new Error("Resource type is required");
Expand Down
16 changes: 11 additions & 5 deletions api/servers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { queryClient } from "@/app/_layout";
import { filterResourceByTeam, filterResourcesByTeam } from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { coolifyFetch } from "./client";
import { Server, SingleServer } from "./types/server.types";
Expand All @@ -15,19 +16,24 @@ export const ServerKeys = {
// Fetch functions
export const getServers = async () => {
const data = await coolifyFetch<Server[]>("/servers");
data.forEach((server) => {
const filtered = await filterResourcesByTeam(
data,
(server) => server.team_id,
);
filtered.forEach((server) => {
queryClient.setQueryData(ServerKeys.queries.single(server.uuid), server);
});
return data;
return filtered;
};

export const getServer = async (uuid: string) => {
return coolifyFetch<SingleServer>(`/servers/${uuid}`);
const server = await coolifyFetch<SingleServer>(`/servers/${uuid}`);
return filterResourceByTeam(server, (s) => s.team_id);
};

// Query hooks
export const useServers = (
options?: Omit<UseQueryOptions<Server[], Error>, "queryKey">
options?: Omit<UseQueryOptions<Server[], Error>, "queryKey">,
) => {
return useQuery({
queryKey: ServerKeys.queries.all(),
Expand All @@ -38,7 +44,7 @@ export const useServers = (

export const useServer = (
uuid: string,
options?: Omit<UseQueryOptions<SingleServer, Error>, "queryKey">
options?: Omit<UseQueryOptions<SingleServer | null, Error>, "queryKey">,
) => {
return useQuery({
queryKey: ServerKeys.queries.single(uuid),
Expand Down
Loading