refactor(patch): unify patch retrieval and application logic

- Consolidated patch retrieval for applications and composes into a single query method, improving code clarity and reducing redundancy.
- Updated the ShowPatches component to utilize the new unified query, simplifying data fetching logic.
- Refactored patch application commands to streamline the process for both application and compose types, enhancing maintainability and consistency across the codebase.
This commit is contained in:
Mauricio Siu
2026-02-17 01:35:08 -06:00
parent 9818e3c3ba
commit 46ac272f3f
5 changed files with 86 additions and 168 deletions

View File

@@ -33,22 +33,8 @@ export const ShowPatches = ({ id, type }: Props) => {
const utils = api.useUtils();
const queryMap = {
application: () =>
api.patch.byApplicationId.useQuery(
{ applicationId: id },
{ enabled: !!id },
),
compose: () =>
api.patch.byComposeId.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data: patches, isLoading: isPatchesLoading } = queryMap[type]
? queryMap[type]()
: api.patch.byApplicationId.useQuery(
{ applicationId: id },
{ enabled: !!id },
);
const { data: patches, isLoading: isPatchesLoading } =
api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
const mutationMap = {
application: () => api.patch.delete.useMutation(),
@@ -166,11 +152,9 @@ export const ShowPatches = ({ id, type }: Props) => {
})
.then(() => {
toast.success("Patch updated");
utils.patch.byApplicationId.invalidate({
applicationId: id,
});
utils.patch.byComposeId.invalidate({
composeId: id,
utils.patch.byEntityId.invalidate({
id,
type,
});
})
.catch((err) => {
@@ -190,11 +174,9 @@ export const ShowPatches = ({ id, type }: Props) => {
mutateAsync({ patchId: patch.patchId })
.then(() => {
toast.success("Patch deleted");
utils.patch.byApplicationId.invalidate({
applicationId: id,
});
utils.patch.byComposeId.invalidate({
composeId: id,
utils.patch.byEntityId.invalidate({
id,
type,
});
})
.catch((err) => {

View File

@@ -8,9 +8,7 @@ import {
findComposeById,
findPatchByFilePath,
findPatchById,
findPatchesByApplicationId,
findPatchesByComposeId,
generatePatch,
findPatchesByEntityId,
readPatchRepoDirectory,
readPatchRepoFile,
updatePatch,
@@ -26,8 +24,6 @@ import {
apiCreatePatch,
apiDeletePatch,
apiFindPatch,
apiFindPatchesByApplicationId,
apiFindPatchesByComposeId,
apiTogglePatchEnabled,
apiUpdatePatch,
} from "@/server/db/schema";
@@ -77,38 +73,37 @@ export const patchRouter = createTRPCRouter({
return await findPatchById(input.patchId);
}),
byApplicationId: protectedProcedure
.input(apiFindPatchesByApplicationId)
byEntityId: protectedProcedure
.input(
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
)
.query(async ({ input, ctx }) => {
const app = await findApplicationById(input.applicationId);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
const result = await findPatchesByEntityId(input.id, input.type);
return await findPatchesByApplicationId(input.applicationId);
}),
byComposeId: protectedProcedure
.input(apiFindPatchesByComposeId)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
return await findPatchesByComposeId(input.composeId);
return result;
}),
update: protectedProcedure

View File

@@ -44,10 +44,7 @@ import {
issueCommentExists,
updateIssueComment,
} from "./github";
import {
findPatchesByApplicationId,
generateApplyPatchesCommand,
} from "./patch";
import { generateApplyPatchesCommand } from "./patch";
import {
findPreviewDeploymentById,
updatePreviewDeployment,
@@ -206,20 +203,12 @@ export const deployApplication = async ({
command += await buildRemoteDocker(application);
}
// Apply patches after cloning (for non-docker sources only)
if (application.sourceType !== "docker") {
const patches = await findPatchesByApplicationId(
application.applicationId,
);
const enabledPatches = patches.filter((p) => p.enabled);
if (enabledPatches.length > 0) {
command += generateApplyPatchesCommand({
appName: application.appName,
type: "application",
serverId,
patches: enabledPatches,
});
}
command += await generateApplyPatchesCommand({
id: application.applicationId,
type: "application",
serverId,
});
}
command += await getBuildCommand(application);

View File

@@ -40,7 +40,7 @@ import {
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { findPatchesByComposeId, generateApplyPatchesCommand } from "./patch";
import { generateApplyPatchesCommand } from "./patch";
import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
@@ -249,24 +249,12 @@ export const deployCompose = async ({
await execAsync(commandWithLog);
}
// Apply patches after cloning (for non-raw sources only)
if (compose.sourceType !== "raw") {
const patches = await findPatchesByComposeId(compose.composeId);
const enabledPatches = patches.filter((p) => p.enabled);
if (enabledPatches.length > 0) {
const patchCommand = generateApplyPatchesCommand({
appName: compose.appName,
type: "compose",
serverId: compose.serverId,
patches: enabledPatches,
});
const patchCommandWithLog = `(${patchCommand}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, patchCommandWithLog);
} else {
await execAsync(patchCommandWithLog);
}
}
command += await generateApplyPatchesCommand({
id: compose.composeId,
type: "compose",
serverId: compose.serverId,
});
}
command = "set -e;";

View File

@@ -2,12 +2,11 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreatePatch, patch } from "@dokploy/server/db/schema";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { and, eq, isNotNull } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { findApplicationById } from "./application";
import { findComposeById } from "./compose";
export type Patch = typeof patch.$inferSelect;
@@ -54,33 +53,29 @@ export const findPatchById = async (patchId: string) => {
return result;
};
export const findPatchesByApplicationId = async (applicationId: string) => {
export const findPatchesByEntityId = async (
id: string,
type: "application" | "compose",
) => {
return await db.query.patch.findMany({
where: and(
eq(patch.applicationId, applicationId),
isNotNull(patch.applicationId),
where: eq(
type === "application" ? patch.applicationId : patch.composeId,
id,
),
orderBy: (patch, { asc }) => [asc(patch.filePath)],
});
};
export const findPatchesByComposeId = async (composeId: string) => {
return await db.query.patch.findMany({
where: and(eq(patch.composeId, composeId), isNotNull(patch.composeId)),
orderBy: (patch, { asc }) => [asc(patch.filePath)],
});
};
export const findPatchByFilePath = async (
filePath: string,
id: string,
type: "application" | "compose",
) => {
return await db.query.patch.findFirst({
where:
type === "application"
? and(eq(patch.filePath, filePath), eq(patch.applicationId, id))
: and(eq(patch.filePath, filePath), eq(patch.composeId, id)),
where: and(
eq(patch.filePath, filePath),
eq(type === "application" ? patch.applicationId : patch.composeId, id),
),
});
};
@@ -111,76 +106,45 @@ export const deletePatch = async (patchId: string) => {
return result[0];
};
// Patch Application Functions
interface ApplyPatchesOptions {
appName: string;
id: string;
type: "application" | "compose";
serverId: string | null;
patches: Patch[];
}
/**
* Generate shell commands to apply patches to cloned repository
* Uses git apply to apply unified diff patches
*/
export const generateApplyPatchesCommand = ({
appName,
export const generateApplyPatchesCommand = async ({
id,
type,
patches,
serverId,
}: ApplyPatchesOptions): string => {
}: ApplyPatchesOptions) => {
const entity =
type === "application"
? await findApplicationById(id)
: await findComposeById(id);
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const codePath = join(basePath, entity.appName, "code");
const patches = (await findPatchesByEntityId(id, type)).filter(
(p) => p.enabled,
);
if (patches.length === 0) {
return "";
}
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const codePath = join(basePath, appName, "code");
let command = `echo "Applying ${patches.length} patch(es)...";`;
for (const p of patches) {
// Create a temporary patch file and apply it
const patchFileName = `/tmp/patch_${p.patchId}.patch`;
// Escape content for shell - use base64 encoding
const encodedContent = Buffer.from(p.content).toString("base64");
if (!p.enabled) {
continue;
}
const filePath = join(codePath, p.filePath);
command += `
echo "${encodedContent}" | base64 -d > ${patchFileName};
cd ${codePath} && git apply --whitespace=fix ${patchFileName} && echo "✅ Applied patch for: ${p.filePath}" || echo "⚠️ Warning: Failed to apply patch for: ${p.filePath}";
rm -f ${patchFileName};
rm -f ${filePath};
echo "${encodeBase64(p.content)}" | base64 -d > ${filePath};
`;
}
return command;
};
/**
* Apply patches during build process
*/
export const applyPatches = async ({
appName,
type,
serverId,
patches,
}: ApplyPatchesOptions): Promise<void> => {
const enabledPatches = patches.filter((p) => p.enabled);
if (enabledPatches.length === 0) {
return;
}
const command = generateApplyPatchesCommand({
appName,
type,
serverId,
patches: enabledPatches,
});
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
};