Skip to content
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
2 changes: 1 addition & 1 deletion apps/webservice/src/app/api/v1/jobs/[jobId]/get-job.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Tx } from "@ctrlplane/db";
import { getResourceParents } from "node_modules/@ctrlplane/db/src/queries/get-resource-parents";

import { eq } from "@ctrlplane/db";
import { getResourceParents } from "@ctrlplane/db/queries";
import * as schema from "@ctrlplane/db/schema";
import { logger } from "@ctrlplane/logger";
import { variablesAES256 } from "@ctrlplane/secrets";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Swagger } from "atlassian-openapi";

export const openapi: Swagger.SwaggerV3 = {
openapi: "3.0.0",
info: {
title: "Ctrlplane API",
version: "1.0.0",
},
components: {
schemas: {
UpdateResourceRelationshipRule: {
type: "object",
properties: {
name: { type: "string" },
reference: { type: "string" },
dependencyType: {
$ref: "#/components/schemas/ResourceRelationshipRuleDependencyType",
},
dependencyDescription: { type: "string" },
description: { type: "string" },
sourceKind: { type: "string" },
sourceVersion: { type: "string" },
targetKind: { type: "string" },
targetVersion: { type: "string" },
metadataKeysMatch: {
type: "array",
items: { type: "string" },
},
metadataTargetKeysEquals: {
type: "array",
items: {
type: "object",
properties: {
key: { type: "string" },
value: { type: "string" },
},
required: ["key", "value"],
},
},
},
},
},
},
paths: {
"/v1/resource-relationship-rules/{ruleId}": {
patch: {
summary: "Update a resource relationship rule",
operationId: "updateResourceRelationshipRule",
parameters: [
{
name: "ruleId",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
},
],
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/UpdateResourceRelationshipRule",
},
},
},
},
responses: {
200: {
description: "The updated resource relationship rule",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/ResourceRelationshipRule",
},
},
},
},
404: {
description: "The resource relationship rule was not found",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
required: ["error"],
},
},
},
},
500: {
description:
"An error occurred while updating the resource relationship rule",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
required: ["error"],
},
},
},
},
},
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { Tx } from "@ctrlplane/db";
import type { z } from "zod";
import { NextResponse } from "next/server";
import { INTERNAL_SERVER_ERROR, NOT_FOUND } from "http-status";
import _ from "lodash";

import { eq, takeFirst } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { logger } from "@ctrlplane/logger";
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";

const log = logger.child({ route: "/v1/resource-relationship-rules/[ruleId]" });

const replaceMetadataMatchRules = async (
tx: Tx,
ruleId: string,
metadataKeysMatch?: string[],
) => {
await tx
.delete(schema.resourceRelationshipRuleMetadataMatch)
.where(
eq(
schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId,
ruleId,
),
);

const metadataKeys = _.uniq(metadataKeysMatch ?? []);
if (metadataKeys.length > 0)
await tx.insert(schema.resourceRelationshipRuleMetadataMatch).values(
metadataKeys.map((key) => ({
resourceRelationshipRuleId: ruleId,
key,
})),
);

return metadataKeys;
Comment on lines +32 to +41
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

Duplicate key handling may silently drop conflicting values

_.uniq(metadataKeysMatch) (and uniqBy below) keeps only the first occurrence.
If the client unintentionally supplies the same key twice with different values in metadataKeysEquals, one of them will be discarded without warning.

Consider:

  • validating in Zod that keys are unique and consistent, returning 400 when conflicts are detected, or
  • merging duplicates deterministically and logging a warning so operators understand why data disappeared.

};

const replaceMetadataEqualsRules = async (
tx: Tx,
ruleId: string,
metadataKeysEquals?: { key: string; value: string }[],
) => {
await tx
.delete(schema.resourceRelationshipTargetRuleMetadataEquals)
.where(
eq(
schema.resourceRelationshipTargetRuleMetadataEquals
.resourceRelationshipRuleId,
ruleId,
),
);

const metadataKeys = _.uniqBy(metadataKeysEquals ?? [], (m) => m.key);
if (metadataKeys.length > 0)
await tx.insert(schema.resourceRelationshipTargetRuleMetadataEquals).values(
metadataKeys.map(({ key, value }) => ({
resourceRelationshipRuleId: ruleId,
key,
value,
})),
);

return metadataKeys;
};

export const PATCH = request()
.use(authn)
.use(parseBody(schema.updateResourceRelationshipRule))
.use(
authz(({ can, params }) =>
can.perform(Permission.ResourceRelationshipRuleUpdate).on({
type: "resourceRelationshipRule",
id: params.ruleId ?? "",
}),
),
)
.handle<
{ body: z.infer<typeof schema.updateResourceRelationshipRule> },
{ params: Promise<{ ruleId: string }> }
>(async ({ db, body }, { params }) => {
try {
const { ruleId } = await params;

const existingRule = await db.query.resourceRelationshipRule.findFirst({
where: eq(schema.resourceRelationshipRule.id, ruleId),
});

if (!existingRule)
return NextResponse.json(
{ error: "Resource relationship rule not found" },
{ status: NOT_FOUND },
);

const rule = await db.transaction(async (tx) => {
const rule = await tx
.update(schema.resourceRelationshipRule)
.set(body)
.where(eq(schema.resourceRelationshipRule.id, ruleId))
.returning()
.then(takeFirst);

Comment on lines +100 to +107
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

Avoid passing non-column & undefined fields into .set()

update().set(body) receives the entire parsed payload, which includes:

  • properties that do not exist on resourceRelationshipRule (metadataKeysMatch, metadataKeysEquals)
  • properties that may be undefined (because the schema is partial())

Drizzle will happily accept the object at runtime but will try to write all keys.
This can surface as:

  • TS compilation errors (extra keys) that get silenced with any
  • SQL errors such as “column metadata_keys_match does not exist”
  • Columns overwritten with NULL / DEFAULT when undefined sneaks in

Sanitise the payload before issuing the update:

-const rule = await tx
-  .update(schema.resourceRelationshipRule)
-  .set(body)
+const updateData = _.omitBy(
+  _.omit(body, ["metadataKeysMatch", "metadataKeysEquals"]),
+  _.isUndefined,
+) as Partial<typeof schema.resourceRelationshipRule.$inferInsert>;
+
+const rule = await tx
+  .update(schema.resourceRelationshipRule)
+  .set(updateData)
📝 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 rule = await db.transaction(async (tx) => {
const rule = await tx
.update(schema.resourceRelationshipRule)
.set(body)
.where(eq(schema.resourceRelationshipRule.id, ruleId))
.returning()
.then(takeFirst);
const rule = await db.transaction(async (tx) => {
// sanitize: drop non-columns and undefined fields
const updateData = _.omitBy(
_.omit(body, ["metadataKeysMatch", "metadataKeysEquals"]),
_.isUndefined,
) as Partial<typeof schema.resourceRelationshipRule.$inferInsert>;
const rule = await tx
.update(schema.resourceRelationshipRule)
.set(updateData)
.where(eq(schema.resourceRelationshipRule.id, ruleId))
.returning()
.then(takeFirst);
return rule;
});

const metadataKeysMatch = await replaceMetadataMatchRules(
tx,
ruleId,
body.metadataKeysMatch,
);

const metadataKeysEquals = await replaceMetadataEqualsRules(
tx,
ruleId,
body.metadataKeysEquals,
);

return { ...rule, metadataKeysMatch, metadataKeysEquals };
});

return NextResponse.json(rule);
} catch (error) {
log.error(error);
return NextResponse.json(
{ error: "Failed to update resource relationship rule" },
{ status: INTERNAL_SERVER_ERROR },
);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const openapi: Swagger.SwaggerV3 = {
"/v1/resource-relationship-rules": {
post: {
summary: "Create a resource relationship rule",
operationId: "upsertResourceRelationshipRule",
operationId: "createResourceRelationshipRule",
requestBody: {
required: true,
content: {
Expand All @@ -32,7 +32,20 @@ export const openapi: Swagger.SwaggerV3 = {
},
},
},
"400": {
"409": {
description: "Resource relationship rule already exists",
content: {
"application/json": {
schema: {
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
"500": {
description: "Failed to create resource relationship rule",
content: {
"application/json": {
Expand Down Expand Up @@ -65,8 +78,8 @@ export const openapi: Swagger.SwaggerV3 = {
ResourceRelationshipRule: {
type: "object",
properties: {
id: { type: "string" },
workspaceId: { type: "string" },
id: { type: "string", format: "uuid" },
workspaceId: { type: "string", format: "uuid" },
name: { type: "string" },
reference: { type: "string" },
dependencyType: {
Expand All @@ -78,6 +91,21 @@ export const openapi: Swagger.SwaggerV3 = {
sourceVersion: { type: "string" },
targetKind: { type: "string" },
targetVersion: { type: "string" },
metadataKeysMatch: {
type: "array",
items: { type: "string" },
},
metadataTargetKeysEquals: {
type: "array",
items: {
type: "object",
properties: {
key: { type: "string" },
value: { type: "string" },
},
required: ["key", "value"],
},
},
},
required: [
"id",
Expand Down Expand Up @@ -108,6 +136,17 @@ export const openapi: Swagger.SwaggerV3 = {
type: "array",
items: { type: "string" },
},
metadataTargetKeysEquals: {
type: "array",
items: {
type: "object",
properties: {
key: { type: "string" },
value: { type: "string" },
},
required: ["key", "value"],
},
},
},
required: [
"workspaceId",
Expand All @@ -118,7 +157,6 @@ export const openapi: Swagger.SwaggerV3 = {
"sourceVersion",
"targetKind",
"targetVersion",
"metadataKeysMatch",
],
},
},
Expand Down
Loading
Loading