diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/CreateRelease.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/CreateRelease.tsx index 728672eb9..6701dd573 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/CreateRelease.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/CreateRelease.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { IconTrash } from "@tabler/icons-react"; -import { valid } from "semver"; +import { + IconFilterExclamation, + IconFilterFilled, + IconX, +} from "@tabler/icons-react"; import { z } from "zod"; import { Button } from "@ctrlplane/ui/button"; -import { Card } from "@ctrlplane/ui/card"; import { Dialog, DialogContent, @@ -38,21 +40,29 @@ import { SelectValue, } from "@ctrlplane/ui/select"; import { toast } from "@ctrlplane/ui/toast"; +import { + isEmptyCondition, + releaseCondition, + ReleaseFilterType, + ReleaseOperator, +} from "@ctrlplane/validators/releases"; import { api } from "~/trpc/react"; +import { ReleaseConditionDialog } from "./release-condition/ReleaseConditionDialog"; const releaseDependency = z.object({ - targetMetadataGroupId: z.string().uuid().optional(), deploymentId: z.string().uuid(), - rule: z.string().min(1).max(255), - ruleType: z.enum(["semver", "regex"]), + releaseFilter: releaseCondition, }); const releaseForm = z.object({ systemId: z.string().uuid(), deploymentId: z.string().uuid(), version: z.string().min(1).max(255), - releaseDependencies: z.array(releaseDependency), + releaseDependencies: z.array(releaseDependency).refine((deps) => { + const deploymentIds = deps.map((d) => d.deploymentId); + return new Set(deploymentIds).size === deploymentIds.length; + }, "Cannot reuse a deployment in multiple release dependencies"), }); export const CreateReleaseDialog: React.FC<{ @@ -87,14 +97,6 @@ export const CreateReleaseDialog: React.FC<{ workspace.data?.id ?? "", { enabled: workspace.data != null && workspace.data.id !== "" }, ); - const targetMetadataGroups = api.target.metadataGroup.groups.useQuery( - workspace.data?.id ?? "", - { enabled: workspace.data != null && workspace.data.id !== "" }, - ); - const latestRelease = api.release.list.useQuery( - { deploymentId, limit: 1 }, - { enabled: deploymentId !== "" }, - ); const [open, setOpen] = useState(false); useEffect(() => { @@ -130,10 +132,7 @@ export const CreateReleaseDialog: React.FC<{ numOfReleaseJobTriggers === 0 ? `No targets to deploy release too.` : `Dispatching ${release.releaseJobTriggers.length} job configuration${release.releaseJobTriggers.length > 1 ? "s" : ""}.`, - { - dismissible: true, - duration: 2_000, - }, + { dismissible: true, duration: 2_000 }, ); props.onClose?.(); @@ -145,15 +144,7 @@ export const CreateReleaseDialog: React.FC<{ name: "releaseDependencies", }); - useEffect(() => { - if ((latestRelease.data?.items.length ?? 0) > 0) - latestRelease.data?.items[0]!.releaseDependencies.forEach((rd) => { - append({ - ...rd, - targetMetadataGroupId: rd.targetMetadataGroupId ?? undefined, - }); - }); - }, [latestRelease.data, append, deploymentId]); + const formErrors = form.formState.errors.releaseDependencies ?? null; return ( @@ -243,17 +234,22 @@ export const CreateReleaseDialog: React.FC<{
+ + Dependencies must be fulfilled for a target before this Release + can be applied to that target. Read more about release + dependencies here. + {fields.map((_, index) => ( - -
- ( - +
+ ( + + - - )} - /> + + + )} + /> - ( - - - - )} - /> -
+ ( + + + + + + + + )} + /> -
- ( - - - - )} - /> - - ( - - )} - /> - - -
- + +
))}
+ {formErrors?.root?.message && ( +
+ {formErrors.root.message} +
+ )} + diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/JobDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/JobDrawer.tsx index 2b588ebc9..4ebc02f8a 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/JobDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/JobDrawer.tsx @@ -18,6 +18,7 @@ import { api } from "~/trpc/react"; import { JobAgent } from "./JobAgent"; import { JobMetadata } from "./JobMetadata"; import { JobPropertiesTable } from "./JobProperties"; +import { DependenciesDiagram } from "./RelationshipsDiagramDependencies"; import { useJobDrawer } from "./useJobDrawer"; export const JobDrawer: React.FC = () => { @@ -43,7 +44,7 @@ export const JobDrawer: React.FC = () => { {jobQ.isLoading && (
@@ -96,10 +97,24 @@ export const JobDrawer: React.FC = () => { )} {job != null && ( -
- +
+
+
+ +
+
+ +
+
+ - + +
)} diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/RelationshipsDiagramDependencies.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/RelationshipsDiagramDependencies.tsx new file mode 100644 index 000000000..6b6d7eba5 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/job-drawer/RelationshipsDiagramDependencies.tsx @@ -0,0 +1,243 @@ +"use client"; + +import type * as schema from "@ctrlplane/db/schema"; +import type { EdgeTypes, NodeTypes, ReactFlowInstance } from "reactflow"; +import { useCallback, useEffect, useState } from "react"; +import ReactFlow, { + MarkerType, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, +} from "reactflow"; +import colors from "tailwindcss/colors"; + +import { Card } from "@ctrlplane/ui/card"; +import { Label } from "@ctrlplane/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ctrlplane/ui/select"; + +import { getLayoutedElementsDagre } from "~/app/[workspaceSlug]/_components/reactflow/layout"; +import { DepEdge } from "~/app/[workspaceSlug]/_components/relationships/DepEdge"; +import { TargetNode } from "~/app/[workspaceSlug]/_components/relationships/TargetNode"; + +const nodeTypes: NodeTypes = { target: TargetNode }; +const edgeTypes: EdgeTypes = { default: DepEdge }; + +const useOnLayout = () => { + const { getNodes, fitView, setNodes, setEdges, getEdges } = useReactFlow(); + return useCallback(() => { + const layouted = getLayoutedElementsDagre( + getNodes(), + getEdges(), + "BT", + 200, + 50, + ); + setNodes([...layouted.nodes]); + setEdges([...layouted.edges]); + + fitView({ padding: 0.12, maxZoom: 1 }); + }, [getNodes, getEdges, setNodes, setEdges, fitView]); +}; + +const getDFSPath = ( + startId: string, + goalId: string, + graph: Record, + visited: Set = new Set(), + path: string[] = [], +): string[] | null => { + if (startId === goalId) return path; + visited.add(startId); + + for (const neighbor of graph[startId] ?? []) { + if (visited.has(neighbor)) continue; + path.push(neighbor); + const result = getDFSPath(neighbor, goalId, graph, visited, path); + if (result !== null) return result; + path.pop(); + } + + return null; +}; + +const getUndirectedGraph = ( + relationships: Array, +) => { + const graph: Record> = {}; + + for (const relationship of relationships) { + if (!graph[relationship.sourceId]) graph[relationship.sourceId] = new Set(); + if (!graph[relationship.targetId]) graph[relationship.targetId] = new Set(); + graph[relationship.sourceId]!.add(relationship.targetId); + graph[relationship.targetId]!.add(relationship.sourceId); + } + return Object.fromEntries( + Object.entries(graph).map(([key, value]) => [key, Array.from(value)]), + ); +}; + +type DependenciesDiagramProps = { + targetId: string; + relationships: Array; + targets: Array; + releaseDependencies: (schema.ReleaseDependency & { + deploymentName: string; + target?: string; + })[]; +}; + +const TargetDiagramDependencies: React.FC = ({ + targetId, + relationships, + targets, + releaseDependencies, +}) => { + const [nodes, _, onNodesChange] = useNodesState( + targets.map((t) => ({ + id: t.id, + type: "target", + position: { x: 100, y: 100 }, + data: { + ...t, + targetId, + isOrphanNode: !relationships.some( + (r) => r.targetId === t.id || r.sourceId === t.id, + ), + }, + })), + ); + const [edges, setEdges, onEdgesChange] = useEdgesState( + relationships.map((t) => ({ + id: `${t.sourceId}-${t.targetId}`, + source: t.sourceId, + target: t.targetId, + markerEnd: { + type: MarkerType.Arrow, + color: colors.neutral[700], + }, + style: { + stroke: colors.neutral[700], + }, + label: t.type, + })), + ); + const onLayout = useOnLayout(); + + const graph = getUndirectedGraph(relationships); + + const resetEdges = () => + setEdges( + relationships.map((t) => ({ + id: `${t.sourceId}-${t.targetId}`, + source: t.sourceId, + target: t.targetId, + markerEnd: { + type: MarkerType.Arrow, + color: colors.neutral[700], + }, + style: { + stroke: colors.neutral[700], + }, + label: t.type, + })), + ); + + const getHighlightedEdgesFromPath = (path: string[]) => { + const highlightedEdges: string[] = []; + for (let i = 0; i < path.length - 1; i++) { + highlightedEdges.push(`${path[i]}-${path[i + 1]}`); + highlightedEdges.push(`${path[i + 1]}-${path[i]}`); + } + return highlightedEdges; + }; + + const onDependencySelect = (value: string) => { + const goalId = releaseDependencies.find((rd) => rd.id === value)?.target; + if (goalId == null) { + resetEdges(); + return; + } + const nodesInPath = getDFSPath(targetId, goalId, graph, new Set(), [ + targetId, + ]); + if (nodesInPath == null) { + resetEdges(); + return; + } + const highlightedEdges = getHighlightedEdgesFromPath(nodesInPath); + const newEdges = relationships.map((t) => { + const isHighlighted = highlightedEdges.includes( + `${t.sourceId}-${t.targetId}`, + ); + const color = isHighlighted ? colors.blue[500] : colors.neutral[700]; + + return { + id: `${t.sourceId}-${t.targetId}`, + source: t.sourceId, + target: t.targetId, + markerEnd: { type: MarkerType.Arrow, color }, + style: { stroke: color }, + label: t.type, + }; + }); + setEdges(newEdges); + }; + + const [reactFlowInstance, setReactFlowInstance] = + useState(null); + useEffect(() => { + if (reactFlowInstance != null) onLayout(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reactFlowInstance]); + return ( +
+
+ +
+ +
+ ); +}; + +export const DependenciesDiagram: React.FC = ( + props, +) => ( +
+ + + + + + +
+); diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/reactflow/layout.ts b/apps/webservice/src/app/[workspaceSlug]/_components/reactflow/layout.ts index cc7574d96..42a305cd1 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/reactflow/layout.ts +++ b/apps/webservice/src/app/[workspaceSlug]/_components/reactflow/layout.ts @@ -36,10 +36,11 @@ export const getLayoutedElementsDagre = ( edges: Edge[], direction = "TB", extraEdgeLength = 0, + nodesep = 50, ) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ rankdir: direction }); + dagreGraph.setGraph({ rankdir: direction, nodesep }); nodes.forEach((node) => dagreGraph.setNode(node.id, node)); edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target)); diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/relationships/DepEdge.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/relationships/DepEdge.tsx new file mode 100644 index 000000000..22f513ee6 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/relationships/DepEdge.tsx @@ -0,0 +1,49 @@ +import type { EdgeProps } from "reactflow"; +import { capitalCase } from "change-case"; +import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "reactflow"; + +export const DepEdge: React.FC = ({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + label, + style = {}, + markerEnd, +}) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const edgeLabel = capitalCase(String(label).replace("_", " ")); + + return ( + <> + + +
+ {edgeLabel} +
+
+ + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/relationships/TargetNode.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/relationships/TargetNode.tsx new file mode 100644 index 000000000..68f05f1b3 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/relationships/TargetNode.tsx @@ -0,0 +1,124 @@ +import type { CSSProperties } from "react"; +import type { NodeProps } from "reactflow"; +import { Handle, Position } from "reactflow"; + +import { cn } from "@ctrlplane/ui"; + +import { TargetIcon } from "../TargetIcon"; +import { getBorderColor } from "../targets/getBorderColor"; + +type TargetNodeProps = NodeProps<{ + name: string; + label: string; + id: string; + kind: string; + version: string; + targetId: string; + isOrphanNode: boolean; +}>; +export const TargetNode: React.FC = (node) => { + const { data } = node; + + const isKubernetes = data.version.includes("kubernetes"); + const isTerraform = data.version.includes("terraform"); + const isSharedCluster = data.kind.toLowerCase().includes("sharedcluster"); + const isSelected = data.id === data.targetId && !data.isOrphanNode; + const animatedBorderColor = getBorderColor(data.version, data.kind); + const selectedStyle: CSSProperties | undefined = isSelected + ? { + position: "relative", + borderColor: "transparent", + backgroundClip: "padding-box", + } + : undefined; + + return ( + <> +
+ {isSelected &&
} +
+ + {data.kind} +
+
{data.name}
+
+ + + + + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/TargetDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/TargetDrawer.tsx index 5a023e6a3..81ab4dcf0 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/TargetDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/TargetDrawer.tsx @@ -148,7 +148,7 @@ export const TargetDrawer: React.FC = () => {
{target != null && ( -
+
{ label="Relationships" />
-
+
{activeTab === "deployments" && ( )} diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipContent.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipContent.tsx index b43f4a064..65ecba843 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipContent.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipContent.tsx @@ -8,15 +8,11 @@ export const RelationshipsContent: React.FC<{ target: Target; }> = ({ target }) => { return ( -
-
-
Hierarchy
- -
- -
-
-
+
+
Hierarchy
+ + +
); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipsDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipsDiagram.tsx index f998d39de..7ba7a8b40 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipsDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/relationships/RelationshipsDiagram.tsx @@ -1,20 +1,10 @@ "use client"; -import type { - EdgeProps, - EdgeTypes, - NodeProps, - NodeTypes, - ReactFlowInstance, -} from "reactflow"; +import type * as schema from "@ctrlplane/db/schema"; +import type { EdgeTypes, NodeTypes, ReactFlowInstance } from "reactflow"; import { useCallback, useEffect, useState } from "react"; import ReactFlow, { - BaseEdge, - EdgeLabelRenderer, - getBezierPath, - Handle, MarkerType, - Position, ReactFlowProvider, useEdgesState, useNodesState, @@ -22,106 +12,11 @@ import ReactFlow, { } from "reactflow"; import colors from "tailwindcss/colors"; -import { cn } from "@ctrlplane/ui"; - import { getLayoutedElementsDagre } from "~/app/[workspaceSlug]/_components/reactflow/layout"; -import { TargetIcon } from "~/app/[workspaceSlug]/_components/TargetIcon"; +import { DepEdge } from "~/app/[workspaceSlug]/_components/relationships/DepEdge"; +import { TargetNode } from "~/app/[workspaceSlug]/_components/relationships/TargetNode"; import { api } from "~/trpc/react"; -type TargetNodeProps = NodeProps<{ - name: string; - label: string; - id: string; - kind: string; - version: string; -}>; -const TargetNode: React.FC = (node) => { - const { data } = node; - - const isKubernetes = data.version.includes("kubernetes"); - const isTerraform = data.version.includes("terraform"); - - return ( - <> -
-
- -
-
- {data.kind} -
-
{data.label}
-
- - - - - ); -}; - -const DepEdge: React.FC = ({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - label, - style = {}, - markerEnd, -}) => { - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - return ( - <> - - -
- {label} -
-
- - ); -}; - const nodeTypes: NodeTypes = { target: TargetNode }; const edgeTypes: EdgeTypes = { default: DepEdge }; @@ -131,8 +26,9 @@ const useOnLayout = () => { const layouted = getLayoutedElementsDagre( getNodes(), getEdges(), - "LR", - 100, + "BT", + 0, + 50, ); setNodes([...layouted.nodes]); setEdges([...layouted.edges]); @@ -142,42 +38,33 @@ const useOnLayout = () => { }; const TargetDiagram: React.FC<{ - targets: Array<{ - id: string; - workpace_id: string; - name: string; - identifier: string; - level: number; - parent_identifier?: string; - parent_workspace_id?: string; - }>; -}> = ({ targets }) => { + relationships: Array; + targets: Array; + targetId: string; +}> = ({ relationships, targets, targetId }) => { const [nodes, _, onNodesChange] = useNodesState( - targets.map((d) => ({ - id: `${d.workpace_id}-${d.identifier}`, + targets.map((t) => ({ + id: t.id, type: "target", - position: { x: 0, y: 0 }, - data: { ...d, label: d.name }, + position: { x: 100, y: 100 }, + data: { + ...t, + targetId, + isOrphanNode: !relationships.some( + (r) => r.targetId === t.id || r.sourceId === t.id, + ), + }, })), ); const [edges, __, onEdgesChange] = useEdgesState( - targets - .filter((t) => t.parent_identifier != null) - .map((t) => { - return { - id: `${t.id}-${t.parent_identifier}`, - source: - t.level > 0 - ? `${t.workpace_id}-${t.parent_identifier}` - : `${t.workpace_id}-${t.identifier}`, - target: - t.level > 0 - ? `${t.workpace_id}-${t.identifier}` - : `${t.workpace_id}-${t.parent_identifier}`, - markerEnd: { type: MarkerType.Arrow, color: colors.neutral[500] }, - style: { stroke: colors.neutral[500] }, - }; - }), + relationships.map((t) => ({ + id: `${t.sourceId}-${t.targetId}`, + source: t.sourceId, + target: t.targetId, + markerEnd: { type: MarkerType.Arrow, color: colors.neutral[700] }, + style: { stroke: colors.neutral[700] }, + label: t.type, + })), ); const onLayout = useOnLayout(); @@ -210,9 +97,14 @@ export const TargetHierarchyRelationshipsDiagram: React.FC<{ const hierarchy = api.target.relations.hierarchy.useQuery(targetId); if (hierarchy.data == null) return null; + const { relationships, targets } = hierarchy.data; return ( - + ); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/targets/getBorderColor.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/targets/getBorderColor.tsx new file mode 100644 index 000000000..6b4cb6e39 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/targets/getBorderColor.tsx @@ -0,0 +1,6 @@ +export const getBorderColor = (version: string, kind?: string): string => { + if (version.includes("kubernetes")) return "#3b82f6"; + if (version.includes("terraform")) return "#8b5cf6"; + if (kind?.toLowerCase().includes("sharedcluster")) return "#3b82f6"; + return "#a3a3a3"; +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/dependencies/DependencyDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/dependencies/DependencyDiagram.tsx index ef4257b04..a9e14102c 100644 --- a/apps/webservice/src/app/[workspaceSlug]/dependencies/DependencyDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/dependencies/DependencyDiagram.tsx @@ -169,7 +169,7 @@ const DeploymentNode: React.FC< const newEdges = deps.map((d) => ({ id: d.id, - label: d.rule, + label: d.releaseFilter.type, target: d.deploymentId, source: data.id, animated: true, diff --git a/apps/webservice/src/app/api/v1/workspaces/[workspaceId]/targets/route.ts b/apps/webservice/src/app/api/v1/workspaces/[workspaceId]/targets/route.ts index 8fbcf4cb7..fefb80fb2 100644 --- a/apps/webservice/src/app/api/v1/workspaces/[workspaceId]/targets/route.ts +++ b/apps/webservice/src/app/api/v1/workspaces/[workspaceId]/targets/route.ts @@ -128,7 +128,7 @@ const bodySchema = z.array( ) .optional() .refine( - (vars) => new Set(vars?.map((v) => v.key)).size === vars?.length, + (vars) => vars?.length ?? 0 === new Set(vars?.map((v) => v.key)).size, "Duplicate variable keys are not allowed", ), }), diff --git a/packages/api/src/router/deployment.ts b/packages/api/src/router/deployment.ts index b5d63616c..af3d8c529 100644 --- a/packages/api/src/router/deployment.ts +++ b/packages/api/src/router/deployment.ts @@ -63,7 +63,7 @@ export const deploymentRouter = createTRPCRouter({ id: job.id, status: job.status, targetId: releaseJobTrigger.targetId, - rank: sql`ROW_NUMBER() OVER (PARTITION BY release_job_trigger.target_id ORDER BY job.created_at DESC)`.as( + rank: sql`ROW_NUMBER() OVER (PARTITION BY ${releaseJobTrigger.targetId} ORDER BY ${job.createdAt} DESC)`.as( "rank", ), }) diff --git a/packages/api/src/router/job.ts b/packages/api/src/router/job.ts index eb1c186a1..86fb43e5a 100644 --- a/packages/api/src/router/job.ts +++ b/packages/api/src/router/job.ts @@ -8,6 +8,7 @@ import type { JobAgent, JobMetadata, Release, + ReleaseDependency, ReleaseJobTrigger, Target, User, @@ -21,6 +22,7 @@ import { asc, desc, eq, + inArray, isNull, notInArray, sql, @@ -36,7 +38,9 @@ import { jobAgent, jobMetadata, release, + releaseDependency, releaseJobTrigger, + releaseMatchesCondition, system, target, updateJob, @@ -78,6 +82,8 @@ const processReleaseJobTriggerWithAdditionalDataRows = ( environment_policy: EnvironmentPolicy | null; environment_policy_release_window: EnvironmentPolicyReleaseWindow | null; user?: User | null; + release_dependency?: ReleaseDependency | null; + deployment_name?: { deploymentName: string; deploymentId: string } | null; }>, ) => _.chain(rows) @@ -94,6 +100,16 @@ const processReleaseJobTriggerWithAdditionalDataRows = ( target: v[0]!.target, release: { ...v[0]!.release, deployment: v[0]!.deployment }, environment: v[0]!.environment, + releaseDependencies: v + .map((r) => + r.release_dependency != null + ? { + ...r.release_dependency, + deploymentName: r.deployment_name!.deploymentName, + } + : null, + ) + .filter(isPresent), rolloutDate: v[0]!.environment_policy != null ? rolloutDateFromReleaseJobTrigger( @@ -287,8 +303,16 @@ const releaseJobTriggerRouter = createTRPCRouter({ authorizationCheck: ({ canUser, input }) => canUser.perform(Permission.JobGet).on({ type: "job", id: input }), }) - .query(({ ctx, input }) => - releaseJobTriggerQuery(ctx.db) + .query(async ({ ctx, input }) => { + const deploymentName = ctx.db + .select({ + deploymentName: deployment.name, + deploymentId: deployment.id, + }) + .from(deployment) + .as("deployment_name"); + + const data = await releaseJobTriggerQuery(ctx.db) .leftJoin(user, eq(releaseJobTrigger.causedById, user.id)) .leftJoin(jobMetadata, eq(jobMetadata.jobId, job.id)) .leftJoin( @@ -299,10 +323,132 @@ const releaseJobTriggerRouter = createTRPCRouter({ environmentPolicyReleaseWindow, eq(environmentPolicyReleaseWindow.policyId, environmentPolicy.id), ) + .leftJoin( + releaseDependency, + eq(releaseDependency.releaseId, release.id), + ) + .leftJoin( + deploymentName, + eq(deploymentName.deploymentId, releaseDependency.deploymentId), + ) .where(eq(job.id, input)) .then(processReleaseJobTriggerWithAdditionalDataRows) - .then(takeFirst), - ), + .then(takeFirst); + + const { releaseDependencies } = data; + + const results = await ctx.db.execute( + sql` + WITH RECURSIVE reachable_relationships(id, visited, tr_id, source_id, target_id, type) AS ( + -- Base case: start with the given ID and no relationship + SELECT + ${data.target.id}::uuid AS id, + ARRAY[${data.target.id}::uuid] AS visited, + NULL::uuid AS tr_id, + NULL::uuid AS source_id, + NULL::uuid AS target_id, + NULL::target_relationship_type AS type + UNION ALL + -- Recursive case: find all relationships connected to the current set of IDs + SELECT + CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END AS id, + rr.visited || CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END, + tr.id AS tr_id, + tr.source_id, + tr.target_id, + tr.type + FROM reachable_relationships rr + JOIN target_relationship tr ON tr.source_id = rr.id OR tr.target_id = rr.id + WHERE + NOT CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END = ANY(rr.visited) + AND tr.target_id != ${data.target.id} + ) + SELECT DISTINCT tr_id AS id, source_id, target_id, type + FROM reachable_relationships + WHERE tr_id IS NOT NULL; + `, + ); + + // db.execute does not return the types even if the sql`` is annotated with the type + // so we need to cast them here + const relationships = results.rows.map((r) => ({ + id: String(r.id), + sourceId: String(r.source_id), + targetId: String(r.target_id), + type: r.type as "associated_with" | "depends_on", + })); + + const sourceIds = relationships.map((r) => r.sourceId); + const targetIds = relationships.map((r) => r.targetId); + + const allIds = _.uniq([...sourceIds, ...targetIds, data.target.id]); + + const targets = await ctx.db + .select() + .from(target) + .where(inArray(target.id, allIds)); + + const releaseDependenciesWithTargetPromises = releaseDependencies.map( + async (rd) => { + const latestJobSubquery = ctx.db + .select({ + id: releaseJobTrigger.id, + targetId: releaseJobTrigger.targetId, + releaseId: releaseJobTrigger.releaseId, + status: job.status, + createdAt: job.createdAt, + rank: sql`ROW_NUMBER() OVER ( + PARTITION BY ${releaseJobTrigger.targetId}, ${releaseJobTrigger.releaseId} + ORDER BY ${job.createdAt} DESC + )`.as("rank"), + }) + .from(job) + .innerJoin(releaseJobTrigger, eq(releaseJobTrigger.jobId, job.id)) + .as("latest_job"); + + const targetFulfillingDependency = await ctx.db + .select() + .from(release) + .innerJoin(deployment, eq(release.deploymentId, deployment.id)) + .innerJoin( + latestJobSubquery, + eq(latestJobSubquery.releaseId, release.id), + ) + .where( + and( + releaseMatchesCondition(ctx.db, rd.releaseFilter), + eq(deployment.id, rd.deploymentId), + inArray(latestJobSubquery.targetId, allIds), + eq(latestJobSubquery.rank, 1), + eq(latestJobSubquery.status, JobStatus.Completed), + ), + ); + + return { + ...rd, + target: targetFulfillingDependency.at(0)?.latest_job.targetId, + }; + }, + ); + + return { + ...data, + releaseDependencies: await Promise.all( + releaseDependenciesWithTargetPromises, + ), + relationships, + relatedTargets: targets, + }; + }), }); const rolloutDateFromReleaseJobTrigger = ( diff --git a/packages/api/src/router/release.ts b/packages/api/src/router/release.ts index e164301da..952fc56b3 100644 --- a/packages/api/src/router/release.ts +++ b/packages/api/src/router/release.ts @@ -325,13 +325,12 @@ export const releaseRouter = createTRPCRouter({ .returning() .then(takeFirst); - if (input.releaseDependencies.length > 0) - await db.insert(releaseDependency).values( - input.releaseDependencies.map((rd) => ({ - ...rd, - releaseId: rel.id, - })), - ); + const releaseDeps = input.releaseDependencies.map((rd) => ({ + ...rd, + releaseId: rel.id, + })); + if (releaseDeps.length > 0) + await db.insert(releaseDependency).values(releaseDeps); const releaseJobTriggers = await createReleaseJobTriggers( db, diff --git a/packages/api/src/router/target.ts b/packages/api/src/router/target.ts index 05bb75135..3911c1adf 100644 --- a/packages/api/src/router/target.ts +++ b/packages/api/src/router/target.ts @@ -1,4 +1,5 @@ import type { SQL, Tx } from "@ctrlplane/db"; +import _ from "lodash"; import { isPresent } from "ts-is-present"; import { z } from "zod"; @@ -25,126 +26,66 @@ const targetRelations = createTRPCRouter({ hierarchy: protectedProcedure .input(z.string().uuid()) .query(async ({ ctx, input }) => { - const results = await ctx.db.execute<{ - id: string; - workpace_id: string; - name: string; - identifier: string; - level: number; - parent_identifier: string; - parent_workspace_id: string; - }>( + const results = await ctx.db.execute( sql` - -- Recursive CTE to find ancestors (parents) - WITH RECURSIVE ancestors AS ( - -- Base case: start with the given target id, including parent info if exists - SELECT - t.id, - t.identifier, - t.workspace_id, - t.kind, - t.version, - t.name, - 0 AS level, - ARRAY[t.id] AS path, - parent_tm.value AS parent_identifier, - parent_t.workspace_id AS parent_workspace_id - FROM - target t - LEFT JOIN target_metadata parent_tm ON parent_tm.target_id = t.id AND parent_tm.key = 'ctrlplane/parent-target-identifier' - LEFT JOIN target parent_t ON parent_t.identifier = parent_tm.value AND parent_t.workspace_id = t.workspace_id - WHERE - t.id = ${input} - - UNION ALL - - -- Recursive term: find the parent - SELECT - parent_t.id, - parent_t.identifier, - parent_t.workspace_id, - parent_t.kind, - parent_t.version, - parent_t.name, -- Added name - a.level - 1 AS level, - a.path || parent_t.id, - grandparent_tm.value AS parent_identifier, - grandparent_t.workspace_id AS parent_workspace_id - FROM - ancestors a - JOIN target_metadata tm ON tm.target_id = a.id AND tm.key = 'ctrlplane/parent-target-identifier' - JOIN target parent_t ON parent_t.identifier = tm.value AND parent_t.workspace_id = a.workspace_id - LEFT JOIN target_metadata grandparent_tm ON grandparent_tm.target_id = parent_t.id AND grandparent_tm.key = 'ctrlplane/parent-target-identifier' - LEFT JOIN target grandparent_t ON grandparent_t.identifier = grandparent_tm.value AND grandparent_t.workspace_id = parent_t.workspace_id - WHERE - NOT parent_t.id = ANY(a.path) - ), - - -- Recursive CTE to find descendants (children) - descendants AS ( - -- Base case: start with the given target id, including parent info if exists - SELECT - t.id, - t.identifier, - t.workspace_id, - t.kind, - t.version, - t.name, - 0 AS level, - ARRAY[t.id] AS path, - parent_tm.value AS parent_identifier, - parent_t.workspace_id AS parent_workspace_id - FROM - target t - LEFT JOIN target_metadata parent_tm ON parent_tm.target_id = t.id AND parent_tm.key = 'ctrlplane/parent-target-identifier' - LEFT JOIN target parent_t ON parent_t.identifier = parent_tm.value AND parent_t.workspace_id = t.workspace_id - WHERE - t.id = ${input} - - UNION ALL - - -- Recursive term: find the children - SELECT - child_t.id, - child_t.identifier, - child_t.workspace_id, - child_t.kind, - child_t.version, - child_t.name, -- Added name - d.level + 1 AS level, - d.path || child_t.id, - child_parent_tm.value AS parent_identifier, - child_parent_t.workspace_id AS parent_workspace_id - FROM - descendants d - JOIN target_metadata tm ON tm.key = 'ctrlplane/parent-target-identifier' AND tm.value = d.identifier - JOIN target child_t ON child_t.id = tm.target_id AND child_t.workspace_id = d.workspace_id - LEFT JOIN target_metadata child_parent_tm ON child_parent_tm.target_id = child_t.id AND child_parent_tm.key = 'ctrlplane/parent-target-identifier' - LEFT JOIN target child_parent_t ON child_parent_t.identifier = child_parent_tm.value AND child_parent_t.workspace_id = child_t.workspace_id - WHERE - NOT child_t.id = ANY(d.path) - ) - - -- Combine the results from ancestors and descendants - SELECT DISTINCT - id, - identifier, - workspace_id, - kind, - version, - name, - level, - parent_identifier, - parent_workspace_id - FROM - ( - SELECT * FROM ancestors - UNION ALL - SELECT * FROM descendants - ) AS combined; + WITH RECURSIVE reachable_relationships(id, visited, tr_id, source_id, target_id, type) AS ( + -- Base case: start with the given ID and no relationship + SELECT + ${input}::uuid AS id, + ARRAY[${input}::uuid] AS visited, + NULL::uuid AS tr_id, + NULL::uuid AS source_id, + NULL::uuid AS target_id, + NULL::target_relationship_type AS type + UNION ALL + -- Recursive case: find all relationships connected to the current set of IDs + SELECT + CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END AS id, + rr.visited || CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END, + tr.id AS tr_id, + tr.source_id, + tr.target_id, + tr.type + FROM reachable_relationships rr + JOIN target_relationship tr ON tr.source_id = rr.id OR tr.target_id = rr.id + WHERE + NOT CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END = ANY(rr.visited) + ) + SELECT DISTINCT tr_id AS id, source_id, target_id, type + FROM reachable_relationships + WHERE tr_id IS NOT NULL; `, ); - return results.rows; + + // db.execute does not return the types even if the sql`` is annotated with the type + // so we need to cast them here + const relationships = results.rows.map((r) => ({ + id: String(r.id), + sourceId: String(r.source_id), + targetId: String(r.target_id), + type: r.type as "associated_with" | "depends_on", + })); + + const sourceIds = relationships.map((r) => r.sourceId); + const targetIds = relationships.map((r) => r.targetId); + + const allIds = _.uniq([...sourceIds, ...targetIds, input]); + + const targets = await ctx.db + .select() + .from(schema.target) + .where(inArray(schema.target.id, allIds)); + + return { relationships, targets }; }), }); diff --git a/packages/db/drizzle/0022_clean_wrecking_crew.sql b/packages/db/drizzle/0022_clean_wrecking_crew.sql new file mode 100644 index 000000000..b9134d8db --- /dev/null +++ b/packages/db/drizzle/0022_clean_wrecking_crew.sql @@ -0,0 +1,29 @@ +ALTER TYPE "target_relationship_type" ADD VALUE 'associated_with'; +--> statement-breakpoint +ALTER TABLE "release_dependency" +DROP CONSTRAINT "release_dependency_target_metadata_group_id_target_metadata_group_id_fk"; +--> statement-breakpoint +DROP INDEX IF EXISTS "release_dependency_release_id_deployment_id_target_metadata_group_id_index"; +--> statement-breakpoint +ALTER TABLE "target_relationship" +ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid () NOT NULL; +--> statement-breakpoint +ALTER TABLE "target_relationship" +ADD COLUMN "type" "target_relationship_type" NOT NULL; +--> statement-breakpoint +ALTER TABLE "release_dependency" +ADD COLUMN "release_filter" jsonb NOT NULL; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "release_dependency_release_id_deployment_id_index" ON "release_dependency" USING btree ("release_id", "deployment_id"); +--> statement-breakpoint +ALTER TABLE "target_relationship" DROP COLUMN IF EXISTS "uuid"; +--> statement-breakpoint +ALTER TABLE "target_relationship" +DROP COLUMN IF EXISTS "relationship_type"; +--> statement-breakpoint +ALTER TABLE "release_dependency" +DROP COLUMN IF EXISTS "target_metadata_group_id"; +--> statement-breakpoint +ALTER TABLE "release_dependency" DROP COLUMN IF EXISTS "rule_type"; +--> statement-breakpoint +ALTER TABLE "release_dependency" DROP COLUMN IF EXISTS "rule"; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0022_snapshot.json b/packages/db/drizzle/meta/0022_snapshot.json new file mode 100644 index 000000000..9ef6567e5 --- /dev/null +++ b/packages/db/drizzle/meta/0022_snapshot.json @@ -0,0 +1,3683 @@ +{ + "id": "e8680276-619b-4d5e-959e-34922de62dd6", + "prevId": "1029e824-f8ff-4867-8740-28c5c3cfe3a5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "active_workspace_id": { + "name": "active_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + } + }, + "indexes": {}, + "foreignKeys": { + "user_active_workspace_id_workspace_id_fk": { + "name": "user_active_workspace_id_workspace_id_fk", + "tableFrom": "user", + "tableTo": "workspace", + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user_api_key": { + "name": "user_api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key_preview": { + "name": "key_preview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_api_key_key_prefix_key_hash_index": { + "name": "user_api_key_key_prefix_key_hash_index", + "columns": [ + { + "expression": "key_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_api_key_user_id_user_id_fk": { + "name": "user_api_key_user_id_user_id_fk", + "tableFrom": "user_api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.dashboard_widget": { + "name": "dashboard_widget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "widget": { + "name": "widget", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "w": { + "name": "w", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "h": { + "name": "h", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_widget_dashboard_id_dashboard_id_fk": { + "name": "dashboard_widget_dashboard_id_dashboard_id_fk", + "tableFrom": "dashboard_widget", + "tableTo": "dashboard", + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment_variable": { + "name": "deployment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_value_id": { + "name": "default_value_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_variable_deployment_id_key_index": { + "name": "deployment_variable_deployment_id_key_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_deployment_id_deployment_id_fk": { + "name": "deployment_variable_deployment_id_deployment_id_fk", + "tableFrom": "deployment_variable", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployment_variable_default_value_id_deployment_variable_value_id_fk": { + "name": "deployment_variable_default_value_id_deployment_variable_value_id_fk", + "tableFrom": "deployment_variable", + "tableTo": "deployment_variable_value", + "columnsFrom": ["default_value_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment_variable_set": { + "name": "deployment_variable_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_variable_set_deployment_id_variable_set_id_index": { + "name": "deployment_variable_set_deployment_id_variable_set_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variable_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_set_deployment_id_deployment_id_fk": { + "name": "deployment_variable_set_deployment_id_deployment_id_fk", + "tableFrom": "deployment_variable_set", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployment_variable_set_variable_set_id_variable_set_id_fk": { + "name": "deployment_variable_set_variable_set_id_variable_set_id_fk", + "tableFrom": "deployment_variable_set", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment_variable_value": { + "name": "deployment_variable_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_id": { + "name": "variable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_filter": { + "name": "target_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "deployment_variable_value_variable_id_value_index": { + "name": "deployment_variable_value_variable_id_value_index", + "columns": [ + { + "expression": "variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_value_variable_id_deployment_variable_id_fk": { + "name": "deployment_variable_value_variable_id_deployment_variable_id_fk", + "tableFrom": "deployment_variable_value", + "tableTo": "deployment_variable", + "columnsFrom": ["variable_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "deployment_system_id_slug_index": { + "name": "deployment_system_id_slug_index", + "columns": [ + { + "expression": "system_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_system_id_system_id_fk": { + "name": "deployment_system_id_system_id_fk", + "tableFrom": "deployment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployment_job_agent_id_job_agent_id_fk": { + "name": "deployment_job_agent_id_job_agent_id_fk", + "tableFrom": "deployment", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment_meta_dependency": { + "name": "deployment_meta_dependency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depends_on_id": { + "name": "depends_on_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_meta_dependency_depends_on_id_deployment_id_index": { + "name": "deployment_meta_dependency_depends_on_id_deployment_id_index", + "columns": [ + { + "expression": "depends_on_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_meta_dependency_deployment_id_deployment_id_fk": { + "name": "deployment_meta_dependency_deployment_id_deployment_id_fk", + "tableFrom": "deployment_meta_dependency", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployment_meta_dependency_depends_on_id_deployment_id_fk": { + "name": "deployment_meta_dependency_depends_on_id_deployment_id_fk", + "tableFrom": "deployment_meta_dependency", + "tableTo": "deployment", + "columnsFrom": ["depends_on_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_filter": { + "name": "target_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_system_id_system_id_fk": { + "name": "environment_system_id_system_id_fk", + "tableFrom": "environment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_id_environment_policy_id_fk": { + "name": "environment_policy_id_environment_policy_id_fk", + "tableFrom": "environment", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.environment_policy": { + "name": "environment_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_required": { + "name": "approval_required", + "type": "environment_policy_approval_requirement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "success_status": { + "name": "success_status", + "type": "environment_policy_deployment_success_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "minimum_success": { + "name": "minimum_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "concurrency_type": { + "name": "concurrency_type", + "type": "concurrency_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "concurrency_limit": { + "name": "concurrency_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "duration": { + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "release_filter": { + "name": "release_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "release_sequencing": { + "name": "release_sequencing", + "type": "release_sequencing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cancel'" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_policy_system_id_system_id_fk": { + "name": "environment_policy_system_id_system_id_fk", + "tableFrom": "environment_policy", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.environment_policy_approval": { + "name": "environment_policy_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": { + "environment_policy_approval_policy_id_release_id_index": { + "name": "environment_policy_approval_policy_id_release_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_policy_approval_policy_id_environment_policy_id_fk": { + "name": "environment_policy_approval_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_approval", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_approval_release_id_release_id_fk": { + "name": "environment_policy_approval_release_id_release_id_fk", + "tableFrom": "environment_policy_approval", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.environment_policy_deployment": { + "name": "environment_policy_deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "environment_policy_deployment_policy_id_environment_id_index": { + "name": "environment_policy_deployment_policy_id_environment_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_policy_deployment_policy_id_environment_policy_id_fk": { + "name": "environment_policy_deployment_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_deployment", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_deployment_environment_id_environment_id_fk": { + "name": "environment_policy_deployment_environment_id_environment_id_fk", + "tableFrom": "environment_policy_deployment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.environment_policy_release_window": { + "name": "environment_policy_release_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp (0) with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp (0) with time zone", + "primaryKey": false, + "notNull": true + }, + "recurrence": { + "name": "recurrence", + "type": "recurrence_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_policy_release_window_policy_id_environment_policy_id_fk": { + "name": "environment_policy_release_window_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_release_window", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.github_organization": { + "name": "github_organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + } + }, + "indexes": { + "unique_installation_workspace": { + "name": "unique_installation_workspace", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_organization_added_by_user_id_user_id_fk": { + "name": "github_organization_added_by_user_id_user_id_fk", + "tableFrom": "github_organization", + "tableTo": "user", + "columnsFrom": ["added_by_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_organization_workspace_id_workspace_id_fk": { + "name": "github_organization_workspace_id_workspace_id_fk", + "tableFrom": "github_organization", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.github_user": { + "name": "github_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_username": { + "name": "github_username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_user_user_id_user_id_fk": { + "name": "github_user_user_id_user_id_fk", + "tableFrom": "github_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target": { + "name": "target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "target_identifier_workspace_id_index": { + "name": "target_identifier_workspace_id_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_provider_id_target_provider_id_fk": { + "name": "target_provider_id_target_provider_id_fk", + "tableFrom": "target", + "tableTo": "target_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "target_workspace_id_workspace_id_fk": { + "name": "target_workspace_id_workspace_id_fk", + "tableFrom": "target", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_metadata": { + "name": "target_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "target_metadata_key_target_id_index": { + "name": "target_metadata_key_target_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_metadata_target_id_target_id_fk": { + "name": "target_metadata_target_id_target_id_fk", + "tableFrom": "target_metadata", + "tableTo": "target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_relationship": { + "name": "target_relationship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "target_relationship_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "target_relationship_target_id_source_id_index": { + "name": "target_relationship_target_id_source_id_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_relationship_source_id_target_id_fk": { + "name": "target_relationship_source_id_target_id_fk", + "tableFrom": "target_relationship", + "tableTo": "target", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "target_relationship_target_id_target_id_fk": { + "name": "target_relationship_target_id_target_id_fk", + "tableFrom": "target_relationship", + "tableTo": "target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_schema": { + "name": "target_schema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "json_schema": { + "name": "json_schema", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "target_schema_version_kind_workspace_id_index": { + "name": "target_schema_version_kind_workspace_id_index", + "columns": [ + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_schema_workspace_id_workspace_id_fk": { + "name": "target_schema_workspace_id_workspace_id_fk", + "tableFrom": "target_schema", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_variable": { + "name": "target_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "target_variable_target_id_key_index": { + "name": "target_variable_target_id_key_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_variable_target_id_target_id_fk": { + "name": "target_variable_target_id_target_id_fk", + "tableFrom": "target_variable", + "tableTo": "target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_view": { + "name": "target_view", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "target_view_workspace_id_workspace_id_fk": { + "name": "target_view_workspace_id_workspace_id_fk", + "tableFrom": "target_view", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_provider": { + "name": "target_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "target_provider_workspace_id_name_index": { + "name": "target_provider_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "target_provider_workspace_id_workspace_id_fk": { + "name": "target_provider_workspace_id_workspace_id_fk", + "tableFrom": "target_provider", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.target_provider_google": { + "name": "target_provider_google", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_provider_id": { + "name": "target_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_ids": { + "name": "project_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "target_provider_google_target_provider_id_target_provider_id_fk": { + "name": "target_provider_google_target_provider_id_target_provider_id_fk", + "tableFrom": "target_provider_google", + "tableTo": "target_provider", + "columnsFrom": ["target_provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.release": { + "name": "release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "release_deployment_id_version_index": { + "name": "release_deployment_id_version_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_deployment_id_deployment_id_fk": { + "name": "release_deployment_id_deployment_id_fk", + "tableFrom": "release", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.release_dependency": { + "name": "release_dependency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_filter": { + "name": "release_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "release_dependency_release_id_deployment_id_index": { + "name": "release_dependency_release_id_deployment_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_dependency_release_id_release_id_fk": { + "name": "release_dependency_release_id_release_id_fk", + "tableFrom": "release_dependency", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_dependency_deployment_id_deployment_id_fk": { + "name": "release_dependency_deployment_id_deployment_id_fk", + "tableFrom": "release_dependency", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.release_job_trigger": { + "name": "release_job_trigger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "release_job_trigger_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "caused_by_id": { + "name": "caused_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "release_job_trigger_job_id_job_id_fk": { + "name": "release_job_trigger_job_id_job_id_fk", + "tableFrom": "release_job_trigger", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "release_job_trigger_caused_by_id_user_id_fk": { + "name": "release_job_trigger_caused_by_id_user_id_fk", + "tableFrom": "release_job_trigger", + "tableTo": "user", + "columnsFrom": ["caused_by_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "release_job_trigger_release_id_release_id_fk": { + "name": "release_job_trigger_release_id_release_id_fk", + "tableFrom": "release_job_trigger", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_job_trigger_target_id_target_id_fk": { + "name": "release_job_trigger_target_id_target_id_fk", + "tableFrom": "release_job_trigger", + "tableTo": "target", + "columnsFrom": ["target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_job_trigger_environment_id_environment_id_fk": { + "name": "release_job_trigger_environment_id_environment_id_fk", + "tableFrom": "release_job_trigger", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "release_job_trigger_job_id_unique": { + "name": "release_job_trigger_job_id_unique", + "nullsNotDistinct": false, + "columns": ["job_id"] + } + } + }, + "public.release_metadata": { + "name": "release_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "release_metadata_key_release_id_index": { + "name": "release_metadata_key_release_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_metadata_release_id_release_id_fk": { + "name": "release_metadata_release_id_release_id_fk", + "tableFrom": "release_metadata", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.system": { + "name": "system", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "system_workspace_id_slug_index": { + "name": "system_workspace_id_slug_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "system_workspace_id_workspace_id_fk": { + "name": "system_workspace_id_workspace_id_fk", + "tableFrom": "system", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.runbook": { + "name": "runbook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "runbook_system_id_system_id_fk": { + "name": "runbook_system_id_system_id_fk", + "tableFrom": "runbook", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runbook_job_agent_id_job_agent_id_fk": { + "name": "runbook_job_agent_id_job_agent_id_fk", + "tableFrom": "runbook", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.runbook_job_trigger": { + "name": "runbook_job_trigger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "runbook_id": { + "name": "runbook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "runbook_job_trigger_job_id_job_id_fk": { + "name": "runbook_job_trigger_job_id_job_id_fk", + "tableFrom": "runbook_job_trigger", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runbook_job_trigger_runbook_id_runbook_id_fk": { + "name": "runbook_job_trigger_runbook_id_runbook_id_fk", + "tableFrom": "runbook_job_trigger", + "tableTo": "runbook", + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "runbook_job_trigger_job_id_unique": { + "name": "runbook_job_trigger_job_id_unique", + "nullsNotDistinct": false, + "columns": ["job_id"] + } + } + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_workspace_id_workspace_id_fk": { + "name": "team_workspace_id_workspace_id_fk", + "tableFrom": "team", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "team_member_team_id_user_id_index": { + "name": "team_member_team_id_user_id_index", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": ["team_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "job_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'policy_passing'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "job_job_agent_id_job_agent_id_fk": { + "name": "job_job_agent_id_job_agent_id_fk", + "tableFrom": "job", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.job_metadata": { + "name": "job_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_metadata_key_job_id_index": { + "name": "job_metadata_key_job_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_metadata_job_id_job_id_fk": { + "name": "job_metadata_job_id_job_id_fk", + "tableFrom": "job_metadata", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.job_variable": { + "name": "job_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_variable_job_id_key_index": { + "name": "job_variable_job_id_key_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_variable_job_id_job_id_fk": { + "name": "job_variable_job_id_job_id_fk", + "tableFrom": "job_variable", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_service_account_email": { + "name": "google_service_account_email", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + } + }, + "public.workspace_email_domain_matching": { + "name": "workspace_email_domain_matching", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verification_code": { + "name": "verification_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_email": { + "name": "verification_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_email_domain_matching_workspace_id_domain_index": { + "name": "workspace_email_domain_matching_workspace_id_domain_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_email_domain_matching_workspace_id_workspace_id_fk": { + "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_email_domain_matching_role_id_role_id_fk": { + "name": "workspace_email_domain_matching_role_id_role_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.variable_set": { + "name": "variable_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_system_id_system_id_fk": { + "name": "variable_set_system_id_system_id_fk", + "tableFrom": "variable_set", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.variable_set_assignment": { + "name": "variable_set_assignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_assignment_variable_set_id_variable_set_id_fk": { + "name": "variable_set_assignment_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_assignment", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_set_assignment_environment_id_environment_id_fk": { + "name": "variable_set_assignment_environment_id_environment_id_fk", + "tableFrom": "variable_set_assignment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.variable_set_value": { + "name": "variable_set_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "variable_set_value_variable_set_id_key_index": { + "name": "variable_set_value_variable_set_id_key_index", + "columns": [ + { + "expression": "variable_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_set_value_variable_set_id_variable_set_id_fk": { + "name": "variable_set_value_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_value", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.workspace_invite_token": { + "name": "workspace_invite_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invite_token_role_id_role_id_fk": { + "name": "workspace_invite_token_role_id_role_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_workspace_id_workspace_id_fk": { + "name": "workspace_invite_token_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_created_by_user_id_fk": { + "name": "workspace_invite_token_created_by_user_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invite_token_token_unique": { + "name": "workspace_invite_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + } + }, + "public.target_metadata_group": { + "name": "target_metadata_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keys": { + "name": "keys", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "include_null_combinations": { + "name": "include_null_combinations", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "target_metadata_group_workspace_id_workspace_id_fk": { + "name": "target_metadata_group_workspace_id_workspace_id_fk", + "tableFrom": "target_metadata_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.runbook_variable": { + "name": "runbook_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "runbook_id": { + "name": "runbook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "runbook_variable_runbook_id_key_index": { + "name": "runbook_variable_runbook_id_key_index", + "columns": [ + { + "expression": "runbook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "runbook_variable_runbook_id_runbook_id_fk": { + "name": "runbook_variable_runbook_id_runbook_id_fk", + "tableFrom": "runbook_variable", + "tableTo": "runbook", + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.entity_role": { + "name": "entity_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "scope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index": { + "name": "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_role_role_id_role_id_fk": { + "name": "entity_role_role_id_role_id_fk", + "tableFrom": "entity_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_workspace_id_workspace_id_fk": { + "name": "role_workspace_id_workspace_id_fk", + "tableFrom": "role", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "role_permission_role_id_permission_index": { + "name": "role_permission_role_id_permission_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_role_id_role_id_fk": { + "name": "role_permission_role_id_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.job_agent": { + "name": "job_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "job_agent_workspace_id_name_index": { + "name": "job_agent_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_agent_workspace_id_workspace_id_fk": { + "name": "job_agent_workspace_id_workspace_id_fk", + "tableFrom": "job_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.environment_policy_approval_requirement": { + "name": "environment_policy_approval_requirement", + "schema": "public", + "values": ["manual", "automatic"] + }, + "public.approval_status_type": { + "name": "approval_status_type", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.concurrency_type": { + "name": "concurrency_type", + "schema": "public", + "values": ["all", "some"] + }, + "public.environment_policy_deployment_success_type": { + "name": "environment_policy_deployment_success_type", + "schema": "public", + "values": ["all", "some", "optional"] + }, + "public.recurrence_type": { + "name": "recurrence_type", + "schema": "public", + "values": ["hourly", "daily", "weekly", "monthly"] + }, + "public.release_sequencing_type": { + "name": "release_sequencing_type", + "schema": "public", + "values": ["wait", "cancel"] + }, + "public.target_relationship_type": { + "name": "target_relationship_type", + "schema": "public", + "values": ["associated_with", "depends_on"] + }, + "public.release_job_trigger_type": { + "name": "release_job_trigger_type", + "schema": "public", + "values": [ + "new_release", + "new_target", + "target_changed", + "api", + "redeploy", + "force_deploy" + ] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "completed", + "cancelled", + "skipped", + "in_progress", + "action_required", + "pending", + "failure", + "invalid_job_agent", + "invalid_integration", + "external_run_not_found" + ] + }, + "public.entity_type": { + "name": "entity_type", + "schema": "public", + "values": ["user", "team"] + }, + "public.scope_type": { + "name": "scope_type", + "schema": "public", + "values": [ + "release", + "target", + "targetProvider", + "targetMetadataGroup", + "workspace", + "environment", + "environmentPolicy", + "deploymentVariable", + "variableSet", + "system", + "deployment", + "job", + "jobAgent", + "runbook", + "targetView" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 7ee9aa69c..41f66dc12 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1729744338553, "tag": "0021_first_ikaris", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1729806557821, + "tag": "0022_clean_wrecking_crew", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/release.ts b/packages/db/src/schema/release.ts index f7dca0cf7..dec84793d 100644 --- a/packages/db/src/schema/release.ts +++ b/packages/db/src/schema/release.ts @@ -32,6 +32,7 @@ import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; import { + releaseCondition, ReleaseFilterType, ReleaseOperator, } from "@ctrlplane/validators/releases"; @@ -41,14 +42,8 @@ import { user } from "./auth.js"; import { deployment } from "./deployment.js"; import { environment } from "./environment.js"; import { job } from "./job.js"; -import { targetMetadataGroup } from "./target-group.js"; import { target } from "./target.js"; -export const releaseDependencyRuleType = pgEnum( - "release_dependency_rule_type", - ["regex", "semver"], -); - export const releaseDependency = pgTable( "release_dependency", { @@ -59,21 +54,16 @@ export const releaseDependency = pgTable( deploymentId: uuid("deployment_id") .notNull() .references(() => deployment.id, { onDelete: "cascade" }), - targetMetadataGroupId: uuid("target_metadata_group_id").references( - () => targetMetadataGroup.id, - { onDelete: "cascade" }, - ), - ruleType: releaseDependencyRuleType("rule_type").notNull(), - rule: text("rule").notNull(), + releaseFilter: jsonb("release_filter").notNull().$type(), }, - (t) => ({ - unq: uniqueIndex().on(t.releaseId, t.deploymentId, t.targetMetadataGroupId), - }), + (t) => ({ unq: uniqueIndex().on(t.releaseId, t.deploymentId) }), ); export type ReleaseDependency = InferSelectModel; -const createReleaseDependency = createInsertSchema(releaseDependency).omit({ +const createReleaseDependency = createInsertSchema(releaseDependency, { + releaseFilter: releaseCondition, +}).omit({ id: true, }); diff --git a/packages/db/src/schema/target.ts b/packages/db/src/schema/target.ts index 22321acc5..b6f9c0a27 100644 --- a/packages/db/src/schema/target.ts +++ b/packages/db/src/schema/target.ts @@ -192,21 +192,21 @@ export function targetMatchesMetadata( } export const targetRelationshipType = pgEnum("target_relationship_type", [ + "associated_with", "depends_on", - "created_by", ]); export const targetRelationship = pgTable( "target_relationship", { - id: uuid("uuid"), + id: uuid("id").primaryKey().defaultRandom(), sourceId: uuid("source_id") .references(() => target.id, { onDelete: "cascade" }) .notNull(), - relationshipType: targetRelationshipType("relationship_type").notNull(), targetId: uuid("target_id") .references(() => target.id, { onDelete: "cascade" }) .notNull(), + type: targetRelationshipType("type").notNull(), }, (t) => ({ uniq: uniqueIndex().on(t.targetId, t.sourceId) }), ); @@ -216,6 +216,7 @@ export const createTargetRelationship = createInsertSchema( ).omit({ id: true }); export const updateTargetRelationship = createTargetRelationship.partial(); +export type TargetRelationship = InferSelectModel; export const targetVariable = pgTable( "target_variable", diff --git a/packages/job-dispatch/src/policies/release-dependency.ts b/packages/job-dispatch/src/policies/release-dependency.ts index 954b789d1..9bc4b104c 100644 --- a/packages/job-dispatch/src/policies/release-dependency.ts +++ b/packages/job-dispatch/src/policies/release-dependency.ts @@ -1,11 +1,11 @@ import type { Tx } from "@ctrlplane/db"; import type { ReleaseJobTrigger } from "@ctrlplane/db/schema"; import _ from "lodash"; -import { satisfies } from "semver"; import { isPresent } from "ts-is-present"; -import { and, eq, inArray } from "@ctrlplane/db"; +import { and, eq, inArray, sql } from "@ctrlplane/db"; import * as schema from "@ctrlplane/db/schema"; +import { JobStatus } from "@ctrlplane/validators/jobs"; export const isPassingReleaseDependencyPolicy = async ( db: Tx, @@ -13,126 +13,135 @@ export const isPassingReleaseDependencyPolicy = async ( ) => { if (releaseJobTriggers.length === 0) return []; - const jcs = await db - .select() - .from(schema.releaseJobTrigger) - .leftJoin( - schema.target, - eq(schema.releaseJobTrigger.targetId, schema.target.id), - ) - .leftJoin( - schema.targetMetadata, - eq(schema.targetMetadata.targetId, schema.target.id), - ) - .leftJoin( - schema.releaseDependency, - eq( - schema.releaseDependency.releaseId, - schema.releaseJobTrigger.releaseId, - ), - ) - .leftJoin( - schema.targetMetadataGroup, - eq( - schema.releaseDependency.targetMetadataGroupId, - schema.targetMetadataGroup.id, - ), - ) - .where( - inArray( - schema.releaseJobTrigger.id, - releaseJobTriggers.map((jc) => jc.id), - ), - ) - .then((rows) => - _.chain(rows) - .groupBy((row) => row.release_job_trigger.id) - .map((jc) => ({ - releaseJobTrigger: jc[0]!.release_job_trigger, - target: jc[0]!.target, - releaseDependencies: _.chain(jc) - .filter( - (v) => - v.release_dependency != null && v.target_metadata_group != null, - ) - .groupBy((v) => v.release_dependency!.id) - .map((v) => ({ - releaseDependency: v[0]!.release_dependency!, - targetMetadataGroup: v[0]!.target_metadata_group!, - })) - .value(), - targetMetadata: _.chain(jc) - .filter((v) => v.target_metadata != null) - .groupBy((v) => v.target_metadata!.id) - .map((v) => ({ - ...v[0]!.target_metadata!, - })) - .value(), - })) - .value(), - ); - - return Promise.all( - jcs.map(async (jc) => { - if (jc.releaseDependencies.length === 0 || jc.target == null) - return jc.releaseJobTrigger; - - const { targetMetadata } = jc; - - const numDepsPassing = await Promise.all( - jc.releaseDependencies.map(async (rd) => { - const { releaseDependency: releaseDep, targetMetadataGroup: tlg } = - rd; - - const relevantTargetMetadata = targetMetadata.filter((tm) => - tlg.keys.includes(tm.key), - ); + const passingReleasesJobTriggersPromises = releaseJobTriggers.map( + async (trigger) => { + const release = await db + .select() + .from(schema.release) + .innerJoin( + schema.releaseDependency, + eq(schema.release.id, schema.releaseDependency.releaseId), + ) + .where(eq(schema.release.id, trigger.releaseId)); + + if (release.length === 0) return trigger; + + const deps = release.map((r) => r.release_dependency); + + if (deps.length === 0) return trigger; + + const results = await db.execute( + sql` + WITH RECURSIVE reachable_relationships(id, visited, tr_id, source_id, target_id, type) AS ( + -- Base case: start with the given ID and no relationship + SELECT + ${trigger.targetId}::uuid AS id, + ARRAY[${trigger.targetId}::uuid] AS visited, + NULL::uuid AS tr_id, + NULL::uuid AS source_id, + NULL::uuid AS target_id, + NULL::target_relationship_type AS type + UNION ALL + -- Recursive case: find all relationships connected to the current set of IDs + SELECT + CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END AS id, + rr.visited || CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END, + tr.id AS tr_id, + tr.source_id, + tr.target_id, + tr.type + FROM reachable_relationships rr + JOIN target_relationship tr ON tr.source_id = rr.id OR tr.target_id = rr.id + WHERE + NOT CASE + WHEN tr.source_id = rr.id THEN tr.target_id + ELSE tr.source_id + END = ANY(rr.visited) + AND tr.target_id != ${trigger.targetId} + ) + SELECT DISTINCT tr_id AS id, source_id, target_id, type + FROM reachable_relationships + WHERE tr_id IS NOT NULL; + `, + ); + + // db.execute does not return the types even if the sql`` is annotated with the type + // so we need to cast them here + const relationships = results.rows.map((r) => ({ + id: String(r.id), + sourceId: String(r.source_id), + targetId: String(r.target_id), + type: r.type as "associated_with" | "depends_on", + })); + + const sourceIds = relationships.map((r) => r.sourceId); + const targetIds = relationships.map((r) => r.targetId); - const dependentJobs = await db - .select() - .from(schema.job) - .innerJoin( - schema.releaseJobTrigger, - eq(schema.job.id, schema.releaseJobTrigger.jobId), - ) - .innerJoin( - schema.target, - eq(schema.releaseJobTrigger.targetId, schema.target.id), - ) - .innerJoin( - schema.release, - eq(schema.releaseJobTrigger.releaseId, schema.release.id), - ) - .innerJoin( - schema.deployment, - eq(schema.release.deploymentId, schema.deployment.id), - ) - .where( - and( - eq(schema.job.status, "completed"), - eq(schema.deployment.id, releaseDep.deploymentId), - schema.targetMatchesMetadata(db, { - type: "comparison", - operator: "and", - conditions: relevantTargetMetadata.map((tm) => ({ - ...tm, - type: "metadata", - })), - }), - ), - ); - - return dependentJobs.some((je) => - releaseDep.ruleType === "semver" - ? satisfies(je.release.version, releaseDep.rule) - : new RegExp(releaseDep.rule).test(je.release.version), + const allIds = _.uniq([...sourceIds, ...targetIds]); + + const passingDepsPromises = deps.map(async (dep) => { + const latestJobSubquery = db + .select({ + id: schema.releaseJobTrigger.id, + targetId: schema.releaseJobTrigger.targetId, + releaseId: schema.releaseJobTrigger.releaseId, + status: schema.job.status, + createdAt: schema.job.createdAt, + rank: sql`ROW_NUMBER() OVER ( + PARTITION BY ${schema.releaseJobTrigger.targetId}, ${schema.releaseJobTrigger.releaseId} + ORDER BY ${schema.job.createdAt} DESC + )`.as("rank"), + }) + .from(schema.job) + .innerJoin( + schema.releaseJobTrigger, + eq(schema.releaseJobTrigger.jobId, schema.job.id), + ) + .as("latest_job"); + + const targetFulfillingDependency = await db + .select() + .from(schema.release) + .innerJoin( + schema.deployment, + eq(schema.release.deploymentId, schema.deployment.id), + ) + .innerJoin( + latestJobSubquery, + eq(latestJobSubquery.releaseId, schema.release.id), + ) + .where( + and( + schema.releaseMatchesCondition(db, dep.releaseFilter), + eq(schema.deployment.id, dep.deploymentId), + inArray(latestJobSubquery.targetId, allIds), + eq(latestJobSubquery.rank, 1), + eq(latestJobSubquery.status, JobStatus.Completed), + ), ); - }), - ).then((data) => data.filter(Boolean).length); - - const isAllDependenciesMet = - numDepsPassing === jc.releaseDependencies.length; - return isAllDependenciesMet ? jc.releaseJobTrigger : null; - }), - ).then((v) => v.filter(isPresent)); + + const isPassing = targetFulfillingDependency.length > 0; + return isPassing ? dep : null; + }); + + const passingDeps = await Promise.all(passingDepsPromises).then((deps) => + deps.filter(isPresent), + ); + + const isPassingAllDeps = passingDeps.length === deps.length; + return isPassingAllDeps ? trigger : null; + }, + ); + + const passingTriggers = await Promise.all( + passingReleasesJobTriggersPromises, + ).then((triggers) => triggers.filter(isPresent)); + + return passingTriggers; }; diff --git a/packages/job-dispatch/src/policy-checker.ts b/packages/job-dispatch/src/policy-checker.ts index 77cfb4178..b9e252f0f 100644 --- a/packages/job-dispatch/src/policy-checker.ts +++ b/packages/job-dispatch/src/policy-checker.ts @@ -22,10 +22,10 @@ export const isPassingAllPolicies = async ( isPassingApprovalPolicy, isPassingCriteriaPolicy, isPassingConcurrencyPolicy, + isPassingReleaseDependencyPolicy, isPassingJobRolloutPolicy, isPassingNoActiveJobsPolicy, isPassingReleaseWindowPolicy, - isPassingReleaseDependencyPolicy, ]; let passingJobs = releaseJobTriggers; diff --git a/packages/job-dispatch/src/policy-create.ts b/packages/job-dispatch/src/policy-create.ts index bbaefbec3..e6e9e99d5 100644 --- a/packages/job-dispatch/src/policy-create.ts +++ b/packages/job-dispatch/src/policy-create.ts @@ -45,11 +45,5 @@ export const createJobApprovals = async ( releaseId: p.release.id, })), ) - .onConflictDoUpdate({ - target: [ - environmentPolicyApproval.policyId, - environmentPolicyApproval.releaseId, - ], - set: { status: "pending" }, - }); + .onConflictDoNothing(); };