import { Result } from "neverthrow"; import { z } from "zod"; export const EnvSlugSchema = z.enum(["dev", "stg", "prod", "preview"]); export type EnvSlug = z.infer; export const ALL_ENV_SLUGS: EnvSlug[] = ["dev", "stg", "prod", "preview"]; const safeJsonParse = Result.fromThrowable( (val: string) => JSON.parse(val) as unknown, () => null ); /** * Zod transform for form fields that submit JSON-encoded arrays. * Parses the string as JSON and returns the array, or null if invalid. */ export const jsonArrayField = z .string() .optional() .transform((val) => { if (!val) return null; return safeJsonParse(val).match( (parsed) => (Array.isArray(parsed) ? parsed : null), () => null ); }); /** * Zod transform for form fields that submit JSON-encoded EnvSlug arrays. * Parses the string as JSON and validates each element is a valid EnvSlug. * Invalid elements are filtered out rather than rejecting the whole array. */ export const envSlugArrayField = z .string() .optional() .transform((val): EnvSlug[] | null => { if (!val) return null; return safeJsonParse(val).match( (parsed) => { if (!Array.isArray(parsed)) return null; return parsed.filter((item): item is EnvSlug => EnvSlugSchema.safeParse(item).success); }, () => null ); }); export const VercelIntegrationConfigSchema = z.object({ atomicBuilds: z.array(EnvSlugSchema).nullable().optional(), pullEnvVarsBeforeBuild: z.array(EnvSlugSchema).nullable().optional(), /** Maps a custom Vercel environment to Trigger.dev's staging environment. */ vercelStagingEnvironment: z .object({ environmentId: z.string(), displayName: z.string(), }) .nullable() .optional(), discoverEnvVars: z.array(EnvSlugSchema).nullable().optional(), autoPromote: z.boolean().optional().default(true), }); export type VercelIntegrationConfig = z.infer; export const TriggerEnvironmentType = z.enum(["PRODUCTION", "STAGING", "PREVIEW", "DEVELOPMENT"]); export type TriggerEnvironmentType = z.infer; /** * Per-environment, per-variable sync settings. * Missing env slug = sync all vars. Missing var in env = sync by default. * Only explicitly `false` entries disable sync. */ export const SyncEnvVarsMappingSchema = z .record(EnvSlugSchema, z.record(z.string(), z.boolean())) .default({}); export type SyncEnvVarsMapping = z.infer; export const VercelProjectIntegrationDataSchema = z.object({ config: VercelIntegrationConfigSchema, syncEnvVarsMapping: SyncEnvVarsMappingSchema, vercelProjectName: z.string(), vercelTeamId: z.string().nullable(), vercelTeamSlug: z.string().optional(), vercelProjectId: z.string(), onboardingCompleted: z.boolean().optional(), onboardingOrigin: z.enum(["marketplace", "dashboard"]).optional(), }); export type VercelProjectIntegrationData = z.infer; export function createDefaultVercelIntegrationData( vercelProjectId: string, vercelProjectName: string, vercelTeamId: string | null, vercelTeamSlug?: string ): VercelProjectIntegrationData { return { config: { atomicBuilds: ["prod"], pullEnvVarsBeforeBuild: ["prod", "preview"], discoverEnvVars: ["prod", "preview"], vercelStagingEnvironment: null, autoPromote: true, }, syncEnvVarsMapping: {}, vercelProjectId, vercelProjectName, vercelTeamId, vercelTeamSlug, }; } /** * Maps a Trigger.dev environment type to its Vercel target identifier(s). * Returns null for STAGING when no custom environment is configured. */ export function envTypeToVercelTarget( envType: TriggerEnvironmentType, stagingEnvironmentId?: string | null ): string[] | null { switch (envType) { case "PRODUCTION": return ["production"]; case "STAGING": return stagingEnvironmentId ? [stagingEnvironmentId] : null; case "PREVIEW": return ["preview"]; case "DEVELOPMENT": return ["development"]; } } export function getAvailableEnvSlugs( hasStagingEnvironment: boolean, hasPreviewEnvironment: boolean ): EnvSlug[] { return ALL_ENV_SLUGS.filter((s) => { if (s === "stg" && !hasStagingEnvironment) return false; if (s === "preview" && !hasPreviewEnvironment) return false; return true; }); } export function getAvailableEnvSlugsForBuildSettings( hasStagingEnvironment: boolean, hasPreviewEnvironment: boolean ): EnvSlug[] { return getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment).filter( (s) => s !== "dev" ); } export function isDiscoverEnvVarsEnabledForEnvironment( discoverEnvVars: EnvSlug[] | null | undefined, environmentType: TriggerEnvironmentType ): boolean { if (!discoverEnvVars || discoverEnvVars.length === 0) { return false; } const envSlug = envTypeToSlug(environmentType); return discoverEnvVars.includes(envSlug); } export function envTypeToSlug(environmentType: TriggerEnvironmentType): EnvSlug { switch (environmentType) { case "DEVELOPMENT": return "dev"; case "STAGING": return "stg"; case "PRODUCTION": return "prod"; case "PREVIEW": return "preview"; } } export function envSlugToType(slug: EnvSlug): TriggerEnvironmentType { switch (slug) { case "dev": return "DEVELOPMENT"; case "stg": return "STAGING"; case "prod": return "PRODUCTION"; case "preview": return "PREVIEW"; } } export function shouldSyncEnvVar( mapping: SyncEnvVarsMapping, envVarName: string, environmentType: TriggerEnvironmentType ): boolean { const envSlug = envTypeToSlug(environmentType); const envSettings = mapping[envSlug]; if (!envSettings) { return true; } return envSettings[envVarName] !== false; } export function shouldSyncEnvVarForAnyEnvironment( mapping: SyncEnvVarsMapping, envVarName: string ): boolean { for (const slug of ALL_ENV_SLUGS) { const envSettings = mapping[slug]; if (!envSettings) { return true; } if (envSettings[envVarName] !== false) { return true; } } return false; } export function buildVercelDeploymentUrl( vercelTeamSlug: string, vercelProjectName: string, integrationDeploymentId: string ): string { const vercelId = integrationDeploymentId.replace(/^dpl_/, ""); return `https://vercel.com/${vercelTeamSlug}/${vercelProjectName}/${vercelId}`; } export function isPullEnvVarsEnabledForEnvironment( pullEnvVarsBeforeBuild: EnvSlug[] | null | undefined, environmentType: TriggerEnvironmentType ): boolean { if (!pullEnvVarsBeforeBuild || pullEnvVarsBeforeBuild.length === 0) { return false; } const envSlug = envTypeToSlug(environmentType); return pullEnvVarsBeforeBuild.includes(envSlug); } export function isAtomicBuildsEnabledForEnvironment( atomicBuilds: EnvSlug[] | null | undefined, environmentType: TriggerEnvironmentType ): boolean { if (!atomicBuilds || atomicBuilds.length === 0) { return false; } const envSlug = envTypeToSlug(environmentType); return atomicBuilds.includes(envSlug); }