import { relations } from "drizzle-orm"; import { pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; import { organization } from "./account"; import { applications } from "./application"; /** * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * database instance for multiple projects. * * @see https://orm.drizzle.team/docs/goodies#multi-project-schema */ export const registryType = pgEnum("RegistryType", ["selfHosted", "cloud"]); export const registry = pgTable("registry", { registryId: text("registryId") .notNull() .primaryKey() .$defaultFn(() => nanoid()), registryName: text("registryName").notNull(), imagePrefix: text("imagePrefix"), username: text("username").notNull(), password: text("password").notNull(), registryUrl: text("registryUrl").notNull().default(""), createdAt: text("createdAt") .notNull() .$defaultFn(() => new Date().toISOString()), registryType: registryType("selfHosted").notNull().default("cloud"), organizationId: text("organizationId") .notNull() .references(() => organization.id, { onDelete: "cascade" }), }); export const registryRelations = relations(registry, ({ many }) => ({ applications: many(applications, { relationName: "applicationRegistry", }), buildApplications: many(applications, { relationName: "applicationBuildRegistry", }), rollbackApplications: many(applications, { relationName: "applicationRollbackRegistry", }), })); // Registry usernames should NOT be lowercased. // Some registries (e.g. AWS ECR) require a specific case: the username must be // exactly "AWS" (uppercase) for ECR authentication. Docker Hub usernames are // case-insensitive for login, so preserving case is safe for all providers. const registryUsernameSchema = z.string().trim().min(1); // Registry URLs must be hostname[:port] only — no shell metacharacters // Empty string is allowed (means default/Docker Hub registry) const registryUrlSchema = z .string() .refine( (val) => val === "" || /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(val), "Registry URL must be a valid hostname or hostname:port (e.g. registry.example.com or localhost:5000)", ); const createSchema = createInsertSchema(registry, { registryName: z.string().min(1), username: registryUsernameSchema, password: z.string().min(1), registryUrl: registryUrlSchema, organizationId: z.string().min(1), registryId: z.string().min(1), registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), }); export const apiCreateRegistry = createSchema .pick({}) .extend({ registryName: z.string().min(1), username: registryUsernameSchema, password: z.string().min(1), registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), }) .required() .extend({ serverId: z.string().optional(), }); export const apiTestRegistry = createSchema.pick({}).extend({ registryName: z.string().optional(), username: registryUsernameSchema, password: z.string().min(1), registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), serverId: z.string().optional(), }); export const apiTestRegistryById = createSchema .pick({ registryId: true, }) .extend({ serverId: z.string().optional(), }); export const apiRemoveRegistry = createSchema .pick({ registryId: true, }) .required(); export const apiFindOneRegistry = z.object({ registryId: z.string().min(1), }); export const apiUpdateRegistry = createSchema.partial().extend({ registryId: z.string().min(1), serverId: z.string().optional(), }); export const apiEnableSelfHostedRegistry = createSchema .pick({ registryUrl: true, username: true, password: true, }) .required();