diff --git a/packages/db/src/common.ts b/packages/db/src/common.ts index 9cc05a534..cdc4fd81c 100644 --- a/packages/db/src/common.ts +++ b/packages/db/src/common.ts @@ -1,6 +1,8 @@ import type { SQL } from "drizzle-orm"; import type { PgTable } from "drizzle-orm/pg-core"; -import { getTableColumns, sql } from "drizzle-orm"; +import { eq, getTableColumns, ilike, sql } from "drizzle-orm"; + +import { ColumnOperator } from "@ctrlplane/validators/conditions"; import type { db } from "./client"; @@ -19,9 +21,15 @@ export const takeFirstOrNull = ( export type Tx = Omit; +type ColumnKey = keyof T["_"]["columns"]; +type ColumnType< + T extends PgTable, + Q extends ColumnKey, +> = T["_"]["columns"][Q]; + export const buildConflictUpdateColumns = < T extends PgTable, - Q extends keyof T["_"]["columns"], + Q extends ColumnKey, >( table: T, columns: Q[], @@ -42,3 +50,16 @@ export function enumToPgEnum>( ): [T[keyof T], ...T[keyof T][]] { return Object.values(myEnum).map((value: any) => `${value}`) as any; } + +export const ColumnOperatorFn: Record< + ColumnOperator, + >( + column: ColumnType, + value: string, + ) => SQL +> = { + [ColumnOperator.Equals]: (column, value) => eq(column, value), + [ColumnOperator.StartsWith]: (column, value) => ilike(column, `${value}%`), + [ColumnOperator.EndsWith]: (column, value) => ilike(column, `%${value}`), + [ColumnOperator.Contains]: (column, value) => ilike(column, `%${value}%`), +}; diff --git a/packages/db/src/schema/deployment.ts b/packages/db/src/schema/deployment.ts index 8a864460d..3bba981f4 100644 --- a/packages/db/src/schema/deployment.ts +++ b/packages/db/src/schema/deployment.ts @@ -1,7 +1,7 @@ -import type { DeploymentCondition } from "@ctrlplane/validators/jobs"; +import type { DeploymentCondition } from "@ctrlplane/validators/deployments"; import type { ResourceCondition } from "@ctrlplane/validators/resources"; import type { InferSelectModel, SQL } from "drizzle-orm"; -import { relations, sql } from "drizzle-orm"; +import { and, eq, not, or, relations, sql } from "drizzle-orm"; import { integer, jsonb, @@ -13,12 +13,13 @@ import { import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; +import { ComparisonOperator } from "@ctrlplane/validators/conditions"; import { isValidResourceCondition, resourceCondition, } from "@ctrlplane/validators/resources"; -import type { Tx } from "../common.js"; +import { ColumnOperatorFn } from "../common.js"; import { jobAgent } from "./job-agent.js"; import { system } from "./system.js"; @@ -117,11 +118,25 @@ export const deploymentDependency = pgTable( (t) => ({ uniq: uniqueIndex().on(t.dependsOnId, t.deploymentId) }), ); -export function deploymentMatchSelector( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - tx: Tx, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - metadata?: DeploymentCondition | null, -): SQL | undefined { - throw new Error("Not implemented"); -} +const buildCondition = (cond: DeploymentCondition): SQL => { + if (cond.type === "name") + return ColumnOperatorFn[cond.operator](deployment.name, cond.value); + if (cond.type === "slug") + return ColumnOperatorFn[cond.operator](deployment.slug, cond.value); + if (cond.type === "system") return eq(deployment.systemId, cond.value); + if (cond.type === "id") return eq(deployment.id, cond.value); + + if (cond.conditions.length === 0) return sql`FALSE`; + + const subCon = cond.conditions.map((c) => buildCondition(c)); + const con = + cond.operator === ComparisonOperator.And ? and(...subCon)! : or(...subCon)!; + return cond.not ? not(con) : con; +}; + +export const deploymentMatchSelector = ( + condition?: DeploymentCondition | null, +): SQL | undefined => + condition == null || Object.keys(condition).length === 0 + ? undefined + : buildCondition(condition); diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index 2a700f55f..7d36cedae 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -1,7 +1,5 @@ -import type { - DeploymentCondition, - EnvironmentCondition, -} from "@ctrlplane/validators/jobs"; +import type { DeploymentCondition } from "@ctrlplane/validators/deployments"; +import type { EnvironmentCondition } from "@ctrlplane/validators/jobs"; import type { InferSelectModel } from "drizzle-orm"; import type { Options } from "rrule"; import { sql } from "drizzle-orm"; diff --git a/packages/rule-engine/src/db/get-applicable-policies.ts b/packages/rule-engine/src/db/get-applicable-policies.ts index f45f532ee..94c33db97 100644 --- a/packages/rule-engine/src/db/get-applicable-policies.ts +++ b/packages/rule-engine/src/db/get-applicable-policies.ts @@ -41,7 +41,7 @@ const matchPolicyTargetForResource = async ( .from(schema.deployment) .where( and( - schema.deploymentMatchSelector(db, deploymentSelector), + schema.deploymentMatchSelector(deploymentSelector), eq(schema.deployment.id, deploymentId), ), ) diff --git a/packages/validators/src/deployments/conditions/comparison-condition.ts b/packages/validators/src/deployments/conditions/comparison-condition.ts new file mode 100644 index 000000000..0585b2ffd --- /dev/null +++ b/packages/validators/src/deployments/conditions/comparison-condition.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import type { IdCondition } from "./id-condition.js"; +import type { NameCondition } from "./name-condition.js"; +import type { SlugCondition } from "./slug-condition.js"; +import type { SystemCondition } from "./system-condition.js"; +import { idCondition } from "./id-condition.js"; +import { nameCondition } from "./name-condition.js"; +import { slugCondition } from "./slug-condition.js"; +import { systemCondition } from "./system-condition.js"; + +export const comparisonCondition: z.ZodType = z.lazy(() => + z.object({ + type: z.literal("comparison"), + operator: z.literal("and").or(z.literal("or")), + not: z.boolean().optional().default(false), + conditions: z.array( + z.union([nameCondition, slugCondition, systemCondition, idCondition]), + ), + }), +); + +export type ComparisonCondition = { + type: "comparison"; + operator: "and" | "or"; + not?: boolean; + conditions: Array< + NameCondition | SlugCondition | SystemCondition | IdCondition + >; +}; diff --git a/packages/validators/src/deployments/conditions/deployment-condition.ts b/packages/validators/src/deployments/conditions/deployment-condition.ts new file mode 100644 index 000000000..f6c17821f --- /dev/null +++ b/packages/validators/src/deployments/conditions/deployment-condition.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +import type { ComparisonCondition } from "./comparison-condition.js"; +import type { IdCondition } from "./id-condition.js"; +import type { NameCondition } from "./name-condition.js"; +import type { SlugCondition } from "./slug-condition.js"; +import type { SystemCondition } from "./system-condition.js"; +import { comparisonCondition } from "./comparison-condition.js"; +import { idCondition } from "./id-condition.js"; +import { nameCondition } from "./name-condition.js"; +import { slugCondition } from "./slug-condition.js"; +import { systemCondition } from "./system-condition.js"; + +export type DeploymentCondition = + | ComparisonCondition + | NameCondition + | SlugCondition + | SystemCondition + | IdCondition; + +export const deploymentCondition: z.ZodType = z.lazy(() => + z.union([ + comparisonCondition, + nameCondition, + slugCondition, + systemCondition, + idCondition, + ]), +); diff --git a/packages/validators/src/deployments/conditions/id-condition.ts b/packages/validators/src/deployments/conditions/id-condition.ts new file mode 100644 index 000000000..dfbb24924 --- /dev/null +++ b/packages/validators/src/deployments/conditions/id-condition.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const idCondition = z.object({ + type: z.literal("id"), + operator: z.literal("equals"), + value: z.string(), +}); + +export type IdCondition = z.infer; diff --git a/packages/validators/src/deployments/conditions/index.ts b/packages/validators/src/deployments/conditions/index.ts new file mode 100644 index 000000000..bb8cf97cf --- /dev/null +++ b/packages/validators/src/deployments/conditions/index.ts @@ -0,0 +1,5 @@ +export * from "./comparison-condition.js"; +export * from "./name-condition.js"; +export * from "./slug-condition.js"; +export * from "./deployment-condition.js"; +export * from "./system-condition.js"; diff --git a/packages/validators/src/deployments/conditions/name-condition.ts b/packages/validators/src/deployments/conditions/name-condition.ts new file mode 100644 index 000000000..57fbb0b58 --- /dev/null +++ b/packages/validators/src/deployments/conditions/name-condition.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { columnOperator } from "../../conditions/index.js"; + +export const nameCondition = z.object({ + type: z.literal("name"), + operator: columnOperator, + value: z.string(), +}); + +export type NameCondition = z.infer; diff --git a/packages/validators/src/deployments/conditions/slug-condition.ts b/packages/validators/src/deployments/conditions/slug-condition.ts new file mode 100644 index 000000000..0992d6cb3 --- /dev/null +++ b/packages/validators/src/deployments/conditions/slug-condition.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { columnOperator } from "../../conditions/index.js"; + +export const slugCondition = z.object({ + type: z.literal("slug"), + operator: columnOperator, + value: z.string(), +}); + +export type SlugCondition = z.infer; diff --git a/packages/validators/src/deployments/conditions/system-condition.ts b/packages/validators/src/deployments/conditions/system-condition.ts new file mode 100644 index 000000000..012fefbcc --- /dev/null +++ b/packages/validators/src/deployments/conditions/system-condition.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const systemCondition = z.object({ + type: z.literal("system"), + operator: z.literal("equals"), + value: z.string(), +}); + +export type SystemCondition = z.infer; diff --git a/packages/validators/src/deployments/index.ts b/packages/validators/src/deployments/index.ts index 50d5b984d..69b00a383 100644 --- a/packages/validators/src/deployments/index.ts +++ b/packages/validators/src/deployments/index.ts @@ -18,3 +18,5 @@ export enum StatsOrder { } export const statsOrder = z.nativeEnum(StatsOrder); + +export * from "./conditions/index.js";