wip: db schema overhaul
This commit is contained in:
parent
92b414b50d
commit
f0877ebc83
|
@ -50,6 +50,7 @@
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"datastructures-js": "^13.0.0",
|
"datastructures-js": "^13.0.0",
|
||||||
"date-fns": "^3.0.6",
|
"date-fns": "^3.0.6",
|
||||||
|
"deterministic-object-hash": "^2.0.2",
|
||||||
"docker-cli-js": "^2.10.0",
|
"docker-cli-js": "^2.10.0",
|
||||||
"docker-modem": "^5.0.3",
|
"docker-modem": "^5.0.3",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"extensionless": "^1.9.6",
|
"extensionless": "^1.9.6",
|
||||||
"framer-motion": "^11.0.3",
|
"framer-motion": "^11.0.3",
|
||||||
"ipaddr.js": "^2.1.0",
|
"ipaddr.js": "^2.1.0",
|
||||||
|
"jsondiffpatch": "^0.6.0",
|
||||||
"lucide-react": "^0.298.0",
|
"lucide-react": "^0.298.0",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"next-nprogress-bar": "^2.1.2",
|
"next-nprogress-bar": "^2.1.2",
|
||||||
|
|
|
@ -104,6 +104,9 @@ dependencies:
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.0.6
|
specifier: ^3.0.6
|
||||||
version: 3.0.6
|
version: 3.0.6
|
||||||
|
deterministic-object-hash:
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2
|
||||||
docker-cli-js:
|
docker-cli-js:
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.10.0
|
version: 2.10.0
|
||||||
|
@ -131,6 +134,9 @@ dependencies:
|
||||||
ipaddr.js:
|
ipaddr.js:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
jsondiffpatch:
|
||||||
|
specifier: ^0.6.0
|
||||||
|
version: 0.6.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.298.0
|
specifier: ^0.298.0
|
||||||
version: 0.298.0(react@18.2.0)
|
version: 0.298.0(react@18.2.0)
|
||||||
|
@ -2975,6 +2981,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/diff-match-patch@1.0.36:
|
||||||
|
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/docker-modem@3.0.6:
|
/@types/docker-modem@3.0.6:
|
||||||
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3679,6 +3689,10 @@ packages:
|
||||||
/balanced-match@1.0.2:
|
/balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
/base-64@1.0.0:
|
||||||
|
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/base64-js@1.5.1:
|
/base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -4386,9 +4400,20 @@ packages:
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/deterministic-object-hash@2.0.2:
|
||||||
|
resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
base-64: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/didyoumean@1.2.2:
|
/didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
|
/diff-match-patch@1.0.5:
|
||||||
|
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/difflib@0.2.4:
|
/difflib@0.2.4:
|
||||||
resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==}
|
resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6194,6 +6219,16 @@ packages:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jsondiffpatch@0.6.0:
|
||||||
|
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@types/diff-match-patch': 1.0.36
|
||||||
|
chalk: 5.3.0
|
||||||
|
diff-match-patch: 1.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jsx-ast-utils@3.3.5:
|
/jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import chalk from "chalk";
|
||||||
import logger from "~/server/utils/logger";
|
import logger from "~/server/utils/logger";
|
||||||
|
|
||||||
const log = logger.child({ module: "trpc:server" });
|
const log = logger.child({ module: "trpc:server" });
|
||||||
|
|
||||||
export const loggerMiddleware = experimental_standaloneMiddleware().create(
|
export const loggerMiddleware = experimental_standaloneMiddleware().create(
|
||||||
async ({ type, path, next }) => {
|
async ({ type, path, next }) => {
|
||||||
const result = await next();
|
const result = await next();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { TRPCError, experimental_standaloneMiddleware } from "@trpc/server";
|
import { TRPCError, experimental_standaloneMiddleware } from "@trpc/server";
|
||||||
import { eq, or } from "drizzle-orm";
|
|
||||||
import { type db } from "~/server/db";
|
import { type db } from "~/server/db";
|
||||||
import { projects } from "~/server/db/schema";
|
import ProjectManager from "~/server/managers/ProjectManager";
|
||||||
|
|
||||||
export type BasicProjectDetails = {
|
export type BasicProjectDetails = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -21,21 +20,7 @@ export const projectMiddleware = experimental_standaloneMiddleware<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [project] = await ctx.db
|
const project = await ProjectManager.findByNameOrId(input.projectId);
|
||||||
.select({
|
|
||||||
id: projects.id,
|
|
||||||
friendlyName: projects.friendlyName,
|
|
||||||
internalName: projects.internalName,
|
|
||||||
createdAt: projects.createdAt,
|
|
||||||
})
|
|
||||||
.from(projects)
|
|
||||||
.where(
|
|
||||||
or(
|
|
||||||
eq(projects.id, input.projectId),
|
|
||||||
eq(projects.internalName, input.projectId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!project)
|
if (!project)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
@ -45,7 +30,7 @@ export const projectMiddleware = experimental_standaloneMiddleware<{
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
project: project as BasicProjectDetails,
|
project: project,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,20 +23,16 @@ export const serviceMiddleware = experimental_standaloneMiddleware<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [serviceDetails] = await ctx.db
|
const serviceDetails = await ctx.db.query.service.findFirst({
|
||||||
.select({
|
where: and(
|
||||||
id: service.id,
|
eq(service.projectId, ctx.project.id),
|
||||||
name: service.name,
|
or(eq(service.name, input.serviceId), eq(service.id, input.serviceId)),
|
||||||
createdAt: service.createdAt,
|
),
|
||||||
})
|
|
||||||
.from(service)
|
with: {
|
||||||
.where(
|
latestGeneration: true,
|
||||||
and(
|
},
|
||||||
eq(service.projectId, ctx.project.id),
|
});
|
||||||
or(eq(service.name, input.serviceId), eq(service.id, input.serviceId)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!serviceDetails)
|
if (!serviceDetails)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { BuildManager } from "~/server/build/BuildManager";
|
import { BuildManager } from "~/server/build/BuildManager";
|
||||||
import { service } from "~/server/db/schema";
|
import { serviceGeneration } from "~/server/db/schema";
|
||||||
import { buildDockerStackFile } from "~/server/docker/stack";
|
import { buildDockerStackFile } from "~/server/docker/stack";
|
||||||
import logger from "~/server/utils/logger";
|
import logger from "~/server/utils/logger";
|
||||||
import { projectMiddleware } from "../../middleware/project";
|
import { projectMiddleware } from "../../middleware/project";
|
||||||
|
@ -23,7 +23,7 @@ export const deployProject = authenticatedProcedure
|
||||||
.use(projectMiddleware)
|
.use(projectMiddleware)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const services = await ctx.db.query.service.findMany({
|
const services = await ctx.db.query.service.findMany({
|
||||||
where: eq(service.projectId, input.projectId),
|
where: eq(serviceGeneration.serviceId, input.projectId),
|
||||||
|
|
||||||
with: {
|
with: {
|
||||||
domains: true,
|
domains: true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { projects, service } from "~/server/db/schema";
|
import { projects, serviceGeneration } from "~/server/db/schema";
|
||||||
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
||||||
import { deployProject } from "./deploy";
|
import { deployProject } from "./deploy";
|
||||||
import { getProject } from "./project";
|
import { getProject } from "./project";
|
||||||
|
@ -34,11 +34,11 @@ export const projectRouter = createTRPCRouter({
|
||||||
userProjects.map(async (project) => {
|
userProjects.map(async (project) => {
|
||||||
const projServices = await ctx.db
|
const projServices = await ctx.db
|
||||||
.select({
|
.select({
|
||||||
id: service.id,
|
id: serviceGeneration.id,
|
||||||
name: service.name,
|
name: serviceGeneration.name,
|
||||||
})
|
})
|
||||||
.from(service)
|
.from(serviceGeneration)
|
||||||
.where(eq(service.projectId, project.id));
|
.where(eq(serviceGeneration.serviceId, project.id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { service } from "~/server/db/schema";
|
import { serviceGeneration } from "~/server/db/schema";
|
||||||
import { projectMiddleware } from "../../middleware/project";
|
import { projectMiddleware } from "../../middleware/project";
|
||||||
import { authenticatedProcedure } from "../../trpc";
|
import { authenticatedProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ export const getProject = authenticatedProcedure
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const projServices = await ctx.db
|
const projServices = await ctx.db
|
||||||
.select({
|
.select({
|
||||||
id: service.id,
|
id: serviceGeneration.id,
|
||||||
name: service.name,
|
name: serviceGeneration.name,
|
||||||
})
|
})
|
||||||
.from(service)
|
.from(serviceGeneration)
|
||||||
.where(eq(service.projectId, ctx.project.id));
|
.where(eq(serviceGeneration.serviceId, ctx.project.id));
|
||||||
|
|
||||||
// get docker stats
|
// get docker stats
|
||||||
const stats = await ctx.docker.listServices({
|
const stats = await ctx.docker.listServices({
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { env } from "~/env";
|
||||||
import { projectMiddleware } from "~/server/api/middleware/project";
|
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||||
import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
|
import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
|
||||||
import { service } from "~/server/db/schema";
|
import { serviceGeneration } from "~/server/db/schema";
|
||||||
import { DOCKER_DEPLOY_MODE_MAP, ServiceSource } from "~/server/db/types";
|
import { DOCKER_DEPLOY_MODE_MAP, ServiceSource } from "~/server/db/types";
|
||||||
import { zDockerName } from "~/server/utils/zod";
|
import { zDockerName } from "~/server/utils/zod";
|
||||||
import { getServiceContainers } from "./containers";
|
import { getServiceContainers } from "./containers";
|
||||||
|
@ -35,7 +35,7 @@ export const serviceRouter = createTRPCRouter({
|
||||||
.use(serviceMiddleware)
|
.use(serviceMiddleware)
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const fullServiceData = await ctx.db.query.service.findFirst({
|
const fullServiceData = await ctx.db.query.service.findFirst({
|
||||||
where: eq(service.id, ctx.service.id),
|
where: eq(serviceGeneration.id, ctx.service.id),
|
||||||
with: {
|
with: {
|
||||||
domains: true,
|
domains: true,
|
||||||
ports: true,
|
ports: true,
|
||||||
|
@ -71,7 +71,7 @@ export const serviceRouter = createTRPCRouter({
|
||||||
.use(projectMiddleware)
|
.use(projectMiddleware)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const [data] = await ctx.db
|
const [data] = await ctx.db
|
||||||
.insert(service)
|
.insert(serviceGeneration)
|
||||||
.values({
|
.values({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
projectId: ctx.project.id,
|
projectId: ctx.project.id,
|
||||||
|
@ -83,7 +83,7 @@ export const serviceRouter = createTRPCRouter({
|
||||||
dockerImage: "traefik/whoami",
|
dockerImage: "traefik/whoami",
|
||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
id: service.id,
|
id: serviceGeneration.id,
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -108,8 +108,8 @@ export const serviceRouter = createTRPCRouter({
|
||||||
.use(projectMiddleware)
|
.use(projectMiddleware)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.delete(service)
|
.delete(serviceGeneration)
|
||||||
.where(eq(service.id, input.serviceId))
|
.where(eq(serviceGeneration.id, input.serviceId))
|
||||||
.execute();
|
.execute();
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { z } from "zod";
|
||||||
import { projectMiddleware } from "~/server/api/middleware/project";
|
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||||
import { authenticatedProcedure } from "~/server/api/trpc";
|
import { authenticatedProcedure } from "~/server/api/trpc";
|
||||||
import { service, serviceDomain } from "~/server/db/schema";
|
import { serviceDomain, serviceGeneration } from "~/server/db/schema";
|
||||||
import { DockerDeployMode } from "~/server/db/types";
|
import { DockerDeployMode } from "~/server/db/types";
|
||||||
import { zDockerDuration, zDockerImage, zDomain } from "~/server/utils/zod";
|
import { zDockerDuration, zDockerImage, zDomain } from "~/server/utils/zod";
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export const updateServiceProcedure = authenticatedProcedure
|
||||||
serviceId: z.string(),
|
serviceId: z.string(),
|
||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
createInsertSchema(service, {
|
createInsertSchema(serviceGeneration, {
|
||||||
dockerImage: zDockerImage,
|
dockerImage: zDockerImage,
|
||||||
gitUrl: (schema) =>
|
gitUrl: (schema) =>
|
||||||
schema.gitUrl.regex(
|
schema.gitUrl.regex(
|
||||||
|
@ -93,9 +93,9 @@ export const updateServiceProcedure = authenticatedProcedure
|
||||||
delete queryUpdate.id;
|
delete queryUpdate.id;
|
||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(service)
|
.update(serviceGeneration)
|
||||||
.set(queryUpdate)
|
.set(queryUpdate)
|
||||||
.where(eq(service.id, ctx.service.id))
|
.where(eq(serviceGeneration.id, ctx.service.id))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { updateTraefik } from "~/server/docker/traefik";
|
import { updateTraefik } from "~/server/docker/traefik";
|
||||||
import { BasicServerStats, stats } from "~/server/modules/stats";
|
import { stats, type BasicServerStats } from "~/server/modules/stats";
|
||||||
|
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
||||||
|
|
||||||
export const systemRouter = createTRPCRouter({
|
export const systemRouter = createTRPCRouter({
|
||||||
currentStats: authenticatedProcedure.query(async ({ ctx }) => {
|
currentStats: authenticatedProcedure.query(async () => {
|
||||||
return stats.getCurrentStats();
|
return stats.getCurrentStats();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
liveStats: authenticatedProcedure.subscription(async ({ ctx }) => {
|
liveStats: authenticatedProcedure.subscription(() => {
|
||||||
return observable<BasicServerStats>((observer) => {
|
return observable<BasicServerStats>((observer) => {
|
||||||
const update = observer.next.bind(observer);
|
const update = observer.next.bind(observer);
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ export const systemRouter = createTRPCRouter({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
history: authenticatedProcedure.query(async ({ ctx }) => {
|
history: authenticatedProcedure.query(async () => {
|
||||||
return await stats.getStatsInRange(
|
return await stats.getStatsInRange(
|
||||||
new Date(Date.now() - 1000 * 60 * 60 * 24),
|
new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// core container options
|
// core container options
|
||||||
redeployTraefik: authenticatedProcedure.mutation(async ({ ctx }) => {
|
redeployTraefik: authenticatedProcedure.mutation(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void updateTraefik();
|
void updateTraefik();
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { users, sessions } from "../db/schema";
|
|
||||||
import { randomBytes } from "crypto";
|
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { env } from "~/env";
|
import crypto, { randomBytes } from "crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { IncomingMessage } from "http";
|
import { IncomingMessage } from "http";
|
||||||
|
import { env } from "~/env";
|
||||||
import type { ExtendedRequest } from "../api/trpc";
|
import type { ExtendedRequest } from "../api/trpc";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { sessions, users } from "../db/schema/schema";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
export type SessionUpdateData = Partial<{
|
export type SessionUpdateData = Partial<{
|
||||||
ua: string;
|
ua: string;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { Queue } from "datastructures-js";
|
import { Queue } from "datastructures-js";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { serviceDeployment } from "../db/schema";
|
import { serviceDeployment } from "../db/schema/schema";
|
||||||
import { ServiceDeploymentStatus, ServiceSource } from "../db/types";
|
import { ServiceDeploymentStatus, ServiceSource } from "../db/types";
|
||||||
import { type Service } from "../docker/stack";
|
import { type Service } from "../docker/stack";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { mkdirSync } from "fs";
|
||||||
import { rm, rmdir } from "fs/promises";
|
import { rm, rmdir } from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { service, serviceDeployment } from "../db/schema";
|
import { serviceDeployment, serviceGeneration } from "../db/schema/schema";
|
||||||
import {
|
import {
|
||||||
ServiceBuildMethod,
|
ServiceBuildMethod,
|
||||||
ServiceDeploymentStatus,
|
ServiceDeploymentStatus,
|
||||||
|
@ -122,8 +122,8 @@ export default class BuildTask {
|
||||||
private async fetchServiceDetails() {
|
private async fetchServiceDetails() {
|
||||||
const [serviceDetails] = await db
|
const [serviceDetails] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(service)
|
.from(serviceGeneration)
|
||||||
.where(eq(service.id, this.serviceId));
|
.where(eq(serviceGeneration.id, this.serviceId));
|
||||||
|
|
||||||
assert(serviceDetails, "Service not found");
|
assert(serviceDetails, "Service not found");
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type service } from "../../db/schema";
|
import { type serviceGeneration } from "../../db/schema/schema";
|
||||||
import type BuilderLogger from "../utils/BuilderLogger";
|
import type BuilderLogger from "../utils/BuilderLogger";
|
||||||
|
|
||||||
export default class BaseBuilder {
|
export default class BaseBuilder {
|
||||||
|
@ -6,7 +6,7 @@ export default class BaseBuilder {
|
||||||
public readonly configuration: {
|
public readonly configuration: {
|
||||||
fileLogger: BuilderLogger;
|
fileLogger: BuilderLogger;
|
||||||
workDirectory: string;
|
workDirectory: string;
|
||||||
serviceConfiguration: typeof service.$inferSelect;
|
serviceConfiguration: typeof serviceGeneration.$inferSelect;
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type service } from "~/server/db/schema";
|
import { type serviceGeneration } from "~/server/db/schema";
|
||||||
import type BuilderLogger from "../utils/BuilderLogger";
|
import type BuilderLogger from "../utils/BuilderLogger";
|
||||||
|
|
||||||
export default class BaseSource {
|
export default class BaseSource {
|
||||||
|
@ -6,7 +6,7 @@ export default class BaseSource {
|
||||||
public readonly configuration: {
|
public readonly configuration: {
|
||||||
fileLogger: BuilderLogger;
|
fileLogger: BuilderLogger;
|
||||||
workDirectory: string;
|
workDirectory: string;
|
||||||
serviceConfiguration: typeof service.$inferSelect;
|
serviceConfiguration: typeof serviceGeneration.$inferSelect;
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
2
src/server/db/schema/index.ts
Normal file
2
src/server/db/schema/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./relations";
|
||||||
|
export * from "./schema";
|
121
src/server/db/schema/relations.ts
Normal file
121
src/server/db/schema/relations.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
projectDeployment,
|
||||||
|
projects,
|
||||||
|
service,
|
||||||
|
serviceDeployment,
|
||||||
|
serviceDomain,
|
||||||
|
serviceGeneration,
|
||||||
|
servicePort,
|
||||||
|
serviceSysctl,
|
||||||
|
serviceUlimit,
|
||||||
|
serviceVolume,
|
||||||
|
sessions,
|
||||||
|
users,
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
|
export const userRelations = relations(users, ({ many }) => ({
|
||||||
|
sessions: many(sessions),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sessionRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [sessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const projectRelations = relations(projects, ({ one, many }) => ({
|
||||||
|
owner: one(users, {
|
||||||
|
fields: [projects.ownerId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
services: many(serviceGeneration),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const serviceGenerationRelations = relations(
|
||||||
|
serviceGeneration,
|
||||||
|
({ one, many }) => ({
|
||||||
|
service: one(service, {
|
||||||
|
fields: [serviceGeneration.serviceId],
|
||||||
|
references: [service.id],
|
||||||
|
}),
|
||||||
|
|
||||||
|
domains: many(serviceDomain),
|
||||||
|
ports: many(servicePort),
|
||||||
|
sysctls: many(serviceSysctl),
|
||||||
|
volumes: many(serviceVolume),
|
||||||
|
ulimits: many(serviceUlimit),
|
||||||
|
|
||||||
|
deployment: one(serviceDeployment, {
|
||||||
|
fields: [serviceGeneration.deploymentId],
|
||||||
|
references: [serviceDeployment.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const serviceRelations = relations(service, ({ one, many }) => ({
|
||||||
|
project: one(projects, {
|
||||||
|
fields: [service.projectId],
|
||||||
|
references: [projects.id],
|
||||||
|
}),
|
||||||
|
|
||||||
|
generations: many(serviceGeneration),
|
||||||
|
|
||||||
|
latestGeneration: one(serviceGeneration, {
|
||||||
|
fields: [service.latestGenerationId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
|
||||||
|
deployedGeneration: one(serviceGeneration, {
|
||||||
|
fields: [service.deployedGenerationId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const projectDeploymentRelations = relations(
|
||||||
|
projectDeployment,
|
||||||
|
({ one, many }) => ({
|
||||||
|
project: one(projects, {
|
||||||
|
fields: [projectDeployment.projectId],
|
||||||
|
references: [projects.id],
|
||||||
|
}),
|
||||||
|
|
||||||
|
serviceDeployments: many(serviceDeployment),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const serviceDomainRelations = relations(serviceDomain, ({ one }) => ({
|
||||||
|
service: one(serviceGeneration, {
|
||||||
|
fields: [serviceDomain.serviceId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const servicePortRelations = relations(servicePort, ({ one }) => ({
|
||||||
|
service: one(serviceGeneration, {
|
||||||
|
fields: [servicePort.serviceId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const serviceSysctlRelations = relations(serviceSysctl, ({ one }) => ({
|
||||||
|
service: one(serviceGeneration, {
|
||||||
|
fields: [serviceSysctl.serviceId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const serviceVolumeRelations = relations(serviceVolume, ({ one }) => ({
|
||||||
|
service: one(serviceGeneration, {
|
||||||
|
fields: [serviceVolume.serviceId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const serviceUlimitRelations = relations(serviceUlimit, ({ one }) => ({
|
||||||
|
service: one(serviceGeneration, {
|
||||||
|
fields: [serviceUlimit.serviceId],
|
||||||
|
references: [serviceGeneration.id],
|
||||||
|
}),
|
||||||
|
}));
|
|
@ -1,4 +1,4 @@
|
||||||
import { relations, sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
blob,
|
blob,
|
||||||
index,
|
index,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text,
|
text,
|
||||||
unique,
|
unique,
|
||||||
|
type AnySQLiteColumn,
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
import {
|
import {
|
||||||
DockerDeployMode,
|
DockerDeployMode,
|
||||||
|
@ -16,12 +17,17 @@ import {
|
||||||
type ServiceDeploymentStatus,
|
type ServiceDeploymentStatus,
|
||||||
type ServicePortType,
|
type ServicePortType,
|
||||||
type ServiceSource,
|
type ServiceSource,
|
||||||
} from "./types";
|
} from "../types";
|
||||||
|
|
||||||
// util
|
// util
|
||||||
const uuidv7 = sql`(uuid_generate_v7())`;
|
const uuidv7 = sql`(uuid_generate_v7())`;
|
||||||
const now = sql<number>`CURRENT_TIMESTAMP`;
|
const now = sql<number>`CURRENT_TIMESTAMP`;
|
||||||
|
|
||||||
|
// overview of the project schema layout
|
||||||
|
// https://drawsql.app/teams/derock/diagrams/hostforge
|
||||||
|
// does not include all fields, just the main ones
|
||||||
|
// easy to see the relations between tables
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User table.
|
* User table.
|
||||||
* Represents a global user.
|
* Represents a global user.
|
||||||
|
@ -41,11 +47,6 @@ export const users = sqliteTable(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const userRelations = relations(users, ({ many }) => ({
|
|
||||||
sessions: many(sessions),
|
|
||||||
mfaRequestSessions: many(MFARequestSessions),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Session table.
|
* User Session table.
|
||||||
* Represents a user's session.
|
* Represents a user's session.
|
||||||
|
@ -54,6 +55,7 @@ export const sessions = sqliteTable("session", {
|
||||||
token: text("token").primaryKey(),
|
token: text("token").primaryKey(),
|
||||||
lastUA: text("last_useragent"),
|
lastUA: text("last_useragent"),
|
||||||
lastIP: text("last_ip"),
|
lastIP: text("last_ip"),
|
||||||
|
|
||||||
// NOT IN MILLISECONDS!
|
// NOT IN MILLISECONDS!
|
||||||
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
||||||
createdAt: integer("created_at").default(now).notNull(),
|
createdAt: integer("created_at").default(now).notNull(),
|
||||||
|
@ -62,33 +64,6 @@ export const sessions = sqliteTable("session", {
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sessionRelations = relations(sessions, ({ one }) => ({
|
|
||||||
user: one(users, {
|
|
||||||
fields: [sessions.userId],
|
|
||||||
references: [users.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MFA Request Session.
|
|
||||||
* The intermediate between successful basic/oauth login and a full session for users with MFA enabled
|
|
||||||
*/
|
|
||||||
export const MFARequestSessions = sqliteTable("mfa_request_sessions", {
|
|
||||||
token: text("token").primaryKey(),
|
|
||||||
userId: text("user_id").references(() => users.id),
|
|
||||||
createdAt: integer("created_at").default(now),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const MFARequestSessionRelations = relations(
|
|
||||||
MFARequestSessions,
|
|
||||||
({ one }) => ({
|
|
||||||
user: one(users, {
|
|
||||||
fields: [MFARequestSessions.userId],
|
|
||||||
references: [users.id],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System Statistics
|
* System Statistics
|
||||||
* Historical data about the system's usage
|
* Historical data about the system's usage
|
||||||
|
@ -119,9 +94,35 @@ export const projects = sqliteTable("projects", {
|
||||||
createdAt: integer("created_at").default(now).notNull(),
|
createdAt: integer("created_at").default(now).notNull(),
|
||||||
ownerId: text("owner_id")
|
ownerId: text("owner_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate of service deployments for a project
|
||||||
|
*/
|
||||||
|
export const projectDeployment = sqliteTable(
|
||||||
|
"project_deployment",
|
||||||
|
{
|
||||||
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
|
projectId: text("project_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projects.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
deployedAt: integer("deployed_at").default(now).notNull(),
|
||||||
|
status: integer("status").$type<ServiceDeploymentStatus>().notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
proj_deployment_idx: index("proj_deployment_idx").on(
|
||||||
|
table.id,
|
||||||
|
table.projectId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project services
|
* Project services
|
||||||
* Represents a service running in a project
|
* Represents a service running in a project
|
||||||
|
@ -131,11 +132,49 @@ export const service = sqliteTable(
|
||||||
"service",
|
"service",
|
||||||
{
|
{
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
projectId: text("project_id")
|
projectId: text("project_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.id),
|
.references(() => projects.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
name: text("name").notNull(),
|
latestGenerationId: text("latest_generation_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
|
deployedGenerationId: text("deployed_generation_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
|
createdAt: integer("created_at").default(now).notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
name_project_idx: index("name_project_idx").on(table.name, table.projectId),
|
||||||
|
name_project_unq: unique("name_project_unq").on(
|
||||||
|
table.name,
|
||||||
|
table.projectId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration at a point in time for a service
|
||||||
|
*/
|
||||||
|
export const serviceGeneration = sqliteTable(
|
||||||
|
"service_generation",
|
||||||
|
{
|
||||||
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
|
serviceId: text("service_id")
|
||||||
|
.notNull()
|
||||||
|
.references((): AnySQLiteColumn => service.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
deploymentId: text("deployment_id")
|
||||||
|
.notNull()
|
||||||
|
.references((): AnySQLiteColumn => serviceDeployment.id),
|
||||||
|
|
||||||
// service configuration
|
// service configuration
|
||||||
source: integer("source").$type<ServiceSource>().notNull(),
|
source: integer("source").$type<ServiceSource>().notNull(),
|
||||||
|
@ -174,6 +213,7 @@ export const service = sqliteTable(
|
||||||
.$type<DockerDeployMode>()
|
.$type<DockerDeployMode>()
|
||||||
.default(DockerDeployMode.Replicated)
|
.default(DockerDeployMode.Replicated)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
|
||||||
zeroDowntime: integer("zero_downtime", { mode: "boolean" })
|
zeroDowntime: integer("zero_downtime", { mode: "boolean" })
|
||||||
.default(false)
|
.default(false)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
@ -213,48 +253,38 @@ export const service = sqliteTable(
|
||||||
createdAt: integer("created_at").default(now).notNull(),
|
createdAt: integer("created_at").default(now).notNull(),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => ({
|
||||||
name_project_idx: index("name_project_idx").on(table.name, table.projectId),
|
proj_generation_idx: index("proj_generation_idx").on(
|
||||||
name_project_unq: unique("name_project_unq").on(
|
table.id,
|
||||||
table.name,
|
table.serviceId,
|
||||||
table.projectId,
|
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// relations
|
|
||||||
export const projectRelations = relations(projects, ({ one, many }) => ({
|
|
||||||
owner: one(users, {
|
|
||||||
fields: [projects.ownerId],
|
|
||||||
references: [users.id],
|
|
||||||
}),
|
|
||||||
services: many(service),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const serviceRelations = relations(service, ({ one, many }) => ({
|
|
||||||
project: one(projects, {
|
|
||||||
fields: [service.projectId],
|
|
||||||
references: [projects.id],
|
|
||||||
}),
|
|
||||||
domains: many(serviceDomain),
|
|
||||||
ports: many(servicePort),
|
|
||||||
sysctls: many(serviceSysctl),
|
|
||||||
volumes: many(serviceVolume),
|
|
||||||
ulimits: many(serviceUlimit),
|
|
||||||
deployments: many(serviceDeployment),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service deployments
|
* Service deployments
|
||||||
*/
|
*/
|
||||||
export const serviceDeployment = sqliteTable("service_deployment", {
|
export const serviceDeployment = sqliteTable("service_deployment", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
|
projectDeploymentId: text("project_deployment_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => projectDeployment.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
createdAt: integer("created_at").default(now).notNull(),
|
deployedAt: integer("created_at").default(now).notNull(),
|
||||||
|
deployedBy: text("deployed_by").references(() => users.id),
|
||||||
|
|
||||||
|
// logs will be pretty small,
|
||||||
|
// so we can store them as blobs
|
||||||
|
// https://www.sqlite.org/intern-v-extern-blob.html
|
||||||
buildLogs: blob("build_logs"), // COMPRESSED!
|
buildLogs: blob("build_logs"), // COMPRESSED!
|
||||||
|
|
||||||
status: integer("status").$type<ServiceDeploymentStatus>().notNull(),
|
status: integer("status").$type<ServiceDeploymentStatus>().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -265,7 +295,7 @@ export const serviceDomain = sqliteTable("service_domain", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
domain: text("domain").notNull(),
|
domain: text("domain").notNull(),
|
||||||
internalPort: integer("internal_port").notNull(),
|
internalPort: integer("internal_port").notNull(),
|
||||||
|
@ -273,13 +303,6 @@ export const serviceDomain = sqliteTable("service_domain", {
|
||||||
forceSSL: integer("force_ssl", { mode: "boolean" }).default(false).notNull(),
|
forceSSL: integer("force_ssl", { mode: "boolean" }).default(false).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const serviceDomainRelations = relations(serviceDomain, ({ one }) => ({
|
|
||||||
service: one(service, {
|
|
||||||
fields: [serviceDomain.serviceId],
|
|
||||||
references: [service.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project exposed ports
|
* Project exposed ports
|
||||||
*/
|
*/
|
||||||
|
@ -287,7 +310,7 @@ export const servicePort = sqliteTable("service_port", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
internalPort: integer("internal_port").notNull(),
|
internalPort: integer("internal_port").notNull(),
|
||||||
externalPort: integer("external_port").notNull(),
|
externalPort: integer("external_port").notNull(),
|
||||||
|
@ -295,13 +318,6 @@ export const servicePort = sqliteTable("service_port", {
|
||||||
type: integer("type").$type<ServicePortType>().notNull(),
|
type: integer("type").$type<ServicePortType>().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const servicePortRelations = relations(servicePort, ({ one }) => ({
|
|
||||||
service: one(service, {
|
|
||||||
fields: [servicePort.serviceId],
|
|
||||||
references: [service.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project sysctls
|
* Project sysctls
|
||||||
*/
|
*/
|
||||||
|
@ -309,19 +325,12 @@ export const serviceSysctl = sqliteTable("service_sysctl", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
value: text("value").notNull(),
|
value: text("value").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const serviceSysctlRelations = relations(serviceSysctl, ({ one }) => ({
|
|
||||||
service: one(service, {
|
|
||||||
fields: [serviceSysctl.serviceId],
|
|
||||||
references: [service.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project volumes
|
* Project volumes
|
||||||
*/
|
*/
|
||||||
|
@ -329,20 +338,13 @@ export const serviceVolume = sqliteTable("service_volume", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
source: text("source"),
|
source: text("source"),
|
||||||
target: text("target").notNull(),
|
target: text("target").notNull(),
|
||||||
type: text("type").$type<DockerVolumeType>().notNull(),
|
type: text("type").$type<DockerVolumeType>().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const serviceVolumeRelations = relations(serviceVolume, ({ one }) => ({
|
|
||||||
service: one(service, {
|
|
||||||
fields: [serviceVolume.serviceId],
|
|
||||||
references: [service.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project ulimits
|
* Project ulimits
|
||||||
*/
|
*/
|
||||||
|
@ -350,16 +352,9 @@ export const serviceUlimit = sqliteTable("service_ulimit", {
|
||||||
id: text("id").default(uuidv7).primaryKey(),
|
id: text("id").default(uuidv7).primaryKey(),
|
||||||
serviceId: text("service_id")
|
serviceId: text("service_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => service.id),
|
.references(() => serviceGeneration.id),
|
||||||
|
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
soft: integer("soft").notNull(),
|
soft: integer("soft").notNull(),
|
||||||
hard: integer("hard").notNull(),
|
hard: integer("hard").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const serviceUlimitRelations = relations(serviceUlimit, ({ one }) => ({
|
|
||||||
service: one(service, {
|
|
||||||
fields: [serviceUlimit.serviceId],
|
|
||||||
references: [service.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
|
@ -1,12 +1,12 @@
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import {
|
import {
|
||||||
type service,
|
|
||||||
type serviceDomain,
|
type serviceDomain,
|
||||||
|
type serviceGeneration,
|
||||||
type servicePort,
|
type servicePort,
|
||||||
type serviceSysctl,
|
type serviceSysctl,
|
||||||
type serviceUlimit,
|
type serviceUlimit,
|
||||||
type serviceVolume,
|
type serviceVolume,
|
||||||
} from "../db/schema";
|
} from "../db/schema/schema";
|
||||||
import {
|
import {
|
||||||
DOCKER_DEPLOY_MODE_MAP,
|
DOCKER_DEPLOY_MODE_MAP,
|
||||||
DOCKER_RESTART_CONDITION_MAP,
|
DOCKER_RESTART_CONDITION_MAP,
|
||||||
|
@ -19,7 +19,7 @@ import {
|
||||||
type Ulimits,
|
type Ulimits,
|
||||||
} from "./compose";
|
} from "./compose";
|
||||||
|
|
||||||
export type Service = typeof service.$inferSelect & {
|
export type Service = typeof serviceGeneration.$inferSelect & {
|
||||||
domains: (typeof serviceDomain.$inferSelect)[];
|
domains: (typeof serviceDomain.$inferSelect)[];
|
||||||
ports: (typeof servicePort.$inferSelect)[];
|
ports: (typeof servicePort.$inferSelect)[];
|
||||||
sysctls: (typeof serviceSysctl.$inferSelect)[];
|
sysctls: (typeof serviceSysctl.$inferSelect)[];
|
||||||
|
|
37
src/server/managers/ProjectManager.ts
Normal file
37
src/server/managers/ProjectManager.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { eq, or } from "drizzle-orm";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { projects, service } from "../db/schema";
|
||||||
|
import ServiceManager from "./ServiceManager";
|
||||||
|
|
||||||
|
export default class ProjectManager {
|
||||||
|
constructor(private projectData: typeof projects.$inferSelect) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a project by name or ID.
|
||||||
|
*/
|
||||||
|
static async findByNameOrId(nameOrId: string) {
|
||||||
|
const data = await db.query.projects.findFirst({
|
||||||
|
where: or(eq(projects.internalName, nameOrId), eq(projects.id, nameOrId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data ? new ProjectManager(data) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the project data.
|
||||||
|
*/
|
||||||
|
public getData() {
|
||||||
|
return this.projectData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the project services.
|
||||||
|
*/
|
||||||
|
public async getServices() {
|
||||||
|
const serviceData = await db.query.service.findMany({
|
||||||
|
where: eq(service.projectId, this.projectData.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return serviceData.map((data) => new ServiceManager(data));
|
||||||
|
}
|
||||||
|
}
|
100
src/server/managers/ServiceManager.ts
Normal file
100
src/server/managers/ServiceManager.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { deterministicString } from "deterministic-object-hash";
|
||||||
|
import { and, eq, or } from "drizzle-orm";
|
||||||
|
import { create } from "jsondiffpatch";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { service, serviceGeneration } from "../db/schema";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
|
||||||
|
export default class ServiceManager {
|
||||||
|
private static LOGGER = logger.child({
|
||||||
|
module: "ServiceManager",
|
||||||
|
});
|
||||||
|
|
||||||
|
private static JSON_DIFF = create({
|
||||||
|
objectHash: (obj: unknown) => {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return deterministicString(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("id" in obj && typeof obj.id === "string") {
|
||||||
|
return obj.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.LOGGER.warn("Unexpected object in JSON diff.", { obj });
|
||||||
|
return deterministicString(obj);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private serviceData: typeof service.$inferSelect & {
|
||||||
|
latestGeneration?: typeof serviceGeneration.$inferSelect;
|
||||||
|
deployedGeneration?: typeof serviceGeneration.$inferSelect;
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a service by name or ID.
|
||||||
|
*/
|
||||||
|
static async findByNameOrId(nameOrId: string, projectId: string) {
|
||||||
|
const data = await db.query.service.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(service.projectId, projectId),
|
||||||
|
or(eq(service.name, nameOrId), eq(service.id, nameOrId)),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data ? new ServiceManager(data) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of changed parameters between the current deployed service and the latest generation.
|
||||||
|
*/
|
||||||
|
public async buildDeployDiff() {
|
||||||
|
if (
|
||||||
|
this.serviceData.latestGenerationId ===
|
||||||
|
this.serviceData.deployedGenerationId
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the generations
|
||||||
|
const [latest, deployed] = await Promise.all([
|
||||||
|
this.fetchLatestGeneration(),
|
||||||
|
this.fetchDeployedGeneration(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// compare the two
|
||||||
|
return ServiceManager.JSON_DIFF.diff(deployed, latest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest generation of the service.
|
||||||
|
* @param forceRefetch if true, will ignore what's cached internally
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async fetchLatestGeneration(forceRefetch?: boolean) {
|
||||||
|
if (!forceRefetch && this.serviceData.latestGeneration) {
|
||||||
|
return this.serviceData.latestGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.serviceData.latestGeneration =
|
||||||
|
await db.query.serviceGeneration.findFirst({
|
||||||
|
where: eq(serviceGeneration.id, this.serviceData.latestGenerationId),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the deployed generation of the service.
|
||||||
|
* @param forceRefetch if true, will ignore what's cached internally
|
||||||
|
*/
|
||||||
|
private async fetchDeployedGeneration(forceRefetch?: boolean) {
|
||||||
|
if (!forceRefetch && this.serviceData.deployedGeneration) {
|
||||||
|
return this.serviceData.deployedGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.serviceData.deployedGeneration =
|
||||||
|
await db.query.serviceGeneration.findFirst({
|
||||||
|
where: eq(serviceGeneration.id, this.serviceData.deployedGenerationId),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue