-
Notifications
You must be signed in to change notification settings - Fork 2
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 #202
init #202
Changes from all commits
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { pgTable, text, uuid } from "drizzle-orm/pg-core"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { runbook } from "./runbook.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const runhook = pgTable("runhook", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: uuid("id").primaryKey().defaultRandom(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
scopeType: text("scope_type").notNull(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
scopeId: uuid("scope_id").notNull(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
runbookId: uuid("runbook_id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.references(() => runbook.id, { onDelete: "cascade" }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.notNull(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const runhookEvent = pgTable("runhook_event", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: uuid("id").primaryKey().defaultRandom(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
runhookId: uuid("runhook_id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.references(() => runhook.id, { onDelete: "cascade" }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.notNull(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eventType: text("event_type").notNull(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+14
to
+20
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 adding essential fields for event tracking. The
Consider enhancing the schema like this: export const runhookEvent = pgTable("runhook_event", {
id: uuid("id").primaryKey().defaultRandom(),
runhookId: uuid("runhook_id")
.references(() => runhook.id, { onDelete: "cascade" })
.notNull(),
eventType: text("event_type").notNull(),
+ payload: jsonb("payload"),
+ status: text("status").notNull().default("pending"),
+ createdAt: timestamp("created_at").notNull().defaultNow(),
+ processedAt: timestamp("processed_at"),
+ error: text("error"),
-});
+}, (table) => ({
+ runhookIdx: index("runhook_event_runhook_idx").on(table.runhookId),
+ createdAtIdx: index("runhook_event_created_at_idx").on(table.createdAt),
+})); Also, consider adding documentation for valid event types: +/**
+ * Represents events associated with runhooks.
+ *
+ * @property eventType - The type of event.
+ * Valid values: [list valid values here]
+ * @property payload - Additional event data in JSON format
+ * @property status - Event processing status (pending, processing, completed, failed)
+ */
export const runhookEvent = pgTable("runhook_event", { 📝 Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import baseConfig, { requireJsSuffix } from "@ctrlplane/eslint-config/base"; | ||
|
||
/** @type {import('typescript-eslint').Config} */ | ||
export default [ | ||
{ | ||
ignores: ["dist/**"], | ||
}, | ||
...requireJsSuffix, | ||
...baseConfig, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "@ctrlplane/events", | ||
"private": true, | ||
"version": "0.1.0", | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./src/index.ts", | ||
"default": "./dist/index.js" | ||
} | ||
}, | ||
"license": "MIT", | ||
"scripts": { | ||
"build": "tsc", | ||
"dev": "tsc --watch", | ||
"clean": "rm -rf .turbo node_modules", | ||
"format": "prettier --check . --ignore-path ../../.gitignore", | ||
"lint": "eslint", | ||
"typecheck": "tsc --noEmit --emitDeclarationOnly false" | ||
}, | ||
"dependencies": { | ||
"@ctrlplane/db": "workspace:*", | ||
"@ctrlplane/job-dispatch": "workspace:*", | ||
"@ctrlplane/validators": "workspace:*", | ||
"ts-is-present": "^1.2.2", | ||
"zod": "catalog:" | ||
}, | ||
"devDependencies": { | ||
"@ctrlplane/eslint-config": "workspace:*", | ||
"@ctrlplane/prettier-config": "workspace:*", | ||
"@ctrlplane/tsconfig": "workspace:*", | ||
"@types/node": "catalog:node20", | ||
"eslint": "catalog:", | ||
"prettier": "catalog:", | ||
"typescript": "catalog:" | ||
}, | ||
"prettier": "@ctrlplane/prettier-config" | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,111 @@ | ||||||||||||||||||||||||||||||||
import type { EnvironmentDeletedEvent } from "@ctrlplane/validators/events"; | ||||||||||||||||||||||||||||||||
import type { TargetCondition } from "@ctrlplane/validators/targets"; | ||||||||||||||||||||||||||||||||
import { isPresent } from "ts-is-present"; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import { | ||||||||||||||||||||||||||||||||
and, | ||||||||||||||||||||||||||||||||
eq, | ||||||||||||||||||||||||||||||||
inArray, | ||||||||||||||||||||||||||||||||
isNotNull, | ||||||||||||||||||||||||||||||||
ne, | ||||||||||||||||||||||||||||||||
or, | ||||||||||||||||||||||||||||||||
takeFirstOrNull, | ||||||||||||||||||||||||||||||||
} from "@ctrlplane/db"; | ||||||||||||||||||||||||||||||||
import { db } from "@ctrlplane/db/client"; | ||||||||||||||||||||||||||||||||
import * as SCHEMA from "@ctrlplane/db/schema"; | ||||||||||||||||||||||||||||||||
import { dispatchRunbook } from "@ctrlplane/job-dispatch"; | ||||||||||||||||||||||||||||||||
import { ComparisonOperator } from "@ctrlplane/validators/conditions"; | ||||||||||||||||||||||||||||||||
import { TargetFilterType } from "@ctrlplane/validators/targets"; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const handleTargets = async (event: EnvironmentDeletedEvent) => { | ||||||||||||||||||||||||||||||||
const environment = await db | ||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||
.from(SCHEMA.environment) | ||||||||||||||||||||||||||||||||
.where(eq(SCHEMA.environment.id, event.payload.environmentId)) | ||||||||||||||||||||||||||||||||
.then(takeFirstOrNull); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Comment on lines
+20
to
+26
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 Null Check for 'environment' Before Accessing Its Properties While you check if Consider adding a null check for Apply this diff to add the null check: const environment = await db
.select()
.from(SCHEMA.environment)
.where(eq(SCHEMA.environment.id, event.payload.environmentId))
.then(takeFirstOrNull);
+ if (environment == null) return;
if (environment.targetFilter == null) return; 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
if (environment?.targetFilter == null) return; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const targets = await db | ||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||
.from(SCHEMA.target) | ||||||||||||||||||||||||||||||||
.where(SCHEMA.targetMatchesMetadata(db, environment.targetFilter)); | ||||||||||||||||||||||||||||||||
if (targets.length === 0) return; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const checks = and( | ||||||||||||||||||||||||||||||||
isNotNull(SCHEMA.environment.targetFilter), | ||||||||||||||||||||||||||||||||
ne(SCHEMA.environment.id, environment.id), | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
const system = await db.query.system.findFirst({ | ||||||||||||||||||||||||||||||||
where: eq(SCHEMA.system.id, environment.systemId), | ||||||||||||||||||||||||||||||||
with: { environments: { where: checks }, deployments: true }, | ||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||
if (system == null) return; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const envFilters = system.environments | ||||||||||||||||||||||||||||||||
.map((e) => e.targetFilter) | ||||||||||||||||||||||||||||||||
.filter(isPresent); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const removedFromSystemFilter: TargetCondition = { | ||||||||||||||||||||||||||||||||
type: TargetFilterType.Comparison, | ||||||||||||||||||||||||||||||||
operator: ComparisonOperator.Or, | ||||||||||||||||||||||||||||||||
not: true, | ||||||||||||||||||||||||||||||||
conditions: envFilters, | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const removedFromSystemTargets = | ||||||||||||||||||||||||||||||||
envFilters.length > 0 | ||||||||||||||||||||||||||||||||
? await db | ||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||
.from(SCHEMA.target) | ||||||||||||||||||||||||||||||||
.where( | ||||||||||||||||||||||||||||||||
and( | ||||||||||||||||||||||||||||||||
SCHEMA.targetMatchesMetadata(db, removedFromSystemFilter), | ||||||||||||||||||||||||||||||||
inArray( | ||||||||||||||||||||||||||||||||
SCHEMA.target.id, | ||||||||||||||||||||||||||||||||
targets.map((t) => t.id), | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
: targets; | ||||||||||||||||||||||||||||||||
if (removedFromSystemTargets.length === 0) return; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const runhooks = await db | ||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||
.from(SCHEMA.runhook) | ||||||||||||||||||||||||||||||||
.innerJoin( | ||||||||||||||||||||||||||||||||
SCHEMA.runhookEvent, | ||||||||||||||||||||||||||||||||
eq(SCHEMA.runhookEvent.runhookId, SCHEMA.runhook.id), | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
.where( | ||||||||||||||||||||||||||||||||
or( | ||||||||||||||||||||||||||||||||
...system.deployments.map((deployment) => | ||||||||||||||||||||||||||||||||
and( | ||||||||||||||||||||||||||||||||
eq(SCHEMA.runhook.scopeType, "deployment"), | ||||||||||||||||||||||||||||||||
eq(SCHEMA.runhook.scopeId, deployment.id), | ||||||||||||||||||||||||||||||||
eq(SCHEMA.runhookEvent.eventType, "environment.deleted"), | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
.then((r) => r.map((rh) => rh.runhook)); | ||||||||||||||||||||||||||||||||
if (runhooks.length === 0) return; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const handleLifecycleHooksForTargets = removedFromSystemTargets.flatMap((t) => | ||||||||||||||||||||||||||||||||
runhooks.map((rh) => { | ||||||||||||||||||||||||||||||||
const values: Record<string, string> = { | ||||||||||||||||||||||||||||||||
targetId: t.id, | ||||||||||||||||||||||||||||||||
deploymentId: rh.scopeId, | ||||||||||||||||||||||||||||||||
environmentId: environment.id, | ||||||||||||||||||||||||||||||||
systemId: system.id, | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return dispatchRunbook(db, rh.runbookId, values); | ||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
await Promise.all(handleLifecycleHooksForTargets); | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
export const handleEnvironmentDeleted = (event: EnvironmentDeletedEvent) => | ||||||||||||||||||||||||||||||||
handleTargets(event); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { HookEvent } from "@ctrlplane/validators/events"; | ||
|
||
import { isEnvironmentDeletedEvent } from "@ctrlplane/validators/events"; | ||
|
||
import { handleEnvironmentDeleted } from "./environments/environment-delete.js"; | ||
|
||
export const handleHookEvent = (event: HookEvent) => { | ||
if (isEnvironmentDeletedEvent(event)) handleEnvironmentDeleted(event); | ||
throw new Error(`Unknown event type: ${event.type}`); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "@ctrlplane/tsconfig/internal-package.json", | ||
"compilerOptions": { | ||
"outDir": "dist", | ||
"baseUrl": "." | ||
}, | ||
"include": ["*.ts", "src"], | ||
"exclude": ["node_modules"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { z } from "zod"; | ||
|
||
const environmentBaseEvent = z.object({ | ||
createdAt: z.string().datetime(), | ||
payload: z.object({ | ||
environmentId: z.string().uuid(), | ||
}), | ||
}); | ||
|
||
export enum EnvironmentEvent { | ||
Deleted = "environment.deleted", | ||
Created = "environment.created", | ||
} | ||
|
||
export const environmentDeletedEvent = environmentBaseEvent.extend({ | ||
type: z.literal(EnvironmentEvent.Deleted), | ||
}); | ||
export type EnvironmentDeletedEvent = z.infer<typeof environmentDeletedEvent>; | ||
|
||
export const environmentCreatedEvent = environmentBaseEvent.extend({ | ||
type: z.literal(EnvironmentEvent.Created), | ||
}); | ||
export type EnvironmentCreatedEvent = z.infer<typeof environmentCreatedEvent>; | ||
Comment on lines
+15
to
+23
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 extending event payloads with event-specific data. The current implementation uses the same payload structure for both events. Consider adding event-specific fields to capture relevant data: export const environmentDeletedEvent = environmentBaseEvent.extend({
type: z.literal(EnvironmentEvent.Deleted),
+ payload: z.object({
+ environmentId: z.string().uuid(),
+ deletedBy: z.string().uuid(),
+ reason: z.string().optional(),
+ }),
});
export const environmentCreatedEvent = environmentBaseEvent.extend({
type: z.literal(EnvironmentEvent.Created),
+ payload: z.object({
+ environmentId: z.string().uuid(),
+ createdBy: z.string().uuid(),
+ configuration: z.record(z.unknown()),
+ }),
});
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { z } from "zod"; | ||
|
||
import type { | ||
EnvironmentCreatedEvent, | ||
EnvironmentDeletedEvent, | ||
} from "./environment.js"; | ||
import { | ||
environmentCreatedEvent, | ||
environmentDeletedEvent, | ||
EnvironmentEvent, | ||
} from "./environment.js"; | ||
|
||
export * from "./environment.js"; | ||
|
||
export const HookEvent = z.union([ | ||
environmentDeletedEvent, | ||
environmentCreatedEvent, | ||
]); | ||
export type HookEvent = z.infer<typeof HookEvent>; | ||
|
||
// typeguards | ||
export const isEnvironmentDeletedEvent = ( | ||
event: HookEvent, | ||
): event is EnvironmentDeletedEvent => event.type === EnvironmentEvent.Deleted; | ||
export const isEnvironmentCreatedEvent = ( | ||
event: HookEvent, | ||
): event is EnvironmentCreatedEvent => event.type === EnvironmentEvent.Created; |
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
Consider adding error handling for event processing
While concurrent event processing is efficient, a failure in
handleHookEvent
for one environment could cause others to fail silently. Consider adding error handling and possibly a transaction to ensure atomicity.Here's a suggested improvement:
📝 Committable suggestion