Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cursor/rules/expo.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
alwaysApply: true
description: Framework related rules for the project
alwaysApply: false
---

This project is built using **Expo** and **NativeWind** with **TypeScript**. Please adhere to the following conventions and rules when writing, editing, or suggesting code:
Expand Down
21 changes: 11 additions & 10 deletions .cursor/rules/file-structure.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
alwaysApply: true
description: File structure related rules for the project
alwaysApply: false
---

## Component Naming & Structure
Expand Down Expand Up @@ -33,15 +34,15 @@ This project uses Expo with Expo Router and NativeWind. Please follow these nami

### Examples:

| File Path | Component Name |
| -------------------------------------------- | -------------------------- |
| `app/index.tsx` | `Index` |
| `app/applications/index.tsx` | `ApplicationsIndex` |
| `app/applications/[uuid]/index.tsx` | `ApplicationIndex` |
| `app/applications/[uuid]/settings/index.tsx` | `ApplicationSettingsIndex` |
| `app/auth/login.tsx` | `AuthLogin` |
| `app/(user)/dashboard/overview.tsx` | `DashboardOverview` |
| `app/main/applications/[uuid]/(tabs)/logs.tsx` | `MainApplicationsLogs` |
| File Path | Component Name |
| ---------------------------------------------- | -------------------------- |
| `app/index.tsx` | `Index` |
| `app/applications/index.tsx` | `ApplicationsIndex` |
| `app/applications/[uuid]/index.tsx` | `ApplicationIndex` |
| `app/applications/[uuid]/settings/index.tsx` | `ApplicationSettingsIndex` |
| `app/auth/login.tsx` | `AuthLogin` |
| `app/(user)/dashboard/overview.tsx` | `DashboardOverview` |
| `app/main/applications/[uuid]/(tabs)/logs.tsx` | `MainApplicationsLogs` |

### Hooks

Expand Down
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
Copy Markdown

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
Loading