wip: db schema overhaul
This commit is contained in:
parent
92b414b50d
commit
f0877ebc83
|
@ -50,6 +50,7 @@
|
|||
"cookie": "^0.6.0",
|
||||
"datastructures-js": "^13.0.0",
|
||||
"date-fns": "^3.0.6",
|
||||
"deterministic-object-hash": "^2.0.2",
|
||||
"docker-cli-js": "^2.10.0",
|
||||
"docker-modem": "^5.0.3",
|
||||
"dockerode": "^4.0.2",
|
||||
|
@ -59,6 +60,7 @@
|
|||
"extensionless": "^1.9.6",
|
||||
"framer-motion": "^11.0.3",
|
||||
"ipaddr.js": "^2.1.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lucide-react": "^0.298.0",
|
||||
"next": "14.0.4",
|
||||
"next-nprogress-bar": "^2.1.2",
|
||||
|
|
|
@ -104,6 +104,9 @@ dependencies:
|
|||
date-fns:
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
deterministic-object-hash:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
docker-cli-js:
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0
|
||||
|
@ -131,6 +134,9 @@ dependencies:
|
|||
ipaddr.js:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
jsondiffpatch:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
lucide-react:
|
||||
specifier: ^0.298.0
|
||||
version: 0.298.0(react@18.2.0)
|
||||
|
@ -2975,6 +2981,10 @@ packages:
|
|||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
dev: false
|
||||
|
||||
/@types/diff-match-patch@1.0.36:
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
dev: false
|
||||
|
||||
/@types/docker-modem@3.0.6:
|
||||
resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==}
|
||||
dependencies:
|
||||
|
@ -3679,6 +3689,10 @@ packages:
|
|||
/balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
/base-64@1.0.0:
|
||||
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
|
||||
dev: false
|
||||
|
||||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
dev: false
|
||||
|
@ -4386,9 +4400,20 @@ packages:
|
|||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==}
|
||||
dependencies:
|
||||
|
@ -6194,6 +6219,16 @@ packages:
|
|||
minimist: 1.2.8
|
||||
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:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
|
|
@ -3,6 +3,7 @@ import chalk from "chalk";
|
|||
import logger from "~/server/utils/logger";
|
||||
|
||||
const log = logger.child({ module: "trpc:server" });
|
||||
|
||||
export const loggerMiddleware = experimental_standaloneMiddleware().create(
|
||||
async ({ type, path, next }) => {
|
||||
const result = await next();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { TRPCError, experimental_standaloneMiddleware } from "@trpc/server";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { type db } from "~/server/db";
|
||||
import { projects } from "~/server/db/schema";
|
||||
import ProjectManager from "~/server/managers/ProjectManager";
|
||||
|
||||
export type BasicProjectDetails = {
|
||||
id: string;
|
||||
|
@ -21,21 +20,7 @@ export const projectMiddleware = experimental_standaloneMiddleware<{
|
|||
});
|
||||
}
|
||||
|
||||
const [project] = await ctx.db
|
||||
.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);
|
||||
const project = await ProjectManager.findByNameOrId(input.projectId);
|
||||
|
||||
if (!project)
|
||||
throw new TRPCError({
|
||||
|
@ -45,7 +30,7 @@ export const projectMiddleware = experimental_standaloneMiddleware<{
|
|||
|
||||
return next({
|
||||
ctx: {
|
||||
project: project as BasicProjectDetails,
|
||||
project: project,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,20 +23,16 @@ export const serviceMiddleware = experimental_standaloneMiddleware<{
|
|||
});
|
||||
}
|
||||
|
||||
const [serviceDetails] = await ctx.db
|
||||
.select({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
createdAt: service.createdAt,
|
||||
})
|
||||
.from(service)
|
||||
.where(
|
||||
and(
|
||||
eq(service.projectId, ctx.project.id),
|
||||
or(eq(service.name, input.serviceId), eq(service.id, input.serviceId)),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
const serviceDetails = await ctx.db.query.service.findFirst({
|
||||
where: and(
|
||||
eq(service.projectId, ctx.project.id),
|
||||
or(eq(service.name, input.serviceId), eq(service.id, input.serviceId)),
|
||||
),
|
||||
|
||||
with: {
|
||||
latestGeneration: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!serviceDetails)
|
||||
throw new TRPCError({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
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 logger from "~/server/utils/logger";
|
||||
import { projectMiddleware } from "../../middleware/project";
|
||||
|
@ -23,7 +23,7 @@ export const deployProject = authenticatedProcedure
|
|||
.use(projectMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const services = await ctx.db.query.service.findMany({
|
||||
where: eq(service.projectId, input.projectId),
|
||||
where: eq(serviceGeneration.serviceId, input.projectId),
|
||||
|
||||
with: {
|
||||
domains: true,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import assert from "assert";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { projects, service } from "~/server/db/schema";
|
||||
import { projects, serviceGeneration } from "~/server/db/schema";
|
||||
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
||||
import { deployProject } from "./deploy";
|
||||
import { getProject } from "./project";
|
||||
|
@ -34,11 +34,11 @@ export const projectRouter = createTRPCRouter({
|
|||
userProjects.map(async (project) => {
|
||||
const projServices = await ctx.db
|
||||
.select({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
id: serviceGeneration.id,
|
||||
name: serviceGeneration.name,
|
||||
})
|
||||
.from(service)
|
||||
.where(eq(service.projectId, project.id));
|
||||
.from(serviceGeneration)
|
||||
.where(eq(serviceGeneration.serviceId, project.id));
|
||||
|
||||
return {
|
||||
...project,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { service } from "~/server/db/schema";
|
||||
import { serviceGeneration } from "~/server/db/schema";
|
||||
import { projectMiddleware } from "../../middleware/project";
|
||||
import { authenticatedProcedure } from "../../trpc";
|
||||
|
||||
|
@ -18,11 +18,11 @@ export const getProject = authenticatedProcedure
|
|||
.query(async ({ ctx }) => {
|
||||
const projServices = await ctx.db
|
||||
.select({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
id: serviceGeneration.id,
|
||||
name: serviceGeneration.name,
|
||||
})
|
||||
.from(service)
|
||||
.where(eq(service.projectId, ctx.project.id));
|
||||
.from(serviceGeneration)
|
||||
.where(eq(serviceGeneration.serviceId, ctx.project.id));
|
||||
|
||||
// get docker stats
|
||||
const stats = await ctx.docker.listServices({
|
||||
|
|
|
@ -6,7 +6,7 @@ import { env } from "~/env";
|
|||
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||
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 { zDockerName } from "~/server/utils/zod";
|
||||
import { getServiceContainers } from "./containers";
|
||||
|
@ -35,7 +35,7 @@ export const serviceRouter = createTRPCRouter({
|
|||
.use(serviceMiddleware)
|
||||
.query(async ({ ctx }) => {
|
||||
const fullServiceData = await ctx.db.query.service.findFirst({
|
||||
where: eq(service.id, ctx.service.id),
|
||||
where: eq(serviceGeneration.id, ctx.service.id),
|
||||
with: {
|
||||
domains: true,
|
||||
ports: true,
|
||||
|
@ -71,7 +71,7 @@ export const serviceRouter = createTRPCRouter({
|
|||
.use(projectMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const [data] = await ctx.db
|
||||
.insert(service)
|
||||
.insert(serviceGeneration)
|
||||
.values({
|
||||
name: input.name,
|
||||
projectId: ctx.project.id,
|
||||
|
@ -83,7 +83,7 @@ export const serviceRouter = createTRPCRouter({
|
|||
dockerImage: "traefik/whoami",
|
||||
})
|
||||
.returning({
|
||||
id: service.id,
|
||||
id: serviceGeneration.id,
|
||||
})
|
||||
.execute()
|
||||
.catch((err) => {
|
||||
|
@ -108,8 +108,8 @@ export const serviceRouter = createTRPCRouter({
|
|||
.use(projectMiddleware)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db
|
||||
.delete(service)
|
||||
.where(eq(service.id, input.serviceId))
|
||||
.delete(serviceGeneration)
|
||||
.where(eq(serviceGeneration.id, input.serviceId))
|
||||
.execute();
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { z } from "zod";
|
|||
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||
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 { zDockerDuration, zDockerImage, zDomain } from "~/server/utils/zod";
|
||||
|
||||
|
@ -27,7 +27,7 @@ export const updateServiceProcedure = authenticatedProcedure
|
|||
serviceId: z.string(),
|
||||
})
|
||||
.merge(
|
||||
createInsertSchema(service, {
|
||||
createInsertSchema(serviceGeneration, {
|
||||
dockerImage: zDockerImage,
|
||||
gitUrl: (schema) =>
|
||||
schema.gitUrl.regex(
|
||||
|
@ -93,9 +93,9 @@ export const updateServiceProcedure = authenticatedProcedure
|
|||
delete queryUpdate.id;
|
||||
|
||||
await ctx.db
|
||||
.update(service)
|
||||
.update(serviceGeneration)
|
||||
.set(queryUpdate)
|
||||
.where(eq(service.id, ctx.service.id))
|
||||
.where(eq(serviceGeneration.id, ctx.service.id))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
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({
|
||||
currentStats: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
currentStats: authenticatedProcedure.query(async () => {
|
||||
return stats.getCurrentStats();
|
||||
}),
|
||||
|
||||
liveStats: authenticatedProcedure.subscription(async ({ ctx }) => {
|
||||
liveStats: authenticatedProcedure.subscription(() => {
|
||||
return observable<BasicServerStats>((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(
|
||||
new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
);
|
||||
}),
|
||||
|
||||
// core container options
|
||||
redeployTraefik: authenticatedProcedure.mutation(async ({ ctx }) => {
|
||||
redeployTraefik: authenticatedProcedure.mutation(() => {
|
||||
setTimeout(() => {
|
||||
void updateTraefik();
|
||||
}, 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 { env } from "~/env";
|
||||
import crypto, { randomBytes } from "crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IncomingMessage } from "http";
|
||||
import { env } from "~/env";
|
||||
import type { ExtendedRequest } from "../api/trpc";
|
||||
import { db } from "../db";
|
||||
import { sessions, users } from "../db/schema/schema";
|
||||
import logger from "../utils/logger";
|
||||
import crypto from "crypto";
|
||||
|
||||
export type SessionUpdateData = Partial<{
|
||||
ua: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import assert from "assert";
|
||||
import { Queue } from "datastructures-js";
|
||||
import { db } from "../db";
|
||||
import { serviceDeployment } from "../db/schema";
|
||||
import { serviceDeployment } from "../db/schema/schema";
|
||||
import { ServiceDeploymentStatus, ServiceSource } from "../db/types";
|
||||
import { type Service } from "../docker/stack";
|
||||
import logger from "../utils/logger";
|
||||
|
|
|
@ -4,7 +4,7 @@ import { mkdirSync } from "fs";
|
|||
import { rm, rmdir } from "fs/promises";
|
||||
import path from "path";
|
||||
import { db } from "../db";
|
||||
import { service, serviceDeployment } from "../db/schema";
|
||||
import { serviceDeployment, serviceGeneration } from "../db/schema/schema";
|
||||
import {
|
||||
ServiceBuildMethod,
|
||||
ServiceDeploymentStatus,
|
||||
|
@ -122,8 +122,8 @@ export default class BuildTask {
|
|||
private async fetchServiceDetails() {
|
||||
const [serviceDetails] = await db
|
||||
.select()
|
||||
.from(service)
|
||||
.where(eq(service.id, this.serviceId));
|
||||
.from(serviceGeneration)
|
||||
.where(eq(serviceGeneration.id, this.serviceId));
|
||||
|
||||
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";
|
||||
|
||||
export default class BaseBuilder {
|
||||
|
@ -6,7 +6,7 @@ export default class BaseBuilder {
|
|||
public readonly configuration: {
|
||||
fileLogger: BuilderLogger;
|
||||
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";
|
||||
|
||||
export default class BaseSource {
|
||||
|
@ -6,7 +6,7 @@ export default class BaseSource {
|
|||
public readonly configuration: {
|
||||
fileLogger: BuilderLogger;
|
||||
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 {
|
||||
blob,
|
||||
index,
|
||||
|
@ -7,6 +7,7 @@ import {
|
|||
sqliteTable,
|
||||
text,
|
||||
unique,
|
||||
type AnySQLiteColumn,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import {
|
||||
DockerDeployMode,
|
||||
|
@ -16,12 +17,17 @@ import {
|
|||
type ServiceDeploymentStatus,
|
||||
type ServicePortType,
|
||||
type ServiceSource,
|
||||
} from "./types";
|
||||
} from "../types";
|
||||
|
||||
// util
|
||||
const uuidv7 = sql`(uuid_generate_v7())`;
|
||||
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.
|
||||
* 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.
|
||||
* Represents a user's session.
|
||||
|
@ -54,6 +55,7 @@ export const sessions = sqliteTable("session", {
|
|||
token: text("token").primaryKey(),
|
||||
lastUA: text("last_useragent"),
|
||||
lastIP: text("last_ip"),
|
||||
|
||||
// NOT IN MILLISECONDS!
|
||||
lastAccessed: integer("last_accessed", { mode: "timestamp" }),
|
||||
createdAt: integer("created_at").default(now).notNull(),
|
||||
|
@ -62,33 +64,6 @@ export const sessions = sqliteTable("session", {
|
|||
.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
|
||||
* Historical data about the system's usage
|
||||
|
@ -119,9 +94,35 @@ export const projects = sqliteTable("projects", {
|
|||
createdAt: integer("created_at").default(now).notNull(),
|
||||
ownerId: text("owner_id")
|
||||
.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
|
||||
* Represents a service running in a project
|
||||
|
@ -131,11 +132,49 @@ export const service = sqliteTable(
|
|||
"service",
|
||||
{
|
||||
id: text("id").default(uuidv7).primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
|
||||
projectId: text("project_id")
|
||||
.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
|
||||
source: integer("source").$type<ServiceSource>().notNull(),
|
||||
|
@ -174,6 +213,7 @@ export const service = sqliteTable(
|
|||
.$type<DockerDeployMode>()
|
||||
.default(DockerDeployMode.Replicated)
|
||||
.notNull(),
|
||||
|
||||
zeroDowntime: integer("zero_downtime", { mode: "boolean" })
|
||||
.default(false)
|
||||
.notNull(),
|
||||
|
@ -213,48 +253,38 @@ export const service = sqliteTable(
|
|||
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,
|
||||
proj_generation_idx: index("proj_generation_idx").on(
|
||||
table.id,
|
||||
table.serviceId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
// 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
|
||||
*/
|
||||
export const serviceDeployment = sqliteTable("service_deployment", {
|
||||
id: text("id").default(uuidv7).primaryKey(),
|
||||
projectDeploymentId: text("project_deployment_id")
|
||||
.notNull()
|
||||
.references(() => projectDeployment.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
|
||||
serviceId: text("service_id")
|
||||
.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!
|
||||
|
||||
status: integer("status").$type<ServiceDeploymentStatus>().notNull(),
|
||||
});
|
||||
|
||||
|
@ -265,7 +295,7 @@ export const serviceDomain = sqliteTable("service_domain", {
|
|||
id: text("id").default(uuidv7).primaryKey(),
|
||||
serviceId: text("service_id")
|
||||
.notNull()
|
||||
.references(() => service.id),
|
||||
.references(() => serviceGeneration.id),
|
||||
|
||||
domain: text("domain").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(),
|
||||
});
|
||||
|
||||
export const serviceDomainRelations = relations(serviceDomain, ({ one }) => ({
|
||||
service: one(service, {
|
||||
fields: [serviceDomain.serviceId],
|
||||
references: [service.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Project exposed ports
|
||||
*/
|
||||
|
@ -287,7 +310,7 @@ export const servicePort = sqliteTable("service_port", {
|
|||
id: text("id").default(uuidv7).primaryKey(),
|
||||
serviceId: text("service_id")
|
||||
.notNull()
|
||||
.references(() => service.id),
|
||||
.references(() => serviceGeneration.id),
|
||||
|
||||
internalPort: integer("internal_port").notNull(),
|
||||
externalPort: integer("external_port").notNull(),
|
||||
|
@ -295,13 +318,6 @@ export const servicePort = sqliteTable("service_port", {
|
|||
type: integer("type").$type<ServicePortType>().notNull(),
|
||||
});
|
||||
|
||||
export const servicePortRelations = relations(servicePort, ({ one }) => ({
|
||||
service: one(service, {
|
||||
fields: [servicePort.serviceId],
|
||||
references: [service.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Project sysctls
|
||||
*/
|
||||
|
@ -309,19 +325,12 @@ export const serviceSysctl = sqliteTable("service_sysctl", {
|
|||
id: text("id").default(uuidv7).primaryKey(),
|
||||
serviceId: text("service_id")
|
||||
.notNull()
|
||||
.references(() => service.id),
|
||||
.references(() => serviceGeneration.id),
|
||||
|
||||
key: text("key").notNull(),
|
||||
value: text("value").notNull(),
|
||||
});
|
||||
|
||||
export const serviceSysctlRelations = relations(serviceSysctl, ({ one }) => ({
|
||||
service: one(service, {
|
||||
fields: [serviceSysctl.serviceId],
|
||||
references: [service.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Project volumes
|
||||
*/
|
||||
|
@ -329,20 +338,13 @@ export const serviceVolume = sqliteTable("service_volume", {
|
|||
id: text("id").default(uuidv7).primaryKey(),
|
||||
serviceId: text("service_id")
|
||||
.notNull()
|
||||
.references(() => service.id),
|
||||
.references(() => serviceGeneration.id),
|
||||
|
||||
source: text("source"),
|
||||
target: text("target").notNull(),
|
||||
type: text("type").$type<DockerVolumeType>().notNull(),
|
||||
});
|
||||
|
||||
export const serviceVolumeRelations = relations(serviceVolume, ({ one }) => ({
|
||||
service: one(service, {
|
||||
fields: [serviceVolume.serviceId],
|
||||
references: [service.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Project ulimits
|
||||
*/
|
||||
|
@ -350,16 +352,9 @@ export const serviceUlimit = sqliteTable("service_ulimit", {
|
|||
id: text("id").default(uuidv7).primaryKey(),
|
||||
serviceId: text("service_id")
|
||||
.notNull()
|
||||
.references(() => service.id),
|
||||
.references(() => serviceGeneration.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
soft: integer("soft").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 {
|
||||
type service,
|
||||
type serviceDomain,
|
||||
type serviceGeneration,
|
||||
type servicePort,
|
||||
type serviceSysctl,
|
||||
type serviceUlimit,
|
||||
type serviceVolume,
|
||||
} from "../db/schema";
|
||||
} from "../db/schema/schema";
|
||||
import {
|
||||
DOCKER_DEPLOY_MODE_MAP,
|
||||
DOCKER_RESTART_CONDITION_MAP,
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
type Ulimits,
|
||||
} from "./compose";
|
||||
|
||||
export type Service = typeof service.$inferSelect & {
|
||||
export type Service = typeof serviceGeneration.$inferSelect & {
|
||||
domains: (typeof serviceDomain.$inferSelect)[];
|
||||
ports: (typeof servicePort.$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