Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Move deployments between systems #255

Merged
merged 2 commits into from
Dec 9, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";

import type { Deployment } from "@ctrlplane/db/schema";
import { useParams, useRouter } from "next/navigation";
import { z } from "zod";

import { deploymentSchema } from "@ctrlplane/db/schema";
import * as schema from "@ctrlplane/db/schema";
import { Button } from "@ctrlplane/ui/button";
import {
Form,
Expand All @@ -17,35 +16,48 @@ import {
useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ctrlplane/ui/select";
import { Textarea } from "@ctrlplane/ui/textarea";

import { api } from "~/trpc/react";

const deploymentForm = z.object(deploymentSchema.shape);
const deploymentForm = z.object(schema.deploymentSchema.shape);

type EditDeploymentSectionProps = {
deployment: schema.Deployment;
systems: schema.System[];
};

export const EditDeploymentSection: React.FC<{
deployment: Deployment;
}> = ({ deployment }) => {
export const EditDeploymentSection: React.FC<EditDeploymentSectionProps> = ({
deployment,
systems,
}) => {
const form = useForm({
schema: deploymentForm,
defaultValues: { ...deployment },
mode: "onSubmit",
});
const { handleSubmit, setError } = form;

const { workspaceSlug, systemSlug } = useParams<{
workspaceSlug: string;
systemSlug: string;
}>();
const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
const router = useRouter();
const updateDeployment = api.deployment.update.useMutation();
const onSubmit = handleSubmit((data) => {
updateDeployment
.mutateAsync({ id: deployment.id, data })
.then(() => {
if (data.slug !== deployment.slug)
.then((updatedDeployment) => {
if (
data.slug !== deployment.slug ||
updatedDeployment.systemId !== deployment.systemId
)
router.replace(
`/${workspaceSlug}/systems/${systemSlug}/deployments/${data.slug}`,
`/${workspaceSlug}/systems/${updatedDeployment.system.slug}/deployments/${data.slug}`,
);
Comment on lines +54 to 61
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add confirmation dialog for system changes

Moving a deployment between systems is a significant operation that might have downstream effects. Consider adding a confirmation dialog when the system is changed.

   const onSubmit = handleSubmit((data) => {
     updateDeployment
       .mutateAsync({ id: deployment.id, data })
       .then((updatedDeployment) => {
-        if (
-          data.slug !== deployment.slug ||
-          updatedDeployment.systemId !== deployment.systemId
-        )
-          router.replace(
-            `/${workspaceSlug}/systems/${updatedDeployment.system.slug}/deployments/${data.slug}`,
-          );
+        const isSystemChanged = updatedDeployment.systemId !== deployment.systemId;
+        const isSlugChanged = data.slug !== deployment.slug;
+        
+        if (isSystemChanged) {
+          // Add confirmation dialog here
+          const confirmed = window.confirm(
+            "Moving a deployment to a different system may affect its behavior. Are you sure?"
+          );
+          if (!confirmed) return;
+        }
+        
+        if (isSystemChanged || isSlugChanged) {
+          router.replace(
+            `/${workspaceSlug}/systems/${updatedDeployment.system.slug}/deployments/${data.slug}`,
+          );
+        }
         router.refresh();
       })

Committable suggestion skipped: line range outside the PR's diff.

router.refresh();
})
Expand Down Expand Up @@ -116,7 +128,30 @@ export const EditDeploymentSection: React.FC<{
</FormItem>
)}
/>

<FormField
control={form.control}
name="systemId"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormLabel>System</FormLabel>
<FormControl>
<Select value={value} onValueChange={onChange}>
<SelectTrigger>
<SelectValue placeholder="Select a system" />
</SelectTrigger>
<SelectContent>
{systems.map((system) => (
<SelectItem key={system.id} value={system.id}>
{system.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="retryCount"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,11 @@ export default async function DeploymentPage({
}) {
const workspace = await api.workspace.bySlug(params.workspaceSlug);
if (workspace == null) return notFound();
const workspaceId = workspace.id;
const { items: systems } = await api.system.list({ workspaceId });
const deployment = await api.deployment.bySlug(params);
if (deployment == null) return notFound();
const jobAgents = await api.job.agent.byWorkspaceId(workspace.id);
const jobAgents = await api.job.agent.byWorkspaceId(workspaceId);
const jobAgent = jobAgents.find((a) => a.id === deployment.jobAgentId);

return (
Expand All @@ -162,7 +164,7 @@ export default async function DeploymentPage({
</div>
</div>
<div className="mb-16 flex-grow space-y-10">
<EditDeploymentSection deployment={deployment} />
<EditDeploymentSection deployment={deployment} systems={systems} />

<JobAgentSection
jobAgents={jobAgents}
Expand All @@ -172,7 +174,7 @@ export default async function DeploymentPage({
deploymentId={deployment.id}
/>

<Variables workspaceId={workspace.id} deployment={deployment} />
<Variables workspaceId={workspaceId} deployment={deployment} />
</div>
</div>
);
Expand Down
15 changes: 6 additions & 9 deletions packages/api/src/router/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ import {
runbookVariable,
runhook,
system,
updateDeployment,
updateDeployment as updateDeploymentSchema,
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Migration to new function names needs attention

The verification revealed several concerns:

  • updateDeployment is still being used across multiple files in the codebase, suggesting incomplete migration
  • While getEventsForDeploymentDeleted has been fully removed, both new names (updateDeploymentSchema and getEventsForDeploymentRemoved) appear in the expected locations

Files needing attention for updateDeployment usage:

  • packages/api/src/router/deployment-variable.ts
  • packages/db/src/schema/deployment.ts
  • packages/db/src/schema/deployment-variables.ts
  • apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/EditDeploymentSection.tsx
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/EditDeploymentDialog.tsx
🔗 Analysis chain

Verify consistent usage of renamed functions across the codebase.

The imports have been renamed to better reflect their purposes:

  • updateDeploymentupdateDeploymentSchema
  • getEventsForDeploymentDeletedgetEventsForDeploymentRemoved

Also applies to: 42-42, 44-44


Let me modify the search patterns to get more accurate results.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that old function names are not used elsewhere in the codebase

# Test 1: Check for any remaining usage of old function names
echo "Checking for old function names..."
rg -l "updateDeployment(?!Schema)" || echo "No occurrences found"
rg -l "getEventsForDeploymentDeleted" || echo "No occurrences found"

# Test 2: Verify the new function signatures
echo -e "\nVerifying new function signatures..."
ast-grep --pattern 'function updateDeploymentSchema($_) { $$$ }'
ast-grep --pattern 'function getEventsForDeploymentRemoved($_) { $$$ }'

Length of output: 731


Script:

#!/bin/bash
# Description: Verify function names with improved search patterns

# Check for updateDeployment usage (excluding Schema)
echo "Checking updateDeployment usage..."
rg -l --pcre2 'updateDeployment(?!Schema)' || echo "No occurrences found"

# Check for getEventsForDeploymentDeleted
echo -e "\nChecking getEventsForDeploymentDeleted usage..."
rg -l 'getEventsForDeploymentDeleted' || echo "No occurrences found"

# Check for new function names
echo -e "\nChecking new function names..."
rg -l 'updateDeploymentSchema' || echo "No updateDeploymentSchema found"
rg -l 'getEventsForDeploymentRemoved' || echo "No getEventsForDeploymentRemoved found"

# Check the actual implementations
echo -e "\nChecking implementations..."
rg -A 2 'function updateDeploymentSchema' || echo "No updateDeploymentSchema implementation found"
rg -A 2 'function getEventsForDeploymentRemoved' || echo "No getEventsForDeploymentRemoved implementation found"

Length of output: 1463

updateHook,
updateReleaseChannel,
workspace,
} from "@ctrlplane/db/schema";
import {
getEventsForDeploymentDeleted,
getEventsForDeploymentRemoved,
handleEvent,
updateDeployment,
} from "@ctrlplane/job-dispatch";
import { Permission } from "@ctrlplane/validators/auth";
import { JobStatus } from "@ctrlplane/validators/jobs";
Expand Down Expand Up @@ -451,13 +452,9 @@ export const deploymentRouter = createTRPCRouter({
.perform(Permission.DeploymentUpdate)
.on({ type: "deployment", id: input.id }),
})
.input(z.object({ id: z.string().uuid(), data: updateDeployment }))
.input(z.object({ id: z.string().uuid(), data: updateDeploymentSchema }))
.mutation(({ ctx, input }) =>
ctx.db
.update(deployment)
.set(input.data)
.where(eq(deployment.id, input.id))
.returning(),
updateDeployment(input.id, input.data, ctx.session.user.id),
Comment on lines +455 to +457
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for the external updateDeployment call.

The update mutation now delegates to updateDeployment from job-dispatch, which is good for centralization. However, we should ensure proper error handling for this external call.

 .input(z.object({ id: z.string().uuid(), data: updateDeploymentSchema }))
-.mutation(({ ctx, input }) =>
-  updateDeployment(input.id, input.data, ctx.session.user.id),
+.mutation(async ({ ctx, input }) => {
+  try {
+    return await updateDeployment(input.id, input.data, ctx.session.user.id);
+  } catch (error) {
+    // Log the error for debugging
+    console.error('Failed to update deployment:', error);
+    throw new Error('Failed to update deployment. Please try again.');
+  }
+}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.input(z.object({ id: z.string().uuid(), data: updateDeploymentSchema }))
.mutation(({ ctx, input }) =>
ctx.db
.update(deployment)
.set(input.data)
.where(eq(deployment.id, input.id))
.returning(),
updateDeployment(input.id, input.data, ctx.session.user.id),
.input(z.object({ id: z.string().uuid(), data: updateDeploymentSchema }))
.mutation(async ({ ctx, input }) => {
try {
return await updateDeployment(input.id, input.data, ctx.session.user.id);
} catch (error) {
// Log the error for debugging
console.error('Failed to update deployment:', error);
throw new Error('Failed to update deployment. Please try again.');
}
}),

),

delete: protectedProcedure
Expand All @@ -474,7 +471,7 @@ export const deploymentRouter = createTRPCRouter({
.from(deployment)
.where(eq(deployment.id, input))
.then(takeFirst);
const events = await getEventsForDeploymentDeleted(dep);
const events = await getEventsForDeploymentRemoved(dep, dep.systemId);
await Promise.allSettled(events.map(handleEvent));
return ctx.db
.delete(deployment)
Expand Down
1 change: 1 addition & 0 deletions packages/db/src/schema/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const deploymentInsert = createInsertSchema(deployment, {

export const createDeployment = deploymentInsert;
export const updateDeployment = deploymentInsert.partial();
export type UpdateDeployment = z.infer<typeof updateDeployment>;
export type Deployment = InferSelectModel<typeof deployment>;

export const deploymentRelations = relations(deployment, ({ one }) => ({
Expand Down
152 changes: 152 additions & 0 deletions packages/job-dispatch/src/deployment-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import { isPresent } from "ts-is-present";

import { and, eq, inArray, isNotNull, isNull, takeFirst } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import * as SCHEMA from "@ctrlplane/db/schema";
import {
ComparisonOperator,
FilterType,
} from "@ctrlplane/validators/conditions";

import { handleEvent } from "./events/index.js";
import { dispatchReleaseJobTriggers } from "./job-dispatch.js";
import { isPassingReleaseStringCheckPolicy } from "./policies/release-string-check.js";
import { isPassingAllPolicies } from "./policy-checker.js";
import { createJobApprovals } from "./policy-create.js";
import { createReleaseJobTriggers } from "./release-job-trigger.js";

const getResourcesOnlyInNewSystem = async (
newSystemId: string,
oldSystemId: string,
) => {
const hasFilter = isNotNull(SCHEMA.environment.resourceFilter);
const newSystem = await db.query.system.findFirst({
where: eq(SCHEMA.system.id, newSystemId),
with: { environments: { where: hasFilter } },
});

const oldSystem = await db.query.system.findFirst({
where: eq(SCHEMA.system.id, oldSystemId),
with: { environments: { where: hasFilter } },
});

if (newSystem == null || oldSystem == null) return [];

const newSystemFilter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.Or,
conditions: newSystem.environments
.flatMap((env) => env.resourceFilter)
.filter(isPresent),
};

const notInOldSystemFilter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.Or,
not: true,
conditions: oldSystem.environments
.flatMap((env) => env.resourceFilter)
.filter(isPresent),
};

const filter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.And,
conditions: [newSystemFilter, notInOldSystemFilter],
};

return db.query.resource.findMany({
where: and(
SCHEMA.resourceMatchesMetadata(db, filter),
isNull(SCHEMA.resource.deletedAt),
),
});
};
Comment on lines +19 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for database queries

The function should handle potential database errors that might occur during system queries. Currently, errors would propagate up the call stack without any context about the operation that failed.

Apply this diff:

 const getResourcesOnlyInNewSystem = async (
   newSystemId: string,
   oldSystemId: string,
 ) => {
   const hasFilter = isNotNull(SCHEMA.environment.resourceFilter);
-  const newSystem = await db.query.system.findFirst({
-    where: eq(SCHEMA.system.id, newSystemId),
-    with: { environments: { where: hasFilter } },
-  });
+  try {
+    const newSystem = await db.query.system.findFirst({
+      where: eq(SCHEMA.system.id, newSystemId),
+      with: { environments: { where: hasFilter } },
+    });
 
-  const oldSystem = await db.query.system.findFirst({
-    where: eq(SCHEMA.system.id, oldSystemId),
-    with: { environments: { where: hasFilter } },
-  });
+    const oldSystem = await db.query.system.findFirst({
+      where: eq(SCHEMA.system.id, oldSystemId),
+      with: { environments: { where: hasFilter } },
+    });
 
-  if (newSystem == null || oldSystem == null) return [];
+    if (newSystem == null || oldSystem == null) return [];
+    // ... rest of the function
+  } catch (error) {
+    throw new Error(
+      `Failed to fetch systems for comparison: ${error.message}`,
+      { cause: error }
+    );
+  }

Committable suggestion skipped: line range outside the PR's diff.


export const handleDeploymentSystemChanged = async (
deployment: SCHEMA.Deployment,
prevSystemId: string,
userId?: string,
) => {
const resourcesOnlyInNewSystem = await getResourcesOnlyInNewSystem(
deployment.systemId,
prevSystemId,
);

const events = resourcesOnlyInNewSystem.map((resource) => ({
action: "deployment.resource.removed" as const,
payload: { deployment, resource },
}));
await Promise.allSettled(events.map(handleEvent));

const isDeploymentHook = and(
eq(SCHEMA.hook.scopeType, "deployment"),
eq(SCHEMA.hook.scopeId, deployment.id),
);
await db.query.hook
.findMany({
where: isDeploymentHook,
with: { runhooks: { with: { runbook: true } } },
})
.then((hooks) => {
const runbookIds = hooks.flatMap((h) =>
h.runhooks.map((rh) => rh.runbook.id),
);
return db
.update(SCHEMA.runbook)
.set({ systemId: deployment.systemId })
.where(inArray(SCHEMA.runbook.id, runbookIds));
});

const createTriggers =
userId != null
? createReleaseJobTriggers(db, "new_release").causedById(userId)
: createReleaseJobTriggers(db, "new_release");
await createTriggers
.deployments([deployment.id])
.resources(resourcesOnlyInNewSystem.map((r) => r.id))
.filter(isPassingReleaseStringCheckPolicy)
.then(createJobApprovals)
.insert()
.then((triggers) =>
dispatchReleaseJobTriggers(db)
.releaseTriggers(triggers)
.filter(isPassingAllPolicies)
.dispatch(),
);
};
Comment on lines +67 to +118
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Wrap related database operations in a transaction

Multiple database operations (runbook updates and trigger creation) should be wrapped in a transaction to ensure data consistency in case of failures.

Apply this diff:

 export const handleDeploymentSystemChanged = async (
   deployment: SCHEMA.Deployment,
   prevSystemId: string,
   userId?: string,
 ) => {
+  return db.transaction(async (tx) => {
   const resourcesOnlyInNewSystem = await getResourcesOnlyInNewSystem(
     deployment.systemId,
     prevSystemId,
   );

   const events = resourcesOnlyInNewSystem.map((resource) => ({
     action: "deployment.resource.removed" as const,
     payload: { deployment, resource },
   }));
   await Promise.allSettled(events.map(handleEvent));

   const isDeploymentHook = and(
     eq(SCHEMA.hook.scopeType, "deployment"),
     eq(SCHEMA.hook.scopeId, deployment.id),
   );
-  await db.query.hook
+  await tx.query.hook
     .findMany({
       where: isDeploymentHook,
       with: { runhooks: { with: { runbook: true } } },
     })
     .then((hooks) => {
       const runbookIds = hooks.flatMap((h) =>
         h.runhooks.map((rh) => rh.runbook.id),
       );
-      return db
+      return tx
         .update(SCHEMA.runbook)
         .set({ systemId: deployment.systemId })
         .where(inArray(SCHEMA.runbook.id, runbookIds));
     });

   const createTriggers =
     userId != null
-      ? createReleaseJobTriggers(db, "new_release").causedById(userId)
-      : createReleaseJobTriggers(db, "new_release");
+      ? createReleaseJobTriggers(tx, "new_release").causedById(userId)
+      : createReleaseJobTriggers(tx, "new_release");
   await createTriggers
     .deployments([deployment.id])
     .resources(resourcesOnlyInNewSystem.map((r) => r.id))
     .filter(isPassingReleaseStringCheckPolicy)
     .then(createJobApprovals)
     .insert()
     .then((triggers) =>
-      dispatchReleaseJobTriggers(db)
+      dispatchReleaseJobTriggers(tx)
         .releaseTriggers(triggers)
         .filter(isPassingAllPolicies)
         .dispatch(),
     );
+  });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const handleDeploymentSystemChanged = async (
deployment: SCHEMA.Deployment,
prevSystemId: string,
userId?: string,
) => {
const resourcesOnlyInNewSystem = await getResourcesOnlyInNewSystem(
deployment.systemId,
prevSystemId,
);
const events = resourcesOnlyInNewSystem.map((resource) => ({
action: "deployment.resource.removed" as const,
payload: { deployment, resource },
}));
await Promise.allSettled(events.map(handleEvent));
const isDeploymentHook = and(
eq(SCHEMA.hook.scopeType, "deployment"),
eq(SCHEMA.hook.scopeId, deployment.id),
);
await db.query.hook
.findMany({
where: isDeploymentHook,
with: { runhooks: { with: { runbook: true } } },
})
.then((hooks) => {
const runbookIds = hooks.flatMap((h) =>
h.runhooks.map((rh) => rh.runbook.id),
);
return db
.update(SCHEMA.runbook)
.set({ systemId: deployment.systemId })
.where(inArray(SCHEMA.runbook.id, runbookIds));
});
const createTriggers =
userId != null
? createReleaseJobTriggers(db, "new_release").causedById(userId)
: createReleaseJobTriggers(db, "new_release");
await createTriggers
.deployments([deployment.id])
.resources(resourcesOnlyInNewSystem.map((r) => r.id))
.filter(isPassingReleaseStringCheckPolicy)
.then(createJobApprovals)
.insert()
.then((triggers) =>
dispatchReleaseJobTriggers(db)
.releaseTriggers(triggers)
.filter(isPassingAllPolicies)
.dispatch(),
);
};
export const handleDeploymentSystemChanged = async (
deployment: SCHEMA.Deployment,
prevSystemId: string,
userId?: string,
) => {
return db.transaction(async (tx) => {
const resourcesOnlyInNewSystem = await getResourcesOnlyInNewSystem(
deployment.systemId,
prevSystemId,
);
const events = resourcesOnlyInNewSystem.map((resource) => ({
action: "deployment.resource.removed" as const,
payload: { deployment, resource },
}));
await Promise.allSettled(events.map(handleEvent));
const isDeploymentHook = and(
eq(SCHEMA.hook.scopeType, "deployment"),
eq(SCHEMA.hook.scopeId, deployment.id),
);
await tx.query.hook
.findMany({
where: isDeploymentHook,
with: { runhooks: { with: { runbook: true } } },
})
.then((hooks) => {
const runbookIds = hooks.flatMap((h) =>
h.runhooks.map((rh) => rh.runbook.id),
);
return tx
.update(SCHEMA.runbook)
.set({ systemId: deployment.systemId })
.where(inArray(SCHEMA.runbook.id, runbookIds));
});
const createTriggers =
userId != null
? createReleaseJobTriggers(tx, "new_release").causedById(userId)
: createReleaseJobTriggers(tx, "new_release");
await createTriggers
.deployments([deployment.id])
.resources(resourcesOnlyInNewSystem.map((r) => r.id))
.filter(isPassingReleaseStringCheckPolicy)
.then(createJobApprovals)
.insert()
.then((triggers) =>
dispatchReleaseJobTriggers(tx)
.releaseTriggers(triggers)
.filter(isPassingAllPolicies)
.dispatch(),
);
});
};


export const updateDeployment = async (
deploymentId: string,
data: SCHEMA.UpdateDeployment,
userId?: string,
) => {
const prevDeployment = await db
.select()
.from(SCHEMA.deployment)
.where(eq(SCHEMA.deployment.id, deploymentId))
.then(takeFirst);

Comment on lines +125 to +130
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure prevDeployment is valid and handle potential errors

The variable prevDeployment may be undefined if no deployment matches the deploymentId. Consider adding error handling to manage this scenario, possibly by throwing an error if prevDeployment is not found.

Apply this diff to handle the potential undefined value:

 const prevDeployment = await db
     .select()
     .from(SCHEMA.deployment)
     .where(eq(SCHEMA.deployment.id, deploymentId))
     .then(takeFirst);
+ if (!prevDeployment) {
+   throw new Error(`Deployment with id ${deploymentId} not found`);
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const prevDeployment = await db
.select()
.from(SCHEMA.deployment)
.where(eq(SCHEMA.deployment.id, deploymentId))
.then(takeFirst);
const prevDeployment = await db
.select()
.from(SCHEMA.deployment)
.where(eq(SCHEMA.deployment.id, deploymentId))
.then(takeFirst);
if (!prevDeployment) {
throw new Error(`Deployment with id ${deploymentId} not found`);
}

const updatedDeployment = await db
.update(SCHEMA.deployment)
.set(data)
.where(eq(SCHEMA.deployment.id, deploymentId))
.returning()
.then(takeFirst);

Comment on lines +131 to +137
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check if updatedDeployment is undefined after the update

After updating the deployment, updatedDeployment might be undefined if the update did not affect any rows. Add error handling to manage this scenario.

Apply this diff to ensure updatedDeployment is valid:

 const updatedDeployment = await db
     .update(SCHEMA.deployment)
     .set(data)
     .where(eq(SCHEMA.deployment.id, deploymentId))
     .returning()
     .then(takeFirst);
+ if (!updatedDeployment) {
+   throw new Error(`Failed to update deployment with id ${deploymentId}`);
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updatedDeployment = await db
.update(SCHEMA.deployment)
.set(data)
.where(eq(SCHEMA.deployment.id, deploymentId))
.returning()
.then(takeFirst);
const updatedDeployment = await db
.update(SCHEMA.deployment)
.set(data)
.where(eq(SCHEMA.deployment.id, deploymentId))
.returning()
.then(takeFirst);
if (!updatedDeployment) {
throw new Error(`Failed to update deployment with id ${deploymentId}`);
}

if (prevDeployment.systemId !== updatedDeployment.systemId)
await handleDeploymentSystemChanged(
updatedDeployment,
prevDeployment.systemId,
userId,
);
Comment on lines +138 to +143
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add braces to multi-line if statement for clarity

The if statement spans multiple lines and calls an asynchronous function. To improve readability and prevent potential errors, it's recommended to wrap the block in braces.

Apply this diff:

 if (prevDeployment.systemId !== updatedDeployment.systemId)
-   await handleDeploymentSystemChanged(
-     updatedDeployment,
-     prevDeployment.systemId,
-     userId,
-   );
+ {
+   await handleDeploymentSystemChanged(
+     updatedDeployment,
+     prevDeployment.systemId,
+     userId,
+   );
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (prevDeployment.systemId !== updatedDeployment.systemId)
await handleDeploymentSystemChanged(
updatedDeployment,
prevDeployment.systemId,
userId,
);
if (prevDeployment.systemId !== updatedDeployment.systemId) {
await handleDeploymentSystemChanged(
updatedDeployment,
prevDeployment.systemId,
userId,
);
}


const sys = await db
.select()
.from(SCHEMA.system)
.where(eq(SCHEMA.system.id, updatedDeployment.systemId))
.then(takeFirst);

return { ...updatedDeployment, system: sys };
};
Comment on lines +145 to +152
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for system query and validate return object

The final system query could fail silently, and the return object structure should be validated before returning.

Apply this diff:

-  const sys = await db
+  const system = await db
     .select()
     .from(SCHEMA.system)
     .where(eq(SCHEMA.system.id, updatedDeployment.systemId))
     .then(takeFirst);
+  
+  if (!system) {
+    throw new Error(
+      `System not found for updated deployment: ${updatedDeployment.systemId}`
+    );
+  }
 
-  return { ...updatedDeployment, system: sys };
+  return { ...updatedDeployment, system };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sys = await db
.select()
.from(SCHEMA.system)
.where(eq(SCHEMA.system.id, updatedDeployment.systemId))
.then(takeFirst);
return { ...updatedDeployment, system: sys };
};
const system = await db
.select()
.from(SCHEMA.system)
.where(eq(SCHEMA.system.id, updatedDeployment.systemId))
.then(takeFirst);
if (!system) {
throw new Error(
`System not found for updated deployment: ${updatedDeployment.systemId}`
);
}
return { ...updatedDeployment, system };
};

3 changes: 3 additions & 0 deletions packages/job-dispatch/src/events/handlers/resource-removed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const handleResourceRemoved = async (event: ResourceRemoved) => {
.innerJoin(SCHEMA.hook, eq(SCHEMA.runhook.hookId, SCHEMA.hook.id))
.where(isSubscribedToResourceRemoved);

if (runhooks.length === 0) return;
await db.insert(SCHEMA.event).values(event);

const resourceId = resource.id;
const deploymentId = deployment.id;
const handleRunhooksPromises = runhooks.map(({ runhook }) =>
Expand Down
9 changes: 2 additions & 7 deletions packages/job-dispatch/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import type { HookEvent } from "@ctrlplane/validators/events";

import { db } from "@ctrlplane/db/client";
import * as SCHEMA from "@ctrlplane/db/schema";

import { handleResourceRemoved } from "./handlers/index.js";

export * from "./triggers/index.js";
export * from "./handlers/index.js";

export const handleEvent = async (event: HookEvent) => {
await db.insert(SCHEMA.event).values(event);
return handleResourceRemoved(event);
};
export const handleEvent = async (event: HookEvent) =>
handleResourceRemoved(event);
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import * as SCHEMA from "@ctrlplane/db/schema";
import { ComparisonOperator } from "@ctrlplane/validators/conditions";
import { ResourceFilterType } from "@ctrlplane/validators/resources";

export const getEventsForDeploymentDeleted = async (
export const getEventsForDeploymentRemoved = async (
deployment: SCHEMA.Deployment,
systemId: string,
): Promise<HookEvent[]> => {
const hasFilter = isNotNull(SCHEMA.environment.resourceFilter);
const system = await db.query.system.findFirst({
where: eq(SCHEMA.system.id, deployment.systemId),
with: {
environments: { where: isNotNull(SCHEMA.environment.resourceFilter) },
},
where: eq(SCHEMA.system.id, systemId),
with: { environments: { where: hasFilter } },
});
if (system == null) return [];

Expand Down
2 changes: 1 addition & 1 deletion packages/job-dispatch/src/events/triggers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./environment-deleted.js";
export * from "./deployment-deleted.js";
export * from "./deployment-removed.js";
export * from "./resource-deleted.js";
1 change: 1 addition & 0 deletions packages/job-dispatch/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./config.js";
export * from "./release-job-trigger.js";
export * from "./deployment-update.js";
export * from "./job-update.js";
export * from "./job-dispatch.js";
export * from "./policy-checker.js";
Expand Down
Loading