diff --git a/apps/webservice/src/app/api/v1/jobs/[jobId]/get-job.ts b/apps/webservice/src/app/api/v1/jobs/[jobId]/get-job.ts index a66bd81b6..1b8eff6aa 100644 --- a/apps/webservice/src/app/api/v1/jobs/[jobId]/get-job.ts +++ b/apps/webservice/src/app/api/v1/jobs/[jobId]/get-job.ts @@ -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"; diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts new file mode 100644 index 000000000..a59868526 --- /dev/null +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts @@ -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" }, + }, + metadataKeysEquals: { + 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"], + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts new file mode 100644 index 000000000..34e4c532b --- /dev/null +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts @@ -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.resourceRelationshipRuleMetadataEquals) + .where( + eq( + schema.resourceRelationshipRuleMetadataEquals + .resourceRelationshipRuleId, + ruleId, + ), + ); + + const metadataKeys = _.uniqBy(metadataKeysEquals ?? [], (m) => m.key); + if (metadataKeys.length > 0) + await tx.insert(schema.resourceRelationshipRuleMetadataEquals).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 }, + { 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); + + 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 }, + ); + } + }); diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts index 7cae1efb1..68f97e92d 100644 --- a/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts @@ -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: { @@ -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": { @@ -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: { @@ -78,6 +91,20 @@ export const openapi: Swagger.SwaggerV3 = { sourceVersion: { type: "string" }, targetKind: { type: "string" }, targetVersion: { type: "string" }, + metadataKeysMatch: { + type: "array", + items: { type: "string" }, + }, + metadataKeysEquals: { + type: "array", + items: { + type: "object", + properties: { + key: { type: "string" }, + value: { type: "string" }, + }, + }, + }, }, required: [ "id", @@ -108,6 +135,17 @@ export const openapi: Swagger.SwaggerV3 = { type: "array", items: { type: "string" }, }, + metadataKeysEquals: { + type: "array", + items: { + type: "object", + properties: { + key: { type: "string" }, + value: { type: "string" }, + }, + required: ["key", "value"], + }, + }, }, required: [ "workspaceId", @@ -118,7 +156,6 @@ export const openapi: Swagger.SwaggerV3 = { "sourceVersion", "targetKind", "targetVersion", - "metadataKeysMatch", ], }, }, diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/route.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/route.ts index a61689833..6ff8543c8 100644 --- a/apps/webservice/src/app/api/v1/resource-relationship-rules/route.ts +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/route.ts @@ -1,135 +1,95 @@ +import type { z } from "zod"; import { NextResponse } from "next/server"; import { and, eq } from "drizzle-orm"; +import { CONFLICT, INTERNAL_SERVER_ERROR } from "http-status"; import _ from "lodash"; -import { z } from "zod"; -import { takeFirstOrNull } from "@ctrlplane/db"; +import { 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 "../auth"; import { parseBody } from "../body-parser"; import { request } from "../middleware"; -const body = z.object({ - workspaceId: z.string(), - name: z.string(), - reference: z.string(), - dependencyType: z.string(), - dependencyDescription: z.string().optional(), - description: z.string().optional(), - sourceKind: z.string(), - sourceVersion: z.string(), - targetKind: z.string().nullable().optional(), - targetVersion: z.string().nullable().optional(), - - metadataKeysMatch: z.array(z.string()).optional(), -}); +const log = logger.child({ route: "/v1/resource-relationship-rules" }); export const POST = request() .use(authn) - .use(parseBody(body)) + .use(parseBody(schema.createResourceRelationshipRule)) .use( authz(({ ctx, can }) => can - .perform(Permission.SystemUpdate) + .perform(Permission.ResourceRelationshipRuleCreate) .on({ type: "workspace", id: ctx.body.workspaceId }), ), ) - .handle<{ body: z.infer }>(async ({ db, body }) => { - const upsertedResourceRelationshipRule = await db.transaction( - async (tx) => { - // Check if rule already exists based on workspace, reference, and dependency type - const existingRule = await tx - .select() - .from(schema.resourceRelationshipRule) - .where( - and( - eq(schema.resourceRelationshipRule.workspaceId, body.workspaceId), - eq(schema.resourceRelationshipRule.reference, body.reference), - eq( - schema.resourceRelationshipRule.dependencyType, - body.dependencyType as any, - ), + .handle<{ body: z.infer }>( + async ({ db, body }) => { + try { + const existingRule = await db.query.resourceRelationshipRule.findFirst({ + where: and( + eq(schema.resourceRelationshipRule.workspaceId, body.workspaceId), + eq(schema.resourceRelationshipRule.reference, body.reference), + eq( + schema.resourceRelationshipRule.dependencyType, + body.dependencyType, ), - ) - .then(takeFirstOrNull); + ), + }); - let rule; - if (existingRule != null) { - // Update existing rule - rule = await tx - .update(schema.resourceRelationshipRule) - .set({ - name: body.name, - dependencyDescription: body.dependencyDescription, - description: body.description, - sourceKind: body.sourceKind, - sourceVersion: body.sourceVersion, - targetKind: body.targetKind, - targetVersion: body.targetVersion, - }) - .where(eq(schema.resourceRelationshipRule.id, existingRule.id)) - .returning() - .then(takeFirstOrNull); - } else { - // Insert new rule - rule = await tx + if (existingRule != null) + return NextResponse.json( + { + error: `Resource relationship with reference ${body.reference} and dependency type ${body.dependencyType} already exists in workspace ${body.workspaceId}`, + }, + { status: CONFLICT }, + ); + + const rule = await db.transaction(async (tx) => { + const rule = await tx .insert(schema.resourceRelationshipRule) - .values({ - workspaceId: body.workspaceId, - name: body.name, - reference: body.reference, - dependencyType: body.dependencyType as any, - dependencyDescription: body.dependencyDescription, - description: body.description, - sourceKind: body.sourceKind, - sourceVersion: body.sourceVersion, - targetKind: body.targetKind, - targetVersion: body.targetVersion, - }) + .values(body) .returning() - .then(takeFirstOrNull); - } - - if (rule == null) return null; + .then(takeFirst); - // Handle metadata keys - first delete existing ones if updating - if (existingRule != null) { - await tx - .delete(schema.resourceRelationshipRuleMetadataMatch) - .where( - eq( - schema.resourceRelationshipRuleMetadataMatch - .resourceRelationshipRuleId, - rule.id, - ), - ); - } + const metadataKeysMatch = _.uniq(body.metadataKeysMatch ?? []); + if (metadataKeysMatch.length > 0) + await tx + .insert(schema.resourceRelationshipRuleMetadataMatch) + .values( + metadataKeysMatch.map((key) => ({ + resourceRelationshipRuleId: rule.id, + key, + })), + ); - // Insert new metadata keys - const metadataKeys = _.uniq(body.metadataKeysMatch ?? []); - if (metadataKeys.length > 0) { - await tx.insert(schema.resourceRelationshipRuleMetadataMatch).values( - metadataKeys.map((key) => ({ - resourceRelationshipRuleId: rule.id, - key, - })), + const metadataKeysEquals = _.uniqBy( + body.metadataKeysEquals ?? [], + (m) => m.key, ); - } - - return rule; - }, - ); + if (metadataKeysEquals.length > 0) + await tx + .insert(schema.resourceRelationshipRuleMetadataEquals) + .values( + metadataKeysEquals.map((m) => ({ + resourceRelationshipRuleId: rule.id, + key: m.key, + value: m.value, + })), + ); - if (upsertedResourceRelationshipRule == null) { - return NextResponse.json( - { - error: "Failed to upsert resource relationship rule.", - }, - { status: 400 }, - ); - } + return { ...rule, metadataKeysMatch, metadataKeysEquals }; + }); - return NextResponse.json(upsertedResourceRelationshipRule); - }); + return NextResponse.json(rule); + } catch (error) { + log.error(error); + return NextResponse.json( + { error: "Failed to create resource relationship rule." }, + { status: INTERNAL_SERVER_ERROR }, + ); + } + }, + ); diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 9d46c5047..f9c8d60f8 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -509,6 +509,23 @@ export interface paths { patch: operations["setResourceProvidersResources"]; trace?: never; }; + "/v1/resource-relationship-rules/{ruleId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update a resource relationship rule */ + patch: operations["updateResourceRelationshipRule"]; + trace?: never; + }; "/v1/resource-relationship-rules": { parameters: { query?: never; @@ -519,7 +536,7 @@ export interface paths { get?: never; put?: never; /** Create a resource relationship rule */ - post: operations["upsertResourceRelationshipRule"]; + post: operations["createResourceRelationshipRule"]; delete?: never; options?: never; head?: never; @@ -918,7 +935,11 @@ export interface components { version?: components["schemas"]["DeploymentVersion"]; deployment?: components["schemas"]["Deployment"]; runbook?: components["schemas"]["Runbook"]; - resource?: components["schemas"]["Resource"]; + resource?: components["schemas"]["ResourceWithVariablesAndMetadata"] & { + relationships?: { + [key: string]: components["schemas"]["Resource"]; + }; + }; environment?: components["schemas"]["Environment"]; variables: components["schemas"]["VariableMap"]; approval?: { @@ -1252,6 +1273,22 @@ export interface components { versionUserApprovals: components["schemas"]["VersionUserApproval"][]; versionRoleApprovals: components["schemas"]["VersionRoleApproval"][]; }; + UpdateResourceRelationshipRule: { + name?: string; + reference?: string; + dependencyType?: components["schemas"]["ResourceRelationshipRuleDependencyType"]; + dependencyDescription?: string; + description?: string; + sourceKind?: string; + sourceVersion?: string; + targetKind?: string; + targetVersion?: string; + metadataKeysMatch?: string[]; + metadataKeysEquals?: { + key: string; + value: string; + }[]; + }; /** @enum {string} */ ResourceRelationshipRuleDependencyType: | "depends_on" @@ -1261,7 +1298,9 @@ export interface components { | "provisioned_in" | "inherits_from"; ResourceRelationshipRule: { + /** Format: uuid */ id: string; + /** Format: uuid */ workspaceId: string; name: string; reference: string; @@ -1270,8 +1309,13 @@ export interface components { description?: string; sourceKind: string; sourceVersion: string; - targetKind: string; - targetVersion: string; + targetKind?: string; + targetVersion?: string; + metadataKeysMatch?: string[]; + metadataKeysEquals?: { + key?: string; + value?: string; + }[]; }; CreateResourceRelationshipRule: { workspaceId: string; @@ -1284,7 +1328,11 @@ export interface components { sourceVersion: string; targetKind: string; targetVersion: string; - metadataKeysMatch: string[]; + metadataKeysMatch?: string[]; + metadataKeysEquals?: { + key: string; + value: string; + }[]; }; ReleaseTarget: { /** Format: uuid */ @@ -3188,7 +3236,55 @@ export interface operations { }; }; }; - upsertResourceRelationshipRule: { + updateResourceRelationshipRule: { + parameters: { + query?: never; + header?: never; + path: { + ruleId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UpdateResourceRelationshipRule"]; + }; + }; + responses: { + /** @description The updated resource relationship rule */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceRelationshipRule"]; + }; + }; + /** @description The resource relationship rule was not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description An error occurred while updating the resource relationship rule */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + createResourceRelationshipRule: { parameters: { query?: never; header?: never; @@ -3210,8 +3306,19 @@ export interface operations { "application/json": components["schemas"]["ResourceRelationshipRule"]; }; }; + /** @description Resource relationship rule already exists */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; /** @description Failed to create resource relationship rule */ - 400: { + 500: { headers: { [name: string]: unknown; }; diff --git a/e2e/tests/api/resource-relationships.spec.ts b/e2e/tests/api/resource-relationships.spec.ts index b3c68e2d5..7c85ddb58 100644 --- a/e2e/tests/api/resource-relationships.spec.ts +++ b/e2e/tests/api/resource-relationships.spec.ts @@ -1,7 +1,12 @@ import path from "path"; +import { faker } from "@faker-js/faker"; import { expect } from "@playwright/test"; -import { ImportedEntities, importEntitiesFromYaml } from "../../api"; +import { + cleanupImportedEntities, + ImportedEntities, + importEntitiesFromYaml, +} from "../../api"; import { test } from "../fixtures"; const yamlPath = path.join(__dirname, "resource-relationships.spec.yaml"); @@ -17,14 +22,22 @@ test.describe("Resource Relationships API", () => { ); }); - test("create a relationship", async ({ api, workspace }) => { + test.afterAll(async ({ api, workspace }) => { + await cleanupImportedEntities(api, importedEntities, workspace.id); + }); + + test("create a relationship with metadata match", async ({ + api, + workspace, + }) => { + const reference = `${importedEntities.prefix}-${faker.string.alphanumeric(10)}`; const resourceRelationship = await api.POST( "/v1/resource-relationship-rules", { body: { workspaceId: workspace.id, - name: importedEntities.prefix + "-resource-relationship-rule", - reference: importedEntities.prefix, + name: reference + "-resource-relationship-rule", + reference, dependencyType: "depends_on", sourceKind: "Source", sourceVersion: "test-version/v1", @@ -35,7 +48,6 @@ test.describe("Resource Relationships API", () => { }, ); - console.log(JSON.stringify(resourceRelationship.data, null, 2)); expect(resourceRelationship.response.status).toBe(200); const sourceResource = await api.GET( @@ -52,11 +64,60 @@ test.describe("Resource Relationships API", () => { expect(sourceResource.response.status).toBe(200); expect(sourceResource.data?.relationships).toBeDefined(); - const target = - sourceResource.data?.relationships?.[importedEntities.prefix]; + const target = sourceResource.data?.relationships?.[reference]; + expect(target).toBeDefined(); + expect(target?.type).toBe("depends_on"); + expect(target?.reference).toBe(reference); + expect(target?.target?.id).toBeDefined(); + expect(target?.target?.name).toBeDefined(); + expect(target?.target?.version).toBeDefined(); + expect(target?.target?.kind).toBeDefined(); + expect(target?.target?.identifier).toBeDefined(); + expect(target?.target?.config).toBeDefined(); + }); + + test("create a relationship with metadata equals", async ({ + api, + workspace, + }) => { + const reference = `${importedEntities.prefix}-${faker.string.alphanumeric(10)}`; + const resourceRelationship = await api.POST( + "/v1/resource-relationship-rules", + { + body: { + workspaceId: workspace.id, + name: reference + "-resource-relationship-rule", + reference, + dependencyType: "depends_on", + sourceKind: "Source", + sourceVersion: "test-version/v1", + targetKind: "Target", + targetVersion: "test-version/v1", + metadataKeysEquals: [{ key: importedEntities.prefix, value: "true" }], + }, + }, + ); + + expect(resourceRelationship.response.status).toBe(200); + + const sourceResource = await api.GET( + `/v1/workspaces/{workspaceId}/resources/identifier/{identifier}`, + { + params: { + path: { + workspaceId: workspace.id, + identifier: importedEntities.prefix + "-source-resource", + }, + }, + }, + ); + + expect(sourceResource.response.status).toBe(200); + expect(sourceResource.data?.relationships).toBeDefined(); + const target = sourceResource.data?.relationships?.[reference]; expect(target).toBeDefined(); expect(target?.type).toBe("depends_on"); - expect(target?.reference).toBe(importedEntities.prefix); + expect(target?.reference).toBe(reference); expect(target?.target?.id).toBeDefined(); expect(target?.target?.name).toBeDefined(); expect(target?.target?.version).toBeDefined(); @@ -89,21 +150,27 @@ test.describe("Resource Relationships API", () => { expect(initialRule.data?.sourceKind).toBe("SourceA"); expect(initialRule.data?.targetKind).toBe("TargetA"); + const ruleId = initialRule.data?.id ?? ""; + // Update the existing rule with new properties - const updatedRule = await api.POST("/v1/resource-relationship-rules", { - body: { - workspaceId: workspace.id, - name: importedEntities.prefix + "-upsert-rule", // Same name for upsert - reference: importedEntities.prefix + "-upsert", - dependencyType: "depends_on", - sourceKind: "SourceB", - sourceVersion: "test-version/v2", - targetKind: "TargetB", - targetVersion: "test-version/v2", - description: "Updated description", - metadataKeysMatch: ["e2e/test", "additional-key"], + const updatedRule = await api.PATCH( + "/v1/resource-relationship-rules/{ruleId}", + { + params: { + path: { + ruleId: ruleId, + }, + }, + body: { + sourceKind: "SourceB", + sourceVersion: "test-version/v2", + targetKind: "TargetB", + targetVersion: "test-version/v2", + description: "Updated description", + metadataKeysMatch: ["e2e/test", "additional-key"], + }, }, - }); + ); expect(updatedRule.response.status).toBe(200); expect(updatedRule.data?.id).toBe(initialRule.data?.id); // Should maintain same ID diff --git a/openapi.v1.json b/openapi.v1.json index 299c7f625..8a7342a73 100644 --- a/openapi.v1.json +++ b/openapi.v1.json @@ -3070,10 +3070,84 @@ } } }, + "/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" + ] + } + } + } + } + } + } + }, "/v1/resource-relationship-rules": { "post": { "summary": "Create a resource relationship rule", - "operationId": "upsertResourceRelationshipRule", + "operationId": "createResourceRelationshipRule", "requestBody": { "required": true, "content": { @@ -3095,7 +3169,22 @@ } } }, - "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": { @@ -5688,6 +5777,62 @@ "versionRoleApprovals" ] }, + "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" + } + }, + "metadataKeysEquals": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ] + } + } + } + }, "ResourceRelationshipRuleDependencyType": { "type": "string", "enum": [ @@ -5703,10 +5848,12 @@ "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "format": "uuid" }, "workspaceId": { - "type": "string" + "type": "string", + "format": "uuid" }, "name": { "type": "string" @@ -5734,6 +5881,26 @@ }, "targetVersion": { "type": "string" + }, + "metadataKeysMatch": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadataKeysEquals": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } } }, "required": [ @@ -5743,9 +5910,7 @@ "reference", "dependencyType", "sourceKind", - "sourceVersion", - "targetKind", - "targetVersion" + "sourceVersion" ] }, "CreateResourceRelationshipRule": { @@ -5786,6 +5951,24 @@ "items": { "type": "string" } + }, + "metadataKeysEquals": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ] + } } }, "required": [ @@ -5796,8 +5979,7 @@ "sourceKind", "sourceVersion", "targetKind", - "targetVersion", - "metadataKeysMatch" + "targetVersion" ] }, "ReleaseTarget": { diff --git a/packages/api/src/router/resource-relationship-rules.ts b/packages/api/src/router/resource-relationship-rules.ts index 8077b2a51..12f164c43 100644 --- a/packages/api/src/router/resource-relationship-rules.ts +++ b/packages/api/src/router/resource-relationship-rules.ts @@ -18,9 +18,7 @@ export const resourceRelationshipRulesRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { return ctx.db.query.resourceRelationshipRule.findMany({ where: eq(schema.resourceRelationshipRule.workspaceId, input), - with: { - metadataMatches: true, - }, + with: { metadataMatches: true, metadataEquals: true }, }); }), diff --git a/packages/auth/src/utils/rbac.ts b/packages/auth/src/utils/rbac.ts index 171ea63c4..eb0391592 100644 --- a/packages/auth/src/utils/rbac.ts +++ b/packages/auth/src/utils/rbac.ts @@ -27,6 +27,7 @@ import { resource, resourceMetadataGroup, resourceProvider, + resourceRelationshipRule, resourceView, role, rolePermission, @@ -248,6 +249,26 @@ const getResourceViewScopes = async (id: string) => { ]; }; +const getResourceRelationshipRuleScopes = async (id: string) => { + const result = await db + .select() + .from(resourceRelationshipRule) + .innerJoin( + workspace, + eq(resourceRelationshipRule.workspaceId, workspace.id), + ) + .where(eq(resourceRelationshipRule.id, id)) + .then(takeFirst); + + return [ + { + type: "resourceRelationshipRule" as const, + id: result.resource_relationship_rule.id, + }, + { type: "workspace" as const, id: result.workspace.id }, + ]; +}; + const getDeploymentScopes = async (id: string) => { const result = await db .select() @@ -476,6 +497,7 @@ export const scopeHandlers: Record< job: getJobScopes, policy: getPolicyScopes, releaseTarget: getReleaseTargetScopes, + resourceRelationshipRule: getResourceRelationshipRuleScopes, }; const fetchScopeHierarchyForResource = async (resource: { diff --git a/packages/db/drizzle/meta/0099_snapshot.json b/packages/db/drizzle/meta/0099_snapshot.json index ab1381744..be63e3a84 100644 --- a/packages/db/drizzle/meta/0099_snapshot.json +++ b/packages/db/drizzle/meta/0099_snapshot.json @@ -81,12 +81,8 @@ "name": "account_userId_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -94,10 +90,7 @@ "compositePrimaryKeys": { "account_provider_providerAccountId_pk": { "name": "account_provider_providerAccountId_pk", - "columns": [ - "provider", - "providerAccountId" - ] + "columns": ["provider", "providerAccountId"] } }, "uniqueConstraints": {}, @@ -134,12 +127,8 @@ "name": "session_userId_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -221,12 +210,8 @@ "name": "user_active_workspace_id_workspace_id_fk", "tableFrom": "user", "tableTo": "workspace", - "columnsFrom": [ - "active_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -313,12 +298,8 @@ "name": "user_api_key_user_id_user_id_fk", "tableFrom": "user_api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -378,12 +359,8 @@ "name": "dashboard_workspace_id_workspace_id_fk", "tableFrom": "dashboard", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -455,12 +432,8 @@ "name": "dashboard_widget_dashboard_id_dashboard_id_fk", "tableFrom": "dashboard_widget", "tableTo": "dashboard", - "columnsFrom": [ - "dashboard_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -543,12 +516,8 @@ "name": "deployment_variable_deployment_id_deployment_id_fk", "tableFrom": "deployment_variable", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -556,12 +525,8 @@ "name": "deployment_variable_default_value_id_deployment_variable_value_id_fk", "tableFrom": "deployment_variable", "tableTo": "deployment_variable_value", - "columnsFrom": [ - "default_value_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["default_value_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -624,12 +589,8 @@ "name": "deployment_variable_set_deployment_id_deployment_id_fk", "tableFrom": "deployment_variable_set", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -637,12 +598,8 @@ "name": "deployment_variable_set_variable_set_id_variable_set_id_fk", "tableFrom": "deployment_variable_set", "tableTo": "variable_set", - "columnsFrom": [ - "variable_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -719,12 +676,8 @@ "name": "deployment_variable_value_variable_id_deployment_variable_id_fk", "tableFrom": "deployment_variable_value", "tableTo": "deployment_variable", - "columnsFrom": [ - "variable_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "restrict" } @@ -843,12 +796,8 @@ "name": "deployment_version_deployment_id_deployment_id_fk", "tableFrom": "deployment_version", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -925,12 +874,8 @@ "name": "deployment_version_channel_deployment_id_deployment_id_fk", "tableFrom": "deployment_version_channel", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1014,12 +959,8 @@ "name": "deployment_version_metadata_deployment_version_id_deployment_version_id_fk", "tableFrom": "deployment_version_metadata", "tableTo": "deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1089,12 +1030,8 @@ "name": "deployment_version_dependency_deployment_version_id_deployment_version_id_fk", "tableFrom": "deployment_version_dependency", "tableTo": "deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1102,12 +1039,8 @@ "name": "deployment_version_dependency_deployment_id_deployment_id_fk", "tableFrom": "deployment_version_dependency", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1141,12 +1074,8 @@ "name": "computed_deployment_resource_deployment_id_deployment_id_fk", "tableFrom": "computed_deployment_resource", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1154,12 +1083,8 @@ "name": "computed_deployment_resource_resource_id_resource_id_fk", "tableFrom": "computed_deployment_resource", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1167,10 +1092,7 @@ "compositePrimaryKeys": { "computed_deployment_resource_deployment_id_resource_id_pk": { "name": "computed_deployment_resource_deployment_id_resource_id_pk", - "columns": [ - "deployment_id", - "resource_id" - ] + "columns": ["deployment_id", "resource_id"] } }, "uniqueConstraints": {}, @@ -1276,12 +1198,8 @@ "name": "deployment_system_id_system_id_fk", "tableFrom": "deployment", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1289,12 +1207,8 @@ "name": "deployment_job_agent_id_job_agent_id_fk", "tableFrom": "deployment", "tableTo": "job_agent", - "columnsFrom": [ - "job_agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1357,12 +1271,8 @@ "name": "environment_policy_deployment_policy_id_environment_policy_id_fk", "tableFrom": "environment_policy_deployment", "tableTo": "environment_policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1370,12 +1280,8 @@ "name": "environment_policy_deployment_environment_id_environment_id_fk", "tableFrom": "environment_policy_deployment", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1409,12 +1315,8 @@ "name": "computed_environment_resource_environment_id_environment_id_fk", "tableFrom": "computed_environment_resource", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1422,12 +1324,8 @@ "name": "computed_environment_resource_resource_id_resource_id_fk", "tableFrom": "computed_environment_resource", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1435,10 +1333,7 @@ "compositePrimaryKeys": { "computed_environment_resource_environment_id_resource_id_pk": { "name": "computed_environment_resource_environment_id_resource_id_pk", - "columns": [ - "environment_id", - "resource_id" - ] + "columns": ["environment_id", "resource_id"] } }, "uniqueConstraints": {}, @@ -1532,12 +1427,8 @@ "name": "environment_system_id_system_id_fk", "tableFrom": "environment", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1545,12 +1436,8 @@ "name": "environment_policy_id_environment_policy_id_fk", "tableFrom": "environment", "tableTo": "environment_policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1619,12 +1506,8 @@ "name": "environment_metadata_environment_id_environment_id_fk", "tableFrom": "environment_metadata", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1729,12 +1612,8 @@ "name": "environment_policy_system_id_system_id_fk", "tableFrom": "environment_policy", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1742,12 +1621,8 @@ "name": "environment_policy_environment_id_environment_id_fk", "tableFrom": "environment_policy", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1831,12 +1706,8 @@ "name": "environment_policy_approval_policy_id_environment_policy_id_fk", "tableFrom": "environment_policy_approval", "tableTo": "environment_policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1844,12 +1715,8 @@ "name": "environment_policy_approval_release_id_deployment_version_id_fk", "tableFrom": "environment_policy_approval", "tableTo": "deployment_version", - "columnsFrom": [ - "release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1857,12 +1724,8 @@ "name": "environment_policy_approval_user_id_user_id_fk", "tableFrom": "environment_policy_approval", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1916,12 +1779,8 @@ "name": "environment_policy_release_window_policy_id_environment_policy_id_fk", "tableFrom": "environment_policy_release_window", "tableTo": "environment_policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1975,12 +1834,8 @@ "name": "event_workspace_id_workspace_id_fk", "tableFrom": "event", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2087,12 +1942,8 @@ "name": "runhook_hook_id_hook_id_fk", "tableFrom": "runhook", "tableTo": "hook", - "columnsFrom": [ - "hook_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["hook_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2100,12 +1951,8 @@ "name": "runhook_runbook_id_runbook_id_fk", "tableFrom": "runhook", "tableTo": "runbook", - "columnsFrom": [ - "runbook_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2201,12 +2048,8 @@ "name": "github_entity_added_by_user_id_user_id_fk", "tableFrom": "github_entity", "tableTo": "user", - "columnsFrom": [ - "added_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["added_by_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2214,12 +2057,8 @@ "name": "github_entity_workspace_id_workspace_id_fk", "tableFrom": "github_entity", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2266,12 +2105,8 @@ "name": "github_user_user_id_user_id_fk", "tableFrom": "github_user", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2334,12 +2169,8 @@ "name": "job_resource_relationship_job_id_job_id_fk", "tableFrom": "job_resource_relationship", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2458,12 +2289,8 @@ "name": "resource_provider_id_resource_provider_id_fk", "tableFrom": "resource", "tableTo": "resource_provider", - "columnsFrom": [ - "provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2471,12 +2298,8 @@ "name": "resource_workspace_id_workspace_id_fk", "tableFrom": "resource", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2545,12 +2368,8 @@ "name": "resource_metadata_resource_id_resource_id_fk", "tableFrom": "resource_metadata", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2626,12 +2445,8 @@ "name": "resource_relationship_workspace_id_workspace_id_fk", "tableFrom": "resource_relationship", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2712,12 +2527,8 @@ "name": "resource_schema_workspace_id_workspace_id_fk", "tableFrom": "resource_schema", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2818,12 +2629,8 @@ "name": "resource_variable_resource_id_resource_id_fk", "tableFrom": "resource_variable", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2877,12 +2684,8 @@ "name": "resource_view_workspace_id_workspace_id_fk", "tableFrom": "resource_view", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2939,12 +2742,8 @@ "name": "azure_tenant_workspace_id_workspace_id_fk", "tableFrom": "azure_tenant", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3014,12 +2813,8 @@ "name": "resource_provider_workspace_id_workspace_id_fk", "tableFrom": "resource_provider", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3074,12 +2869,8 @@ "name": "resource_provider_aws_resource_provider_id_resource_provider_id_fk", "tableFrom": "resource_provider_aws", "tableTo": "resource_provider", - "columnsFrom": [ - "resource_provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3126,12 +2917,8 @@ "name": "resource_provider_azure_resource_provider_id_resource_provider_id_fk", "tableFrom": "resource_provider_azure", "tableTo": "resource_provider", - "columnsFrom": [ - "resource_provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3139,12 +2926,8 @@ "name": "resource_provider_azure_tenant_id_azure_tenant_id_fk", "tableFrom": "resource_provider_azure", "tableTo": "azure_tenant", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -3220,12 +3003,8 @@ "name": "resource_provider_google_resource_provider_id_resource_provider_id_fk", "tableFrom": "resource_provider_google", "tableTo": "resource_provider", - "columnsFrom": [ - "resource_provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3301,12 +3080,8 @@ "name": "system_workspace_id_workspace_id_fk", "tableFrom": "system", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3366,12 +3141,8 @@ "name": "runbook_system_id_system_id_fk", "tableFrom": "runbook", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3379,12 +3150,8 @@ "name": "runbook_job_agent_id_job_agent_id_fk", "tableFrom": "runbook", "tableTo": "job_agent", - "columnsFrom": [ - "job_agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3432,12 +3199,8 @@ "name": "runbook_job_trigger_job_id_job_id_fk", "tableFrom": "runbook_job_trigger", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3445,12 +3208,8 @@ "name": "runbook_job_trigger_runbook_id_runbook_id_fk", "tableFrom": "runbook_job_trigger", "tableTo": "runbook", - "columnsFrom": [ - "runbook_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3460,9 +3219,7 @@ "runbook_job_trigger_job_id_unique": { "name": "runbook_job_trigger_job_id_unique", "nullsNotDistinct": false, - "columns": [ - "job_id" - ] + "columns": ["job_id"] } }, "policies": {}, @@ -3499,12 +3256,8 @@ "name": "team_workspace_id_workspace_id_fk", "tableFrom": "team", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3567,12 +3320,8 @@ "name": "team_member_team_id_team_id_fk", "tableFrom": "team_member", "tableTo": "team", - "columnsFrom": [ - "team_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["team_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3580,12 +3329,8 @@ "name": "team_member_user_id_user_id_fk", "tableFrom": "team_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3712,12 +3457,8 @@ "name": "job_job_agent_id_job_agent_id_fk", "tableFrom": "job", "tableTo": "job_agent", - "columnsFrom": [ - "job_agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3786,12 +3527,8 @@ "name": "job_metadata_job_id_job_id_fk", "tableFrom": "job_metadata", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3867,12 +3604,8 @@ "name": "job_variable_job_id_job_id_fk", "tableFrom": "job_variable", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3933,9 +3666,7 @@ "workspace_slug_unique": { "name": "workspace_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -4026,12 +3757,8 @@ "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", "tableFrom": "workspace_email_domain_matching", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4039,12 +3766,8 @@ "name": "workspace_email_domain_matching_role_id_role_id_fk", "tableFrom": "workspace_email_domain_matching", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4091,12 +3814,8 @@ "name": "variable_set_system_id_system_id_fk", "tableFrom": "variable_set", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4137,12 +3856,8 @@ "name": "variable_set_environment_variable_set_id_variable_set_id_fk", "tableFrom": "variable_set_environment", "tableTo": "variable_set", - "columnsFrom": [ - "variable_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4150,12 +3865,8 @@ "name": "variable_set_environment_environment_id_environment_id_fk", "tableFrom": "variable_set_environment", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4231,12 +3942,8 @@ "name": "variable_set_value_variable_set_id_variable_set_id_fk", "tableFrom": "variable_set_value", "tableTo": "variable_set", - "columnsFrom": [ - "variable_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4296,12 +4003,8 @@ "name": "workspace_invite_token_role_id_role_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4309,12 +4012,8 @@ "name": "workspace_invite_token_workspace_id_workspace_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4322,12 +4021,8 @@ "name": "workspace_invite_token_created_by_user_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4337,9 +4032,7 @@ "workspace_invite_token_token_unique": { "name": "workspace_invite_token_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -4395,12 +4088,8 @@ "name": "resource_metadata_group_workspace_id_workspace_id_fk", "tableFrom": "resource_metadata_group", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4489,12 +4178,8 @@ "name": "runbook_variable_runbook_id_runbook_id_fk", "tableFrom": "runbook_variable", "tableTo": "runbook", - "columnsFrom": [ - "runbook_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4595,12 +4280,8 @@ "name": "entity_role_role_id_role_id_fk", "tableFrom": "entity_role", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4647,12 +4328,8 @@ "name": "role_workspace_id_workspace_id_fk", "tableFrom": "role", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4715,12 +4392,8 @@ "name": "role_permission_role_id_role_id_fk", "tableFrom": "role_permission", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4796,12 +4469,8 @@ "name": "job_agent_workspace_id_workspace_id_fk", "tableFrom": "job_agent", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4891,12 +4560,8 @@ "name": "environment_policy_deployment_version_channel_policy_id_environment_policy_id_fk", "tableFrom": "environment_policy_deployment_version_channel", "tableTo": "environment_policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4904,12 +4569,8 @@ "name": "environment_policy_deployment_version_channel_channel_id_deployment_version_channel_id_fk", "tableFrom": "environment_policy_deployment_version_channel", "tableTo": "deployment_version_channel", - "columnsFrom": [ - "channel_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4917,12 +4578,8 @@ "name": "environment_policy_deployment_version_channel_deployment_id_deployment_id_fk", "tableFrom": "environment_policy_deployment_version_channel", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4995,12 +4652,8 @@ "name": "release_job_trigger_job_id_job_id_fk", "tableFrom": "release_job_trigger", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -5008,12 +4661,8 @@ "name": "release_job_trigger_caused_by_id_user_id_fk", "tableFrom": "release_job_trigger", "tableTo": "user", - "columnsFrom": [ - "caused_by_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["caused_by_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -5021,12 +4670,8 @@ "name": "release_job_trigger_deployment_version_id_deployment_version_id_fk", "tableFrom": "release_job_trigger", "tableTo": "deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5034,12 +4679,8 @@ "name": "release_job_trigger_resource_id_resource_id_fk", "tableFrom": "release_job_trigger", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5047,12 +4688,8 @@ "name": "release_job_trigger_environment_id_environment_id_fk", "tableFrom": "release_job_trigger", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5062,9 +4699,7 @@ "release_job_trigger_job_id_unique": { "name": "release_job_trigger_job_id_unique", "nullsNotDistinct": false, - "columns": [ - "job_id" - ] + "columns": ["job_id"] } }, "policies": {}, @@ -5094,12 +4729,8 @@ "name": "computed_policy_target_release_target_policy_target_id_policy_target_id_fk", "tableFrom": "computed_policy_target_release_target", "tableTo": "policy_target", - "columnsFrom": [ - "policy_target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5107,12 +4738,8 @@ "name": "computed_policy_target_release_target_release_target_id_release_target_id_fk", "tableFrom": "computed_policy_target_release_target", "tableTo": "release_target", - "columnsFrom": [ - "release_target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5120,10 +4747,7 @@ "compositePrimaryKeys": { "computed_policy_target_release_target_policy_target_id_release_target_id_pk": { "name": "computed_policy_target_release_target_policy_target_id_release_target_id_pk", - "columns": [ - "policy_target_id", - "release_target_id" - ] + "columns": ["policy_target_id", "release_target_id"] } }, "uniqueConstraints": {}, @@ -5188,12 +4812,8 @@ "name": "policy_workspace_id_workspace_id_fk", "tableFrom": "policy", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5203,10 +4823,7 @@ "policy_workspace_id_name_unique": { "name": "policy_workspace_id_name_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id", - "name" - ] + "columns": ["workspace_id", "name"] } }, "policies": {}, @@ -5258,12 +4875,8 @@ "name": "policy_target_policy_id_policy_id_fk", "tableFrom": "policy_target", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5311,12 +4924,8 @@ "name": "release_version_release_id_version_release_id_fk", "tableFrom": "release", "tableTo": "version_release", - "columnsFrom": [ - "version_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["version_release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5324,12 +4933,8 @@ "name": "release_variable_release_id_variable_set_release_id_fk", "tableFrom": "release", "tableTo": "variable_set_release", - "columnsFrom": [ - "variable_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5370,12 +4975,8 @@ "name": "release_job_release_id_release_id_fk", "tableFrom": "release_job", "tableTo": "release", - "columnsFrom": [ - "release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5383,12 +4984,8 @@ "name": "release_job_job_id_job_id_fk", "tableFrom": "release_job", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5470,12 +5067,8 @@ "name": "release_target_resource_id_resource_id_fk", "tableFrom": "release_target", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5483,12 +5076,8 @@ "name": "release_target_environment_id_environment_id_fk", "tableFrom": "release_target", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5496,12 +5085,8 @@ "name": "release_target_deployment_id_deployment_id_fk", "tableFrom": "release_target", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5509,12 +5094,8 @@ "name": "release_target_desired_release_id_release_id_fk", "tableFrom": "release_target", "tableTo": "release", - "columnsFrom": [ - "desired_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["desired_release_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5556,12 +5137,8 @@ "name": "variable_set_release_release_target_id_release_target_id_fk", "tableFrom": "variable_set_release", "tableTo": "release_target", - "columnsFrom": [ - "release_target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5631,12 +5208,8 @@ "name": "variable_set_release_value_variable_set_release_id_variable_set_release_id_fk", "tableFrom": "variable_set_release_value", "tableTo": "variable_set_release", - "columnsFrom": [ - "variable_set_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_set_release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5644,12 +5217,8 @@ "name": "variable_set_release_value_variable_value_snapshot_id_variable_value_snapshot_id_fk", "tableFrom": "variable_set_release_value", "tableTo": "variable_value_snapshot", - "columnsFrom": [ - "variable_value_snapshot_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_value_snapshot_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5738,12 +5307,8 @@ "name": "variable_value_snapshot_workspace_id_workspace_id_fk", "tableFrom": "variable_value_snapshot", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5791,12 +5356,8 @@ "name": "version_release_release_target_id_release_target_id_fk", "tableFrom": "version_release", "tableTo": "release_target", - "columnsFrom": [ - "release_target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5804,12 +5365,8 @@ "name": "version_release_version_id_deployment_version_id_fk", "tableFrom": "version_release", "tableTo": "deployment_version", - "columnsFrom": [ - "version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5871,12 +5428,8 @@ "name": "policy_rule_deny_window_policy_id_policy_id_fk", "tableFrom": "policy_rule_deny_window", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5924,12 +5477,8 @@ "name": "policy_rule_user_approval_policy_id_policy_id_fk", "tableFrom": "policy_rule_user_approval", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5937,12 +5486,8 @@ "name": "policy_rule_user_approval_user_id_user_id_fk", "tableFrom": "policy_rule_user_approval", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -6022,12 +5567,8 @@ "name": "policy_rule_user_approval_record_user_id_user_id_fk", "tableFrom": "policy_rule_user_approval_record", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -6035,12 +5576,8 @@ "name": "policy_rule_user_approval_record_rule_id_policy_rule_user_approval_id_fk", "tableFrom": "policy_rule_user_approval_record", "tableTo": "policy_rule_user_approval", - "columnsFrom": [ - "rule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6095,12 +5632,8 @@ "name": "policy_rule_role_approval_policy_id_policy_id_fk", "tableFrom": "policy_rule_role_approval", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6108,12 +5641,8 @@ "name": "policy_rule_role_approval_role_id_role_id_fk", "tableFrom": "policy_rule_role_approval", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -6193,12 +5722,8 @@ "name": "policy_rule_role_approval_record_user_id_user_id_fk", "tableFrom": "policy_rule_role_approval_record", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -6206,12 +5731,8 @@ "name": "policy_rule_role_approval_record_rule_id_policy_rule_role_approval_id_fk", "tableFrom": "policy_rule_role_approval_record", "tableTo": "policy_rule_role_approval", - "columnsFrom": [ - "rule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6260,12 +5781,8 @@ "name": "policy_rule_any_approval_policy_id_policy_id_fk", "tableFrom": "policy_rule_any_approval", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6361,12 +5878,8 @@ "name": "policy_rule_any_approval_record_user_id_user_id_fk", "tableFrom": "policy_rule_any_approval_record", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -6419,12 +5932,8 @@ "name": "policy_rule_deployment_version_selector_policy_id_policy_id_fk", "tableFrom": "policy_rule_deployment_version_selector", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6434,9 +5943,7 @@ "policy_rule_deployment_version_selector_policy_id_unique": { "name": "policy_rule_deployment_version_selector_policy_id_unique", "nullsNotDistinct": false, - "columns": [ - "policy_id" - ] + "columns": ["policy_id"] } }, "policies": {}, @@ -6556,12 +6063,8 @@ "name": "resource_relationship_rule_workspace_id_workspace_id_fk", "tableFrom": "resource_relationship_rule", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6630,12 +6133,8 @@ "name": "resource_relationship_rule_metadata_equals_resource_relationship_rule_id_resource_relationship_rule_id_fk", "tableFrom": "resource_relationship_rule_metadata_equals", "tableTo": "resource_relationship_rule", - "columnsFrom": [ - "resource_relationship_rule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_relationship_rule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6698,12 +6197,8 @@ "name": "resource_relationship_rule_metadata_match_resource_relationship_rule_id_resource_relationship_rule_id_fk", "tableFrom": "resource_relationship_rule_metadata_match", "tableTo": "resource_relationship_rule", - "columnsFrom": [ - "resource_relationship_rule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_relationship_rule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6719,79 +6214,47 @@ "public.system_role": { "name": "system_role", "schema": "public", - "values": [ - "user", - "admin" - ] + "values": ["user", "admin"] }, "public.deployment_version_status": { "name": "deployment_version_status", "schema": "public", - "values": [ - "building", - "ready", - "failed" - ] + "values": ["building", "ready", "failed"] }, "public.environment_policy_approval_requirement": { "name": "environment_policy_approval_requirement", "schema": "public", - "values": [ - "manual", - "automatic" - ] + "values": ["manual", "automatic"] }, "public.approval_status_type": { "name": "approval_status_type", "schema": "public", - "values": [ - "pending", - "approved", - "rejected" - ] + "values": ["pending", "approved", "rejected"] }, "public.environment_policy_deployment_success_type": { "name": "environment_policy_deployment_success_type", "schema": "public", - "values": [ - "all", - "some", - "optional" - ] + "values": ["all", "some", "optional"] }, "public.recurrence_type": { "name": "recurrence_type", "schema": "public", - "values": [ - "hourly", - "daily", - "weekly", - "monthly" - ] + "values": ["hourly", "daily", "weekly", "monthly"] }, "public.release_sequencing_type": { "name": "release_sequencing_type", "schema": "public", - "values": [ - "wait", - "cancel" - ] + "values": ["wait", "cancel"] }, "public.github_entity_type": { "name": "github_entity_type", "schema": "public", - "values": [ - "organization", - "user" - ] + "values": ["organization", "user"] }, "public.resource_relationship_type": { "name": "resource_relationship_type", "schema": "public", - "values": [ - "associated_with", - "depends_on" - ] + "values": ["associated_with", "depends_on"] }, "public.job_reason": { "name": "job_reason", @@ -6822,10 +6285,7 @@ "public.entity_type": { "name": "entity_type", "schema": "public", - "values": [ - "user", - "team" - ] + "values": ["user", "team"] }, "public.scope_type": { "name": "scope_type", @@ -6870,10 +6330,7 @@ "public.approval_status": { "name": "approval_status", "schema": "public", - "values": [ - "approved", - "rejected" - ] + "values": ["approved", "rejected"] } }, "schemas": {}, @@ -6886,4 +6343,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index e314e3151..512980bbc 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -703,4 +703,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/queries/get-resource-parents.ts b/packages/db/src/queries/get-resource-parents.ts index e0b839808..74b0edf02 100644 --- a/packages/db/src/queries/get-resource-parents.ts +++ b/packages/db/src/queries/get-resource-parents.ts @@ -1,14 +1,9 @@ -import { and, count, eq, inArray, isNull, or } from "drizzle-orm"; +import { and, eq, exists, inArray, isNull, ne, or } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import _ from "lodash"; import type { Tx } from "../common.js"; -import { - resourceRelationshipRule, - resourceRelationshipRuleMetadataEquals, - resourceRelationshipRuleMetadataMatch, -} from "../schema/resource-relationship-rule.js"; -import { resource, resourceMetadata } from "../schema/resource.js"; +import * as schema from "../schema/index.js"; /** * Gets relationships for a resource based on relationship rules @@ -16,140 +11,111 @@ import { resource, resourceMetadata } from "../schema/resource.js"; * @returns Array of relationships with rule info and target resources */ export const getResourceParents = async (tx: Tx, resourceId: string) => { - // First, get all relationship rules and count how many metadata keys each rule requires to match - // This creates a subquery that we'll use later to ensure resources match ALL required metadata keys - const rulesWithCount = tx - .selectDistinctOn([resourceRelationshipRule.id], { - id: resourceRelationshipRule.id, - workspaceId: resourceRelationshipRule.workspaceId, - reference: resourceRelationshipRule.reference, - dependencyType: resourceRelationshipRule.dependencyType, - metadataKeysMatches: count(resourceRelationshipRuleMetadataMatch).as( - "metadataKeysMatches", - ), - metadataKeysEquals: count(resourceRelationshipRuleMetadataEquals).as( - "metadataKeysEquals", - ), - targetKind: resourceRelationshipRule.targetKind, - targetVersion: resourceRelationshipRule.targetVersion, - sourceKind: resourceRelationshipRule.sourceKind, - sourceVersion: resourceRelationshipRule.sourceVersion, - }) - .from(resourceRelationshipRule) - .leftJoin( - resourceRelationshipRuleMetadataMatch, - eq( - resourceRelationshipRule.id, - resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId, - ), - ) - .leftJoin( - resourceRelationshipRuleMetadataEquals, - eq( - resourceRelationshipRule.id, - resourceRelationshipRuleMetadataEquals.resourceRelationshipRuleId, - ), - ) - .groupBy(resourceRelationshipRule.id) - .as("rulesWithCount"); - // Create aliases for tables we'll join multiple times to avoid naming conflicts - const sourceResource = alias(resource, "sourceResource"); - const sourceMetadata = alias(resourceMetadata, "sourceMetadata"); - const targetResource = alias(resource, "targetResource"); - const targetMetadata = alias(resourceMetadata, "targetMetadata"); + const sourceResource = alias(schema.resource, "sourceResource"); + const sourceMetadata = alias(schema.resourceMetadata, "sourceMetadata"); + const targetResource = alias(schema.resource, "targetResource"); + const targetMetadata = alias(schema.resourceMetadata, "targetMetadata"); + + const isMetadataMatchSatisfied = or( + isNull(schema.resourceRelationshipRuleMetadataMatch.key), + exists( + tx + .select() + .from(sourceMetadata) + .innerJoin(targetMetadata, eq(sourceMetadata.key, targetMetadata.key)) + .where( + and( + eq(sourceMetadata.resourceId, sourceResource.id), + eq(targetMetadata.resourceId, targetResource.id), + eq(sourceMetadata.value, targetMetadata.value), + eq( + sourceMetadata.key, + schema.resourceRelationshipRuleMetadataMatch.key, + ), + ), + ), + ), + ); + + const isMetadataEqualsSatisfied = or( + isNull(schema.resourceRelationshipRuleMetadataEquals.key), + exists( + tx + .select() + .from(targetMetadata) + .where( + and( + eq(targetMetadata.resourceId, targetResource.id), + eq( + targetMetadata.key, + schema.resourceRelationshipRuleMetadataEquals.key, + ), + eq( + targetMetadata.value, + schema.resourceRelationshipRuleMetadataEquals.value, + ), + ), + ), + ), + ); + + const ruleMatchesSource = [ + eq(schema.resourceRelationshipRule.workspaceId, sourceResource.workspaceId), + eq(schema.resourceRelationshipRule.sourceKind, sourceResource.kind), + eq(schema.resourceRelationshipRule.sourceVersion, sourceResource.version), + ]; + + const ruleMatchesTarget = [ + or( + isNull(schema.resourceRelationshipRule.targetKind), + eq(schema.resourceRelationshipRule.targetKind, targetResource.kind), + ), + or( + isNull(schema.resourceRelationshipRule.targetVersion), + eq(schema.resourceRelationshipRule.targetVersion, targetResource.version), + ), + ]; - // Main query to find relationships: - // 1. Start with the source resource - // 2. Join its metadata - // 3. Find target resources in same workspace with matching metadata values - // 4. Join with rules that match source/target kinds and versions - // 5. Ensure metadata keys match what the rule requires - // 6. Group and count matches to verify ALL required metadata keys match const relationships = await tx - .selectDistinctOn([sourceResource.id, rulesWithCount.id], { - ruleId: rulesWithCount.id, - type: rulesWithCount.dependencyType, + .selectDistinctOn([targetResource.id, schema.resourceRelationshipRule.id], { + ruleId: schema.resourceRelationshipRule.id, + type: schema.resourceRelationshipRule.dependencyType, target: targetResource, - reference: rulesWithCount.reference, + reference: schema.resourceRelationshipRule.reference, }) .from(sourceResource) - .innerJoin(sourceMetadata, eq(sourceResource.id, sourceMetadata.resourceId)) .innerJoin( targetResource, - eq(sourceResource.workspaceId, targetResource.workspaceId), + eq(targetResource.workspaceId, sourceResource.workspaceId), ) .innerJoin( - targetMetadata, - and( - eq(targetResource.id, targetMetadata.resourceId), - eq(targetMetadata.key, sourceMetadata.key), - eq(targetMetadata.value, sourceMetadata.value), - ), + schema.resourceRelationshipRule, + and(...ruleMatchesSource, ...ruleMatchesTarget), ) - .innerJoin( - rulesWithCount, - and( - eq(rulesWithCount.workspaceId, sourceResource.workspaceId), - eq(rulesWithCount.sourceKind, sourceResource.kind), - eq(rulesWithCount.sourceVersion, sourceResource.version), - or( - eq(rulesWithCount.targetKind, targetResource.kind), - isNull(rulesWithCount.targetKind), - ), - or( - eq(rulesWithCount.targetVersion, targetResource.version), - isNull(rulesWithCount.targetVersion), - ), - ), - ) - .innerJoin( - resourceRelationshipRuleMetadataMatch, + .leftJoin( + schema.resourceRelationshipRuleMetadataMatch, eq( - rulesWithCount.id, - resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId, + schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId, + schema.resourceRelationshipRule.id, ), ) .leftJoin( - resourceRelationshipRuleMetadataEquals, + schema.resourceRelationshipRuleMetadataEquals, eq( - rulesWithCount.id, - resourceRelationshipRuleMetadataEquals.resourceRelationshipRuleId, + schema.resourceRelationshipRuleMetadataEquals + .resourceRelationshipRuleId, + schema.resourceRelationshipRule.id, ), ) .where( and( eq(sourceResource.id, resourceId), - eq(sourceMetadata.key, resourceRelationshipRuleMetadataMatch.key), - or( - isNull(resourceRelationshipRuleMetadataEquals.key), - eq(targetMetadata.key, resourceRelationshipRuleMetadataEquals.key), - ), - or( - isNull(resourceRelationshipRuleMetadataEquals.value), - eq( - targetMetadata.value, - resourceRelationshipRuleMetadataEquals.value, - ), - ), - ), - ) - .groupBy( - sourceResource.workspaceId, - sourceResource.id, - targetResource.id, - rulesWithCount.id, - rulesWithCount.reference, - rulesWithCount.dependencyType, - rulesWithCount.metadataKeysMatches, - rulesWithCount.metadataKeysEquals, - ) - // Only return relationships where the number of matching metadata keys - // equals the number required by the rule (ensures ALL keys match) - .having( - and( - eq(count(sourceMetadata.key), rulesWithCount.metadataKeysMatches), - eq(count(sourceMetadata.key), rulesWithCount.metadataKeysEquals), + ne(targetResource.id, resourceId), + isNull(sourceResource.deletedAt), + isNull(targetResource.deletedAt), + isMetadataEqualsSatisfied, + isMetadataMatchSatisfied, ), ); @@ -157,7 +123,7 @@ export const getResourceParents = async (tx: Tx, resourceId: string) => { await tx.query.resource .findMany({ where: inArray( - resource.id, + schema.resource.id, Object.values(relationships).map((r) => r.target.id), ), with: { @@ -192,23 +158,29 @@ export const getResourceRelationshipRules = async ( ) => { return tx .select() - .from(resource) + .from(schema.resource) .innerJoin( - resourceRelationshipRule, + schema.resourceRelationshipRule, and( - eq(resourceRelationshipRule.workspaceId, resource.workspaceId), - eq(resourceRelationshipRule.sourceKind, resource.kind), - eq(resourceRelationshipRule.sourceVersion, resource.version), + eq( + schema.resourceRelationshipRule.workspaceId, + schema.resource.workspaceId, + ), + eq(schema.resourceRelationshipRule.sourceKind, schema.resource.kind), + eq( + schema.resourceRelationshipRule.sourceVersion, + schema.resource.version, + ), ), ) .innerJoin( - resourceRelationshipRuleMetadataMatch, + schema.resourceRelationshipRuleMetadataMatch, eq( - resourceRelationshipRule.id, - resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId, + schema.resourceRelationshipRule.id, + schema.resourceRelationshipRuleMetadataMatch.resourceRelationshipRuleId, ), ) - .where(eq(resource.id, resourceId)) + .where(eq(schema.resource.id, resourceId)) .then((r) => _.chain(r) .groupBy((v) => v.resource_relationship_rule.id) diff --git a/packages/db/src/schema/rbac.ts b/packages/db/src/schema/rbac.ts index c0cbe383a..37176f510 100644 --- a/packages/db/src/schema/rbac.ts +++ b/packages/db/src/schema/rbac.ts @@ -38,6 +38,7 @@ export const scopeType = pgEnum("scope_type", [ "resource", "resourceProvider", "resourceMetadataGroup", + "resourceRelationshipRule", "workspace", "environment", "environmentPolicy", diff --git a/packages/db/src/schema/resource-relationship-rule.ts b/packages/db/src/schema/resource-relationship-rule.ts index 26cc0b009..84bee3792 100644 --- a/packages/db/src/schema/resource-relationship-rule.ts +++ b/packages/db/src/schema/resource-relationship-rule.ts @@ -1,6 +1,7 @@ import { relations } from "drizzle-orm"; import { pgEnum, pgTable, text, uniqueIndex, uuid } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; import { workspace } from "./workspace.js"; @@ -130,6 +131,7 @@ export const resourceRelationshipRuleRelations = relations( resourceRelationshipRule, ({ many }) => ({ metadataMatches: many(resourceRelationshipRuleMetadataMatch), + metadataEquals: many(resourceRelationshipRuleMetadataEquals), }), ); @@ -145,6 +147,28 @@ export const resourceRelationshipRuleMetadataMatchRelations = relations( }), ); +export const resourceRelationshipRuleMetadataEqualsRelations = relations( + resourceRelationshipRuleMetadataEquals, + ({ one }) => ({ + rule: one(resourceRelationshipRule, { + fields: [ + resourceRelationshipRuleMetadataEquals.resourceRelationshipRuleId, + ], + references: [resourceRelationshipRule.id], + }), + }), +); + export const createResourceRelationshipRule = createInsertSchema( resourceRelationshipRule, -); +) + .omit({ id: true }) + .extend({ + metadataKeysMatch: z.array(z.string()).optional(), + metadataKeysEquals: z + .array(z.object({ key: z.string(), value: z.string() })) + .optional(), + }); + +export const updateResourceRelationshipRule = + createResourceRelationshipRule.partial(); diff --git a/packages/validators/src/auth/index.ts b/packages/validators/src/auth/index.ts index d48689cde..ec4b0b9a9 100644 --- a/packages/validators/src/auth/index.ts +++ b/packages/validators/src/auth/index.ts @@ -68,6 +68,10 @@ export enum Permission { ResourceMetadataGroupUpdate = "resourceMetadataGroup.update", ResourceMetadataGroupDelete = "resourceMetadataGroup.delete", + ResourceRelationshipRuleCreate = "resourceRelationshipRule.create", + ResourceRelationshipRuleUpdate = "resourceRelationshipRule.update", + ResourceRelationshipRuleDelete = "resourceRelationshipRule.delete", + DeploymentCreate = "deployment.create", DeploymentUpdate = "deployment.update", DeploymentGet = "deployment.get",