Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init View Filters #159

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions packages/api/src/router/target-view-metadata-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import _ from "lodash";
import { z } from "zod";

import {
and,
asc,
count,
eq,
inArray,
sql,
takeFirst,
takeFirstOrNull,
} from "@ctrlplane/db";
import {
createTargetViewMetadataGroup,
target,
targetMetadata,
targetView,
targetViewMetadataGroup,
targetViewMetadataGroupKey,
updateTargetViewMetadataGroup,
} from "@ctrlplane/db/schema";
import { Permission } from "@ctrlplane/validators/auth";

import { createTRPCRouter, protectedProcedure } from "../trpc";

export const targetViewMetadataGroupRouter = createTRPCRouter({
groups: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.TargetViewMetadataGroupList)
.on({ type: "workspace", id: input }),
})
.input(z.string().uuid())
.query(async ({ ctx, input }) => {
const groups = await ctx.db
.select({
group: targetViewMetadataGroup,
keys: sql<string[]>`array_agg(${targetViewMetadataGroupKey.key})`.as(
"keys",
),
targets: count(target.id).as("targets"),
})
.from(targetViewMetadataGroup)
.leftJoin(
targetViewMetadataGroupKey,
eq(targetViewMetadataGroup.id, targetViewMetadataGroupKey.groupId),
)
.leftJoin(targetView, eq(targetViewMetadataGroup.viewId, targetView.id))
.leftJoin(target, eq(targetView.workspaceId, target.workspaceId))
.leftJoin(
targetMetadata,
and(
eq(target.id, targetMetadata.targetId),
inArray(
targetMetadata.key,
sql`array_agg(${targetViewMetadataGroupKey.key})`,
),
),
)
.where(eq(targetViewMetadataGroup.viewId, input))
.groupBy(targetViewMetadataGroup.id)
.orderBy(asc(targetViewMetadataGroup.name));

return groups;
}),

byId: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.TargetViewMetadataGroupGet)
.on({ type: "targetViewMetadataGroup", id: input }),
})
.input(z.string().uuid())
.query(async ({ ctx, input }) => {
const group = await ctx.db
.select({
group: targetViewMetadataGroup,
keys: sql<string[]>`array_agg(${targetViewMetadataGroupKey.key})`.as(
"keys",
),
})
.from(targetViewMetadataGroup)
.leftJoin(
targetViewMetadataGroupKey,
eq(targetViewMetadataGroup.id, targetViewMetadataGroupKey.groupId),
)
.where(eq(targetViewMetadataGroup.id, input))
.groupBy(targetViewMetadataGroup.id)
.then(takeFirstOrNull);

if (group == null) throw new Error("Group not found");

const targetMetadataAgg = ctx.db
.select({
id: target.id,
metadata: sql<Record<string, string>>`jsonb_object_agg(
${targetMetadata.key},
${targetMetadata.value}
)`.as("metadata"),
})
.from(target)
.innerJoin(targetView, eq(target.workspaceId, targetView.workspaceId))
.innerJoin(
targetMetadata,
and(
eq(target.id, targetMetadata.targetId),
inArray(targetMetadata.key, group.keys),
),
)
.where(eq(targetView.id, group.group.viewId))
.groupBy(target.id)
.having(
sql<number>`COUNT(DISTINCT ${targetMetadata.key}) = ${group.keys.length}`,
)
.as("target_metadata_agg");
Comment on lines +96 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Review the complex query in the byId procedure for performance

The query in the byId procedure (lines 96-118) includes multiple joins and aggregations, which could affect performance on large datasets. Consider analyzing the query execution plan and optimizing it if necessary, perhaps by adding appropriate indexes or simplifying the query.

[performance]


const combinations = await ctx.db
.with(targetMetadataAgg)
.select({
metadata: targetMetadataAgg.metadata,
targets: sql<number>`COUNT(*)`.as("targets"),
})
.from(targetMetadataAgg)
.groupBy(targetMetadataAgg.metadata);

return {
...group.group,
keys: group.keys,
combinations,
};
}),

create: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.TargetViewMetadataGroupCreate)
.on({ type: "workspace", id: input.viewId }),
})
.input(
createTargetViewMetadataGroup.extend({
keys: z.array(z.string()),
}),
)
.mutation(async ({ ctx, input }) => {
return ctx.db.transaction(async (tx) => {
const [group] = await tx
.insert(targetViewMetadataGroup)
.values(createTargetViewMetadataGroup.parse(input))
.returning();

if (!group) throw new Error("Group creation failed");

await tx.insert(targetViewMetadataGroupKey).values(
input.keys.map((key) => ({
groupId: group.id,
key,
})),
);

return group;
});
}),

update: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.TargetViewMetadataGroupUpdate)
.on({ type: "targetViewMetadataGroup", id: input.id }),
})
.input(
z.object({
id: z.string().uuid(),
data: updateTargetViewMetadataGroup.extend({
keys: z.array(z.string()).optional(),
}),
}),
)
.mutation(async ({ ctx, input }) => {
return ctx.db.transaction(async (tx) => {
if (input.data.name || input.data.viewId) {
await tx
.update(targetViewMetadataGroup)
.set(updateTargetViewMetadataGroup.parse(input.data))
.where(eq(targetViewMetadataGroup.id, input.id));
}

if (input.data.keys) {
await tx
.delete(targetViewMetadataGroupKey)
.where(eq(targetViewMetadataGroupKey.groupId, input.id));

await tx.insert(targetViewMetadataGroupKey).values(
input.data.keys.map((key) => ({
groupId: input.id,
key,
})),
);
}

return tx
.select()
.from(targetViewMetadataGroup)
.where(eq(targetViewMetadataGroup.id, input.id))
.then(takeFirst);
});
}),

delete: protectedProcedure
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.TargetViewMetadataGroupDelete)
.on({ type: "targetViewMetadataGroup", id: input }),
})
.input(z.string().uuid())
.mutation(({ ctx, input }) =>
ctx.db
.delete(targetViewMetadataGroup)
.where(eq(targetViewMetadataGroup.id, input)),
),
});
24 changes: 24 additions & 0 deletions packages/auth/src/utils/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
targetMetadataGroup,
targetProvider,
targetView,
targetViewMetadataGroup,
variableSet,
workspace,
} from "@ctrlplane/db/schema";
Expand Down Expand Up @@ -344,6 +345,28 @@ const getJobScopes = async (id: string) => {
];
};

const getTargetViewMetadataGroupScopes = async (id: string) => {
const result = await db
.select()
.from(workspace)
.innerJoin(targetView, eq(targetView.workspaceId, workspace.id))
.innerJoin(
targetViewMetadataGroup,
eq(targetViewMetadataGroup.viewId, targetView.id),
)
.where(eq(targetViewMetadataGroup.id, id))
.then(takeFirst);

return [
{
type: "targetViewMetadataGroup" as const,
id: result.target_view_metadata_group.id,
},
{ type: "targetView" as const, id: result.target_view.id },
{ type: "workspace" as const, id: result.workspace.id },
];
};

type Scope = { type: ScopeType; id: string };

export const scopeHandlers: Record<
Expand All @@ -365,6 +388,7 @@ export const scopeHandlers: Record<
variableSet: getVariableSetScopes,
jobAgent: getJobAgentScopes,
job: getJobScopes,
targetViewMetadataGroup: getTargetViewMetadataGroupScopes,
};

const fetchScopeHierarchyForResource = async (resource: {
Expand Down
27 changes: 27 additions & 0 deletions packages/db/drizzle/0019_complex_cammi.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
ALTER TYPE "scope_type" ADD VALUE 'targetViewMetadataGroup';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "target_view_metadata_group" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"view_id" uuid NOT NULL,
"name" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "target_view_metadata_group_key" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"group_id" uuid NOT NULL,
"key" text NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "target_view_metadata_group" ADD CONSTRAINT "target_view_metadata_group_view_id_target_view_id_fk" FOREIGN KEY ("view_id") REFERENCES "public"."target_view"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "target_view_metadata_group_key" ADD CONSTRAINT "target_view_metadata_group_key_group_id_target_view_metadata_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."target_view_metadata_group"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "target_view_metadata_group_view_id_name_index" ON "target_view_metadata_group" USING btree ("view_id","name");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "target_view_metadata_group_key_group_id_key_index" ON "target_view_metadata_group_key" USING btree ("group_id","key");
Loading
Loading