diff --git a/apps/event-worker/src/releases/evaluate/index.ts b/apps/event-worker/src/releases/evaluate/index.ts new file mode 100644 index 000000000..5c1a9dc2e --- /dev/null +++ b/apps/event-worker/src/releases/evaluate/index.ts @@ -0,0 +1,28 @@ +import type { ReleaseEvaluateEvent } from "@ctrlplane/validators/events"; +import { Worker } from "bullmq"; +import _ from "lodash"; + +import { db } from "@ctrlplane/db/client"; +import { evaluate } from "@ctrlplane/rule-engine"; +import { createCtx, getApplicablePolicies } from "@ctrlplane/rule-engine/db"; +import { Channel } from "@ctrlplane/validators/events"; + +export const createReleaseEvaluateWorker = () => + new Worker(Channel.ReleaseEvaluate, async (job) => { + job.log( + `Evaluating release for deployment ${job.data.deploymentId} and resource ${job.data.resourceId}`, + ); + + const ctx = await createCtx(db, job.data); + if (ctx == null) { + job.log( + `Resource ${job.data.resourceId} not found for deployment ${job.data.deploymentId} and environment ${job.data.environmentId}`, + ); + return; + } + + const { workspaceId } = ctx.resource; + const policy = await getApplicablePolicies(db, workspaceId, job.data); + const result = await evaluate(policy, [], ctx); + console.log(result); + }); 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/environment.ts b/packages/db/src/schema/environment.ts index b74a788a0..ae43aeb24 100644 --- a/packages/db/src/schema/environment.ts +++ b/packages/db/src/schema/environment.ts @@ -1,8 +1,9 @@ -import type { EnvironmentCondition } from "@ctrlplane/validators/jobs"; +import type { MetadataCondition } from "@ctrlplane/validators/conditions"; +import type { EnvironmentCondition } from "@ctrlplane/validators/environments"; import type { ResourceCondition } from "@ctrlplane/validators/resources"; import type { InferSelectModel, SQL } from "drizzle-orm"; import type { AnyPgColumn, ColumnsWithTable } from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; +import { and, eq, exists, ilike, not, notExists, or, sql } from "drizzle-orm"; import { bigint, foreignKey, @@ -18,12 +19,17 @@ import { import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; +import { + ComparisonOperator, + MetadataOperator, +} from "@ctrlplane/validators/conditions"; import { isValidResourceCondition, resourceCondition, } from "@ctrlplane/validators/resources"; import type { Tx } from "../common.js"; +import { ColumnOperatorFn } from "../common.js"; import { user } from "./auth.js"; import { deploymentVersion } from "./deployment-version.js"; import { system } from "./system.js"; @@ -280,11 +286,114 @@ export type EnvironmentPolicyApproval = InferSelectModel< typeof environmentPolicyApproval >; +const buildMetadataCondition = (tx: Tx, cond: MetadataCondition): SQL => { + if (cond.operator === MetadataOperator.Null) + return notExists( + tx + .select({ value: sql`1` }) + .from(environmentMetadata) + .where( + and( + eq(environmentMetadata.environmentId, environment.id), + eq(environmentMetadata.key, cond.key), + ), + ), + ); + + if (cond.operator === MetadataOperator.StartsWith) + return exists( + tx + .select({ value: sql`1` }) + .from(environmentMetadata) + .where( + and( + eq(environmentMetadata.environmentId, environment.id), + eq(environmentMetadata.key, cond.key), + ilike(environmentMetadata.value, `${cond.value}%`), + ), + ), + ); + + if (cond.operator === MetadataOperator.EndsWith) + return exists( + tx + .select({ value: sql`1` }) + .from(environmentMetadata) + .where( + and( + eq(environmentMetadata.environmentId, environment.id), + eq(environmentMetadata.key, cond.key), + ilike(environmentMetadata.value, `%${cond.value}`), + ), + ), + ); + + if (cond.operator === MetadataOperator.Contains) + return exists( + tx + .select({ value: sql`1` }) + .from(environmentMetadata) + .where( + and( + eq(environmentMetadata.environmentId, environment.id), + eq(environmentMetadata.key, cond.key), + ilike(environmentMetadata.value, `%${cond.value}%`), + ), + ), + ); + + if ("value" in cond) + return exists( + tx + .select({ value: sql`1` }) + .from(environmentMetadata) + .where( + and( + eq(environmentMetadata.environmentId, environment.id), + eq(environmentMetadata.key, cond.key), + eq(environmentMetadata.value, cond.value), + ), + ), + ); + + throw Error("invalid metadata conditions"); +}; + +const buildCondition = ( + tx: Tx, + condition: EnvironmentCondition, +): SQL => { + if (condition.type === "name") + return ColumnOperatorFn[condition.operator]( + environment.name, + condition.value, + ); + if (condition.type === "directory") + return ColumnOperatorFn[condition.operator]( + environment.directory, + condition.value, + ); + if (condition.type === "system") + return eq(environment.systemId, condition.value); + if (condition.type === "id") return eq(environment.id, condition.value); + if (condition.type === "metadata") + return buildMetadataCondition(tx, condition); + + if (condition.conditions.length === 0) return sql`FALSE`; + + const subCon = condition.conditions.map((c) => buildCondition(tx, c)); + const con = + condition.operator === ComparisonOperator.And + ? and(...subCon)! + : or(...subCon)!; + return condition.not ? not(con) : con; +}; + export function environmentMatchSelector( - // eslint-disable-next-line @typescript-eslint/no-unused-vars tx: Tx, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - metadata?: EnvironmentCondition | null, + condition?: EnvironmentCondition | null, ): SQL | undefined { - throw new Error("Not implemented"); + return condition == null || Object.keys(condition).length === 0 + ? undefined + : buildCondition(tx, condition); } diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index 04f82a0b6..782396e4f 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -1,3 +1,5 @@ +import type { DeploymentCondition } from "@ctrlplane/validators/deployments"; +import type { EnvironmentCondition } from "@ctrlplane/validators/environments"; import type { InferSelectModel } from "drizzle-orm"; import type { Options } from "rrule"; import { sql } from "drizzle-orm"; @@ -13,6 +15,9 @@ import { import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; +import { deploymentCondition } from "@ctrlplane/validators/deployments"; +import { environmentCondition } from "@ctrlplane/validators/environments"; + import { workspace } from "./workspace.js"; export const policy = pgTable("policy", { @@ -38,8 +43,12 @@ export const policyTarget = pgTable("policy_target", { policyId: uuid("policy_id") .notNull() .references(() => policy.id, { onDelete: "cascade" }), - deploymentSelector: jsonb("deployment_selector").default(sql`NULL`), - environmentSelector: jsonb("environment_selector").default(sql`NULL`), + deploymentSelector: jsonb("deployment_selector") + .default(sql`NULL`) + .$type(), + environmentSelector: jsonb("environment_selector") + .default(sql`NULL`) + .$type(), }); export const policyRuleDenyWindow = pgTable("policy_rule_deny_window", { @@ -74,8 +83,8 @@ const policyInsertSchema = createInsertSchema(policy, { const policyTargetInsertSchema = createInsertSchema(policyTarget, { policyId: z.string().uuid(), - deploymentSelector: z.record(z.any()).optional(), - environmentSelector: z.record(z.any()).optional(), + deploymentSelector: deploymentCondition.nullable(), + environmentSelector: environmentCondition.nullable(), }).omit({ id: true }); // Define the structure of RRule Options for Zod validation diff --git a/packages/rule-engine/package.json b/packages/rule-engine/package.json index d2b62d4f5..aad5aac77 100644 --- a/packages/rule-engine/package.json +++ b/packages/rule-engine/package.json @@ -30,6 +30,7 @@ "date-fns": "^4.1.0", "lodash": "catalog:", "rrule": "^2.8.1", + "ts-is-present": "catalog:", "zod": "catalog:" }, "devDependencies": { diff --git a/packages/rule-engine/src/db/get-applicable-policies.ts b/packages/rule-engine/src/db/get-applicable-policies.ts new file mode 100644 index 000000000..1c27da039 --- /dev/null +++ b/packages/rule-engine/src/db/get-applicable-policies.ts @@ -0,0 +1,127 @@ +import type { Tx } from "@ctrlplane/db"; +import { isPresent } from "ts-is-present"; + +import { and, desc, eq, takeFirstOrNull } from "@ctrlplane/db"; +import { db } from "@ctrlplane/db/client"; +import * as schema from "@ctrlplane/db/schema"; + +import type { ReleaseRepository } from "../types.js"; + +/** + * Checks if a policy target matches a given release repository by evaluating + * deployment and environment selectors. + * + * The function handles three cases: + * 1. Both environment and deployment match their respective selectors (most + * specific match) + * 2. Environment matches and there is no deployment selector (policy applies to + * all deployments in environment) + * 3. Deployment matches and there is no environment selector (policy applies + * across all environments) + * + * @param tx - Database transaction object for querying + * @param repo - Repository containing deployment and environment IDs to match + * against + * @param target - Policy target containing deployment and environment selectors + * @returns Promise resolving to matching environment and deployment if target + * matches, null otherwise + */ +const matchPolicyTargetForResource = async ( + tx: Tx, + repo: ReleaseRepository, + target: schema.PolicyTarget, +) => { + const { deploymentSelector, environmentSelector } = target; + const { deploymentId, environmentId } = repo; + const deploymentsQuery = + deploymentSelector == null + ? null + : tx + .select() + .from(schema.deployment) + .where( + and( + schema.deploymentMatchSelector(deploymentSelector), + eq(schema.deployment.id, deploymentId), + ), + ) + .then(takeFirstOrNull); + + const envQuery = + environmentSelector == null + ? null + : tx + .select() + .from(schema.environment) + .where( + and( + schema.environmentMatchSelector(db, environmentSelector), + eq(schema.environment.id, environmentId), + ), + ) + .then(takeFirstOrNull); + + const [deployments, env] = await Promise.all([deploymentsQuery, envQuery]); + const hasDeploymentSelector = deploymentSelector != null; + const hasDeployment = deployments != null; + + const hasEnvironmentSelector = environmentSelector != null; + const hasEnvironment = env != null; + + // Case 1: Both environment and deployment match their respective + // selectors This is the most specific match where both selectors exist + // and match + if (hasEnvironment && hasDeployment) { + return { environment: env, deployment: deployments }; + } + + // Case 2: Environment matches and there is no deployment selector This + // means the policy applies to all deployments in this environment + if (hasEnvironment && !hasDeploymentSelector) { + return { environment: env, deployment: null }; + } + + // Case 3: Deployment matches and there is no environment selector This + // means the policy applies to this deployment across all environments + if (hasDeployment && !hasEnvironmentSelector) { + return { environment: null, deployment: deployments }; + } + + return null; +}; + +/** + * Gets applicable policies for a given workspace and release repository. + * Filters policies based on deployment and environment selectors matching the + * repository. + * + * NOTE: This currently iterates through every policy in the workspace to find + * matches. For workspaces with many policies, we may need to add caching or + * optimize the query pattern to improve performance. + * + * @param tx - Database transaction object for querying + * @param workspaceId - ID of the workspace to get policies for + * @param repo - Repository containing deployment, environment and resource IDs + * @returns Promise resolving to array of matching policies with their targets + * and deny windows + */ +export const getApplicablePolicies = async ( + tx: Tx, + workspaceId: string, + repo: ReleaseRepository, +) => { + const policy = await tx.query.policy.findMany({ + where: eq(schema.policy.workspaceId, workspaceId), + with: { targets: true, denyWindows: true }, + orderBy: [desc(schema.policy.priority)], + }); + + return Promise.all( + policy.map(async (p) => { + const matches = await Promise.all( + p.targets.map((t) => matchPolicyTargetForResource(tx, repo, t)), + ); + if (matches.some((match) => match !== null)) return p; + }), + ).then((policies) => policies.filter(isPresent)); +}; diff --git a/packages/rule-engine/src/db/index.ts b/packages/rule-engine/src/db/index.ts index cfb350015..6ddf0bee3 100644 --- a/packages/rule-engine/src/db/index.ts +++ b/packages/rule-engine/src/db/index.ts @@ -1 +1,2 @@ export * from "./create-ctx.js"; +export * from "./get-applicable-policies.js"; diff --git a/packages/validators/package.json b/packages/validators/package.json index d5ea6eeb7..76c2c949a 100644 --- a/packages/validators/package.json +++ b/packages/validators/package.json @@ -51,6 +51,10 @@ "./deployments": { "types": "./src/deployments/index.ts", "default": "./dist/deployments/index.js" + }, + "./environments": { + "types": "./src/environments/index.ts", + "default": "./dist/environments/index.js" } }, "license": "MIT", diff --git a/packages/validators/src/conditions/id-condition.ts b/packages/validators/src/conditions/id-condition.ts new file mode 100644 index 000000000..dfbb24924 --- /dev/null +++ b/packages/validators/src/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/conditions/index.ts b/packages/validators/src/conditions/index.ts index 3c464b644..fb55063e7 100644 --- a/packages/validators/src/conditions/index.ts +++ b/packages/validators/src/conditions/index.ts @@ -2,6 +2,8 @@ import { z } from "zod"; export * from "./metadata-condition.js"; export * from "./date-condition.js"; +export * from "./id-condition.js"; +export * from "./system-condition.js"; export enum ColumnOperator { Equals = "equals", diff --git a/packages/validators/src/conditions/name-condition.ts b/packages/validators/src/conditions/name-condition.ts new file mode 100644 index 000000000..0ecb0e501 --- /dev/null +++ b/packages/validators/src/conditions/name-condition.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { columnOperator } from "./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/conditions/system-condition.ts b/packages/validators/src/conditions/system-condition.ts new file mode 100644 index 000000000..012fefbcc --- /dev/null +++ b/packages/validators/src/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/conditions/comparison-condition.ts b/packages/validators/src/deployments/conditions/comparison-condition.ts new file mode 100644 index 000000000..781399b79 --- /dev/null +++ b/packages/validators/src/deployments/conditions/comparison-condition.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +import type { IdCondition } from "../../conditions/index.js"; +import type { NameCondition } from "../../conditions/name-condition.js"; +import type { SystemCondition } from "../../conditions/system-condition.js"; +import type { SlugCondition } from "./slug-condition.js"; +import { idCondition } from "../../conditions/index.js"; +import { nameCondition } from "../../conditions/name-condition.js"; +import { systemCondition } from "../../conditions/system-condition.js"; +import { slugCondition } from "./slug-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..6f0e82708 --- /dev/null +++ b/packages/validators/src/deployments/conditions/deployment-condition.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +import type { IdCondition } from "../../conditions/index.js"; +import type { NameCondition } from "../../conditions/name-condition.js"; +import type { SystemCondition } from "../../conditions/system-condition.js"; +import type { ComparisonCondition } from "./comparison-condition.js"; +import type { SlugCondition } from "./slug-condition.js"; +import { idCondition } from "../../conditions/index.js"; +import { nameCondition } from "../../conditions/name-condition.js"; +import { systemCondition } from "../../conditions/system-condition.js"; +import { comparisonCondition } from "./comparison-condition.js"; +import { slugCondition } from "./slug-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/index.ts b/packages/validators/src/deployments/conditions/index.ts new file mode 100644 index 000000000..9a8720aa3 --- /dev/null +++ b/packages/validators/src/deployments/conditions/index.ts @@ -0,0 +1,3 @@ +export * from "./comparison-condition.js"; +export * from "./slug-condition.js"; +export * from "./deployment-condition.js"; 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/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"; diff --git a/packages/validators/src/environments/comparison-condition.ts b/packages/validators/src/environments/comparison-condition.ts new file mode 100644 index 000000000..c598cba88 --- /dev/null +++ b/packages/validators/src/environments/comparison-condition.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +import type { IdCondition } from "../conditions/index.js"; +import type { MetadataCondition } from "../conditions/metadata-condition.js"; +import type { NameCondition } from "../conditions/name-condition.js"; +import type { SystemCondition } from "../conditions/system-condition.js"; +import type { DirectoryCondition } from "./directory-condition.js"; +import { idCondition } from "../conditions/index.js"; +import { metadataCondition } from "../conditions/metadata-condition.js"; +import { nameCondition } from "../conditions/name-condition.js"; +import { systemCondition } from "../conditions/system-condition.js"; +import { directoryCondition } from "./directory-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, + directoryCondition, + systemCondition, + idCondition, + metadataCondition, + ]), + ), + }), +); + +export type ComparisonCondition = { + type: "comparison"; + operator: "and" | "or"; + not?: boolean; + conditions: Array< + | NameCondition + | DirectoryCondition + | SystemCondition + | IdCondition + | MetadataCondition + >; +}; diff --git a/packages/validators/src/environments/directory-condition.ts b/packages/validators/src/environments/directory-condition.ts new file mode 100644 index 000000000..8ca26bf23 --- /dev/null +++ b/packages/validators/src/environments/directory-condition.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { columnOperator } from "../conditions/index.js"; + +export const directoryCondition = z.object({ + type: z.literal("directory"), + operator: columnOperator, + value: z.string(), +}); + +export type DirectoryCondition = z.infer; diff --git a/packages/validators/src/environments/environment-condition.ts b/packages/validators/src/environments/environment-condition.ts new file mode 100644 index 000000000..9f413668d --- /dev/null +++ b/packages/validators/src/environments/environment-condition.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +import type { IdCondition } from "../conditions/index.js"; +import type { MetadataCondition } from "../conditions/metadata-condition.js"; +import type { NameCondition } from "../conditions/name-condition.js"; +import type { SystemCondition } from "../conditions/system-condition.js"; +import type { ComparisonCondition } from "./comparison-condition.js"; +import type { DirectoryCondition } from "./directory-condition.js"; +import { idCondition } from "../conditions/index.js"; +import { metadataCondition } from "../conditions/metadata-condition.js"; +import { nameCondition } from "../conditions/name-condition.js"; +import { systemCondition } from "../conditions/system-condition.js"; +import { comparisonCondition } from "./comparison-condition.js"; +import { directoryCondition } from "./directory-condition.js"; + +export type EnvironmentCondition = + | ComparisonCondition + | NameCondition + | SystemCondition + | DirectoryCondition + | IdCondition + | MetadataCondition; + +export const environmentCondition: z.ZodType = z.lazy( + () => + z.union([ + comparisonCondition, + nameCondition, + systemCondition, + directoryCondition, + idCondition, + metadataCondition, + ]), +); diff --git a/packages/validators/src/environments/index.ts b/packages/validators/src/environments/index.ts new file mode 100644 index 000000000..fcd6ce628 --- /dev/null +++ b/packages/validators/src/environments/index.ts @@ -0,0 +1,2 @@ +export * from "./comparison-condition.js"; +export * from "./environment-condition.js"; diff --git a/packages/validators/src/events/index.ts b/packages/validators/src/events/index.ts index 810675de4..57a46449b 100644 --- a/packages/validators/src/events/index.ts +++ b/packages/validators/src/events/index.ts @@ -6,6 +6,7 @@ export enum Channel { JobSync = "job-sync", DispatchJob = "dispatch-job", ResourceScan = "resource-scan", + ReleaseEvaluate = "release-evaluate", } export const resourceScanEvent = z.object({ resourceProviderId: z.string() }); @@ -18,3 +19,10 @@ export type DispatchJobEvent = z.infer; export const jobSyncEvent = z.object({ jobId: z.string() }); export type JobSyncEvent = z.infer; + +export const releaseEvaluateEvent = z.object({ + deploymentId: z.string(), + environmentId: z.string(), + resourceId: z.string(), +}); +export type ReleaseEvaluateEvent = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8ce927fd..166bd393b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1244,9 +1244,15 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + lodash: + specifier: 'catalog:' + version: 4.17.21 rrule: specifier: ^2.8.1 version: 2.8.1 + ts-is-present: + specifier: 'catalog:' + version: 1.2.2 zod: specifier: 'catalog:' version: 3.24.2