Merge branch 'canary' into fix/openapi-bigint-serialization

This commit is contained in:
Mauricio Siu
2026-04-04 23:06:07 -06:00
560 changed files with 210245 additions and 31666 deletions

View File

@@ -1,24 +1,40 @@
import { and, eq } from "drizzle-orm";
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { dbUrl } from "./constants";
import * as schema from "./schema";
declare global {
var db: PostgresJsDatabase<typeof schema> | undefined;
}
export { and, eq };
export * from "./schema";
type Database = PostgresJsDatabase<typeof schema>;
/**
* Evita problemas de redeclaración global en monorepos.
* No usamos `declare global`.
*/
const globalForDb = globalThis as unknown as {
db?: Database;
};
let dbConnection: Database;
export let db: PostgresJsDatabase<typeof schema>;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(dbUrl), {
// En producción no usamos global cache
dbConnection = drizzle(postgres(dbUrl), {
schema,
});
} else {
if (!global.db)
global.db = drizzle(postgres(dbUrl), {
// En desarrollo reutilizamos conexión para evitar múltiples conexiones
if (!globalForDb.db) {
globalForDb.db = drizzle(postgres(dbUrl), {
schema,
});
}
db = global.db;
dbConnection = globalForDb.db;
}
export const db: Database = dbConnection;
export { dbUrl };

View File

@@ -1,6 +1,7 @@
import { relations, sql } from "drizzle-orm";
import {
boolean,
index,
integer,
pgTable,
text,
@@ -69,6 +70,36 @@ export const organization = pgTable("organization", {
.references(() => user.id, { onDelete: "cascade" }),
});
export const organizationRole = pgTable(
"organization_role",
{
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
role: text("role").notNull(),
permission: text("permission").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
},
(table) => [
index("organizationRole_organizationId_idx").on(table.organizationId),
index("organizationRole_role_idx").on(table.role),
],
);
export const organizationRoleRelations = relations(
organizationRole,
({ one }) => ({
organization: one(organization, {
fields: [organizationRole.organizationId],
references: [organization.id],
}),
}),
);
export const organizationRelations = relations(
organization,
({ one, many }) => ({
@@ -80,6 +111,7 @@ export const organizationRelations = relations(
projects: many(projects),
members: many(member),
ssoProviders: many(ssoProvider),
roles: many(organizationRole),
}),
);
@@ -93,7 +125,9 @@ export const member = pgTable("member", {
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
role: text("role")
.notNull()
.$type<"owner" | "member" | "admin" | (string & {})>(),
createdAt: timestamp("created_at").notNull(),
teamId: text("team_id"),
isDefault: boolean("is_default").notNull().default(false),
@@ -129,6 +163,10 @@ export const member = pgTable("member", {
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedGitProviders: text("accessedGitProviders")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
});
export const memberRelations = relations(member, ({ one }) => ({
@@ -148,7 +186,7 @@ export const invitation = pgTable("invitation", {
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
email: text("email").notNull(),
role: text("role").$type<"owner" | "member" | "admin">(),
role: text("role").$type<"owner" | "member" | "admin" | (string & {})>(),
status: text("status").notNull(),
expiresAt: timestamp("expires_at").notNull(),
inviterId: text("inviter_id")
@@ -180,7 +218,8 @@ export const apikey = pgTable("apikey", {
start: text("start"),
prefix: text("prefix"),
key: text("key").notNull(),
userId: text("user_id")
configId: text("config_id").default("default").notNull(),
referenceId: text("reference_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
refillInterval: integer("refill_interval"),
@@ -202,7 +241,7 @@ export const apikey = pgTable("apikey", {
export const apikeyRelations = relations(apikey, ({ one }) => ({
user: one(user, {
fields: [apikey.userId],
fields: [apikey.referenceId],
references: [user.id],
}),
}));

View File

@@ -115,6 +115,7 @@ export const applications = pgTable("application", {
subtitle: text("subtitle"),
command: text("command"),
args: text("args").array(),
icon: text("icon"),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"),
cleanCache: boolean("cleanCache").default(false),
@@ -331,6 +332,7 @@ const createSchema = createInsertSchema(applications, {
sourceType: z
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
.optional(),
triggerType: z.enum(["push", "tag"]).optional(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
buildType: z.enum([
"dockerfile",
@@ -364,12 +366,18 @@ const createSchema = createInsertSchema(applications, {
previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(),
watchPaths: z.array(z.string()).optional().optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.number().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
enableSubmodules: z.boolean().optional(),
icon: z
.string()
.max(2 * 1024 * 1024, "Icon must be less than 2MB")
.nullable()
.optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -380,11 +388,9 @@ export const apiCreateApplication = createSchema.pick({
serverId: true,
});
export const apiFindOneApplication = createSchema
.pick({
applicationId: true,
})
.required();
export const apiFindOneApplication = z.object({
applicationId: z.string().min(1),
});
export const apiDeployApplication = createSchema
.pick({
@@ -434,13 +440,13 @@ export const apiSaveGithubProvider = createSchema
owner: true,
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.extend({
triggerType: z.enum(["push", "tag"]).default("push"),
});
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveGitlabProvider = createSchema
.pick({
@@ -452,10 +458,9 @@ export const apiSaveGitlabProvider = createSchema
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveBitbucketProvider = createSchema
.pick({
@@ -466,10 +471,9 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketRepositorySlug: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveGiteaProvider = createSchema
.pick({
@@ -479,10 +483,9 @@ export const apiSaveGiteaProvider = createSchema
giteaOwner: true,
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveDockerProvider = createSchema
.pick({
@@ -507,6 +510,7 @@ export const apiSaveGitProvider = createSchema
.merge(
createSchema.pick({
customGitSSHKeyId: true,
enableSubmodules: true,
}),
);
@@ -520,11 +524,9 @@ export const apiSaveEnvironmentVariables = createSchema
})
.required();
export const apiFindMonitoringStats = createSchema
.pick({
appName: true,
})
.required();
export const apiFindMonitoringStats = z.object({
appName: z.string().min(1),
});
export const apiUpdateApplication = createSchema
.partial()

View File

@@ -0,0 +1,94 @@
import { relations } from "drizzle-orm";
import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { organization } from "./account";
import { user } from "./user";
export const auditLog = pgTable(
"audit_log",
{
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
organizationId: text("organization_id").references(() => organization.id, {
onDelete: "set null",
}),
userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
userEmail: text("user_email").notNull(),
userRole: text("user_role").notNull(),
action: text("action").notNull(),
resourceType: text("resource_type").notNull(),
resourceId: text("resource_id"),
resourceName: text("resource_name"),
metadata: text("metadata"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(t) => ({
orgIdx: index("auditLog_organizationId_idx").on(t.organizationId),
userIdx: index("auditLog_userId_idx").on(t.userId),
createdAtIdx: index("auditLog_createdAt_idx").on(t.createdAt),
}),
);
export const auditLogRelations = relations(auditLog, ({ one }) => ({
organization: one(organization, {
fields: [auditLog.organizationId],
references: [organization.id],
}),
user: one(user, {
fields: [auditLog.userId],
references: [user.id],
}),
}));
export type AuditLog = typeof auditLog.$inferSelect;
export type NewAuditLog = typeof auditLog.$inferInsert;
export type AuditAction =
| "create"
| "update"
| "delete"
| "deploy"
| "cancel"
| "redeploy"
| "login"
| "logout"
| "restore"
| "run"
| "start"
| "stop"
| "reload"
| "rebuild"
| "move";
export type AuditResourceType =
| "project"
| "service"
| "environment"
| "deployment"
| "user"
| "customRole"
| "domain"
| "certificate"
| "registry"
| "server"
| "sshKey"
| "gitProvider"
| "destination"
| "notification"
| "settings"
| "session"
| "port"
| "redirect"
| "security"
| "schedule"
| "backup"
| "volumeBackup"
| "docker"
| "swarm"
| "previewDeployment"
| "organization"
| "cluster"
| "mount"
| "application"
| "compose";

View File

@@ -15,6 +15,7 @@ import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -26,6 +27,7 @@ export const databaseType = pgEnum("databaseType", [
"mysql",
"mongo",
"web-server",
"libsql",
]);
export const backupType = pgEnum("backupType", ["database", "compose"]);
@@ -74,6 +76,9 @@ export const backups = pgTable("backup", {
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references((): AnyPgColumn => libsql.libsqlId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => user.id),
// Only for compose backups
metadata: jsonb("metadata").$type<
@@ -118,6 +123,10 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
fields: [backups.mongoId],
references: [mongo.mongoId],
}),
libsql: one(libsql, {
fields: [backups.libsqlId],
references: [libsql.libsqlId],
}),
user: one(user, {
fields: [backups.userId],
references: [user.id],
@@ -137,11 +146,19 @@ const createSchema = createInsertSchema(backups, {
database: z.string().min(1),
schedule: z.string(),
keepLatestCount: z.number().optional(),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
databaseType: z.enum([
"postgres",
"mariadb",
"mysql",
"mongo",
"web-server",
"libsql",
]),
postgresId: z.string().optional(),
mariadbId: z.string().optional(),
mysqlId: z.string().optional(),
mongoId: z.string().optional(),
libsqlId: z.string().optional(),
userId: z.string().optional(),
metadata: z.any().optional(),
});
@@ -157,6 +174,7 @@ export const apiCreateBackup = createSchema.pick({
mysqlId: true,
postgresId: true,
mongoId: true,
libsqlId: true,
databaseType: true,
userId: true,
backupType: true,
@@ -165,11 +183,9 @@ export const apiCreateBackup = createSchema.pick({
metadata: true,
});
export const apiFindOneBackup = createSchema
.pick({
backupId: true,
})
.required();
export const apiFindOneBackup = z.object({
backupId: z.string().min(1),
});
export const apiRemoveBackup = createSchema
.pick({
@@ -194,7 +210,14 @@ export const apiUpdateBackup = createSchema
export const apiRestoreBackup = z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
databaseType: z.enum([
"postgres",
"mysql",
"mariadb",
"mongo",
"web-server",
"libsql",
]),
backupType: z.enum(["database", "compose"]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),

View File

@@ -56,7 +56,6 @@ export const apiUpdateCertificate = z.object({
name: z.string().min(1).optional(),
certificateData: z.string().min(1).optional(),
privateKey: z.string().min(1).optional(),
autoRenew: z.boolean().optional(),
});
export const apiDeleteCertificate = z.object({

View File

@@ -164,6 +164,11 @@ const createSchema = createInsertSchema(compose, {
composePath: z.string().min(1),
composeType: z.enum(["docker-compose", "stack"]).optional(),
watchPaths: z.array(z.string()).optional(),
sourceType: z
.enum(["git", "github", "gitlab", "bitbucket", "gitea", "raw"])
.optional(),
triggerType: z.enum(["push", "tag"]).optional(),
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
});
export const apiCreateCompose = createSchema.pick({

View File

@@ -209,44 +209,27 @@ export const apiCreateDeploymentVolumeBackup = schema
volumeBackupId: z.string().min(1),
});
export const apiFindAllByApplication = schema
.pick({
applicationId: true,
})
.extend({
applicationId: z.string().min(1),
})
.required();
export const apiFindAllByApplication = z.object({
applicationId: z.string().min(1),
});
export const apiFindAllByCompose = schema
.pick({
composeId: true,
})
.extend({
composeId: z.string().min(1),
})
.required();
export const apiFindAllByCompose = z.object({
composeId: z.string().min(1),
});
export const apiFindAllByServer = schema
.pick({
serverId: true,
})
.extend({
serverId: z.string().min(1),
})
.required();
export const apiFindAllByServer = z.object({
serverId: z.string().min(1),
});
export const apiFindAllByType = z
.object({
id: z.string().min(1),
type: z.enum([
"application",
"compose",
"server",
"schedule",
"previewDeployment",
"backup",
"volumeBackup",
]),
})
.required();
export const apiFindAllByType = z.object({
id: z.string().min(1),
type: z.enum([
"application",
"compose",
"server",
"schedule",
"previewDeployment",
"backup",
"volumeBackup",
]),
});

View File

@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "../validations/destination";
import { organization } from "./account";
import { backups } from "./backups";
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
bucket: text("bucket").notNull(),
region: text("region").notNull(),
endpoint: text("endpoint").notNull(),
additionalFlags: text("additionalFlags").array(),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
endpoint: z.string(),
secretAccessKey: z.string(),
region: z.string(),
additionalFlags: z
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
.default([]),
});
export const apiCreateDestination = createSchema
@@ -55,17 +63,16 @@ export const apiCreateDestination = createSchema
region: true,
endpoint: true,
secretAccessKey: true,
additionalFlags: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiFindOneDestination = createSchema
.pick({
destinationId: true,
})
.required();
export const apiFindOneDestination = z.object({
destinationId: z.string().min(1),
});
export const apiRemoveDestination = createSchema
.pick({
@@ -83,6 +90,7 @@ export const apiUpdateDestination = createSchema
secretAccessKey: true,
destinationId: true,
provider: true,
additionalFlags: true,
})
.required()
.extend({

View File

@@ -1,4 +1,4 @@
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
type AnyPgColumn,
boolean,
@@ -31,6 +31,7 @@ export const domains = pgTable("domain", {
host: text("host").notNull(),
https: boolean("https").notNull().default(false),
port: integer("port").default(3000),
customEntrypoint: text("customEntrypoint"),
path: text("path").default("/"),
serviceName: text("serviceName"),
domainType: domainType("domainType").default("application"),
@@ -53,6 +54,7 @@ export const domains = pgTable("domain", {
certificateType: certificateType("certificateType").notNull().default("none"),
internalPath: text("internalPath").default("/"),
stripPath: boolean("stripPath").notNull().default(false),
middlewares: text("middlewares").array().default(sql`ARRAY[]::text[]`),
});
export const domainsRelations = relations(domains, ({ one }) => ({
@@ -70,12 +72,17 @@ export const domainsRelations = relations(domains, ({ one }) => ({
}),
}));
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
const createSchema = createInsertSchema(domains, {
...domain.shape,
// Override pgEnum so Zod 4 infers only string literals, not numeric enum index
domainType: z.enum(["compose", "application", "preview"]).optional(),
});
export const apiCreateDomain = createSchema.pick({
host: true,
path: true,
port: true,
customEntrypoint: true,
https: true,
applicationId: true,
certificateType: true,
@@ -86,13 +93,12 @@ export const apiCreateDomain = createSchema.pick({
previewDeploymentId: true,
internalPath: true,
stripPath: true,
middlewares: true,
});
export const apiFindDomain = createSchema
.pick({
domainId: true,
})
.required();
export const apiFindDomain = z.object({
domainId: z.string().min(1),
});
export const apiFindDomainByApplication = createSchema.pick({
applicationId: true,
@@ -111,6 +117,7 @@ export const apiUpdateDomain = createSchema
host: true,
path: true,
port: true,
customEntrypoint: true,
https: true,
certificateType: true,
customCertResolver: true,
@@ -118,5 +125,6 @@ export const apiUpdateDomain = createSchema
domainType: true,
internalPath: true,
stripPath: true,
middlewares: true,
})
.merge(createSchema.pick({ domainId: true }).required());

View File

@@ -1,10 +1,10 @@
import { relations } from "drizzle-orm";
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -37,55 +37,40 @@ export const environmentRelations = relations(
references: [projects.projectId],
}),
applications: many(applications),
mariadb: many(mariadb),
postgres: many(postgres),
mysql: many(mysql),
redis: many(redis),
mongo: many(mongo),
compose: many(compose),
libsql: many(libsql),
mariadb: many(mariadb),
mongo: many(mongo),
mysql: many(mysql),
postgres: many(postgres),
redis: many(redis),
}),
);
const createSchema = createInsertSchema(environments, {
export const apiCreateEnvironment = z.object({
name: z.string().min(1),
description: z.string().optional(),
projectId: z.string().min(1),
});
export const apiFindOneEnvironment = z.object({
environmentId: z.string().min(1),
});
export const apiRemoveEnvironment = z.object({
environmentId: z.string().min(1),
});
export const apiUpdateEnvironment = z.object({
environmentId: z.string().min(1),
name: z.string().min(1).optional(),
description: z.string().optional(),
projectId: z.string().optional(),
env: z.string().optional(),
});
export const apiDuplicateEnvironment = z.object({
environmentId: z.string().min(1),
name: z.string().min(1),
description: z.string().optional(),
});
export const apiCreateEnvironment = createSchema.pick({
name: true,
description: true,
projectId: true,
});
export const apiFindOneEnvironment = createSchema
.pick({
environmentId: true,
})
.required();
export const apiRemoveEnvironment = createSchema
.pick({
environmentId: true,
})
.required();
export const apiUpdateEnvironment = createSchema
.partial()
.extend({
environmentId: z.string().min(1),
})
.omit({
isDefault: true,
});
export const apiDuplicateEnvironment = createSchema
.pick({
environmentId: true,
name: true,
description: true,
})
.required({
environmentId: true,
name: true,
});

View File

@@ -1,6 +1,5 @@
import { relations } from "drizzle-orm";
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
@@ -33,6 +32,9 @@ export const gitProvider = pgTable("git_provider", {
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
sharedWithOrganization: boolean("sharedWithOrganization")
.notNull()
.default(false),
});
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
@@ -62,10 +64,11 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
}),
}));
const createSchema = createInsertSchema(gitProvider);
export const apiRemoveGitProvider = z.object({
gitProviderId: z.string().min(1),
});
export const apiRemoveGitProvider = createSchema
.extend({
gitProviderId: z.string().min(1),
})
.pick({ gitProviderId: true });
export const apiToggleShareGitProvider = z.object({
gitProviderId: z.string().min(1),
sharedWithOrganization: z.boolean(),
});

View File

@@ -1,6 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
@@ -29,8 +28,7 @@ export const githubProviderRelations = relations(github, ({ one }) => ({
}),
}));
const createSchema = createInsertSchema(github);
export const apiCreateGithub = createSchema.extend({
export const apiCreateGithub = z.object({
githubAppName: z.string().optional(),
githubAppId: z.number().optional(),
githubClientId: z.string().optional(),
@@ -48,13 +46,11 @@ export const apiFindGithubBranches = z.object({
githubId: z.string().optional(),
});
export const apiFindOneGithub = createSchema
.extend({
githubId: z.string().min(1),
})
.pick({ githubId: true });
export const apiFindOneGithub = z.object({
githubId: z.string().min(1),
});
export const apiUpdateGithub = createSchema.extend({
export const apiUpdateGithub = z.object({
githubId: z.string().min(1),
name: z.string().min(1),
gitProviderId: z.string().min(1),

View File

@@ -1,6 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
@@ -31,9 +30,7 @@ export const gitlabProviderRelations = relations(gitlab, ({ one }) => ({
}),
}));
const createSchema = createInsertSchema(gitlab);
export const apiCreateGitlab = createSchema.extend({
export const apiCreateGitlab = z.object({
applicationId: z.string().optional(),
secret: z.string().optional(),
groupName: z.string().optional(),
@@ -45,17 +42,14 @@ export const apiCreateGitlab = createSchema.extend({
gitlabInternalUrl: z.string().optional().nullable(),
});
export const apiFindOneGitlab = createSchema
.extend({
gitlabId: z.string().min(1),
})
.pick({ gitlabId: true });
export const apiFindOneGitlab = z.object({
gitlabId: z.string().min(1),
});
export const apiGitlabTestConnection = createSchema
.extend({
groupName: z.string().optional(),
})
.pick({ gitlabId: true, groupName: true });
export const apiGitlabTestConnection = z.object({
gitlabId: z.string().min(1),
groupName: z.string().optional(),
});
export const apiFindGitlabBranches = z.object({
id: z.number().optional(),
@@ -64,7 +58,7 @@ export const apiFindGitlabBranches = z.object({
gitlabId: z.string().optional(),
});
export const apiUpdateGitlab = createSchema.extend({
export const apiUpdateGitlab = z.object({
applicationId: z.string().optional(),
secret: z.string().optional(),
groupName: z.string().optional(),
@@ -72,5 +66,6 @@ export const apiUpdateGitlab = createSchema.extend({
name: z.string().min(1),
gitlabId: z.string().min(1),
gitlabUrl: z.string().min(1),
gitProviderId: z.string().min(1),
gitlabInternalUrl: z.string().optional().nullable(),
});

View File

@@ -1,6 +1,7 @@
export * from "./account";
export * from "./ai";
export * from "./application";
export * from "./audit-log";
export * from "./backups";
export * from "./bitbucket";
export * from "./certificate";
@@ -13,6 +14,7 @@ export * from "./git-provider";
export * from "./gitea";
export * from "./github";
export * from "./gitlab";
export * from "./libsql";
export * from "./mariadb";
export * from "./mongo";
export * from "./mount";
@@ -34,6 +36,7 @@ export * from "./session";
export * from "./shared";
export * from "./ssh-key";
export * from "./sso";
export * from "./tag";
export * from "./user";
export * from "./utils";
export * from "./volume-backups";

View File

@@ -0,0 +1,248 @@
import { relations } from "drizzle-orm";
import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { environments } from "./environment";
import { mounts } from "./mount";
import { server } from "./server";
import {
applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
sqldNode,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import {
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const libsql = pgTable("libsql", {
libsqlId: text("libsqlId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
appName: text("appName")
.notNull()
.$defaultFn(() => generateAppName("libsql"))
.unique(),
description: text("description"),
databaseUser: text("databaseUser").notNull(),
databasePassword: text("databasePassword").notNull(),
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
sqldPrimaryUrl: text("sqldPrimaryUrl"),
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
dockerImage: text("dockerImage").notNull(),
command: text("command"),
env: text("env"),
// RESOURCES
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
cpuLimit: text("cpuLimit"),
//
externalPort: integer("externalPort"),
externalGRPCPort: integer("externalGRPCPort"),
externalAdminPort: integer("externalAdminPort"),
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
environmentId: text("environmentId")
.notNull()
.references(() => environments.environmentId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const libsqlRelations = relations(libsql, ({ one, many }) => ({
environment: one(environments, {
fields: [libsql.environmentId],
references: [environments.environmentId],
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [libsql.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(libsql, {
libsqlId: z.string(),
name: z.string().min(1),
appName: z.string().min(1),
createdAt: z.string(),
databaseUser: z.string().min(1),
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
sqldNode: z.enum(sqldNode.enumValues),
sqldPrimaryUrl: z.string().nullable(),
enableNamespaces: z.boolean().default(false),
dockerImage: z
.string()
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
command: z.string().optional(),
env: z.string().optional(),
memoryReservation: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
cpuLimit: z.string().optional(),
environmentId: z.string(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
externalPort: z.number(),
externalGRPCPort: z.number(),
externalAdminPort: z.number(),
description: z.string().optional(),
serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
});
export const apiCreateLibsql = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
environmentId: true,
description: true,
databaseUser: true,
databasePassword: true,
sqldNode: true,
sqldPrimaryUrl: true,
enableNamespaces: true,
serverId: true,
})
.required()
.superRefine((data, ctx) => {
if (data.sqldNode === "replica" && !data.sqldPrimaryUrl) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sqldPrimaryUrl"],
message: "sqldPrimaryUrl is required when sqldNode is 'replica'.",
});
}
if (data.sqldNode !== "replica" && data.sqldPrimaryUrl) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["sqldPrimaryUrl"],
message:
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
});
}
});
export const apiFindOneLibsql = z.object({
libsqlId: z.string().min(1),
});
export const apiChangeLibsqlStatus = createSchema
.pick({
libsqlId: true,
applicationStatus: true,
})
.required();
export const apiSaveEnvironmentVariablesLibsql = createSchema
.pick({
libsqlId: true,
env: true,
})
.required();
export const apiSaveExternalPortsLibsql = createSchema
.pick({
libsqlId: true,
externalPort: true,
externalGRPCPort: true,
externalAdminPort: true,
})
.required({ libsqlId: true })
.superRefine((data, ctx) => {
if (
data.externalPort === null &&
data.externalGRPCPort === null &&
data.externalAdminPort === null
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
});
}
});
export const apiDeployLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
export const apiResetLibsql = createSchema
.pick({
libsqlId: true,
appName: true,
})
.required();
export const apiUpdateLibsql = createSchema
.partial()
.extend({
libsqlId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildLibsql = createSchema
.pick({
libsqlId: true,
})
.required();

View File

@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId")
@@ -108,17 +114,13 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
})
.optional(),
dockerImage: z.string().default("mariadb:6"),
@@ -160,11 +162,9 @@ export const apiCreateMariaDB = createSchema.pick({
serverId: true,
});
export const apiFindOneMariaDB = createSchema
.pick({
mariadbId: true,
})
.required();
export const apiFindOneMariaDB = z.object({
mariadbId: z.string().min(1),
});
export const apiChangeMariaDBStatus = createSchema
.pick({
@@ -204,6 +204,7 @@ export const apiUpdateMariaDB = createSchema
.partial()
.extend({
mariadbId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -35,7 +35,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mongo = pgTable("mongo", {
mongoId: text("mongoId")
@@ -50,7 +56,7 @@ export const mongo = pgTable("mongo", {
description: text("description"),
databaseUser: text("databaseUser").notNull(),
databasePassword: text("databasePassword").notNull(),
dockerImage: text("dockerImage").notNull(),
dockerImage: text("dockerImage").notNull().default("mongo:8"),
command: text("command"),
args: text("args").array(),
env: text("env"),
@@ -110,12 +116,9 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(),
mongoId: z.string(),
name: z.string().min(1),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
@@ -156,11 +159,9 @@ export const apiCreateMongo = createSchema.pick({
replicaSets: true,
});
export const apiFindOneMongo = createSchema
.pick({
mongoId: true,
})
.required();
export const apiFindOneMongo = z.object({
mongoId: z.string().min(1),
});
export const apiChangeMongoStatus = createSchema
.pick({
@@ -193,6 +194,7 @@ export const apiUpdateMongo = createSchema
.partial()
.extend({
mongoId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -19,8 +20,11 @@ export const serviceType = pgEnum("serviceType", [
"mongo",
"redis",
"compose",
"libsql",
]);
export type ServiceType = (typeof serviceType.enumValues)[number];
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
export const mounts = pgTable("mount", {
@@ -39,7 +43,10 @@ export const mounts = pgTable("mount", {
() => applications.applicationId,
{ onDelete: "cascade" },
),
postgresId: text("postgresId").references(() => postgres.postgresId, {
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
onDelete: "cascade",
}),
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
@@ -51,10 +58,10 @@ export const mounts = pgTable("mount", {
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
onDelete: "cascade",
}),
redisId: text("redisId").references(() => redis.redisId, {
postgresId: text("postgresId").references(() => postgres.postgresId, {
onDelete: "cascade",
}),
composeId: text("composeId").references(() => compose.composeId, {
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
});
@@ -64,9 +71,13 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
fields: [mounts.applicationId],
references: [applications.applicationId],
}),
postgres: one(postgres, {
fields: [mounts.postgresId],
references: [postgres.postgresId],
compose: one(compose, {
fields: [mounts.composeId],
references: [compose.composeId],
}),
libsql: one(libsql, {
fields: [mounts.libsqlId],
references: [libsql.libsqlId],
}),
mariadb: one(mariadb, {
fields: [mounts.mariadbId],
@@ -80,14 +91,14 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
fields: [mounts.mysqlId],
references: [mysql.mysqlId],
}),
postgres: one(postgres, {
fields: [mounts.postgresId],
references: [postgres.postgresId],
}),
redis: one(redis, {
fields: [mounts.redisId],
references: [redis.redisId],
}),
compose: one(compose, {
fields: [mounts.composeId],
references: [compose.composeId],
}),
}));
const createSchema = createInsertSchema(mounts, {
@@ -99,23 +110,18 @@ const createSchema = createInsertSchema(mounts, {
mountPath: z.string().min(1),
mountId: z.string().optional(),
filePath: z.string().optional(),
serviceType: z
.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
])
.default("application"),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
"libsql",
]),
});
export type ServiceType = NonNullable<
z.infer<typeof createSchema>["serviceType"]
>;
export const apiCreateMount = createSchema
.pick({
type: true,
@@ -123,18 +129,16 @@ export const apiCreateMount = createSchema
volumeName: true,
content: true,
mountPath: true,
serviceType: true,
filePath: true,
serviceType: true,
})
.extend({
serviceId: z.string().min(1),
});
export const apiFindOneMount = createSchema
.pick({
mountId: true,
})
.required();
export const apiFindOneMount = z.object({
mountId: z.string().min(1),
});
export const apiRemoveMount = createSchema
.pick({
@@ -145,15 +149,13 @@ export const apiRemoveMount = createSchema
// })
.required();
export const apiFindMountByApplicationId = createSchema
.extend({
serviceId: z.string().min(1),
})
.pick({
serviceId: true,
serviceType: true,
})
.required();
export const apiFindMountByApplicationId = z.object({
serviceType: z
.string()
.min(1)
.transform((val) => val as ServiceType),
serviceId: z.string().min(1),
});
export const apiUpdateMount = createSchema.partial().extend({
mountId: z.string().min(1),

View File

@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId")
@@ -106,17 +112,13 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
})
.optional(),
dockerImage: z.string().default("mysql:8"),
@@ -157,11 +159,9 @@ export const apiCreateMySql = createSchema.pick({
serverId: true,
});
export const apiFindOneMySql = createSchema
.pick({
mysqlId: true,
})
.required();
export const apiFindOneMySql = z.object({
mysqlId: z.string().min(1),
});
export const apiChangeMySqlStatus = createSchema
.pick({
@@ -201,6 +201,7 @@ export const apiUpdateMySql = createSchema
.partial()
.extend({
mysqlId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -20,6 +20,7 @@ export const notificationType = pgEnum("notificationType", [
"resend",
"gotify",
"ntfy",
"mattermost",
"pushover",
"custom",
"lark",
@@ -37,6 +38,7 @@ export const notifications = pgTable("notification", {
databaseBackup: boolean("databaseBackup").notNull().default(false),
volumeBackup: boolean("volumeBackup").notNull().default(false),
dokployRestart: boolean("dokployRestart").notNull().default(false),
dokployBackup: boolean("dokployBackup").notNull().default(false),
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
serverThreshold: boolean("serverThreshold").notNull().default(false),
notificationType: notificationType("notificationType").notNull(),
@@ -64,6 +66,9 @@ export const notifications = pgTable("notification", {
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
mattermostId: text("mattermostId").references(() => mattermost.mattermostId, {
onDelete: "cascade",
}),
customId: text("customId").references(() => custom.customId, {
onDelete: "cascade",
}),
@@ -154,6 +159,16 @@ export const ntfy = pgTable("ntfy", {
priority: integer("priority").notNull().default(3),
});
export const mattermost = pgTable("mattermost", {
mattermostId: text("mattermostId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
channel: text("channel"),
username: text("username"),
});
export const custom = pgTable("custom", {
customId: text("customId")
.notNull()
@@ -220,6 +235,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
mattermost: one(mattermost, {
fields: [notifications.mattermostId],
references: [mattermost.mattermostId],
}),
custom: one(custom, {
fields: [notifications.customId],
references: [custom.customId],
@@ -248,6 +267,7 @@ export const apiCreateSlack = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -276,6 +296,7 @@ export const apiCreateTelegram = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -306,6 +327,7 @@ export const apiCreateDiscord = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -337,6 +359,7 @@ export const apiCreateEmail = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -373,6 +396,7 @@ export const apiCreateResend = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -403,6 +427,7 @@ export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -437,6 +462,7 @@ export const apiCreateNtfy = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -464,16 +490,62 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
priority: true,
});
export const apiFindOneNotification = notificationsSchema
export const apiCreateMattermost = notificationsSchema
.pick({
notificationId: true,
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.required();
.extend({
webhookUrl: z.string().url(),
channel: z.string().optional(),
username: z.string().optional(),
})
.required({
name: true,
webhookUrl: true,
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
});
export const apiUpdateMattermost = apiCreateMattermost.partial().extend({
notificationId: z.string().min(1),
mattermostId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestMattermostConnection = apiCreateMattermost
.pick({
webhookUrl: true,
channel: true,
username: true,
})
.extend({
channel: z.string().optional(),
username: z.string().optional(),
});
export const apiFindOneNotification = z.object({
notificationId: z.string().min(1),
});
export const apiCreateCustom = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -483,7 +555,7 @@ export const apiCreateCustom = notificationsSchema
})
.extend({
endpoint: z.string().min(1),
headers: z.record(z.string()).optional(),
headers: z.record(z.string(), z.string()).optional(),
});
export const apiUpdateCustom = apiCreateCustom.partial().extend({
@@ -494,13 +566,14 @@ export const apiUpdateCustom = apiCreateCustom.partial().extend({
export const apiTestCustomConnection = z.object({
endpoint: z.string().min(1),
headers: z.record(z.string()).optional(),
headers: z.record(z.string(), z.string()).optional(),
});
export const apiCreateLark = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -527,6 +600,7 @@ export const apiCreateTeams = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -553,6 +627,7 @@ export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -587,6 +662,7 @@ export const apiUpdatePushover = z.object({
expire: z.number().min(1).max(10800).nullish(),
appBuildError: z.boolean().optional(),
databaseBackup: z.boolean().optional(),
dokployBackup: z.boolean().optional(),
volumeBackup: z.boolean().optional(),
dokployRestart: z.boolean().optional(),
name: z.string().optional(),

View File

@@ -49,11 +49,9 @@ export const apiCreatePort = createSchema
})
.required();
export const apiFindOnePort = createSchema
.pick({
portId: true,
})
.required();
export const apiFindOnePort = z.object({
portId: z.string().min(1),
});
export const apiUpdatePort = createSchema
.pick({

View File

@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const postgres = pgTable("postgres", {
postgresId: text("postgresId")
@@ -103,12 +109,9 @@ const createSchema = createInsertSchema(postgres, {
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
databasePassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:18"),
@@ -150,11 +153,9 @@ export const apiCreatePostgres = createSchema.pick({
serverId: true,
});
export const apiFindOnePostgres = createSchema
.pick({
postgresId: true,
})
.required();
export const apiFindOnePostgres = z.object({
postgresId: z.string().min(1),
});
export const apiChangePostgresStatus = createSchema
.pick({
@@ -194,6 +195,7 @@ export const apiUpdatePostgres = createSchema
.partial()
.extend({
postgresId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -58,17 +58,12 @@ export const createSchema = createInsertSchema(previewDeployments, {
applicationId: z.string(),
});
export const apiCreatePreviewDeployment = createSchema
.pick({
applicationId: true,
domainId: true,
branch: true,
pullRequestId: true,
pullRequestNumber: true,
pullRequestURL: true,
pullRequestTitle: true,
})
.extend({
applicationId: z.string().min(1),
// deploymentId: z.string().min(1),
});
export const apiCreatePreviewDeployment = z.object({
applicationId: z.string().min(1),
domainId: z.string().optional(),
branch: z.string().min(1),
pullRequestId: z.string().min(1),
pullRequestNumber: z.string().min(1),
pullRequestURL: z.string().min(1),
pullRequestTitle: z.string().min(1),
});

View File

@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { environments } from "./environment";
import { projectTags } from "./tag";
export const projects = pgTable("project", {
projectId: text("projectId")
@@ -25,6 +26,7 @@ export const projects = pgTable("project", {
export const projectRelations = relations(projects, ({ many, one }) => ({
environments: many(environments),
projectTags: many(projectTags),
organization: one(organization, {
fields: [projects.organizationId],
references: [organization.id],
@@ -43,12 +45,9 @@ export const apiCreateProject = createSchema.pick({
env: true,
});
export const apiFindOneProject = createSchema
.pick({
projectId: true,
})
.required();
export const apiFindOneProject = z.object({
projectId: z.string().min(1),
});
export const apiRemoveProject = createSchema
.pick({
projectId: true,

View File

@@ -35,11 +35,9 @@ const createSchema = createInsertSchema(redirects, {
permanent: z.boolean().optional(),
});
export const apiFindOneRedirect = createSchema
.pick({
redirectId: true,
})
.required();
export const apiFindOneRedirect = z.object({
redirectId: z.string().min(1),
});
export const apiCreateRedirect = createSchema
.pick({

View File

@@ -136,11 +136,9 @@ export const apiCreateRedis = createSchema.pick({
serverId: true,
});
export const apiFindOneRedis = createSchema
.pick({
redisId: true,
})
.required();
export const apiFindOneRedis = z.object({
redisId: z.string().min(1),
});
export const apiChangeRedisStatus = createSchema
.pick({
@@ -180,6 +178,7 @@ export const apiUpdateRedis = createSchema
.partial()
.extend({
redisId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -94,11 +94,9 @@ export const apiRemoveRegistry = createSchema
})
.required();
export const apiFindOneRegistry = 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),

View File

@@ -38,11 +38,9 @@ const createSchema = createInsertSchema(security, {
password: z.string().min(1),
});
export const apiFindOneSecurity = createSchema
.pick({
securityId: true,
})
.required();
export const apiFindOneSecurity = z.object({
securityId: z.string().min(1),
});
export const apiCreateSecurity = createSchema
.pick({

View File

@@ -15,6 +15,7 @@ import { applications } from "./application";
import { certificates } from "./certificate";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
@@ -116,6 +117,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
relationName: "applicationBuildServer",
}),
compose: many(compose),
libsql: many(libsql),
redis: many(redis),
mariadb: many(mariadb),
mongo: many(mongo),
@@ -133,6 +135,7 @@ const createSchema = createInsertSchema(server, {
serverId: z.string().min(1),
name: z.string().min(1),
description: z.string().optional(),
serverType: z.enum(["deploy", "build"]).optional(),
});
export const apiCreateServer = createSchema
@@ -147,11 +150,9 @@ export const apiCreateServer = createSchema
})
.required();
export const apiFindOneServer = createSchema
.pick({
serverId: true,
})
.required();
export const apiFindOneServer = z.object({
serverId: z.string().min(1),
});
export const apiRemoveServer = createSchema
.pick({

View File

@@ -2,7 +2,7 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { user } from "./user";
// OLD TABLE
export const session = pgTable("session_temp", {
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),

View File

@@ -16,6 +16,8 @@ export const certificateType = pgEnum("certificateType", [
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
export const sqldNode = pgEnum("sqldNode", ["primary", "replica"]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
@@ -175,12 +177,12 @@ export const NetworkSwarmSchema = z.array(
.object({
Target: z.string().optional(),
Aliases: z.array(z.string()).optional(),
DriverOpts: z.record(z.string()).optional(),
DriverOpts: z.record(z.string(), z.string()).optional(),
})
.strict(),
);
export const LabelsSwarmSchema = z.record(z.string());
export const LabelsSwarmSchema = z.record(z.string(), z.string());
export const EndpointPortConfigSwarmSchema = z
.object({

View File

@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { sshKeyCreate, sshKeyType } from "../validations";
import { organization } from "./account";
import { applications } from "./application";
@@ -52,11 +53,9 @@ export const apiCreateSshKey = createSchema
})
.merge(sshKeyCreate.pick({ privateKey: true }));
export const apiFindOneSshKey = createSchema
.pick({
sshKeyId: true,
})
.required();
export const apiFindOneSshKey = z.object({
sshKeyId: z.string().min(1),
});
export const apiGenerateSSHKey = sshKeyType;

View File

@@ -0,0 +1,99 @@
import { relations } from "drizzle-orm";
import { pgTable, text, unique } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { projects } from "./project";
export const tags = pgTable(
"tag",
{
tagId: text("tagId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
color: text("color"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
},
(table) => ({
// Unique index on (organizationId, name) to prevent duplicate tag names per organization
uniqueOrgName: unique("unique_org_tag_name").on(
table.organizationId,
table.name,
),
}),
);
export const projectTags = pgTable(
"project_tag",
{
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
tagId: text("tagId")
.notNull()
.references(() => tags.tagId, { onDelete: "cascade" }),
},
(table) => ({
// Unique constraint to prevent duplicate project-tag associations
uniqueProjectTag: unique("unique_project_tag").on(
table.projectId,
table.tagId,
),
}),
);
export const tagRelations = relations(tags, ({ one, many }) => ({
organization: one(organization, {
fields: [tags.organizationId],
references: [organization.id],
}),
projectTags: many(projectTags),
}));
export const projectTagRelations = relations(projectTags, ({ one }) => ({
project: one(projects, {
fields: [projectTags.projectId],
references: [projects.projectId],
}),
tag: one(tags, {
fields: [projectTags.tagId],
references: [tags.tagId],
}),
}));
const createSchema = createInsertSchema(tags, {
tagId: z.string().min(1),
name: z.string().min(1),
color: z.string().optional(),
});
export const apiCreateTag = createSchema.pick({
name: true,
color: true,
});
export const apiFindOneTag = z.object({
tagId: z.string().min(1),
});
export const apiRemoveTag = createSchema
.pick({
tagId: true,
})
.required();
export const apiUpdateTag = createSchema.partial().extend({
tagId: z.string().min(1),
});

View File

@@ -1,5 +1,5 @@
import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
boolean,
integer,
@@ -66,6 +66,9 @@ export const user = pgTable("user", {
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
trustedOrigins: text("trustedOrigins").array(),
bookmarkedTemplates: text("bookmarkedTemplates")
.array()
.default(sql`ARRAY[]::text[]`),
});
export const usersRelations = relations(user, ({ one, many }) => ({
@@ -87,6 +90,7 @@ const createSchema = createInsertSchema(user, {
}).omit({
role: true,
trustedOrigins: true,
bookmarkedTemplates: true,
isValidEnterpriseLicense: true,
});
@@ -126,6 +130,7 @@ export const apiAssignPermissions = createSchema
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
accessedGitProviders: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),

View File

@@ -12,6 +12,13 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
export const APP_NAME_MESSAGE =
"App name can only contain letters, numbers, dots, underscores and hyphens";
/** Database password: blocks shell-dangerous characters like $ ! ' " \ / and spaces. */
export const DATABASE_PASSWORD_REGEX =
/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
export const DATABASE_PASSWORD_MESSAGE =
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility";
export const generateAppName = (type: string) => {
const verb = faker.hacker.verb().replace(/ /g, "-");
const adjective = faker.hacker.adjective().replace(/ /g, "-");

View File

@@ -7,6 +7,7 @@ import { applications } from "./application";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { serviceType } from "./mount";
@@ -53,6 +54,9 @@ export const volumeBackups = pgTable("volume_backup", {
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
onDelete: "cascade",
}),
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
@@ -93,6 +97,10 @@ export const volumeBackupsRelations = relations(
fields: [volumeBackups.redisId],
references: [redis.redisId],
}),
libsql: one(libsql, {
fields: [volumeBackups.libsqlId],
references: [libsql.libsqlId],
}),
compose: one(compose, {
fields: [volumeBackups.composeId],
references: [compose.composeId],

View File

@@ -66,6 +66,36 @@ export const webServerSettings = pgTable("webServerSettings", {
},
},
}),
// Whitelabeling Configuration (Enterprise / Proprietary)
whitelabelingConfig: jsonb("whitelabelingConfig")
.$type<{
appName: string | null;
appDescription: string | null;
logoUrl: string | null;
faviconUrl: string | null;
customCss: string | null;
loginLogoUrl: string | null;
supportUrl: string | null;
docsUrl: string | null;
errorPageTitle: string | null;
errorPageDescription: string | null;
metaTitle: string | null;
footerText: string | null;
}>()
.default({
appName: null,
appDescription: null,
logoUrl: null,
faviconUrl: null,
customCss: null,
loginLogoUrl: null,
supportUrl: null,
docsUrl: null,
errorPageTitle: null,
errorPageDescription: null,
metaTitle: null,
footerText: null,
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
@@ -154,6 +184,33 @@ export const apiUpdateDockerCleanup = z.object({
serverId: z.string().optional(),
});
// Whitelabeling validation schemas
const safeUrl = z
.string()
.refine((url) => /^https?:\/\//i.test(url), {
message: "Only http:// and https:// URLs are allowed",
})
.nullable();
export const whitelabelingConfigSchema = z.object({
appName: z.string().nullable(),
appDescription: z.string().nullable(),
logoUrl: safeUrl,
faviconUrl: safeUrl,
customCss: z.string().nullable(),
loginLogoUrl: safeUrl,
supportUrl: safeUrl,
docsUrl: safeUrl,
errorPageTitle: z.string().nullable(),
errorPageDescription: z.string().nullable(),
metaTitle: z.string().nullable(),
footerText: z.string().nullable(),
});
export const apiUpdateWhitelabeling = z.object({
whitelabelingConfig: whitelabelingConfigSchema,
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({

View File

@@ -0,0 +1,3 @@
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
export const ADDITIONAL_FLAG_ERROR =
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";

View File

@@ -20,6 +20,7 @@ export const domain = z
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
customCertResolver: z.string(),
middlewares: z.array(z.string()).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
@@ -83,6 +84,7 @@ export const domainCompose = z
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
customCertResolver: z.string(),
serviceName: z.string().min(1, { message: "Service name is required" }),
middlewares: z.array(z.string()).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {