-
Notifications
You must be signed in to change notification settings - Fork 1
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: Init resource relationship routes #219
Changes from 1 commit
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,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { and, eq, takeFirstOrNull } from "@ctrlplane/db"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import * as SCHEMA from "@ctrlplane/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { authn } from "../../auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { parseBody } from "../../body-parser"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { request } from "../../middleware"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const bodySchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
workspaceId: z.string().uuid(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
deploymentId: z.string().uuid(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
resourceIdentifier: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const POST = request() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.use(authn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.use(parseBody(bodySchema)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.handle<{ body: z.infer<typeof bodySchema> }>(async (ctx) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { body, db } = ctx; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const resource = await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.from(SCHEMA.resource) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.where( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
and( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eq(SCHEMA.resource.identifier, body.resourceIdentifier), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eq(SCHEMA.resource.workspaceId, body.workspaceId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.then(takeFirstOrNull); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!resource) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Response.json({ error: "Resource not found" }, { status: 404 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const deployment = await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.select() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.from(SCHEMA.deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.innerJoin( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
SCHEMA.system, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eq(SCHEMA.deployment.systemId, SCHEMA.system.id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.where( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
and( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eq(SCHEMA.deployment.id, body.deploymentId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
eq(SCHEMA.system.workspaceId, body.workspaceId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.then(takeFirstOrNull); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!deployment) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Response.json({ error: "Deployment not found" }, { status: 404 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
await db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.insert(SCHEMA.deploymentResourceRelationship) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.values(body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.returning(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return Response.json(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 uniqueness check and improve response data Several improvements needed for the relationship creation:
await db
+ .select()
+ .from(SCHEMA.deploymentResourceRelationship)
+ .where(
+ and(
+ eq(SCHEMA.deploymentResourceRelationship.deploymentId, body.deploymentId),
+ eq(SCHEMA.deploymentResourceRelationship.resourceIdentifier, body.resourceIdentifier)
+ )
+ )
+ .then(takeFirstOrNull)
+ .then((existing) => {
+ if (existing) {
+ return Response.json(
+ { error: "Relationship already exists" },
+ { status: 409 }
+ );
+ }
+ });
+
+ try {
+ const [created] = await db
.insert(SCHEMA.deploymentResourceRelationship)
.values(body)
- .returning();
+ .returning({
+ id: SCHEMA.deploymentResourceRelationship.id,
+ workspaceId: SCHEMA.deploymentResourceRelationship.workspaceId,
+ deploymentId: SCHEMA.deploymentResourceRelationship.deploymentId,
+ resourceIdentifier: SCHEMA.deploymentResourceRelationship.resourceIdentifier,
+ createdAt: SCHEMA.deploymentResourceRelationship.createdAt
+ });
- return Response.json(body);
+ return Response.json(created);
+ } catch (error) {
+ return Response.json(
+ { error: "Failed to create relationship" },
+ { status: 500 }
+ );
+ } 📝 Committable suggestion
Suggested change
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { z } from "zod"; | ||
|
||
import { and, eq, takeFirstOrNull } from "@ctrlplane/db"; | ||
import * as SCHEMA from "@ctrlplane/db/schema"; | ||
|
||
import { authn } from "../../auth"; | ||
import { parseBody } from "../../body-parser"; | ||
import { request } from "../../middleware"; | ||
|
||
const bodySchema = z.object({ | ||
workspaceId: z.string().uuid(), | ||
fromIdentifier: z.string(), | ||
toIdentifier: z.string(), | ||
type: z.enum(["associated_with", "depends_on"]), | ||
}); | ||
|
||
export const POST = request() | ||
.use(authn) | ||
.use(parseBody(bodySchema)) | ||
.handle<{ body: z.infer<typeof bodySchema> }>(async (ctx) => { | ||
const { body, db } = ctx; | ||
|
||
const fromResource = await db | ||
.select() | ||
.from(SCHEMA.resource) | ||
.where( | ||
and( | ||
eq(SCHEMA.resource.identifier, body.fromIdentifier), | ||
eq(SCHEMA.resource.workspaceId, body.workspaceId), | ||
), | ||
) | ||
.then(takeFirstOrNull); | ||
if (!fromResource) | ||
return Response.json( | ||
{ error: `${body.fromIdentifier} not found` }, | ||
{ status: 404 }, | ||
); | ||
|
||
const toResource = await db | ||
.select() | ||
.from(SCHEMA.resource) | ||
.where( | ||
and( | ||
eq(SCHEMA.resource.identifier, body.toIdentifier), | ||
eq(SCHEMA.resource.workspaceId, body.workspaceId), | ||
), | ||
) | ||
.then(takeFirstOrNull); | ||
if (!toResource) | ||
return Response.json( | ||
{ error: `${body.toIdentifier} not found` }, | ||
{ status: 404 }, | ||
); | ||
|
||
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 support and uniqueness validation. The resource validation logic should be wrapped in a transaction, and you should check for existing relationships to prevent duplicates. + const existing = await db
+ .select()
+ .from(SCHEMA.resourceRelationship)
+ .where(
+ and(
+ eq(SCHEMA.resourceRelationship.sourceId, fromResource.id),
+ eq(SCHEMA.resourceRelationship.targetId, toResource.id),
+ eq(SCHEMA.resourceRelationship.type, body.type),
+ ),
+ )
+ .then(takeFirstOrNull);
+
+ if (existing) {
+ return Response.json(
+ { error: "Relationship already exists" },
+ { status: 409 },
+ );
+ }
|
||
await db.insert(SCHEMA.resourceRelationship).values({ | ||
sourceId: fromResource.id, | ||
targetId: toResource.id, | ||
type: body.type, | ||
}); | ||
|
||
return Response.json({ message: "Relationship created" }, { status: 200 }); | ||
}); | ||
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 relationship creation with better error handling and response. The relationship creation needs improvements in several areas:
- await db.insert(SCHEMA.resourceRelationship).values({
- sourceId: fromResource.id,
- targetId: toResource.id,
- type: body.type,
- });
-
- return Response.json({ message: "Relationship created" }, { status: 200 });
+ return await db.transaction(async (tx) => {
+ if (body.type === "depends_on") {
+ const hasCycle = await checkForDependencyCycle(
+ tx,
+ fromResource.id,
+ toResource.id
+ );
+ if (hasCycle) {
+ return Response.json(
+ { error: "Circular dependency detected" },
+ { status: 400 }
+ );
+ }
+ }
+
+ const [relationship] = await tx
+ .insert(SCHEMA.resourceRelationship)
+ .values({
+ sourceId: fromResource.id,
+ targetId: toResource.id,
+ type: body.type,
+ })
+ .returning();
+
+ return Response.json({
+ message: "Relationship created",
+ relationship: {
+ fromIdentifier: body.fromIdentifier,
+ toIdentifier: body.toIdentifier,
+ type: body.type,
+ }
+ }, { status: 201 });
+ });
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ import { | |
} from "@ctrlplane/validators/resources"; | ||
|
||
import type { Tx } from "../common.js"; | ||
import { deployment } from "./deployment.js"; | ||
import { resourceProvider } from "./resource-provider.js"; | ||
import { workspace } from "./workspace.js"; | ||
|
||
|
@@ -270,6 +271,22 @@ export type ResourceRelationship = InferSelectModel< | |
typeof resourceRelationship | ||
>; | ||
|
||
export const deploymentResourceRelationship = pgTable( | ||
"deployment_resource_relationship", | ||
{ | ||
id: uuid("id").primaryKey().defaultRandom(), | ||
workspaceId: uuid("workspace_id") | ||
.references(() => workspace.id, { onDelete: "cascade" }) | ||
.notNull(), | ||
deploymentId: uuid("deployment_id") | ||
.references(() => deployment.id, { onDelete: "cascade" }) | ||
.notNull(), | ||
resourceIdentifier: text("resource_identifier") | ||
.references(() => resource.identifier, { onDelete: "cascade" }) | ||
.notNull(), | ||
}, | ||
); | ||
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 Several improvements needed for the deploymentResourceRelationship table.
export const deploymentResourceRelationship = pgTable(
"deployment_resource_relationship",
{
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id")
.references(() => workspace.id, { onDelete: "cascade" })
.notNull(),
deploymentId: uuid("deployment_id")
.references(() => deployment.id, { onDelete: "cascade" })
.notNull(),
resourceIdentifier: text("resource_identifier")
.references(() => resource.identifier, { onDelete: "cascade" })
.notNull(),
},
+ (t) => ({
+ uniq: uniqueIndex().on(t.workspaceId, t.deploymentId, t.resourceIdentifier),
+ }),
);
- resourceIdentifier: text("resource_identifier")
- .references(() => resource.identifier, { onDelete: "cascade" })
+ resourceId: uuid("resource_id")
+ .references(() => resource.id, { onDelete: "cascade" })
export const deploymentResourceRelationshipRelations = relations(
deploymentResourceRelationship,
({ one }) => ({
workspace: one(workspace, {
fields: [deploymentResourceRelationship.workspaceId],
references: [workspace.id],
}),
deployment: one(deployment, {
fields: [deploymentResourceRelationship.deploymentId],
references: [deployment.id],
}),
resource: one(resource, {
fields: [deploymentResourceRelationship.resourceId],
references: [resource.id],
}),
}),
);
export const createDeploymentResourceRelationship = createInsertSchema(
deploymentResourceRelationship,
).omit({ id: true });
export const updateDeploymentResourceRelationship =
createDeploymentResourceRelationship.partial();
export type DeploymentResourceRelationship = InferSelectModel<
typeof deploymentResourceRelationship
>; |
||
|
||
export const resourceVariable = pgTable( | ||
"resource_variable", | ||
{ | ||
|
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
Enhance error handling and add transaction support
Two suggestions for improvement: