-
Notifications
You must be signed in to change notification settings - Fork 11
init #208
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
init #208
Changes from all commits
e4b5807
fc9b854
b816989
927c8a0
4986ac2
fb85ddc
850c745
c21d28c
4c675e5
48b0e3a
4f4dd4d
8285e44
5b60916
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ import { | |
| updateReleaseChannel, | ||
| workspace, | ||
| } from "@ctrlplane/db/schema"; | ||
| import { getEventsForDeploymentDeleted, handleEvent } from "@ctrlplane/events"; | ||
| import { Permission } from "@ctrlplane/validators/auth"; | ||
| import { JobStatus } from "@ctrlplane/validators/jobs"; | ||
|
|
||
|
|
@@ -260,13 +261,20 @@ export const deploymentRouter = createTRPCRouter({ | |
| .on({ type: "deployment", id: input }), | ||
| }) | ||
| .input(z.string().uuid()) | ||
| .mutation(({ ctx, input }) => | ||
| ctx.db | ||
| .mutation(async ({ ctx, input }) => { | ||
| const dep = await ctx.db | ||
| .select() | ||
| .from(deployment) | ||
| .where(eq(deployment.id, input)) | ||
| .then(takeFirst); | ||
| const events = await getEventsForDeploymentDeleted(dep); | ||
| await Promise.allSettled(events.map(handleEvent)); | ||
| return ctx.db | ||
| .delete(deployment) | ||
| .where(eq(deployment.id, input)) | ||
| .returning() | ||
| .then(takeFirst), | ||
| ), | ||
| .then(takeFirst); | ||
| }), | ||
|
Comment on lines
+264
to
+277
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add transaction handling to ensure data consistency The current implementation could lead to inconsistent state if event processing fails after fetching the deployment but before deletion. Consider wrapping the operations in a transaction. Here's a suggested implementation: .mutation(async ({ ctx, input }) => {
+ return ctx.db.transaction(async (tx) => {
const dep = await ctx.db
.select()
.from(deployment)
.where(eq(deployment.id, input))
.then(takeFirst);
const events = await getEventsForDeploymentDeleted(dep);
await Promise.allSettled(events.map(handleEvent));
return ctx.db
.delete(deployment)
.where(eq(deployment.id, input))
.returning()
.then(takeFirst);
+ });
}),
|
||
|
|
||
| byId: protectedProcedure | ||
| .input(z.string().uuid()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| targetMatchesMetadata, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateEnvironment, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@ctrlplane/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getEventsForEnvironmentDeleted, handleEvent } from "@ctrlplane/events"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { dispatchJobsForNewTargets } from "@ctrlplane/job-dispatch"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Permission } from "@ctrlplane/validators/auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -322,12 +323,16 @@ export const environmentRouter = createTRPCRouter({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .input(z.string().uuid()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .mutation(({ ctx, input }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx.db.transaction((db) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .delete(environment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where(eq(environment.id, input)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .returning() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(takeFirst), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx.db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .delete(environment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .where(eq(environment.id, input)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .returning() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(takeFirst) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(async (env) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const events = await getEventsForEnvironmentDeleted(env); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEventPromises = events.map(handleEvent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.allSettled(handleEventPromises); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return env; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+326
to
+336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider improving error handling and transaction safety. The current implementation has several areas for improvement:
Consider refactoring to: delete: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.SystemDelete)
.on({ type: "environment", id: input }),
})
.input(z.string().uuid())
.mutation(({ ctx, input }) =>
- ctx.db
- .delete(environment)
- .where(eq(environment.id, input))
- .returning()
- .then(takeFirst)
- .then(async (env) => {
- const events = await getEventsForEnvironmentDeleted(env);
- const handleEventPromises = events.map(handleEvent);
- await Promise.allSettled(handleEventPromises);
- return env;
- }),
+ ctx.db.transaction(async (tx) => {
+ const env = await tx
+ .delete(environment)
+ .where(eq(environment.id, input))
+ .returning()
+ .then(takeFirst);
+
+ const events = await getEventsForEnvironmentDeleted(env);
+ const results = await Promise.allSettled(events.map(handleEvent));
+
+ // Check for any failed event handling
+ const failures = results
+ .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
+ .map(r => r.reason);
+
+ if (failures.length > 0) {
+ console.error('Failed to process some events:', failures);
+ throw new Error('Failed to process all events after environment deletion');
+ }
+
+ return env;
+ }),This refactoring:
📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |
| takeFirstOrNull, | ||
| } from "@ctrlplane/db"; | ||
| import * as schema from "@ctrlplane/db/schema"; | ||
| import { getEventsForTargetDeleted, handleEvent } from "@ctrlplane/events"; | ||
| import { | ||
| cancelOldReleaseJobTriggersOnJobDispatch, | ||
| createJobApprovals, | ||
|
|
@@ -477,12 +478,20 @@ export const targetRouter = createTRPCRouter({ | |
| ), | ||
| }) | ||
| .input(z.array(z.string().uuid())) | ||
| .mutation(({ ctx, input }) => | ||
| ctx.db | ||
| .mutation(async ({ ctx, input }) => { | ||
| const targets = await ctx.db.query.target.findMany({ | ||
| where: inArray(schema.target.id, input), | ||
| }); | ||
| const events = ( | ||
| await Promise.allSettled(targets.map(getEventsForTargetDeleted)) | ||
| ).flatMap((r) => (r.status === "fulfilled" ? r.value : [])); | ||
| await Promise.allSettled(events.map(handleEvent)); | ||
|
|
||
| return ctx.db | ||
| .delete(schema.target) | ||
| .where(inArray(schema.target.id, input)) | ||
| .returning(), | ||
| ), | ||
| .returning(); | ||
| }), | ||
|
Comment on lines
+481
to
+494
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure proper error handling when processing events during target deletion The current implementation uses Consider explicitly handling rejected promises to ensure failures are not silently ignored. Here's how you might modify the code: .mutation(async ({ ctx, input }) => {
const targets = await ctx.db.query.target.findMany({
where: inArray(schema.target.id, input),
});
- const events = (
- await Promise.allSettled(targets.map(getEventsForTargetDeleted))
- ).flatMap((r) => (r.status === "fulfilled" ? r.value : []));
+ const events = await Promise.all(
+ targets.map(async (target) => {
+ try {
+ return await getEventsForTargetDeleted(target);
+ } catch (error) {
+ // Handle or log the error appropriately
+ console.error(`Failed to get events for target ${target.id}:`, error);
+ throw error;
+ }
+ })
+ );
- await Promise.allSettled(events.map(handleEvent));
+ await Promise.all(
+ events.map(async (event) => {
+ try {
+ await handleEvent(event);
+ } catch (error) {
+ // Handle or log the error appropriately
+ console.error(`Failed to handle event for target deletion:`, error);
+ throw error;
+ }
+ })
+ );
return ctx.db
.delete(schema.target)
.where(inArray(schema.target.id, input))
.returning();
}),This approach ensures that any errors during event generation or handling are caught and can be handled appropriately, possibly aborting the deletion if necessary.
🛠️ Refactor suggestion Wrap database operations in a transaction for consistency The deletion process includes multiple database operations: fetching targets, handling events, and deleting targets. To maintain data consistency, especially in cases where an error might occur during event handling, it's advisable to wrap these operations in a transaction. Consider modifying the code to use a transaction: .mutation(async ({ ctx, input }) => {
+ return ctx.db.transaction(async (tx) => {
const targets = await tx.query.target.findMany({
where: inArray(schema.target.id, input),
});
const events = await Promise.all(
targets.map(async (target) => {
// Error handling as discussed previously
return await getEventsForTargetDeleted(target);
})
);
await Promise.all(
events.map(async (event) => {
// Error handling as discussed previously
await handleEvent(event);
})
);
return tx
.delete(schema.target)
.where(inArray(schema.target.id, input))
.returning();
+ });
}),Wrapping these operations in a transaction ensures that if any part of the process fails, all changes can be rolled back, maintaining the integrity of your data.
|
||
|
|
||
| metadataKeys: protectedProcedure | ||
| .meta({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| CREATE TABLE IF NOT EXISTS "event" ( | ||
| "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, | ||
| "action" text NOT NULL, | ||
| "payload" jsonb NOT NULL, | ||
| "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE TABLE IF NOT EXISTS "hook" ( | ||
| "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, | ||
| "action" text NOT NULL, | ||
| "name" text NOT NULL, | ||
| "scope_type" text NOT NULL, | ||
| "scope_id" uuid NOT NULL | ||
| ); | ||
|
Comment on lines
+8
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance data integrity and query performance Consider the following improvements:
CREATE INDEX IF NOT EXISTS idx_hook_scope ON hook(scope_type, scope_id);
ALTER TABLE hook ADD CONSTRAINT chk_hook_scope_type
CHECK (scope_type IN ('environment', 'deployment', 'target'));
ALTER TABLE hook ADD CONSTRAINT unq_hook_name_scope
UNIQUE (name, scope_type, scope_id); |
||
| --> statement-breakpoint | ||
| CREATE TABLE IF NOT EXISTS "runhook" ( | ||
| "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, | ||
| "hook_id" uuid NOT NULL, | ||
| "runbook_id" uuid NOT NULL | ||
| ); | ||
| --> statement-breakpoint | ||
| DO $$ BEGIN | ||
| ALTER TABLE "runhook" ADD CONSTRAINT "runhook_hook_id_hook_id_fk" FOREIGN KEY ("hook_id") REFERENCES "public"."hook"("id") ON DELETE cascade ON UPDATE no action; | ||
| EXCEPTION | ||
| WHEN duplicate_object THEN null; | ||
| END $$; | ||
| --> statement-breakpoint | ||
| DO $$ BEGIN | ||
| ALTER TABLE "runhook" ADD CONSTRAINT "runhook_runbook_id_runbook_id_fk" FOREIGN KEY ("runbook_id") REFERENCES "public"."runbook"("id") ON DELETE cascade ON UPDATE no action; | ||
| EXCEPTION | ||
| WHEN duplicate_object THEN null; | ||
| END $$; | ||
There was a problem hiding this comment.
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 operations.
The database query could fail due to various reasons (connection issues, permissions, etc.). Consider wrapping it in a try-catch block to handle potential errors gracefully.
export const run = async () => { + try { const expiredEnvironments = await db .select() .from(SCHEMA.environment) .where(lte(SCHEMA.environment.expiresAt, new Date())); if (expiredEnvironments.length === 0) return; + } catch (error) { + console.error('Failed to fetch expired environments:', error); + throw error; + }