Merge branch 'canary' into feat/add-mattermost-notification-provider

This commit is contained in:
Mauricio Siu
2026-03-24 01:29:08 -06:00
298 changed files with 116790 additions and 28183 deletions

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),
@@ -148,7 +182,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 +214,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 +237,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

@@ -365,12 +365,13 @@ 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.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
enableSubmodules: z.boolean().optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -433,13 +434,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({
@@ -451,10 +452,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({
@@ -465,10 +465,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({
@@ -478,10 +477,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({
@@ -506,6 +504,7 @@ export const apiSaveGitProvider = createSchema
.merge(
createSchema.pick({
customGitSSHKeyId: true,
enableSubmodules: true,
}),
);

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,
@@ -192,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

@@ -4,6 +4,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";
@@ -36,12 +37,13 @@ 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),
}),
);

View File

@@ -1,5 +1,6 @@
export * from "./account";
export * from "./ai";
export * from "./audit-log";
export * from "./application";
export * from "./backups";
export * from "./bitbucket";
@@ -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,249 @@
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 { 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(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
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 = createSchema
.pick({
libsqlId: true,
})
.required();
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

@@ -202,6 +202,7 @@ export const apiUpdateMariaDB = createSchema
.partial()
.extend({
mariadbId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -191,6 +191,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,8 +129,8 @@ export const apiCreateMount = createSchema
volumeName: true,
content: true,
mountPath: true,
serviceType: true,
filePath: true,
serviceType: true,
})
.extend({
serviceId: z.string().min(1),
@@ -144,21 +150,12 @@ export const apiRemoveMount = createSchema
.required();
export const apiFindMountByApplicationId = createSchema
.pick({
serviceType: true,
})
.required()
.extend({
serviceId: z.string().min(1),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
})
.pick({
serviceId: true,
serviceType: true,
});
export const apiUpdateMount = createSchema.partial().extend({

View File

@@ -199,6 +199,7 @@ export const apiUpdateMySql = createSchema
.partial()
.extend({
mysqlId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

View File

@@ -192,6 +192,7 @@ export const apiUpdatePostgres = createSchema
.partial()
.extend({
postgresId: 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 { 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],

View File

@@ -178,6 +178,7 @@ export const apiUpdateRedis = createSchema
.partial()
.extend({
redisId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });

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),

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;

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,6 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { serviceType } from "./mount";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,9 +8,9 @@ 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";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
@@ -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({