-
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
feat: Target routes #186
base: main
Are you sure you want to change the base?
feat: Target routes #186
Changes from all commits
45ec4f6
8e4ed30
35bbd28
410c8d5
0f8a9ed
6df58c6
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,49 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
import { NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
import { eq } from "@ctrlplane/db"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
import * as schema from "@ctrlplane/db/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Permission } from "@ctrlplane/validators/auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
import { authn, authz } from "~/app/api/v1/auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
import { request } from "~/app/api/v1/middleware"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
function transformMetadata( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
metadata: Array<{ key: string; value: string }>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
): Record<string, string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return metadata.reduce<Record<string, string>>((acc, m) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
acc[m.key] = m.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return acc; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
export const GET = request() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
.use(authn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
authz(({ can, extra }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return can | ||||||||||||||||||||||||||||||||||||||||||||||||||||
.perform(Permission.TargetGet) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
.on({ type: "target", id: extra.params.targetId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
.handle(async ({ db }, { params }: { params: { targetId: string } }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
const { targetId } = params; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+28
to
+30
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 input validation for targetId. The targetId parameter should be validated to ensure it's a valid UUID format before querying the database. .handle(async ({ db }, { params }: { params: { targetId: string } }) => {
const { targetId } = params;
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(targetId)) {
+ return NextResponse.json({ error: "Invalid target ID format" }, { status: 400 });
+ } 📝 Committable suggestion
Suggested change
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. I think this seems like a good check, not sure if it is worth pulling out to be used everywhere. 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. @zacharyblasczyk, understood. If the input validation for (_/)? ✏️ Learnings added
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. use zod 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.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
const target = await db.query.target.findFirst({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
where: eq(schema.target.id, targetId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
with: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
metadata: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
variables: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
provider: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+31
to
+39
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 error handling for database operations. The database query should be wrapped in a try-catch block to handle potential errors gracefully. - const target = await db.query.target.findFirst({
+ let target;
+ try {
+ target = await db.query.target.findFirst({
where: eq(schema.target.id, targetId),
with: {
metadata: true,
variables: true,
provider: true,
},
});
+ } catch (error) {
+ console.error('Database query failed:', error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ } 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!target) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json({ error: "Target not found" }, { status: 404 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
const metadata = transformMetadata(target.metadata); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
...target, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
metadata, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
}); |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,48 @@ | ||||||||||
import type { z } from "zod"; | ||||||||||
import { NextResponse } from "next/server"; | ||||||||||
|
||||||||||
import { eq } from "@ctrlplane/db"; | ||||||||||
import { db } from "@ctrlplane/db/client"; | ||||||||||
import * as schema from "@ctrlplane/db/schema"; | ||||||||||
import { upsertTargets } from "@ctrlplane/job-dispatch"; | ||||||||||
import { Permission } from "@ctrlplane/validators/auth"; | ||||||||||
|
||||||||||
import { authn, authz } from "~/app/api/v1/auth"; | ||||||||||
import { parseBody } from "~/app/api/v1/body-parser"; | ||||||||||
import { request } from "~/app/api/v1/middleware"; | ||||||||||
import { bodySchema } from "~/app/api/v1/targets/workspaces/[workspaceId]/route"; | ||||||||||
|
||||||||||
export const PATCH = request() | ||||||||||
.use(authn) | ||||||||||
.use(parseBody(bodySchema)) | ||||||||||
.use( | ||||||||||
authz(({ can, extra }) => { | ||||||||||
return can | ||||||||||
.perform(Permission.TargetUpdate) | ||||||||||
.on({ type: "target", id: extra.params.targetId }); | ||||||||||
}), | ||||||||||
) | ||||||||||
.handle< | ||||||||||
{ body: z.infer<typeof bodySchema> }, | ||||||||||
{ params: { targetId: string } } | ||||||||||
>(async (ctx, { params }) => { | ||||||||||
const { body } = ctx; | ||||||||||
|
||||||||||
console.log(body); | ||||||||||
console.log(params.targetId); | ||||||||||
|
||||||||||
const existingTarget = await db.query.target.findFirst({ | ||||||||||
where: eq(schema.target.id, params.targetId), | ||||||||||
}); | ||||||||||
|
||||||||||
if (existingTarget == null) | ||||||||||
return NextResponse.json({ error: "Target not found" }, { status: 404 }); | ||||||||||
|
||||||||||
const targetData = { | ||||||||||
...existingTarget, | ||||||||||
...body.target, | ||||||||||
}; | ||||||||||
|
||||||||||
const target = await upsertTargets(db, [targetData]); | ||||||||||
return NextResponse.json(target); | ||||||||||
Comment on lines
+46
to
+47
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. Return the updated target object instead of an array The Apply this diff to return the updated target object: const target = await upsertTargets(db, [targetData]);
-return NextResponse.json(target);
+return NextResponse.json(target[0]); 📝 Committable suggestion
Suggested change
|
||||||||||
}); |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,61 @@ | ||||||||||||||
import { NextResponse } from "next/server"; | ||||||||||||||
import { z } from "zod"; | ||||||||||||||
|
||||||||||||||
import { db } from "@ctrlplane/db/client"; | ||||||||||||||
import * as schema from "@ctrlplane/db/schema"; | ||||||||||||||
import { upsertTargets } from "@ctrlplane/job-dispatch"; | ||||||||||||||
import { Permission } from "@ctrlplane/validators/auth"; | ||||||||||||||
|
||||||||||||||
import { authn, authz } from "~/app/api/v1/auth"; | ||||||||||||||
import { parseBody } from "~/app/api/v1/body-parser"; | ||||||||||||||
import { request } from "~/app/api/v1/middleware"; | ||||||||||||||
|
||||||||||||||
export const bodySchema = z.object({ | ||||||||||||||
target: schema.createTarget.extend({ | ||||||||||||||
metadata: z.record(z.string()).optional(), | ||||||||||||||
variables: z | ||||||||||||||
.array( | ||||||||||||||
z.object({ | ||||||||||||||
key: z.string(), | ||||||||||||||
value: z.union([z.string(), z.number(), z.boolean(), z.null()]), | ||||||||||||||
sensitive: z.boolean(), | ||||||||||||||
}), | ||||||||||||||
) | ||||||||||||||
.optional() | ||||||||||||||
.refine( | ||||||||||||||
(vars) => | ||||||||||||||
vars == null || new Set(vars.map((v) => v.key)).size === vars.length, | ||||||||||||||
"Duplicate variable keys are not allowed", | ||||||||||||||
), | ||||||||||||||
}), | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
export const POST = request() | ||||||||||||||
.use(authn) | ||||||||||||||
.use(parseBody(bodySchema)) | ||||||||||||||
.use( | ||||||||||||||
authz(({ can, extra }) => { | ||||||||||||||
return can | ||||||||||||||
.perform(Permission.TargetCreate) | ||||||||||||||
.on({ type: "workspace", id: extra.params.workspaceId }); | ||||||||||||||
}), | ||||||||||||||
) | ||||||||||||||
.handle< | ||||||||||||||
{ body: z.infer<typeof bodySchema> }, | ||||||||||||||
{ params: { workspaceId: string } } | ||||||||||||||
>(async (ctx, { params }) => { | ||||||||||||||
const { body } = ctx; | ||||||||||||||
|
||||||||||||||
const targetData = { | ||||||||||||||
...body.target, | ||||||||||||||
workspaceId: params.workspaceId, | ||||||||||||||
variables: body.target.variables?.map((v) => ({ | ||||||||||||||
...v, | ||||||||||||||
value: v.value ?? null, | ||||||||||||||
})), | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
const target = await upsertTargets(db, [targetData]); | ||||||||||||||
|
||||||||||||||
return NextResponse.json(target); | ||||||||||||||
Comment on lines
+58
to
+60
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 the correct response object is returned The Apply this diff to return the first target in the array: const target = await upsertTargets(db, [targetData]);
-return NextResponse.json(target);
+return NextResponse.json(target[0]); 📝 Committable suggestion
Suggested change
|
||||||||||||||
}); |
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.
Object.fromEntries(metadata.map(v => [v.key, v.value])