-
Notifications
You must be signed in to change notification settings - Fork 11
add netadata key matching #550
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
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,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; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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
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. Avoid passing non-column &
Drizzle will happily accept the object at runtime but will try to write all keys.
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
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
Duplicate key handling may silently drop conflicting values
_.uniq(metadataKeysMatch)(anduniqBybelow) 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:
400when conflicts are detected, or