diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f3a6b..5ee0a56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,11 +69,12 @@ jobs: run: | VERSION="${{ steps.version.outputs.VERSION }}" TAR_DIR=_tar - mkdir -p "$TAR_DIR/infra/caddy" "$TAR_DIR/infra/monitoring/grafana/datasources" + mkdir -p "$TAR_DIR/infra/caddy" "$TAR_DIR/infra/monitoring/grafana/datasources" "$TAR_DIR/scripts/auth" cp docker-compose.yml "$TAR_DIR/" cp scripts/dequel "$TAR_DIR/dequel" cp infra/caddy/Caddyfile "$TAR_DIR/infra/caddy/" cp -r infra/monitoring "$TAR_DIR/infra/" + cp -r scripts/auth/* "$TAR_DIR/scripts/auth/" cd "$TAR_DIR" && tar -czf "../dequel-config-${VERSION}.tar.gz" . - name: Extract changelog entry for this version diff --git a/AGENTS.md b/AGENTS.md index 94b30d0..0652f71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ docker compose up -d # Start full stack docker compose up -d --build # Rebuild and start # Install / Manage -curl -fsSL https://raw.githubusercontent.com/Lftobs/dequel/main/scripts/install.sh | sh +curl -fsSL https://github.com/Lftobs/dequel/releases/latest/download/install.sh | bash scripts/dequel start # Start platform scripts/dequel uninstall # Remove everything (prompts) diff --git a/README.md b/README.md index 3987f05..f447cdb 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ and in the `apps/docs/` directory of this repository. ## Quick Start ```bash -curl -fsSL https://raw.githubusercontent.com/Lftobs/dequel/main/scripts/install.sh | sh +curl -fsSL https://github.com/Lftobs/dequel/releases/latest/download/install.sh | bash ``` Once installed, manage Dequel with the `dequel` CLI: diff --git a/VERSION b/VERSION index 0ea3a94..7dff5b8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index b48ac4b..8b5b81f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "dequel-api", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "scripts": { @@ -10,6 +10,7 @@ }, "dependencies": { "@elysiajs/cors": "^1.1.1", + "@sinclair/typebox": "^0.34.13", "drizzle-orm": "^0.45.2", "elysia": "^1.1.26", "ioredis": "^5.4.1", diff --git a/apps/api/src/api/deployments/index.ts b/apps/api/src/api/deployments/index.ts index 99b1cf0..7cf7d66 100644 --- a/apps/api/src/api/deployments/index.ts +++ b/apps/api/src/api/deployments/index.ts @@ -5,6 +5,7 @@ import { createDeployment, countDeployments, getDeploymentById, + getProjectById, getLogs, listDeployments, } from "../../db/repo"; @@ -66,6 +67,7 @@ export const deploymentsRoutes = new Elysia() String(form.get("commitSha") ?? "").trim() || undefined; const clearCache = form.get("clearCache") === "true"; + const resolvedBranch = branch || (projectId ? (await getProjectById(projectId))?.repoBranch || undefined : undefined); if ( sourceType !== "git" && sourceType !== "upload" && @@ -88,7 +90,7 @@ export const deploymentsRoutes = new Elysia() projectId, sourceType: "git", sourceRef: gitUrl, - branch, + branch: resolvedBranch, environment, commitSha, clearCache, @@ -122,7 +124,7 @@ export const deploymentsRoutes = new Elysia() projectId, sourceType: "upload", sourceRef: uploadPath, - branch, + branch: resolvedBranch, environment, clearCache, }); @@ -186,11 +188,12 @@ export const deploymentsRoutes = new Elysia() error: "Cannot redeploy an image-based (rollback) deployment — rollback to an earlier source deployment instead", }; } + const project = original.projectId ? await getProjectById(original.projectId) : null; const deployment = await createDeployment({ projectId: original.projectId || undefined, sourceType: original.sourceType, sourceRef: original.sourceRef, - branch: original.branch || undefined, + branch: original.branch || project?.repoBranch || undefined, environment: original.environment || undefined, }); orchestrator.enqueue(deployment.id); diff --git a/apps/api/src/api/domains/index.ts b/apps/api/src/api/domains/index.ts index bc899ca..10fd9ed 100644 --- a/apps/api/src/api/domains/index.ts +++ b/apps/api/src/api/domains/index.ts @@ -7,6 +7,7 @@ import { listDomains, updateDomainValidation, } from "../../db/repo"; +import { SERVICE_NAME_RE, isPort } from "../../utils/validate"; export const domainsRoutes = new Elysia() .get( @@ -20,10 +21,21 @@ export const domainsRoutes = new Elysia() set.status = 400; return { error: "domain is required" }; } + if (body.targetService && !SERVICE_NAME_RE.test(String(body.targetService))) { + set.status = 400; + return { error: "targetService may only contain letters, numbers, underscores and hyphens" }; + } + const targetPort = body.targetPort ? Number(body.targetPort) : null; + if (targetPort !== null && !isPort(targetPort)) { + set.status = 400; + return { error: "targetPort must be an integer between 1 and 65535" }; + } const domain = await createDomain({ projectId: params.id, domain: body.domain, type: body.type ?? "custom", + targetService: body.targetService || null, + targetPort, }); const { validateDomain, resolveServerIp } = await import( "../../utils/dns", diff --git a/apps/api/src/api/projects/index.ts b/apps/api/src/api/projects/index.ts index 11e5bcf..a0cf7b9 100644 --- a/apps/api/src/api/projects/index.ts +++ b/apps/api/src/api/projects/index.ts @@ -13,6 +13,24 @@ import { tryRun, reloadCaddy } from "../../orchestrator/runtime"; import { removeFromCaddyRoute } from "../../utils/domain-verifier"; import { config } from "../../utils/config"; import { dockerBin } from "../../utils/docker-bin"; +import { SERVICE_NAME_RE, isPort, validateComposeServices } from "../../utils/validate"; + +const validateComposeFields = (body: any): string | null => { + if (body?.composeService && !SERVICE_NAME_RE.test(String(body.composeService))) { + return "composeService may only contain letters, numbers, underscores and hyphens"; + } + if (body?.composePort) { + const port = Number(body.composePort); + if (!isPort(port)) { + return "composePort must be an integer between 1 and 65535"; + } + } + if (body?.composeServices) { + const result = validateComposeServices(body.composeServices); + if (!result.ok) return result.error; + } + return null; +}; export const projectsRoutes = new Elysia() .get("/projects", async () => listProjects()) @@ -34,15 +52,10 @@ export const projectsRoutes = new Elysia() set.status = 400; return { error: "name is required" }; } - if (body?.repoUrl) { - const projects = await listProjects(); - const normalize = (u: string) => u.replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase(); - const incoming = normalize(body.repoUrl); - const duplicate = projects.find((p) => p.repoUrl && normalize(p.repoUrl) === incoming); - if (duplicate) { - set.status = 409; - return { error: `A project with this repository URL already exists: "${duplicate.name}"` }; - } + const validationError = validateComposeFields(body); + if (validationError) { + set.status = 400; + return { error: validationError }; } const project = await createProject({ name: body.name, @@ -55,6 +68,13 @@ export const projectsRoutes = new Elysia() port: body.port ? Number(body.port) : null, sourceDir: body.sourceDir || null, sourceType: body.sourceType || "git", + projectType: body.projectType || "web", + buildType: body.buildType || "railpack", + composeService: body.composeService || null, + composePort: body.composePort ? Number(body.composePort) : null, + composeServices: body.composeServices ? (typeof body.composeServices === 'string' ? body.composeServices : JSON.stringify(body.composeServices)) : null, + buildCommand: body.buildCommand || undefined, + startCommand: body.startCommand || undefined, }); return project; }, @@ -62,15 +82,10 @@ export const projectsRoutes = new Elysia() .patch( "/projects/:id", async ({ params: { id }, body, set }: any) => { - if (body?.repoUrl) { - const projects = await listProjects(); - const normalize = (u: string) => u.replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase(); - const incoming = normalize(body.repoUrl); - const duplicate = projects.find((p) => p.id !== id && p.repoUrl && normalize(p.repoUrl) === incoming); - if (duplicate) { - set.status = 409; - return { error: `A project with this repository URL already exists: "${duplicate.name}"` }; - } + const validationError = validateComposeFields(body); + if (validationError) { + set.status = 400; + return { error: validationError }; } const project = await updateProject(id, { name: body?.name, @@ -82,6 +97,13 @@ export const projectsRoutes = new Elysia() memoryLimitMb: body?.memoryLimitMb, port: body?.port ? Number(body.port) : body?.port === null ? null : undefined, sourceDir: body?.sourceDir ?? undefined, + projectType: body?.projectType ?? undefined, + buildType: body?.buildType ?? undefined, + composeService: "composeService" in (body ?? {}) ? (body.composeService || null) : undefined, + composePort: "composePort" in (body ?? {}) ? (body.composePort ? Number(body.composePort) : null) : undefined, + composeServices: "composeServices" in (body ?? {}) ? (body.composeServices ? (typeof body.composeServices === 'string' ? body.composeServices : JSON.stringify(body.composeServices)) : null) : undefined, + buildCommand: "buildCommand" in (body ?? {}) ? (body.buildCommand ?? "") || null : undefined, + startCommand: "startCommand" in (body ?? {}) ? (body.startCommand ?? "") || null : undefined, }); if (!project) { set.status = 404; diff --git a/apps/api/src/api/scaling/index.ts b/apps/api/src/api/scaling/index.ts index f0c5292..2914cc2 100644 --- a/apps/api/src/api/scaling/index.ts +++ b/apps/api/src/api/scaling/index.ts @@ -3,6 +3,7 @@ import { deleteScalingPolicy, getScalingPolicy, upsertScalingPolicy, + getProjectById, } from "../../db/repo"; export const scalingRoutes = new Elysia() @@ -24,6 +25,15 @@ export const scalingRoutes = new Elysia() set.status = 400; return { error: "body is required" }; } + const project = await getProjectById(params.id); + if (!project) { + set.status = 404; + return { error: "Project not found" }; + } + if (body.enabled !== false && (!project.cpuLimit || project.cpuLimit <= 0)) { + set.status = 400; + return { error: "Cannot enable autoscaling on a project without CPU resource limits configured." }; + } return upsertScalingPolicy({ projectId: params.id, ...body, diff --git a/apps/api/src/databases/manager.ts b/apps/api/src/databases/manager.ts index 8c02b04..152bb38 100644 --- a/apps/api/src/databases/manager.ts +++ b/apps/api/src/databases/manager.ts @@ -24,59 +24,63 @@ const tryRun = (cmd: string, args: string[]) => export const provisionDatabase = async (dbRecord: Database): Promise => { const containerName = dbRecord.internalHost; - const version = dbRecord.version || (dbRecord.type === 'mysql' ? '8.0' : '16-alpine'); - const image = dbRecord.type === 'mysql' ? `mysql:${version}` : `postgres:${version}`; + try { + const version = dbRecord.version || (dbRecord.type === 'mysql' ? '8.0' : '16-alpine'); + const image = dbRecord.type === 'mysql' ? `mysql:${version}` : `postgres:${version}`; - // Pull image first (non-blocking) - await tryRun(dockerBin, ['pull', image]); + await tryRun(dockerBin, ['pull', image]); - const envVars = dbRecord.type === 'mysql' - ? [ - `MYSQL_ROOT_PASSWORD=${dbRecord.password}`, - `MYSQL_DATABASE=${dbRecord.databaseName}`, - `MYSQL_USER=${dbRecord.username}`, - `MYSQL_PASSWORD=${dbRecord.password}`, - ] - : [ - `POSTGRES_USER=${dbRecord.username}`, - `POSTGRES_PASSWORD=${dbRecord.password}`, - `POSTGRES_DB=${dbRecord.databaseName}`, - ]; + const envVars = dbRecord.type === 'mysql' + ? [ + `MYSQL_ROOT_PASSWORD=${dbRecord.password}`, + `MYSQL_DATABASE=${dbRecord.databaseName}`, + `MYSQL_USER=${dbRecord.username}`, + `MYSQL_PASSWORD=${dbRecord.password}`, + ] + : [ + `POSTGRES_USER=${dbRecord.username}`, + `POSTGRES_PASSWORD=${dbRecord.password}`, + `POSTGRES_DB=${dbRecord.databaseName}`, + ]; - const volumeName = `db-${dbRecord.id.slice(0, 12)}`; + const volumeName = `db-${dbRecord.id.slice(0, 12)}`; - // Create volume - await tryRun(dockerBin, ['volume', 'create', volumeName]); + await tryRun(dockerBin, ['volume', 'create', volumeName]); + await tryRun(dockerBin, ['rm', '-f', containerName]); - const args = [ - 'run', '-d', - '--name', containerName, - '--network', config.dockerNetwork, - '--network-alias', containerName, - '-l', DEQUEL_MANAGED_LABEL, - ...(dbRecord.cpuLimit ? ['--cpus', String(dbRecord.cpuLimit)] : []), - ...(dbRecord.memoryLimitMb ? ['--memory', `${Math.round(dbRecord.memoryLimitMb)}m`] : []), - '-v', `${volumeName}:/var/lib/${dbRecord.type === 'mysql' ? 'mysql' : 'postgresql/data'}`, - '-e', `TZ=UTC`, - ...envVars.flatMap(e => ['-e', e]), - image, - ]; + const args = [ + 'run', '-d', + '--name', containerName, + '--network', config.dockerNetwork, + '--network-alias', containerName, + '-l', DEQUEL_MANAGED_LABEL, + ...(dbRecord.cpuLimit ? ['--cpus', String(dbRecord.cpuLimit)] : []), + ...(dbRecord.memoryLimitMb ? ['--memory', `${Math.round(dbRecord.memoryLimitMb)}m`] : []), + '-v', `${volumeName}:/var/lib/${dbRecord.type === 'mysql' ? 'mysql' : 'postgresql/data'}`, + '-e', `TZ=UTC`, + ...envVars.flatMap(e => ['-e', e]), + image, + ]; - await run(dockerBin, args); + await run(dockerBin, args); - // Wait for healthy - for (let i = 0; i < 30; i++) { - try { - const status = await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName]); - if (status.trim() === 'running') { - await updateDatabaseStatus(dbRecord.id, 'running', containerName); - return; - } - } catch {} - await new Promise(r => setTimeout(r, 2000)); - } + for (let i = 0; i < 30; i++) { + try { + const status = await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName]); + if (status.trim() === 'running') { + await updateDatabaseStatus(dbRecord.id, 'running', containerName); + return; + } + } catch {} + await new Promise(r => setTimeout(r, 2000)); + } - await updateDatabaseStatus(dbRecord.id, 'failed', containerName); + await updateDatabaseStatus(dbRecord.id, 'failed', containerName); + } catch (err) { + console.error(`[DB Provisioner] Error provisioning database ${dbRecord.id}:`, err); + await updateDatabaseStatus(dbRecord.id, 'failed', containerName).catch(() => {}); + throw err; + } }; export const deprovisionDatabase = async (dbRecord: Database): Promise => { diff --git a/apps/api/src/db/migrate.ts b/apps/api/src/db/migrate.ts index 70797f7..f7e8cc4 100644 --- a/apps/api/src/db/migrate.ts +++ b/apps/api/src/db/migrate.ts @@ -30,6 +30,7 @@ export const migrate = async () => { drizzleMigrate(db, { migrationsFolder }); await addClearCacheColumn(db); + await addFinishedAtColumn(db); await seedFromConfig(); }; @@ -45,6 +46,18 @@ const addClearCacheColumn = async (db: ReturnType) => { } }; +const addFinishedAtColumn = async (db: ReturnType) => { + try { + db.run(sql`ALTER TABLE deployments ADD COLUMN finished_at text`); + console.log("[Migrate] Added finished_at column to deployments table"); + } catch (err) { + const cause = err instanceof Error && "cause" in err ? err.cause : err; + if (cause instanceof Error && cause.message.includes("duplicate column name")) return; + console.error("[Migrate] Failed to add finished_at column:", err); + throw err; + } +}; + const seedFromConfig = async () => { if (config.githubClientId && config.githubClientSecret) { const existing = await getGithubIntegration(); diff --git a/apps/api/src/db/migrations/0002_colossal_flatman.sql b/apps/api/src/db/migrations/0002_colossal_flatman.sql new file mode 100644 index 0000000..107987e --- /dev/null +++ b/apps/api/src/db/migrations/0002_colossal_flatman.sql @@ -0,0 +1 @@ +ALTER TABLE `projects` ADD `project_type` text DEFAULT 'web' NOT NULL; diff --git a/apps/api/src/db/migrations/0003_aberrant_viper.sql b/apps/api/src/db/migrations/0003_aberrant_viper.sql new file mode 100644 index 0000000..cebfba7 --- /dev/null +++ b/apps/api/src/db/migrations/0003_aberrant_viper.sql @@ -0,0 +1,2 @@ +ALTER TABLE `projects` ADD `build_command` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `start_command` text; \ No newline at end of file diff --git a/apps/api/src/db/migrations/0004_early_sunfire.sql b/apps/api/src/db/migrations/0004_early_sunfire.sql new file mode 100644 index 0000000..3d35187 --- /dev/null +++ b/apps/api/src/db/migrations/0004_early_sunfire.sql @@ -0,0 +1,3 @@ +ALTER TABLE `projects` ADD `build_type` text DEFAULT 'railpack' NOT NULL;--> statement-breakpoint +ALTER TABLE `projects` ADD `compose_service` text;--> statement-breakpoint +ALTER TABLE `projects` ADD `compose_port` integer; \ No newline at end of file diff --git a/apps/api/src/db/migrations/0005_famous_grandmaster.sql b/apps/api/src/db/migrations/0005_famous_grandmaster.sql new file mode 100644 index 0000000..663e8e1 --- /dev/null +++ b/apps/api/src/db/migrations/0005_famous_grandmaster.sql @@ -0,0 +1,2 @@ +ALTER TABLE `domains` ADD `target_service` text;--> statement-breakpoint +ALTER TABLE `domains` ADD `target_port` integer; \ No newline at end of file diff --git a/apps/api/src/db/migrations/0006_cheerful_vance_astro.sql b/apps/api/src/db/migrations/0006_cheerful_vance_astro.sql new file mode 100644 index 0000000..6470972 --- /dev/null +++ b/apps/api/src/db/migrations/0006_cheerful_vance_astro.sql @@ -0,0 +1 @@ +ALTER TABLE `projects` ADD `compose_services` text; \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0002_snapshot.json b/apps/api/src/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..9b18484 --- /dev/null +++ b/apps/api/src/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,1311 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a478725b-8dd8-478f-add2-60b5385e91dc", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alerts_project_id_projects_id_fk": { + "name": "alerts_project_id_projects_id_fk", + "tableFrom": "alerts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'deploy:read'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_host": { + "name": "internal_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_logs": { + "name": "deployment_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_logs_dep_seq": { + "name": "idx_logs_dep_seq", + "columns": [ + "deployment_id", + "sequence" + ], + "isUnique": true + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployments": { + "name": "deployments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route_path": { + "name": "route_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "live_url": { + "name": "live_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clear_cache": { + "name": "clear_cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "domains": { + "name": "domains", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "validation_status": { + "name": "validation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "ssl_status": { + "name": "ssl_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_encrypted": { + "name": "value_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_iv": { + "name": "value_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_tag": { + "name": "value_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'production'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_env_vars_project": { + "name": "idx_env_vars_project", + "columns": [ + "project_id", + "environment" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_integrations": { + "name": "github_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Dequel'" + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_branch": { + "name": "repo_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_domain": { + "name": "base_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_dir": { + "name": "source_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'git'" + }, + "project_type": { + "name": "project_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'web'" + }, + "github_token_encrypted": { + "name": "github_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_iv": { + "name": "github_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_tag": { + "name": "github_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blacklisted_at": { + "name": "blacklisted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scaling_policies": { + "name": "scaling_policies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_replicas": { + "name": "min_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas": { + "name": "max_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "cpu_threshold_percent": { + "name": "cpu_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 70 + }, + "memory_threshold_percent": { + "name": "memory_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "cooldown_seconds": { + "name": "cooldown_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 120 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "scaling_policies_project_id_unique": { + "name": "scaling_policies_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "scaling_policies_project_id_projects_id_fk": { + "name": "scaling_policies_project_id_projects_id_fk", + "tableFrom": "scaling_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "servers": { + "name": "servers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2375 + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "cpu_total": { + "name": "cpu_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_mb": { + "name": "memory_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_total_mb": { + "name": "disk_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_used_percent": { + "name": "cpu_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_mb": { + "name": "memory_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "smtp_settings": { + "name": "smtp_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pass_encrypted": { + "name": "pass_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_iv": { + "name": "pass_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_tag": { + "name": "pass_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dequel@localhost'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes": { + "name": "volumes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/app/data'" + }, + "size_mb": { + "name": "size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_volume_name": { + "name": "docker_volume_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "volumes_project_id_projects_id_fk": { + "name": "volumes_project_id_projects_id_fk", + "tableFrom": "volumes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0003_snapshot.json b/apps/api/src/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..1fe1ebe --- /dev/null +++ b/apps/api/src/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,1325 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b951d02c-f63a-48e5-925c-aac34aa7a759", + "prevId": "a478725b-8dd8-478f-add2-60b5385e91dc", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alerts_project_id_projects_id_fk": { + "name": "alerts_project_id_projects_id_fk", + "tableFrom": "alerts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'deploy:read'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_host": { + "name": "internal_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_logs": { + "name": "deployment_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_logs_dep_seq": { + "name": "idx_logs_dep_seq", + "columns": [ + "deployment_id", + "sequence" + ], + "isUnique": true + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployments": { + "name": "deployments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route_path": { + "name": "route_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "live_url": { + "name": "live_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clear_cache": { + "name": "clear_cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "domains": { + "name": "domains", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "validation_status": { + "name": "validation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "ssl_status": { + "name": "ssl_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_encrypted": { + "name": "value_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_iv": { + "name": "value_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_tag": { + "name": "value_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'production'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_env_vars_project": { + "name": "idx_env_vars_project", + "columns": [ + "project_id", + "environment" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_integrations": { + "name": "github_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Dequel'" + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_branch": { + "name": "repo_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_domain": { + "name": "base_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_dir": { + "name": "source_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'git'" + }, + "project_type": { + "name": "project_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'web'" + }, + "build_command": { + "name": "build_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_encrypted": { + "name": "github_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_iv": { + "name": "github_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_tag": { + "name": "github_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blacklisted_at": { + "name": "blacklisted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scaling_policies": { + "name": "scaling_policies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_replicas": { + "name": "min_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas": { + "name": "max_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "cpu_threshold_percent": { + "name": "cpu_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 70 + }, + "memory_threshold_percent": { + "name": "memory_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "cooldown_seconds": { + "name": "cooldown_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 120 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "scaling_policies_project_id_unique": { + "name": "scaling_policies_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "scaling_policies_project_id_projects_id_fk": { + "name": "scaling_policies_project_id_projects_id_fk", + "tableFrom": "scaling_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "servers": { + "name": "servers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2375 + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "cpu_total": { + "name": "cpu_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_mb": { + "name": "memory_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_total_mb": { + "name": "disk_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_used_percent": { + "name": "cpu_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_mb": { + "name": "memory_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "smtp_settings": { + "name": "smtp_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pass_encrypted": { + "name": "pass_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_iv": { + "name": "pass_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_tag": { + "name": "pass_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dequel@localhost'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes": { + "name": "volumes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/app/data'" + }, + "size_mb": { + "name": "size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_volume_name": { + "name": "docker_volume_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "volumes_project_id_projects_id_fk": { + "name": "volumes_project_id_projects_id_fk", + "tableFrom": "volumes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0004_snapshot.json b/apps/api/src/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..91db366 --- /dev/null +++ b/apps/api/src/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,1347 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9f22f4b0-41ab-482d-b095-81d9e6735b7c", + "prevId": "b951d02c-f63a-48e5-925c-aac34aa7a759", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alerts_project_id_projects_id_fk": { + "name": "alerts_project_id_projects_id_fk", + "tableFrom": "alerts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'deploy:read'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_host": { + "name": "internal_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_logs": { + "name": "deployment_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_logs_dep_seq": { + "name": "idx_logs_dep_seq", + "columns": [ + "deployment_id", + "sequence" + ], + "isUnique": true + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployments": { + "name": "deployments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route_path": { + "name": "route_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "live_url": { + "name": "live_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clear_cache": { + "name": "clear_cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "domains": { + "name": "domains", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "validation_status": { + "name": "validation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "ssl_status": { + "name": "ssl_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_encrypted": { + "name": "value_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_iv": { + "name": "value_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_tag": { + "name": "value_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'production'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_env_vars_project": { + "name": "idx_env_vars_project", + "columns": [ + "project_id", + "environment" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_integrations": { + "name": "github_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Dequel'" + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_branch": { + "name": "repo_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_domain": { + "name": "base_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_dir": { + "name": "source_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'git'" + }, + "project_type": { + "name": "project_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'web'" + }, + "build_type": { + "name": "build_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'railpack'" + }, + "compose_service": { + "name": "compose_service", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_port": { + "name": "compose_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_command": { + "name": "build_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_encrypted": { + "name": "github_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_iv": { + "name": "github_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_tag": { + "name": "github_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blacklisted_at": { + "name": "blacklisted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scaling_policies": { + "name": "scaling_policies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_replicas": { + "name": "min_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas": { + "name": "max_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "cpu_threshold_percent": { + "name": "cpu_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 70 + }, + "memory_threshold_percent": { + "name": "memory_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "cooldown_seconds": { + "name": "cooldown_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 120 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "scaling_policies_project_id_unique": { + "name": "scaling_policies_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "scaling_policies_project_id_projects_id_fk": { + "name": "scaling_policies_project_id_projects_id_fk", + "tableFrom": "scaling_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "servers": { + "name": "servers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2375 + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "cpu_total": { + "name": "cpu_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_mb": { + "name": "memory_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_total_mb": { + "name": "disk_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_used_percent": { + "name": "cpu_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_mb": { + "name": "memory_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "smtp_settings": { + "name": "smtp_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pass_encrypted": { + "name": "pass_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_iv": { + "name": "pass_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_tag": { + "name": "pass_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dequel@localhost'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes": { + "name": "volumes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/app/data'" + }, + "size_mb": { + "name": "size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_volume_name": { + "name": "docker_volume_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "volumes_project_id_projects_id_fk": { + "name": "volumes_project_id_projects_id_fk", + "tableFrom": "volumes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0005_snapshot.json b/apps/api/src/db/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..f9fbea8 --- /dev/null +++ b/apps/api/src/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,1361 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c3c49a94-32f1-4700-a21d-1a2b1ab78644", + "prevId": "9f22f4b0-41ab-482d-b095-81d9e6735b7c", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alerts_project_id_projects_id_fk": { + "name": "alerts_project_id_projects_id_fk", + "tableFrom": "alerts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'deploy:read'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_host": { + "name": "internal_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_logs": { + "name": "deployment_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_logs_dep_seq": { + "name": "idx_logs_dep_seq", + "columns": [ + "deployment_id", + "sequence" + ], + "isUnique": true + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployments": { + "name": "deployments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route_path": { + "name": "route_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "live_url": { + "name": "live_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clear_cache": { + "name": "clear_cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "domains": { + "name": "domains", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "validation_status": { + "name": "validation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "ssl_status": { + "name": "ssl_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "target_service": { + "name": "target_service", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_port": { + "name": "target_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_encrypted": { + "name": "value_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_iv": { + "name": "value_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_tag": { + "name": "value_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'production'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_env_vars_project": { + "name": "idx_env_vars_project", + "columns": [ + "project_id", + "environment" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_integrations": { + "name": "github_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Dequel'" + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_branch": { + "name": "repo_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_domain": { + "name": "base_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_dir": { + "name": "source_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'git'" + }, + "project_type": { + "name": "project_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'web'" + }, + "build_type": { + "name": "build_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'railpack'" + }, + "compose_service": { + "name": "compose_service", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_port": { + "name": "compose_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_command": { + "name": "build_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_encrypted": { + "name": "github_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_iv": { + "name": "github_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_tag": { + "name": "github_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blacklisted_at": { + "name": "blacklisted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scaling_policies": { + "name": "scaling_policies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_replicas": { + "name": "min_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas": { + "name": "max_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "cpu_threshold_percent": { + "name": "cpu_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 70 + }, + "memory_threshold_percent": { + "name": "memory_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "cooldown_seconds": { + "name": "cooldown_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 120 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "scaling_policies_project_id_unique": { + "name": "scaling_policies_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "scaling_policies_project_id_projects_id_fk": { + "name": "scaling_policies_project_id_projects_id_fk", + "tableFrom": "scaling_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "servers": { + "name": "servers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2375 + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "cpu_total": { + "name": "cpu_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_mb": { + "name": "memory_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_total_mb": { + "name": "disk_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_used_percent": { + "name": "cpu_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_mb": { + "name": "memory_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "smtp_settings": { + "name": "smtp_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pass_encrypted": { + "name": "pass_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_iv": { + "name": "pass_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_tag": { + "name": "pass_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dequel@localhost'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes": { + "name": "volumes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/app/data'" + }, + "size_mb": { + "name": "size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_volume_name": { + "name": "docker_volume_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "volumes_project_id_projects_id_fk": { + "name": "volumes_project_id_projects_id_fk", + "tableFrom": "volumes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0006_snapshot.json b/apps/api/src/db/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..c7af04c --- /dev/null +++ b/apps/api/src/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,1368 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c4054dd7-dc6f-44e1-90e6-7e5ae9eaa8f9", + "prevId": "c3c49a94-32f1-4700-a21d-1a2b1ab78644", + "tables": { + "alerts": { + "name": "alerts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "threshold": { + "name": "threshold", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "alerts_project_id_projects_id_fk": { + "name": "alerts_project_id_projects_id_fk", + "tableFrom": "alerts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'deploy:read'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "databases": { + "name": "databases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_host": { + "name": "internal_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_port": { + "name": "internal_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployment_logs": { + "name": "deployment_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_logs_dep_seq": { + "name": "idx_logs_dep_seq", + "columns": [ + "deployment_id", + "sequence" + ], + "isUnique": true + } + }, + "foreignKeys": { + "deployment_logs_deployment_id_deployments_id_fk": { + "name": "deployment_logs_deployment_id_deployments_id_fk", + "tableFrom": "deployment_logs", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deployments": { + "name": "deployments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "route_path": { + "name": "route_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "live_url": { + "name": "live_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clear_cache": { + "name": "clear_cache", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "domains": { + "name": "domains", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'custom'" + }, + "validation_status": { + "name": "validation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "ssl_status": { + "name": "ssl_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "target_service": { + "name": "target_service", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_port": { + "name": "target_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_variables": { + "name": "environment_variables", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_encrypted": { + "name": "value_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_iv": { + "name": "value_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value_tag": { + "name": "value_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'production'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_env_vars_project": { + "name": "idx_env_vars_project", + "columns": [ + "project_id", + "environment" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environment_variables_project_id_projects_id_fk": { + "name": "environment_variables_project_id_projects_id_fk", + "tableFrom": "environment_variables", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "github_integrations": { + "name": "github_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Dequel'" + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_branch": { + "name": "repo_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_domain": { + "name": "base_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_limit": { + "name": "cpu_limit", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_limit_mb": { + "name": "memory_limit_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_dir": { + "name": "source_dir", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'git'" + }, + "project_type": { + "name": "project_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'web'" + }, + "build_type": { + "name": "build_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'railpack'" + }, + "compose_service": { + "name": "compose_service", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_port": { + "name": "compose_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_services": { + "name": "compose_services", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build_command": { + "name": "build_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_encrypted": { + "name": "github_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_iv": { + "name": "github_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_token_tag": { + "name": "github_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "blacklisted_at": { + "name": "blacklisted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scaling_policies": { + "name": "scaling_policies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_replicas": { + "name": "min_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_replicas": { + "name": "max_replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "cpu_threshold_percent": { + "name": "cpu_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 70 + }, + "memory_threshold_percent": { + "name": "memory_threshold_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "cooldown_seconds": { + "name": "cooldown_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 120 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "scaling_policies_project_id_unique": { + "name": "scaling_policies_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "scaling_policies_project_id_projects_id_fk": { + "name": "scaling_policies_project_id_projects_id_fk", + "tableFrom": "scaling_policies", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "servers": { + "name": "servers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2375 + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "cpu_total": { + "name": "cpu_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total_mb": { + "name": "memory_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_total_mb": { + "name": "disk_total_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_used_percent": { + "name": "cpu_used_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used_mb": { + "name": "memory_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "smtp_settings": { + "name": "smtp_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pass_encrypted": { + "name": "pass_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_iv": { + "name": "pass_iv", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pass_tag": { + "name": "pass_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dequel@localhost'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes": { + "name": "volumes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'/app/data'" + }, + "size_mb": { + "name": "size_mb", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_volume_name": { + "name": "docker_volume_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "volumes_project_id_projects_id_fk": { + "name": "volumes_project_id_projects_id_fk", + "tableFrom": "volumes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/_journal.json b/apps/api/src/db/migrations/meta/_journal.json index d7da1e3..038b6a3 100644 --- a/apps/api/src/db/migrations/meta/_journal.json +++ b/apps/api/src/db/migrations/meta/_journal.json @@ -15,6 +15,41 @@ "when": 1782100000000, "tag": "0001_refresh_tokens", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1784587675597, + "tag": "0002_colossal_flatman", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1784681960309, + "tag": "0003_aberrant_viper", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1785247979558, + "tag": "0004_early_sunfire", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1785254008342, + "tag": "0005_famous_grandmaster", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1785409331053, + "tag": "0006_cheerful_vance_astro", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/db/repo/deployments.ts b/apps/api/src/db/repo/deployments.ts index bdaf080..843ac82 100644 --- a/apps/api/src/db/repo/deployments.ts +++ b/apps/api/src/db/repo/deployments.ts @@ -21,6 +21,7 @@ const mapDeployment = (row: typeof deployments.$inferSelect): Deployment => ({ environment: row.environment, failureReason: row.failureReason, clearCache: Boolean(row.clearCache), + finishedAt: row.finishedAt ?? null, createdAt: row.createdAt, updatedAt: row.updatedAt, }); @@ -74,18 +75,41 @@ export const updateDeploymentCommitSha = async (id: string, commitSha: string) = db.update(deployments).set({ commitSha, updatedAt: now() }).where(eq(deployments.id, id)).run(); }; +const ACTIVE_STATUSES: DeploymentStatus[] = [ + "pending", + "building", + "deploying", +]; + +const STAMP_FINISHED_UNCONDITIONALLY: DeploymentStatus[] = [ + "running", + "failed", +]; + export const updateDeploymentStatus = async ( id: string, status: DeploymentStatus, patch: Partial> = {}, ) => { const db = await getDrizzle(); + const existing = db + .select({ finishedAt: deployments.finishedAt }) + .from(deployments) + .where(eq(deployments.id, id)) + .get(); const updates: Record = { status, updatedAt: now() }; if (patch.imageTag !== undefined) updates.imageTag = patch.imageTag; if (patch.containerName !== undefined) updates.containerName = patch.containerName; if (patch.liveUrl !== undefined) updates.liveUrl = patch.liveUrl; if (patch.failureReason !== undefined) updates.failureReason = patch.failureReason; if (patch.replicas !== undefined) updates.replicas = patch.replicas; + if (!ACTIVE_STATUSES.includes(status)) { + if (!existing?.finishedAt) { + updates.finishedAt = now(); + } else if (STAMP_FINISHED_UNCONDITIONALLY.includes(status)) { + updates.finishedAt = now(); + } + } db.update(deployments).set(updates).where(eq(deployments.id, id)).run(); }; diff --git a/apps/api/src/db/repo/domains.ts b/apps/api/src/db/repo/domains.ts index 3885059..a0ab10a 100644 --- a/apps/api/src/db/repo/domains.ts +++ b/apps/api/src/db/repo/domains.ts @@ -12,6 +12,8 @@ const mapDomain = (row: typeof domains.$inferSelect): Domain => ({ type: row.type as Domain["type"], validationStatus: row.validationStatus as DomainValidationStatus, sslStatus: row.sslStatus as SslStatus, + targetService: row.targetService ?? null, + targetPort: row.targetPort ?? null, createdAt: row.createdAt, updatedAt: row.updatedAt, }); @@ -27,6 +29,8 @@ export const createDomain = async (input: CreateDomainInput): Promise => type: input.type, validationStatus: "pending", sslStatus: "pending", + targetService: input.targetService ?? null, + targetPort: input.targetPort ?? null, createdAt: timestamp, updatedAt: timestamp, }).run(); diff --git a/apps/api/src/db/repo/projects.ts b/apps/api/src/db/repo/projects.ts index 27fbee5..850c8a1 100644 --- a/apps/api/src/db/repo/projects.ts +++ b/apps/api/src/db/repo/projects.ts @@ -28,6 +28,13 @@ const mapProject = (row: typeof projects.$inferSelect): Project => ({ port: row.port ?? null, sourceDir: row.sourceDir ?? null, sourceType: row.sourceType, + projectType: row.projectType, + buildType: row.buildType, + composeService: row.composeService ?? null, + composePort: row.composePort ?? null, + composeServices: row.composeServices ?? null, + buildCommand: row.buildCommand ?? null, + startCommand: row.startCommand ?? null, githubTokenEncrypted: row.githubTokenEncrypted ?? null, githubTokenIv: row.githubTokenIv ?? null, githubTokenTag: row.githubTokenTag ?? null, @@ -51,6 +58,13 @@ export const createProject = async (input: CreateProjectInput): Promise port: input.port ?? null, sourceDir: input.sourceDir ?? null, sourceType: input.sourceType ?? "git", + projectType: input.projectType ?? "web", + buildType: input.buildType ?? "railpack", + composeService: input.composeService ?? null, + composePort: input.composePort ?? null, + composeServices: input.composeServices ?? null, + buildCommand: input.buildCommand ?? null, + startCommand: input.startCommand ?? null, createdAt: timestamp, updatedAt: timestamp, }).run(); @@ -88,6 +102,13 @@ export const updateProject = async (id: string, patch: Partial [ diff --git a/apps/api/src/orchestrator/__tests__/compose.test.ts b/apps/api/src/orchestrator/__tests__/compose.test.ts new file mode 100644 index 0000000..0817740 --- /dev/null +++ b/apps/api/src/orchestrator/__tests__/compose.test.ts @@ -0,0 +1,69 @@ +import { describe, test, expect } from "bun:test"; +import { parseComposeTarget } from "../compose"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("parseComposeTarget", () => { + test("parses single service with ports correctly", () => { + const dir = mkdtempSync(join(tmpdir(), "dequel-compose-test-")); + try { + const yaml = ` +version: '3.8' +services: + server: + build: . + ports: + - "3001" +`; + writeFileSync(join(dir, "docker-compose.yml"), yaml); + const target = parseComposeTarget(dir); + expect(target.serviceName).toBe("server"); + expect(target.port).toBe(3001); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + test("parses mapping with host:container ports", () => { + const dir = mkdtempSync(join(tmpdir(), "dequel-compose-test-")); + try { + const yaml = ` +version: '3.8' +services: + web: + image: nginx + ports: + - "8080:80" + db: + image: postgres +`; + writeFileSync(join(dir, "docker-compose.yml"), yaml); + const target = parseComposeTarget(dir); + expect(target.serviceName).toBe("web"); + expect(target.port).toBe(80); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + test("respects preferred service and port overrides", () => { + const dir = mkdtempSync(join(tmpdir(), "dequel-compose-test-")); + try { + const yaml = ` +version: '3.8' +services: + api: + build: ./api + web: + build: ./web +`; + writeFileSync(join(dir, "docker-compose.yml"), yaml); + const target = parseComposeTarget(dir, null, "api", 4000); + expect(target.serviceName).toBe("api"); + expect(target.port).toBe(4000); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/api/src/orchestrator/compose.ts b/apps/api/src/orchestrator/compose.ts new file mode 100644 index 0000000..1aaf452 --- /dev/null +++ b/apps/api/src/orchestrator/compose.ts @@ -0,0 +1,365 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { spawn } from "node:child_process"; +import { dockerBin } from "../utils/docker-bin"; + +export interface ComposeTarget { + serviceName: string; + port: number; +} + +export const findComposeFilePath = (workspacePath: string, sourceDir?: string | null): string | null => { + const base = sourceDir ? join(workspacePath, sourceDir.replace(/^\//, "")) : workspacePath; + const ymlPath = join(base, "docker-compose.yml"); + if (existsSync(ymlPath)) return ymlPath; + const yamlPath = join(base, "docker-compose.yaml"); + if (existsSync(yamlPath)) return yamlPath; + return null; +}; + +export const parseComposeTarget = ( + workspacePath: string, + sourceDir?: string | null, + preferredService?: string | null, + preferredPort?: number | null, +): ComposeTarget => { + const composeFile = findComposeFilePath(workspacePath, sourceDir); + if (!composeFile) { + throw new Error("No docker-compose.yml or docker-compose.yaml found in workspace."); + } + + const content = readFileSync(composeFile, "utf-8"); + const lines = content.split("\n"); + + let currentService = ""; + const servicePorts: Record = {}; + const servicesList: string[] = []; + + let inServicesBlock = false; + let inPortsBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + // Check if entering services block + if (line.match(/^services:/)) { + inServicesBlock = true; + continue; + } + + // Top level block exit + if (line.match(/^[a-zA-Z0-9_-]+:/) && !line.startsWith("services:")) { + inServicesBlock = false; + currentService = ""; + } + + if (!inServicesBlock) continue; + + // Service declaration under services: (2 spaces indentation) + const serviceMatch = line.match(/^ ([a-zA-Z0-9_-]+):/); + if (serviceMatch) { + currentService = serviceMatch[1]; + servicesList.push(currentService); + inPortsBlock = false; + continue; + } + + if (currentService) { + // Check if entering ports block + if (line.match(/^\s+ports:/)) { + inPortsBlock = true; + // Inline array syntax: ports: ["8080:80"] or ports: [3000] + const inlineMatch = trimmed.match(/ports:\s*\[\s*["']?(\d+)(?::(\d+))?["']?\s*\]/i); + if (inlineMatch) { + const target = inlineMatch[2] || inlineMatch[1]; + servicePorts[currentService] = Number(target); + inPortsBlock = false; + } + continue; + } + + // Port item under ports block + if (inPortsBlock && line.match(/^\s+-\s*/)) { + const portMatch = trimmed.match(/-\s*["']?(\d+)(?::(\d+))?["']?/); + if (portMatch) { + const targetPort = portMatch[2] || portMatch[1]; + if (!servicePorts[currentService]) { + servicePorts[currentService] = Number(targetPort); + } + } + } else if (line.match(/^\s+[a-zA-Z0-9_-]+:/)) { + inPortsBlock = false; + } + } + } + + let targetService = preferredService?.trim() || ""; + let targetPort = preferredPort || 0; + + if (!targetService) { + const commonNames = ["web", "frontend", "client", "app", "server", "api"]; + for (const name of commonNames) { + if (servicesList.includes(name)) { + targetService = name; + break; + } + } + if (!targetService && servicesList.length > 0) { + targetService = servicesList[0]; + } + } + + if (!targetService) { + throw new Error("Could not detect any services in docker-compose.yml."); + } + + if (!targetPort) { + targetPort = servicePorts[targetService] || 3000; + } + + return { + serviceName: targetService, + port: targetPort, + }; +}; + +export interface ComposeService { + serviceName: string; + port: number; + isPrimary: boolean; +} + +export const parseAllComposeServices = ( + workspacePath: string, + sourceDir?: string | null, + preferredService?: string | null, + preferredPort?: number | null, +): ComposeService[] => { + const primaryTarget = parseComposeTarget(workspacePath, sourceDir, preferredService, preferredPort); + const composeFile = findComposeFilePath(workspacePath, sourceDir); + if (!composeFile) return [{ ...primaryTarget, isPrimary: true }]; + + const content = readFileSync(composeFile, "utf-8"); + const lines = content.split("\n"); + + let currentService = ""; + const servicePorts: Record = {}; + const servicesList: string[] = []; + + let inServicesBlock = false; + let inPortsBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + if (line.match(/^services:/)) { + inServicesBlock = true; + continue; + } + + if (line.match(/^[a-zA-Z0-9_-]+:/) && !line.startsWith("services:")) { + inServicesBlock = false; + currentService = ""; + } + + if (!inServicesBlock) continue; + + const serviceMatch = line.match(/^ ([a-zA-Z0-9_-]+):/); + if (serviceMatch) { + currentService = serviceMatch[1]; + servicesList.push(currentService); + inPortsBlock = false; + continue; + } + + if (currentService) { + if (line.match(/^\s+ports:/)) { + inPortsBlock = true; + const inlineMatch = trimmed.match(/ports:\s*\[\s*["']?(\d+)(?::(\d+))?["']?\s*\]/i); + if (inlineMatch) { + const target = inlineMatch[2] || inlineMatch[1]; + servicePorts[currentService] = Number(target); + inPortsBlock = false; + } + continue; + } + + if (inPortsBlock && line.match(/^\s+-\s*/)) { + const portMatch = trimmed.match(/-\s*["']?(\d+)(?::(\d+))?["']?/); + if (portMatch) { + const targetPort = portMatch[2] || portMatch[1]; + if (!servicePorts[currentService]) { + servicePorts[currentService] = Number(targetPort); + } + } + } else if (line.match(/^\s+[a-zA-Z0-9_-]+:/)) { + inPortsBlock = false; + } + } + } + + const results: ComposeService[] = []; + for (const service of servicesList) { + const isPrimary = service === primaryTarget.serviceName; + const port = isPrimary ? primaryTarget.port : (servicePorts[service] || 3000); + results.push({ + serviceName: service, + port, + isPrimary, + }); + } + + if (results.length === 0) { + results.push({ ...primaryTarget, isPrimary: true }); + } + + return results; +}; + +const spawnComposeCommand = ( + args: string[], + cwd: string, + envVars?: Record, + onLog?: (line: string) => Promise, +): Promise<{ code: number; stdout: string; stderr: string }> => { + return new Promise((resolve, reject) => { + const child = spawn(dockerBin, ["compose", ...args], { + cwd, + env: { ...process.env, ...(envVars || {}) }, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (chunk) => { + const str = String(chunk); + stdout += str; + if (onLog) { + for (const line of str.split("\n").map((l) => l.trim()).filter(Boolean)) { + void onLog(line); + } + } + }); + + child.stderr.on("data", (chunk) => { + const str = String(chunk); + stderr += str; + if (onLog) { + for (const line of str.split("\n").map((l) => l.trim()).filter(Boolean)) { + void onLog(line); + } + } + }); + + child.on("error", reject); + child.on("close", (code) => { + resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }); + }); + }); +}; + +export const buildWithCompose = async ( + workspacePath: string, + projectName: string, + onLog: (line: string) => Promise, + sourceDir?: string | null, + envVars?: Record, +): Promise => { + const composeFile = findComposeFilePath(workspacePath, sourceDir); + if (!composeFile) { + throw new Error("No docker-compose.yml found in workspace."); + } + + const cwd = sourceDir ? join(workspacePath, sourceDir.replace(/^\//, "")) : workspacePath; + await onLog(`Starting Docker Compose build (project: ${projectName})...`); + + const res = await spawnComposeCommand(["-f", composeFile, "-p", projectName, "build"], cwd, envVars, onLog); + if (res.code !== 0) { + throw new Error(`docker compose build failed: ${res.stderr || res.stdout}`); + } + + await onLog("Docker Compose build completed successfully."); +}; + +export const deployWithCompose = async ( + workspacePath: string, + projectName: string, + onLog: (line: string) => Promise, + sourceDir?: string | null, + envVars?: Record, +): Promise => { + const composeFile = findComposeFilePath(workspacePath, sourceDir); + if (!composeFile) { + throw new Error("No docker-compose.yml found in workspace."); + } + + const cwd = sourceDir ? join(workspacePath, sourceDir.replace(/^\//, "")) : workspacePath; + await onLog(`Starting Docker Compose services (project: ${projectName})...`); + + const res = await spawnComposeCommand(["-f", composeFile, "-p", projectName, "up", "-d"], cwd, envVars, onLog); + if (res.code !== 0) { + throw new Error(`docker compose up failed: ${res.stderr || res.stdout}`); + } + + await onLog("Docker Compose stack started."); +}; + +export const destroyComposeStack = async (projectName: string): Promise => { + const filter = `label=com.docker.compose.project=${projectName}`; + const ps = await runDocker(["ps", "-aq", "--filter", filter]); + const ids = ps.stdout.split(/\s+/).filter(Boolean); + for (const id of ids) { + await runDocker(["rm", "-f", id]); + } + await runDocker(["network", "rm", `${projectName}_default`]).catch(() => {}); +}; + +export const getComposeContainerNames = async ( + projectName: string, +): Promise> => { + const res = await runDocker([ + "ps", + "-a", + "--filter", + `label=com.docker.compose.project=${projectName}`, + "--format", + '{{.Label "com.docker.compose.service"}}|{{.Names}}', + ]); + const map = new Map(); + for (const line of res.stdout.split("\n")) { + const [service, name] = line.split("|"); + if (service && name) map.set(service, name); + } + return map; +}; + +const runDocker = ( + args: string[], +): Promise<{ code: number; stdout: string; stderr: string }> => { + return new Promise((resolve, reject) => { + const child = spawn(dockerBin, args, { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + child.on("error", reject); + child.on("close", (code) => { + resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }); + }); + }); +}; diff --git a/apps/api/src/orchestrator/pipeline.ts b/apps/api/src/orchestrator/pipeline.ts index 2e1fcd1..859775c 100644 --- a/apps/api/src/orchestrator/pipeline.ts +++ b/apps/api/src/orchestrator/pipeline.ts @@ -1,4 +1,5 @@ -import { rm } from "node:fs/promises"; +import { rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { appendLog, deleteDeploymentAndLogs, @@ -11,6 +12,7 @@ import { } from "../db/repo"; import { listEnvironmentVariablesForDeploy } from "../db/repo"; import { listVolumes } from "../db/repo"; +import { filterBuildEnvVars } from "../utils/env-filter"; import { logBus } from "./log-bus"; import { DeploymentQueue } from "./queue"; import { buildWithRailpack, CancelledError } from "./railpack"; @@ -28,6 +30,10 @@ import { tryRun, } from "./runtime"; import { ensureProjectDashboard } from "../utils/grafana"; +import { buildWithCompose, deployWithCompose, parseComposeTarget, parseAllComposeServices, destroyComposeStack, getComposeContainerNames } from "./compose"; +import { buildCaddySnippet } from "../utils/domain-verifier"; +import { dockerBin } from "../utils/docker-bin"; +import { config } from "../utils/config"; const now = () => new Date() @@ -145,22 +151,37 @@ export class PipelineOrchestrator { await this.queue.remove(deploymentId); - if (deployment.containerName) { - await tryRun("docker", [ - "stop", - "-t", - "5", - deployment.containerName, - ]); - await tryRun("docker", [ - "rm", - "-f", - deployment.containerName, - ]); - } + const project = deployment.projectId + ? await getProjectById(deployment.projectId) + : null; + + if (project?.buildType === "compose") { + await destroyComposeStack( + `deploy-${deploymentId}`, + ).catch((e) => + console.warn( + `[Pipeline] Compose teardown failed for ${deploymentId}:`, + e, + ), + ); + } else { + if (deployment.containerName) { + await tryRun("docker", [ + "stop", + "-t", + "5", + deployment.containerName, + ]); + await tryRun("docker", [ + "rm", + "-f", + deployment.containerName, + ]); + } - if (deployment.imageTag && deployment.sourceType !== "image") { - await tryRun("docker", ["rmi", "-f", deployment.imageTag]); + if (deployment.imageTag && deployment.sourceType !== "image") { + await tryRun("docker", ["rmi", "-f", deployment.imageTag]); + } } await deleteDeploymentAndLogs( @@ -297,6 +318,7 @@ export class PipelineOrchestrator { null; let imageTag: string | undefined; let projectName: string | undefined; + let project: import("../../types").Project | null = null; let deployed = false; try { @@ -311,6 +333,10 @@ export class PipelineOrchestrator { deployment, ); + project = deployment.projectId ? await getProjectById( + deployment.projectId, + ) : null; + if ( deployment.sourceType !== "image" ) { @@ -382,19 +408,46 @@ export class PipelineOrchestrator { const cacheKey = deployment.projectId || deploymentId; - const project = deployment.projectId ? await getProjectById(deployment.projectId) : null; - await buildWithRailpack( - workspacePath, - imageTag, - async (line) => { - await emitLog( - deploymentId, - "build", - line, - ); - }, - { cacheKey, sourceDir: project?.sourceDir, signal: controller.signal, clearCache: deployment.clearCache }, - ); + const envVars = deployment.projectId ? await listEnvironmentVariablesForDeploy(deployment.projectId, "production") : []; + if (project?.buildType === "compose") { + const envMap: Record = {}; + for (const v of envVars) envMap[v.key] = v.value; + await buildWithCompose( + workspacePath, + `deploy-${deploymentId}`, + async (line) => { + await emitLog( + deploymentId, + "build", + line, + ); + }, + project?.sourceDir, + envMap, + ); + } else { + await buildWithRailpack( + workspacePath, + imageTag, + async (line) => { + await emitLog( + deploymentId, + "build", + line, + ); + }, + { + cacheKey, + sourceDir: project?.sourceDir, + projectType: project?.projectType, + buildCommand: project?.buildCommand, + startCommand: project?.startCommand, + environmentVariables: filterBuildEnvVars(envVars), + signal: controller.signal, + clearCache: deployment.clearCache + }, + ); + } } else { await emitLog( deploymentId, @@ -468,6 +521,9 @@ export class PipelineOrchestrator { let oldContainerName: | string | undefined; + let oldDeploymentId: + | string + | undefined; if (deployment.projectId) { const all = await listDeployments( deployment.projectId, @@ -482,6 +538,7 @@ export class PipelineOrchestrator { if (prev?.containerName) { oldContainerName = prev.containerName; + oldDeploymentId = prev.id; await emitLog( deploymentId, "deploy", @@ -504,42 +561,143 @@ export class PipelineOrchestrator { | number | null | undefined; - if (deployment.projectId) { - const proj = await getProjectById( - deployment.projectId, - ); - if (proj) { - projectName = proj.name; - cpuLimit = proj.cpuLimit; - memoryLimitMb = - proj.memoryLimitMb; - appPort = proj.port; - } + if (project) { + projectName = project.name; + cpuLimit = project.cpuLimit; + memoryLimitMb = + project.memoryLimitMb; + appPort = project.port; } - const runtime = await deployContainer( - deploymentId, - imageTag, - async (line) => { + let runtimeContainerName = ""; + let runtimeLiveUrl = ""; + + if (project?.buildType === "compose") { + if (oldDeploymentId) { await emitLog( deploymentId, "deploy", - line, + `Stopping previous compose stack deploy-${oldDeploymentId}`, ); - }, - { - projectId: - deployment.projectId || - undefined, - projectName, - oldContainerName, + await destroyComposeStack( + `deploy-${oldDeploymentId}`, + ); + } + + await deployWithCompose( + workspacePath, + `deploy-${deploymentId}`, + async (line) => { + await emitLog( + deploymentId, + "deploy", + line, + ); + }, + project?.sourceDir, envVars, - volumes, - cpuLimit, - memoryLimitMb, - appPort: appPort ?? undefined, - }, - ); + ); + + const allServices = parseAllComposeServices( + workspacePath, + project?.sourceDir, + project?.composeService, + project?.composePort, + ); + + const composeContainers = await getComposeContainerNames( + `deploy-${deploymentId}`, + ); + const containerFor = (serviceName: string) => + composeContainers.get(serviceName) || `deploy-${deploymentId}-${serviceName}-1`; + + for (const s of allServices) { + const cName = containerFor(s.serviceName); + await tryRun(dockerBin, ["network", "connect", config.dockerNetwork, cName]); + } + + const target = allServices.find((s) => s.isPrimary) || allServices[0]; + const containerName = containerFor(target.serviceName); + + const slug = project.name + ? project.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) + : project.id; + + let snippet = await buildCaddySnippet( + slug, + containerName, + project.id, + undefined, + target.port, + ); + + const rawBaseDomain = config.caddyBaseDomain || 'localhost'; + const baseDomainForCaddy = rawBaseDomain === 'localhost' ? `${rawBaseDomain}:80` : rawBaseDomain; + let customMappings: { serviceName: string; port: number | string; subdomain?: string }[] = []; + if ((project as any).composeServices) { + try { + customMappings = typeof (project as any).composeServices === 'string' + ? JSON.parse((project as any).composeServices) + : (project as any).composeServices; + } catch (e) {} + } + + for (const s of allServices) { + if (s.isPrimary) continue; + const sContainer = containerFor(s.serviceName); + const customMatch = customMappings.find((c) => c.serviceName === s.serviceName); + const domains: string[] = []; + if (customMatch?.subdomain?.trim()) { + domains.push(`${customMatch.subdomain.trim()}.${slug}.${baseDomainForCaddy}`); + } else { + domains.push(`${s.serviceName}.${slug}.${baseDomainForCaddy}`); + if (s.serviceName === "server" && !domains.includes(`api.${slug}.${baseDomainForCaddy}`)) { + domains.push(`api.${slug}.${baseDomainForCaddy}`); + } + } + const targetPort = customMatch?.port ? Number(customMatch.port) : s.port; + snippet += `\n${domains.join(", ")} {\n log {\n output stdout\n format json\n }\n reverse_proxy ${sContainer}:${targetPort} {\n header_up Host {upstream_hostport}\n }\n}\n`; + } + + const caddyRouteFile = join(config.caddyRoutesDir, `${slug}.caddy`); + await writeFile(caddyRouteFile, snippet, "utf8"); + try { + await reloadCaddy(); + } catch { + console.warn( + "[Pipeline] Caddy not ready for reload after compose deploy", + ); + } + + runtimeContainerName = containerName; + runtimeLiveUrl = rawBaseDomain === 'localhost' ? `http://${slug}.localhost` : `https://${slug}.${rawBaseDomain}`; + } else { + const runtime = await deployContainer( + deploymentId, + imageTag, + async (line) => { + await emitLog( + deploymentId, + "deploy", + line, + ); + }, + { + projectId: + deployment.projectId || + undefined, + projectName, + oldContainerName, + envVars, + volumes, + cpuLimit, + memoryLimitMb, + appPort: appPort ?? undefined, + }, + ); + runtimeContainerName = runtime.containerName; + runtimeLiveUrl = runtime.liveUrl; + } deployed = true; @@ -548,9 +706,8 @@ export class PipelineOrchestrator { "running", { imageTag, - containerName: - runtime.containerName, - liveUrl: runtime.liveUrl, + containerName: runtimeContainerName, + liveUrl: runtimeLiveUrl, failureReason: null, }, ); @@ -639,14 +796,22 @@ export class PipelineOrchestrator { "system", "Cleaning up Docker resources from failed deployment", ); - await cleanupFailedDeployment( - deploymentId, - deployment.sourceType === "image" ? undefined : imageTag, - projectName, - deployment.projectId, - ).catch(e => - console.warn(`[Cleanup] Failed to clean deployment ${deploymentId}:`, e), - ); + if (project?.buildType === "compose") { + await destroyComposeStack( + `deploy-${deploymentId}`, + ).catch((e) => + console.warn(`[Cleanup] Failed to tear down compose stack for ${deploymentId}:`, e), + ); + } else { + await cleanupFailedDeployment( + deploymentId, + deployment.sourceType === "image" ? undefined : imageTag, + projectName, + deployment.projectId, + ).catch(e => + console.warn(`[Cleanup] Failed to clean deployment ${deploymentId}:`, e), + ); + } } if (deployment.projectId) { diff --git a/apps/api/src/orchestrator/railpack-config-utils.ts b/apps/api/src/orchestrator/railpack-config-utils.ts new file mode 100644 index 0000000..5a4f64b --- /dev/null +++ b/apps/api/src/orchestrator/railpack-config-utils.ts @@ -0,0 +1,652 @@ +import { join } from "node:path"; +import { readdir } from "node:fs/promises"; + +export const rewriteLocalhostBinding = async ( + dir: string, + onLog: (line: string) => Promise, +): Promise => { + const ignoreDirs = new Set([ + "node_modules", + "target", + ".git", + ".cargo", + "dist", + "build", + ".next", + ".svelte-kit", + "vendor", + ]); + const allowedExts = new Set([ + "rs", + "go", + "js", + "ts", + "py", + "java", + "json", + "yaml", + "yml", + "toml", + ]); + + try { + const entries = await readdir(dir, { + withFileTypes: true, + }); + for (const entry of entries) { + const fullPath = join( + dir, + entry.name, + ); + if (entry.isDirectory()) { + if (ignoreDirs.has(entry.name)) { + continue; + } + await rewriteLocalhostBinding( + fullPath, + onLog, + ); + } else if (entry.isFile()) { + const ext = entry.name + .split(".") + .pop(); + if (ext && allowedExts.has(ext)) { + try { + let content = + await Bun.file( + fullPath, + ).text(); + if ( + content.includes( + "127.0.0.1", + ) + ) { + content = + content.replaceAll( + "127.0.0.1", + "0.0.0.0", + ); + await Bun.write( + fullPath, + content, + ); + await onLog( + `Auto-rewrote 127.0.0.1 to 0.0.0.0 in ${entry.name} for container compatibility.`, + ); + } + } catch { + // Ignore read errors + } + } + } + } + } catch { + // Ignore readdir/directory access errors + } +}; + +export const generateDynamicRailpackJson = async ( + workspace: string, + sourceDir: string | null, + projectType: string | null, + buildCommandOverride: string | null, + startCommandOverride: string | null, + onLog: (line: string) => Promise, +): Promise => { + const cleanSourceDir = sourceDir + ? sourceDir.replace(/^\//, "") + : ""; + const buildDir = cleanSourceDir + ? join(workspace, cleanSourceDir) + : workspace; + await rewriteLocalhostBinding( + buildDir, + onLog, + ); + const configPath = join( + workspace, + "railpack.json", + ); + + // Default configuration template + const config: Record = { + caches: {}, + steps: {}, + deploy: {}, + }; + + let configured = false; + + // 1. Check if user provided manual build / start command overrides + if (buildCommandOverride || startCommandOverride) { + await onLog("Applying custom build/start settings"); + if (buildCommandOverride) { + config.steps.build = { + commands: [ + cleanSourceDir + ? `cd ${cleanSourceDir} && ${buildCommandOverride}` + : buildCommandOverride, + ], + }; + } + if (startCommandOverride) { + config.deploy.startCommand = cleanSourceDir + ? `cd ${cleanSourceDir} && ${startCommandOverride}` + : startCommandOverride; + } + configured = true; + } + + const hasPackageJson = await Bun.file( + join(buildDir, "package.json"), + ).exists(); + if (hasPackageJson && !configured) { + try { + const packageJson = await Bun.file( + join(buildDir, "package.json"), + ).json(); + const scripts = + packageJson.scripts || {}; + + let pm = "npm"; + if ( + await Bun.file( + join( + workspace, + "pnpm-lock.yaml", + ), + ).exists() + ) { + pm = "pnpm"; + } else if ( + await Bun.file( + join(workspace, "yarn.lock"), + ).exists() + ) { + pm = "yarn"; + } else if ( + await Bun.file( + join(workspace, "bun.lockb"), + ).exists() + ) { + pm = "bun"; + } + + await onLog( + `Configuring Node.js project using ${pm}`, + ); + + const hasNext = (packageJson.dependencies && packageJson.dependencies.next) || + (packageJson.devDependencies && packageJson.devDependencies.next); + + if (pm === "pnpm") { + config.caches.pnpm_store = { + directory: "/root/.local/share/pnpm/store", + type: "shared", + }; + } else if (pm === "yarn") { + config.caches.yarn_cache = { + directory: "/root/.cache/yarn", + type: "shared", + }; + } else if (pm === "bun") { + config.caches.bun_cache = { + directory: "/root/.bun/install/cache", + type: "shared", + }; + } else { + config.caches.npm_cache = { + directory: "/root/.npm", + type: "shared", + }; + } + + + + if (scripts.build) { + const buildCmds = [ + cleanSourceDir + ? `cd ${cleanSourceDir} && ${pm} run build` + : `${pm} run build`, + ]; + if (hasNext) { + const nextCacheDir = cleanSourceDir + ? `${cleanSourceDir}/.next/cache` + : ".next/cache"; + config.caches.next_cache = { + directory: nextCacheDir, + type: "shared", + }; + config.steps.build = { + commands: buildCmds, + caches: ["next_cache"], + }; + } else { + config.steps.build = { + commands: buildCmds, + }; + } + } + + const hasServerScript = !!scripts.server; + const isStatic = projectType === "static" || (!scripts.start && !hasServerScript && (scripts.build || hasPackageJson)); + + if (isStatic) { + const serveScript = ` +const fs = require("fs"); +const path = require("path"); +const PORT = Number(process.env.PORT || 3000); +const cleanSourceDir = "${cleanSourceDir}"; +let staticDir = "."; +const candidates = [ + path.join(cleanSourceDir, "dist"), + path.join(cleanSourceDir, "build"), + path.join(cleanSourceDir, "out"), + path.join(cleanSourceDir, "public"), + "dist", + "build", + "out", + "public", + "." +]; +for (const dir of candidates) { + const fullPath = path.join(process.cwd(), dir); + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { + if (fs.existsSync(path.join(fullPath, "index.html"))) { + staticDir = dir; + break; + } + } +} +console.log("Serving static directory:", staticDir, "on port", PORT); +Bun.serve({ + port: PORT, + async fetch(req) { + const url = new URL(req.url); + let decodedPathname = "/"; + try { + decodedPathname = decodeURIComponent(url.pathname); + } catch { + decodedPathname = url.pathname; + } + let filePath = path.join(staticDir, decodedPathname); + if (decodedPathname.endsWith("/")) { + filePath = path.join(filePath, "index.html"); + } + let file = Bun.file(filePath); + if (await file.exists()) { + return new Response(file, { + headers: { + "content-type": file.type || "application/octet-stream" + } + }); + } + const fallbackPath = path.join(staticDir, "index.html"); + const fallbackFile = Bun.file(fallbackPath); + if (await fallbackFile.exists()) { + return new Response(fallbackFile, { + headers: { + "content-type": fallbackFile.type || "text/html" + } + }); + } + return new Response("Not Found", { status: 404 }); + } +}); +`; + await Bun.write(join(workspace, "dequel-serve.js"), serveScript); + config.deploy.startCommand = "bun dequel-serve.js"; + } else if (scripts.start) { + config.deploy.startCommand = + cleanSourceDir + ? `cd ${cleanSourceDir} && ${pm} run start` + : `${pm} run start`; + } else if (scripts.server) { + config.deploy.startCommand = + cleanSourceDir + ? `cd ${cleanSourceDir} && ${pm} run server` + : `${pm} run server`; + } else { + config.deploy.startCommand = + cleanSourceDir + ? `cd ${cleanSourceDir} && node dist/index.js` + : "node dist/index.js"; + } + + configured = true; + } catch (err) { + await onLog( + `Error parsing package.json: ${err}`, + ); + } + } + + const hasCargoToml = await Bun.file( + join(buildDir, "Cargo.toml"), + ).exists(); + if (hasCargoToml && !configured) { + try { + let cargoContent = await Bun.file( + join(buildDir, "Cargo.toml"), + ).text(); + + let hasWorkspaceRoot = false; + let currentDir = buildDir; + while ( + currentDir.startsWith(workspace) + ) { + const parentCargo = join( + currentDir, + "Cargo.toml", + ); + if ( + currentDir !== buildDir && + (await Bun.file( + parentCargo, + ).exists()) + ) { + const content = + await Bun.file( + parentCargo, + ).text(); + if ( + content.includes( + "[workspace]", + ) + ) { + hasWorkspaceRoot = true; + break; + } + } + const nextDir = join( + currentDir, + "..", + ); + if ( + nextDir === currentDir || + !nextDir.startsWith(workspace) + ) + break; + currentDir = nextDir; + } + + if ( + !hasWorkspaceRoot && + (cargoContent.includes( + ".workspace = true", + ) || + cargoContent.includes( + "workspace = true", + )) + ) { + await onLog( + `Detected workspace inheritance in Cargo.toml without workspace root. Resolving workspace variables...`, + ); + const commonDeps: Record< + string, + string + > = { + "actix-web": '"4"', + "actix-files": '"0.6"', + "actix-rt": '"2.9"', + serde: '{"version": "1", "features": ["derive"]}', + serde_json: '"1"', + tokio: '{"version": "1", "features": ["full"]}', + futures: '"0.3"', + log: '"0.4"', + env_logger: '"0.11"', + uuid: '{"version": "1", "features": ["v4"]}', + chrono: '"0.4"', + reqwest: + '{"version": "0.12", "features": ["json"]}', + anyhow: '"1"', + thiserror: '"1"', + }; + + cargoContent = + cargoContent.replace( + /edition\.workspace\s*=\s*true/g, + 'edition = "2021"', + ); + cargoContent = + cargoContent.replace( + /rust-version\.workspace\s*=\s*true/g, + 'rust-version = "1.89"', + ); + cargoContent = + cargoContent.replace( + /version\.workspace\s*=\s*true/g, + 'version = "0.1.0"', + ); + + const lines = + cargoContent.split("\n"); + for ( + let i = 0; + i < lines.length; + i++ + ) { + const line = lines[i]; + const matchSimple = + line.match( + /^(\s*)([a-zA-Z0-9_-]+)\.workspace\s*=\s*true/, + ); + if (matchSimple) { + const indent = + matchSimple[1]; + const name = + matchSimple[2]; + const ver = + commonDeps[name] || + '"*"'; + lines[i] = + `${indent}${name} = ${ver}`; + continue; + } + const matchComplex = + line.match( + /^(\s*)([a-zA-Z0-9_-]+)\s*=\s*\{\s*workspace\s*=\s*true\s*,?\s*(.*)\}/, + ); + if (matchComplex) { + const indent = + matchComplex[1]; + const name = + matchComplex[2]; + const rest = + matchComplex[3].trim(); + const restComma = rest + ? `, ${rest}` + : ""; + lines[i] = + `${indent}${name} = { version = "*"${restComma} }`; + continue; + } + } + cargoContent = lines.join("\n"); + await Bun.write( + join(buildDir, "Cargo.toml"), + cargoContent, + ); + await onLog( + `Wrote standalone Cargo.toml to resolve workspace inheritance.`, + ); + } + + const match = cargoContent.match( + /\[package\][^]*?name\s*=\s*"([^"]+)"/, + ); + const pkgName = match + ? match[1] + : null; + + if (pkgName) { + await onLog( + `Configuring Rust project: ${pkgName}`, + ); + config.caches = { + cargo_registry: { + directory: + "/root/.cargo/registry", + type: "shared", + }, + cargo_git: { + directory: + "/root/.cargo/git", + type: "shared", + }, + cargo_target: { + directory: "target", + type: "shared", + }, + }; + config.steps.build = { + commands: [ + `cargo build --release -p ${pkgName}`, + "mkdir -p bin", + `cp target/release/${pkgName} bin/`, + ], + caches: [ + "cargo_registry", + "cargo_git", + "cargo_target", + ], + }; + config.deploy.startCommand = `./bin/${pkgName}`; + configured = true; + } + } catch (err) { + await onLog( + `Error parsing Cargo.toml: ${err}`, + ); + } + } + + let hasGoMod = false; + let currentDir = buildDir; + while (currentDir.startsWith(workspace)) { + if ( + await Bun.file( + join(currentDir, "go.mod"), + ).exists() + ) { + hasGoMod = true; + break; + } + const nextDir = join(currentDir, ".."); + if ( + nextDir === currentDir || + !nextDir.startsWith(workspace) + ) + break; + currentDir = nextDir; + } + if (hasGoMod && !configured) { + await onLog(`Configuring Go project`); + config.caches = { + go_build: { + directory: + "/root/.cache/go-build", + type: "shared", + }, + go_mod: { + directory: "/go/pkg/mod", + type: "shared", + }, + }; + config.steps.build = { + commands: [ + cleanSourceDir + ? `cd ${cleanSourceDir} && go build -o bin/app .` + : "go build -o bin/app .", + ], + caches: ["go_build", "go_mod"], + }; + config.deploy.startCommand = + cleanSourceDir + ? `cd ${cleanSourceDir} && ./bin/app` + : "./bin/app"; + configured = true; + } + + if (!configured && projectType === "static") { + const serveScript = ` +const fs = require("fs"); +const path = require("path"); +const PORT = Number(process.env.PORT || 3000); +const cleanSourceDir = "${cleanSourceDir}"; +let staticDir = "."; +const candidates = [ + path.join(cleanSourceDir, "dist"), + path.join(cleanSourceDir, "build"), + path.join(cleanSourceDir, "out"), + path.join(cleanSourceDir, "public"), + "dist", + "build", + "out", + "public", + "." +]; +for (const dir of candidates) { + const fullPath = path.join(process.cwd(), dir); + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { + if (fs.existsSync(path.join(fullPath, "index.html"))) { + staticDir = dir; + break; + } + } +} +console.log("Serving static directory:", staticDir, "on port", PORT); +Bun.serve({ + port: PORT, + async fetch(req) { + const url = new URL(req.url); + let decodedPathname = "/"; + try { + decodedPathname = decodeURIComponent(url.pathname); + } catch { + decodedPathname = url.pathname; + } + let filePath = path.join(staticDir, decodedPathname); + if (decodedPathname.endsWith("/")) { + filePath = path.join(filePath, "index.html"); + } + let file = Bun.file(filePath); + if (await file.exists()) { + return new Response(file, { + headers: { + "content-type": file.type || "application/octet-stream" + } + }); + } + const fallbackPath = path.join(staticDir, "index.html"); + const fallbackFile = Bun.file(fallbackPath); + if (await fallbackFile.exists()) { + return new Response(fallbackFile, { + headers: { + "content-type": fallbackFile.type || "text/html" + } + }); + } + return new Response("Not Found", { status: 404 }); + } +}); +`; + await Bun.write(join(workspace, "dequel-serve.js"), serveScript); + config.deploy.startCommand = "bun dequel-serve.js"; + configured = true; + } + + if (!configured) { + await onLog( + `Relying on default Railpack language auto-detection`, + ); + } + + await Bun.write( + configPath, + JSON.stringify(config, null, 2), + ); + await onLog( + `Wrote dynamic railpack.json for caching and monorepo resolution`, + ); +}; diff --git a/apps/api/src/orchestrator/railpack.ts b/apps/api/src/orchestrator/railpack.ts index 541d64c..cc6ac61 100644 --- a/apps/api/src/orchestrator/railpack.ts +++ b/apps/api/src/orchestrator/railpack.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import { join } from "node:path"; -import { readdir } from "node:fs/promises"; import { dockerBin } from "../utils/docker-bin"; +import { generateDynamicRailpackJson } from "./railpack-config-utils"; export interface RailpackBuildResult { imageTag: string; @@ -270,449 +270,6 @@ const ensureBuilder = async (): Promise => { } }; -const rewriteLocalhostBinding = async ( - dir: string, - onLog: (line: string) => Promise, -): Promise => { - const ignoreDirs = new Set([ - "node_modules", - "target", - ".git", - ".cargo", - "dist", - "build", - ".next", - ".svelte-kit", - "vendor", - ]); - const allowedExts = new Set([ - "rs", - "go", - "js", - "ts", - "py", - "java", - "json", - "yaml", - "yml", - "toml", - ]); - - try { - const entries = await readdir(dir, { - withFileTypes: true, - }); - for (const entry of entries) { - const fullPath = join( - dir, - entry.name, - ); - if (entry.isDirectory()) { - if (ignoreDirs.has(entry.name)) { - continue; - } - await rewriteLocalhostBinding( - fullPath, - onLog, - ); - } else if (entry.isFile()) { - const ext = entry.name - .split(".") - .pop(); - if (ext && allowedExts.has(ext)) { - try { - let content = - await Bun.file( - fullPath, - ).text(); - if ( - content.includes( - "127.0.0.1", - ) - ) { - content = - content.replaceAll( - "127.0.0.1", - "0.0.0.0", - ); - await Bun.write( - fullPath, - content, - ); - await onLog( - `Auto-rewrote 127.0.0.1 to 0.0.0.0 in ${entry.name} for container compatibility.`, - ); - } - } catch { - // Ignore read errors - } - } - } - } - } catch { - // Ignore readdir/directory access errors - } -}; - -const generateDynamicRailpackJson = async ( - workspace: string, - sourceDir: string | null, - onLog: (line: string) => Promise, -): Promise => { - const cleanSourceDir = sourceDir - ? sourceDir.replace(/^\//, "") - : ""; - const buildDir = cleanSourceDir - ? join(workspace, cleanSourceDir) - : workspace; - await rewriteLocalhostBinding( - buildDir, - onLog, - ); - const configPath = join( - workspace, - "railpack.json", - ); - - // Default configuration template - const config: Record = { - caches: {}, - steps: {}, - deploy: {}, - }; - - let configured = false; - - const hasPackageJson = await Bun.file( - join(buildDir, "package.json"), - ).exists(); - if (hasPackageJson) { - try { - const packageJson = await Bun.file( - join(buildDir, "package.json"), - ).json(); - const scripts = - packageJson.scripts || {}; - - let pm = "npm"; - if ( - await Bun.file( - join( - workspace, - "pnpm-lock.yaml", - ), - ).exists() - ) { - pm = "pnpm"; - } else if ( - await Bun.file( - join(workspace, "yarn.lock"), - ).exists() - ) { - pm = "yarn"; - } else if ( - await Bun.file( - join(workspace, "bun.lockb"), - ).exists() - ) { - pm = "bun"; - } - - await onLog( - `Configuring Node.js project using ${pm}`, - ); - - if (scripts.build) { - config.steps.build = { - commands: [ - cleanSourceDir - ? `cd ${cleanSourceDir} && ${pm} run build` - : `${pm} run build`, - ], - }; - } - - if (scripts.start) { - config.deploy.startCommand = - cleanSourceDir - ? `cd ${cleanSourceDir} && ${pm} run start` - : `${pm} run start`; - } else { - config.deploy.startCommand = - cleanSourceDir - ? `cd ${cleanSourceDir} && node dist/index.js` - : "node dist/index.js"; - } - - configured = true; - } catch (err) { - await onLog( - `Error parsing package.json: ${err}`, - ); - } - } - - const hasCargoToml = await Bun.file( - join(buildDir, "Cargo.toml"), - ).exists(); - if (hasCargoToml && !configured) { - try { - let cargoContent = await Bun.file( - join(buildDir, "Cargo.toml"), - ).text(); - - let hasWorkspaceRoot = false; - let currentDir = buildDir; - while ( - currentDir.startsWith(workspace) - ) { - const parentCargo = join( - currentDir, - "Cargo.toml", - ); - if ( - currentDir !== buildDir && - (await Bun.file( - parentCargo, - ).exists()) - ) { - const content = - await Bun.file( - parentCargo, - ).text(); - if ( - content.includes( - "[workspace]", - ) - ) { - hasWorkspaceRoot = true; - break; - } - } - const nextDir = join( - currentDir, - "..", - ); - if ( - nextDir === currentDir || - !nextDir.startsWith(workspace) - ) - break; - currentDir = nextDir; - } - - if ( - !hasWorkspaceRoot && - (cargoContent.includes( - ".workspace = true", - ) || - cargoContent.includes( - "workspace = true", - )) - ) { - await onLog( - `Detected workspace inheritance in Cargo.toml without workspace root. Resolving workspace variables...`, - ); - const commonDeps: Record< - string, - string - > = { - "actix-web": '"4"', - "actix-files": '"0.6"', - "actix-rt": '"2.9"', - serde: '{"version": "1", "features": ["derive"]}', - serde_json: '"1"', - tokio: '{"version": "1", "features": ["full"]}', - futures: '"0.3"', - log: '"0.4"', - env_logger: '"0.11"', - uuid: '{"version": "1", "features": ["v4"]}', - chrono: '"0.4"', - reqwest: - '{"version": "0.12", "features": ["json"]}', - anyhow: '"1"', - thiserror: '"1"', - }; - - cargoContent = - cargoContent.replace( - /edition\.workspace\s*=\s*true/g, - 'edition = "2021"', - ); - cargoContent = - cargoContent.replace( - /rust-version\.workspace\s*=\s*true/g, - 'rust-version = "1.89"', - ); - cargoContent = - cargoContent.replace( - /version\.workspace\s*=\s*true/g, - 'version = "0.1.0"', - ); - - const lines = - cargoContent.split("\n"); - for ( - let i = 0; - i < lines.length; - i++ - ) { - const line = lines[i]; - const matchSimple = - line.match( - /^(\s*)([a-zA-Z0-9_-]+)\.workspace\s*=\s*true/, - ); - if (matchSimple) { - const indent = - matchSimple[1]; - const name = - matchSimple[2]; - const ver = - commonDeps[name] || - '"*"'; - lines[i] = - `${indent}${name} = ${ver}`; - continue; - } - const matchComplex = - line.match( - /^(\s*)([a-zA-Z0-9_-]+)\s*=\s*\{\s*workspace\s*=\s*true\s*,?\s*(.*)\}/, - ); - if (matchComplex) { - const indent = - matchComplex[1]; - const name = - matchComplex[2]; - const rest = - matchComplex[3].trim(); - const restComma = rest - ? `, ${rest}` - : ""; - lines[i] = - `${indent}${name} = { version = "*"${restComma} }`; - continue; - } - } - cargoContent = lines.join("\n"); - await Bun.write( - join(buildDir, "Cargo.toml"), - cargoContent, - ); - await onLog( - `Wrote standalone Cargo.toml to resolve workspace inheritance.`, - ); - } - - const match = cargoContent.match( - /\[package\][^]*?name\s*=\s*"([^"]+)"/, - ); - const pkgName = match - ? match[1] - : null; - - if (pkgName) { - await onLog( - `Configuring Rust project: ${pkgName}`, - ); - config.caches = { - cargo_registry: { - directory: - "/root/.cargo/registry", - type: "shared", - }, - cargo_git: { - directory: - "/root/.cargo/git", - type: "shared", - }, - cargo_target: { - directory: "target", - type: "shared", - }, - }; - config.steps.build = { - commands: [ - `cargo build --release -p ${pkgName}`, - "mkdir -p bin", - `cp target/release/${pkgName} bin/`, - ], - caches: [ - "cargo_registry", - "cargo_git", - "cargo_target", - ], - }; - config.deploy.startCommand = `./bin/${pkgName}`; - configured = true; - } - } catch (err) { - await onLog( - `Error parsing Cargo.toml: ${err}`, - ); - } - } - - let hasGoMod = false; - let currentDir = buildDir; - while (currentDir.startsWith(workspace)) { - if ( - await Bun.file( - join(currentDir, "go.mod"), - ).exists() - ) { - hasGoMod = true; - break; - } - const nextDir = join(currentDir, ".."); - if ( - nextDir === currentDir || - !nextDir.startsWith(workspace) - ) - break; - currentDir = nextDir; - } - if (hasGoMod && !configured) { - await onLog(`Configuring Go project`); - config.caches = { - go_build: { - directory: - "/root/.cache/go-build", - type: "shared", - }, - go_mod: { - directory: "/go/pkg/mod", - type: "shared", - }, - }; - config.steps.build = { - commands: [ - cleanSourceDir - ? `cd ${cleanSourceDir} && go build -o bin/app .` - : "go build -o bin/app .", - ], - caches: ["go_build", "go_mod"], - }; - config.deploy.startCommand = - cleanSourceDir - ? `cd ${cleanSourceDir} && ./bin/app` - : "./bin/app"; - configured = true; - } - - if (!configured) { - await onLog( - `Relying on default Railpack language auto-detection`, - ); - } - - await Bun.write( - configPath, - JSON.stringify(config, null, 2), - ); - await onLog( - `Wrote dynamic railpack.json for caching and monorepo resolution`, - ); -}; export const buildWithRailpack = async ( workspace: string, @@ -721,6 +278,10 @@ export const buildWithRailpack = async ( opts?: { cacheKey?: string; sourceDir?: string | null; + projectType?: string | null; + buildCommand?: string | null; + startCommand?: string | null; + environmentVariables?: { key: string; value: string }[]; signal?: AbortSignal; clearCache?: boolean; }, @@ -753,6 +314,9 @@ export const buildWithRailpack = async ( await generateDynamicRailpackJson( workspace, cleanSourceDir, + opts?.projectType ?? null, + opts?.buildCommand ?? null, + opts?.startCommand ?? null, onLog, ); @@ -773,10 +337,31 @@ export const buildWithRailpack = async ( "--env", "RUSTUP_AUTO_SELF_UPDATE=off", "--env", - "NPM_CONFIG_TIMEOUT=120000", - workspace, + "NPM_CONFIG_TIMEOUT=600000", + "--env", + "NPM_CONFIG_AUDIT=false", + "--env", + "NPM_CONFIG_FUND=false", + "--env", + "PNPM_CONFIG_TRUST_LOCKFILE=true", + "--env", + "NPM_CONFIG_MAXSOCKETS=4", + "--env", + "PNPM_CONFIG_NETWORK_CONCURRENCY=4", + "--env", + "PNPM_CONFIG_CHILD_CONCURRENCY=4", + "--env", + "PNPM_CONFIG_FETCH_RETRIES=10", ]; + if (opts?.environmentVariables) { + for (const env of opts.environmentVariables) { + args.push("--env", `${env.key}=${env.value}`); + } + } + + args.push(workspace); + const build = await spawnAsync( "railpack", args, diff --git a/apps/api/src/orchestrator/runtime.ts b/apps/api/src/orchestrator/runtime.ts index 3592523..10ca24f 100644 --- a/apps/api/src/orchestrator/runtime.ts +++ b/apps/api/src/orchestrator/runtime.ts @@ -54,7 +54,14 @@ const waitForRunningContainer = async ( ) => { for (let attempt = 0; attempt < retries; attempt++) { try { - const status = (await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName])).trim(); + const exists = await run(dockerBin, ['inspect', '-f', '{{.Id}}', containerName]) + .then(() => true) + .catch(() => false); + if (!exists) { + await new Promise(r => setTimeout(r, 500)); + continue; + } + const status = (await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName])).trim(); if (status === 'running') { await new Promise(r => setTimeout(r, 2000)); const stabilityStatus = (await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName])).trim(); @@ -83,6 +90,11 @@ const waitForRunningContainer = async ( export const ensureContainerRunning = async (containerName: string) => { try { + const exists = await run(dockerBin, ['inspect', '-f', '{{.Id}}', containerName]).then(() => true).catch(() => false); + if (!exists) { + console.warn(`Container ${containerName} not found — skipping reconciliation`); + return; + } const status = (await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName])).trim(); if (status !== 'running') { await tryRun(dockerBin, ['network', 'disconnect', '-f', config.dockerNetwork, containerName]); diff --git a/apps/api/src/scaling/__tests__/engine.test.ts b/apps/api/src/scaling/__tests__/engine.test.ts index e4cf27a..40f6dbf 100644 --- a/apps/api/src/scaling/__tests__/engine.test.ts +++ b/apps/api/src/scaling/__tests__/engine.test.ts @@ -92,7 +92,7 @@ mock.module('ioredis', () => ({ // Mock db/repo mock.module(fileUrl('../../db/repo'), () => ({ - getProjectById: mock(() => Promise.resolve(null)), + getProjectById: mock(() => Promise.resolve({ id: "proj-1", name: "proj-1", cpuLimit: 0.5, memoryLimitMb: 512 })), listDeployments: mock(() => Promise.resolve([])), getScalingPolicy: mock((id: string) => mockGetScalingPolicyResult ? Promise.resolve(mockGetScalingPolicyResult(id)) : Promise.resolve(null) diff --git a/apps/api/src/scaling/engine.ts b/apps/api/src/scaling/engine.ts index afdc173..b527321 100644 --- a/apps/api/src/scaling/engine.ts +++ b/apps/api/src/scaling/engine.ts @@ -5,7 +5,7 @@ import { config } from '../utils/config'; import { dockerBin } from '../utils/docker-bin'; import { DEQUEL_MANAGED_LABEL } from '../utils/dequel-labels'; import { run, tryRun } from './docker-utils'; -import { getScalingPolicy, listDeployments, updateDeploymentStatus, listEnvironmentVariablesForDeploy } from '../db/repo'; +import { getScalingPolicy, listDeployments, updateDeploymentStatus, listEnvironmentVariablesForDeploy, getProjectById } from '../db/repo'; interface ContainerStats { containerName: string; @@ -57,8 +57,12 @@ class ScalingEngine { const policy = await getScalingPolicy(dep.projectId); if (!policy || !policy.enabled) return; + const project = await getProjectById(dep.projectId); + if (!project || !project.cpuLimit || project.cpuLimit <= 0) return; + const stats = await this.getContainerStats(dep.containerName); if (!stats) return; + const slug = project ? project.name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63) : dep.projectId; const state = await this.getCooldownState(dep.id); const now = Date.now(); @@ -69,7 +73,7 @@ class ScalingEngine { state.highCpuSince = now; } else if (now - state.highCpuSince > policy.cooldownSeconds * 1000) { if (now - state.lastScaleUp > policy.cooldownSeconds * 1000) { - await this.scaleUp(dep, policy.maxReplicas); + await this.scaleUp(dep, policy.maxReplicas, slug, project?.cpuLimit, project?.memoryLimitMb); state.lastScaleUp = now; state.highCpuSince = null; } @@ -85,7 +89,7 @@ class ScalingEngine { state.lowCpuSince = now; } else if (now - state.lowCpuSince > 300_000) { // 5 min if (now - state.lastScaleDown > policy.cooldownSeconds * 1000) { - await this.scaleDown(dep, policy.minReplicas); + await this.scaleDown(dep, policy.minReplicas, slug); state.lastScaleDown = now; state.lowCpuSince = null; } @@ -166,8 +170,10 @@ class ScalingEngine { private async scaleUp( dep: { id: string; projectId: string | null; containerName: string | null }, maxReplicas: number, + slug: string, + cpuLimit?: number | null, + memoryLimitMb?: number | null, ) { - const slug = dep.projectId || dep.id; const currentReplicas = await this.getCurrentReplicas(slug); if (currentReplicas >= maxReplicas) return; @@ -198,11 +204,20 @@ class ScalingEngine { } } catch {} + const resources: string[] = []; + if (cpuLimit && cpuLimit > 0) { + resources.push('--cpus', String(cpuLimit)); + } + if (memoryLimitMb && memoryLimitMb > 0) { + resources.push('--memory', `${Math.round(memoryLimitMb)}m`); + } + try { await run(dockerBin, [ 'run', '-d', '--name', containerName, '--network', config.dockerNetwork, '-l', DEQUEL_MANAGED_LABEL, + ...resources, ...volumes, ...envVars, imageTag, ]); @@ -219,8 +234,8 @@ class ScalingEngine { private async scaleDown( dep: { id: string; projectId: string | null; containerName: string | null }, minReplicas: number, + slug: string, ) { - const slug = dep.projectId || dep.id; const currentReplicas = await this.getCurrentReplicas(slug); if (currentReplicas <= minReplicas) return; @@ -265,7 +280,7 @@ class ScalingEngine { } const baseDomain = config.caddyBaseDomain === 'localhost' ? `${config.caddyBaseDomain}:80` : config.caddyBaseDomain; - const caddySnippet = `${slug}.${baseDomain} {\n reverse_proxy ${targets.join(' ')}\n}\n`; + const caddySnippet = `${slug}.${baseDomain} {\n reverse_proxy ${targets.join(' ')} {\n header_up Host {upstream_hostport}\n }\n}\n`; await writeFile(routeFile, caddySnippet, 'utf8'); // Reload Caddy diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index e7bb643..330aa6c 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -22,6 +22,13 @@ export interface Project { port: number | null; sourceDir: string | null; sourceType: string; + projectType: string; + buildType: string; + composeService: string | null; + composePort: number | null; + composeServices: string | null; + buildCommand: string | null; + startCommand: string | null; githubTokenEncrypted: string | null; githubTokenIv: string | null; githubTokenTag: string | null; @@ -49,6 +56,13 @@ export interface CreateProjectInput { port?: number | null; sourceDir?: string | null; sourceType?: string; + projectType?: string; + buildType?: string; + composeService?: string | null; + composePort?: number | null; + composeServices?: string | null; + buildCommand?: string | null; + startCommand?: string | null; } export interface EnvironmentVariable { @@ -116,6 +130,8 @@ export interface Domain { type: DomainType; validationStatus: DomainValidationStatus; sslStatus: SslStatus; + targetService: string | null; + targetPort: number | null; createdAt: string; updatedAt: string; } @@ -124,6 +140,8 @@ export interface CreateDomainInput { projectId: string; domain: string; type: DomainType; + targetService?: string | null; + targetPort?: number | null; } export interface ScalingPolicy { @@ -145,6 +163,8 @@ export interface CreateScalingPolicyInput { maxReplicas?: number; cpuThresholdPercent?: number; memoryThresholdPercent?: number; + cooldownSeconds?: number; + enabled?: boolean; } export interface Server { @@ -222,6 +242,7 @@ export interface Deployment { environment: string | null; failureReason: string | null; clearCache: boolean; + finishedAt: string | null; createdAt: string; updatedAt: string; } diff --git a/apps/api/src/utils/__tests__/domain-verifier.test.ts b/apps/api/src/utils/__tests__/domain-verifier.test.ts index 42cb577..24dbc45 100644 --- a/apps/api/src/utils/__tests__/domain-verifier.test.ts +++ b/apps/api/src/utils/__tests__/domain-verifier.test.ts @@ -8,8 +8,8 @@ let routesDir: string; const testConfig = { caddyRoutesDir: '', appInternalPort: 3000, caddyBaseDomain: 'localhost' }; const fileUrl = (path: string) => new URL(path, import.meta.url).toString(); -mock.module(fileUrl('../config'), () => ({ config: testConfig })); -mock.module(fileUrl('../../orchestrator/runtime'), () => ({ +mock.module(fileUrl('../config.ts'), () => ({ config: testConfig })); +mock.module(fileUrl('../../orchestrator/runtime.ts'), () => ({ reloadCaddy: mock(() => Promise.resolve()), })); @@ -151,13 +151,13 @@ describe('buildCaddySnippet', () => { it('creates snippet with just localhost domain when no verified domains', async () => { const snippet = await module.buildCaddySnippet('elysia', 'deploy-abc', 'proj-1', noDomains); - expect(snippet).toBe('elysia.localhost:80 {\n log {\n output stdout\n format json\n }\n reverse_proxy deploy-abc:3000\n}\n'); + expect(snippet).toBe('elysia.localhost:80 {\n log {\n output stdout\n format json\n }\n reverse_proxy deploy-abc:3000 {\n header_up Host {upstream_hostport}\n }\n}\n'); }); it('creates snippet with verified domains when projectId is provided', async () => { const listFn = mockDomains(['app.example.com']); const snippet = await module.buildCaddySnippet('elysia', 'deploy-abc', 'proj-1', listFn); - expect(snippet).toBe('elysia.localhost:80, app.example.com:80 {\n log {\n output stdout\n format json\n }\n reverse_proxy deploy-abc:3000\n}\n'); + expect(snippet).toBe('elysia.localhost:80, app.example.com:80 {\n log {\n output stdout\n format json\n }\n reverse_proxy deploy-abc:3000 {\n header_up Host {upstream_hostport}\n }\n}\n'); }); it('only includes verified (not pending) domains', async () => { diff --git a/apps/api/src/utils/__tests__/env-filter.test.ts b/apps/api/src/utils/__tests__/env-filter.test.ts new file mode 100644 index 0000000..a8c79cf --- /dev/null +++ b/apps/api/src/utils/__tests__/env-filter.test.ts @@ -0,0 +1,100 @@ +import { describe, test, expect } from "bun:test"; +import { isBuildSafeEnv, filterBuildEnvVars } from "../env-filter"; + +describe("isBuildSafeEnv", () => { + test("allows NEXT_PUBLIC_ prefix", () => { + expect(isBuildSafeEnv("NEXT_PUBLIC_API_URL")).toBe(true); + expect(isBuildSafeEnv("NEXT_PUBLIC_STRIPE_KEY")).toBe(true); + }); + + test("allows VITE_ prefix", () => { + expect(isBuildSafeEnv("VITE_API_URL")).toBe(true); + expect(isBuildSafeEnv("VITE_APP_TITLE")).toBe(true); + }); + + test("allows REACT_APP_ prefix", () => { + expect(isBuildSafeEnv("REACT_APP_API_URL")).toBe(true); + }); + + test("allows NUXT_PUBLIC_ prefix", () => { + expect(isBuildSafeEnv("NUXT_PUBLIC_API_BASE")).toBe(true); + }); + + test("allows NUXT_ENV_ prefix", () => { + expect(isBuildSafeEnv("NUXT_ENV_COOL_WORD")).toBe(true); + }); + + test("allows GATSBY_ prefix", () => { + expect(isBuildSafeEnv("GATSBY_API_URL")).toBe(true); + }); + + test("allows PUBLIC_ prefix (Astro/SvelteKit)", () => { + expect(isBuildSafeEnv("PUBLIC_SUPABASE_URL")).toBe(true); + expect(isBuildSafeEnv("PUBLIC_ANALYTICS_ID")).toBe(true); + }); + + + test("allows STORYBOOK_ prefix", () => { + expect(isBuildSafeEnv("STORYBOOK_API_URL")).toBe(true); + }); + + test("allows safe exact matches", () => { + expect(isBuildSafeEnv("NODE_ENV")).toBe(true); + expect(isBuildSafeEnv("CI")).toBe(true); + expect(isBuildSafeEnv("GENERATE_SOURCEMAP")).toBe(true); + expect(isBuildSafeEnv("SENTRY_DSN")).toBe(true); + }); + + test("rejects secrets and runtime vars", () => { + expect(isBuildSafeEnv("DATABASE_URL")).toBe(false); + expect(isBuildSafeEnv("API_SECRET_KEY")).toBe(false); + expect(isBuildSafeEnv("JWT_SECRET")).toBe(false); + expect(isBuildSafeEnv("SMTP_PASSWORD")).toBe(false); + expect(isBuildSafeEnv("REDIS_URL")).toBe(false); + expect(isBuildSafeEnv("AWS_SECRET_ACCESS_KEY")).toBe(false); + expect(isBuildSafeEnv("STRIPE_SECRET_KEY")).toBe(false); + }); + + test("rejects partial prefix matches", () => { + expect(isBuildSafeEnv("NEXT_PRIVATE_KEY")).toBe(false); + expect(isBuildSafeEnv("VITEPRESS_KEY")).toBe(false); + }); +}); + +describe("filterBuildEnvVars", () => { + test("filters mixed env vars correctly", () => { + const input = [ + { key: "NEXT_PUBLIC_API_URL", value: "https://api.example.com" }, + { key: "DATABASE_URL", value: "postgres://user:pass@host/db" }, + { key: "VITE_APP_TITLE", value: "My App" }, + { key: "JWT_SECRET", value: "supersecret" }, + { key: "NODE_ENV", value: "production" }, + { key: "STRIPE_SECRET_KEY", value: "sk_live_xxx" }, + { key: "PUBLIC_SUPABASE_URL", value: "https://xxx.supabase.co" }, + ]; + + const result = filterBuildEnvVars(input); + expect(result).toEqual([ + { key: "NEXT_PUBLIC_API_URL", value: "https://api.example.com" }, + { key: "VITE_APP_TITLE", value: "My App" }, + { key: "NODE_ENV", value: "production" }, + { key: "PUBLIC_SUPABASE_URL", value: "https://xxx.supabase.co" }, + ]); + }); + + test("returns empty array when no build-safe vars", () => { + const input = [ + { key: "DATABASE_URL", value: "postgres://..." }, + { key: "API_KEY", value: "secret" }, + ]; + expect(filterBuildEnvVars(input)).toEqual([]); + }); + + test("returns all vars when all are build-safe", () => { + const input = [ + { key: "NEXT_PUBLIC_URL", value: "https://..." }, + { key: "NODE_ENV", value: "production" }, + ]; + expect(filterBuildEnvVars(input)).toEqual(input); + }); +}); diff --git a/apps/api/src/utils/__tests__/validate.test.ts b/apps/api/src/utils/__tests__/validate.test.ts new file mode 100644 index 0000000..0692ce2 --- /dev/null +++ b/apps/api/src/utils/__tests__/validate.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "bun:test"; +import { validateComposeServices, validateComposeServiceMapping } from "../validate"; + +describe("validateComposeServiceMapping", () => { + it("accepts a valid mapping", () => { + expect(validateComposeServiceMapping({ serviceName: "web", subdomain: "app", port: 3000 })).toBeNull(); + }); + + it("rejects serviceName with spaces or braces", () => { + expect(validateComposeServiceMapping({ serviceName: "x} { respond" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "a b" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "" })).not.toBeNull(); + }); + + it("rejects subdomain injection attempts", () => { + expect(validateComposeServiceMapping({ serviceName: "web", subdomain: "x} { respond \"owned\" } {.x" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", subdomain: "UPPER" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", subdomain: "-bad" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", subdomain: "ok-label" })).toBeNull(); + }); + + it("rejects invalid ports", () => { + expect(validateComposeServiceMapping({ serviceName: "web", port: 0 })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", port: 65536 })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", port: "80} {" })).not.toBeNull(); + expect(validateComposeServiceMapping({ serviceName: "web", port: "8080" })).toBeNull(); + }); + + it("rejects non-object mappings", () => { + expect(validateComposeServiceMapping("web")).not.toBeNull(); + expect(validateComposeServiceMapping(null)).not.toBeNull(); + }); +}); + +describe("validateComposeServices", () => { + it("accepts a valid array", () => { + const result = validateComposeServices([{ serviceName: "web", subdomain: "app", port: 3000 }]); + expect(result.ok).toBe(true); + }); + + it("accepts a valid JSON string", () => { + const result = validateComposeServices('[{"serviceName":"web","subdomain":"app"}]'); + expect(result.ok).toBe(true); + }); + + it("rejects invalid JSON", () => { + const result = validateComposeServices('{not json'); + expect(result.ok).toBe(false); + }); + + it("rejects non-array values", () => { + expect(validateComposeServices({}).ok).toBe(false); + expect(validateComposeServices("web").ok).toBe(false); + }); + + it("rejects an array with an injection payload", () => { + const result = validateComposeServices([{ serviceName: "web", subdomain: "x} { respond \"owned\" } {.x" }]); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("subdomain"); + } + }); +}); diff --git a/apps/api/src/utils/domain-verifier.ts b/apps/api/src/utils/domain-verifier.ts index 4f0877a..cffaeeb 100644 --- a/apps/api/src/utils/domain-verifier.ts +++ b/apps/api/src/utils/domain-verifier.ts @@ -119,21 +119,35 @@ export const buildCaddySnippet = async ( appPort?: number, ): Promise => { const baseDomain = config.caddyBaseDomain === 'localhost' ? `${config.caddyBaseDomain}:80` : config.caddyBaseDomain; - let domains = [`${slug}.${baseDomain}`]; + let defaultDomains = [`${slug}.${baseDomain}`]; let port = appPort ?? config.appInternalPort; + const customBlocks: string[] = []; if (projectId) { const projectDomains = await listDomainsFn(projectId); const verified = projectDomains.filter(d => d.validationStatus === 'verified'); for (const d of verified) { const withPort = `${d.domain}:80`; - if (!domains.includes(withPort)) domains.push(withPort); + if (d.targetService || d.targetPort) { + let targetContainer = containerName; + if (d.targetService) { + const parts = containerName.split('-'); + if (parts.length >= 3) { + parts[parts.length - 2] = d.targetService; + targetContainer = parts.join('-'); + } + } + const tPort = d.targetPort || port; + customBlocks.push(`${withPort} {\n log {\n output stdout\n format json\n }\n reverse_proxy ${targetContainer}:${tPort} {\n header_up Host {upstream_hostport}\n }\n}\n`); + } else { + if (!defaultDomains.includes(withPort)) defaultDomains.push(withPort); + } } try { const envVars = await listEnvironmentVariablesForDeploy(projectId); const portVar = envVars.find(v => v.key === 'PORT'); - if (portVar && portVar.value) { + if (portVar && portVar.value && !appPort) { const parsedPort = Number(portVar.value); if (!isNaN(parsedPort) && parsedPort > 0) { port = parsedPort; @@ -144,5 +158,7 @@ export const buildCaddySnippet = async ( } } - return `${domains.join(', ')} {\n log {\n output stdout\n format json\n }\n reverse_proxy ${containerName}:${port}\n}\n`; + const primaryBlock = `${defaultDomains.join(', ')} {\n log {\n output stdout\n format json\n }\n reverse_proxy ${containerName}:${port} {\n header_up Host {upstream_hostport}\n }\n}\n`; + + return [primaryBlock, ...customBlocks].join('\n'); }; diff --git a/apps/api/src/utils/env-filter.ts b/apps/api/src/utils/env-filter.ts new file mode 100644 index 0000000..3ce1921 --- /dev/null +++ b/apps/api/src/utils/env-filter.ts @@ -0,0 +1,42 @@ +const BUILD_SAFE_PREFIXES = [ + "NEXT_PUBLIC_", + "VITE_", + "REACT_APP_", + "NUXT_PUBLIC_", + "NUXT_ENV_", + "GATSBY_", + "PUBLIC_", + "STORYBOOK_", +]; + +const BUILD_SAFE_EXACT = new Set([ + "NODE_ENV", + "NODE_OPTIONS", + "NPM_CONFIG_PRODUCTION", + "GENERATE_SOURCEMAP", + "INLINE_RUNTIME_CHUNK", + "CI", + "BUILD_PATH", + "DISABLE_ESLINT_PLUGIN", + "TSC_COMPILE_ON_ERROR", + "IMAGE_INLINE_SIZE_LIMIT", + "FAST_REFRESH", + "ESLINT_NO_DEV_ERRORS", + "TAILWIND_MODE", + "ANALYZE", + "SENTRY_DSN", + "SENTRY_AUTH_TOKEN", +]); + +export const isBuildSafeEnv = (key: string): boolean => { + if (BUILD_SAFE_EXACT.has(key)) return true; + return BUILD_SAFE_PREFIXES.some((prefix) => + key.startsWith(prefix), + ); +}; + +export const filterBuildEnvVars = ( + vars: { key: string; value: string }[], +): { key: string; value: string }[] => { + return vars.filter((v) => isBuildSafeEnv(v.key)); +}; diff --git a/apps/api/src/utils/validate.ts b/apps/api/src/utils/validate.ts new file mode 100644 index 0000000..cb4be3b --- /dev/null +++ b/apps/api/src/utils/validate.ts @@ -0,0 +1,53 @@ +export const SERVICE_NAME_RE = /^[a-zA-Z0-9_-]+$/; + +export const SUBDOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; + +export const isPort = (value: unknown): value is number => + typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535; + +export interface ComposeServiceMapping { + serviceName: string; + subdomain?: string; + port?: number | string; +} + +export const validateComposeServiceMapping = (mapping: unknown): string | null => { + if (!mapping || typeof mapping !== "object") { + return "compose service mapping must be an object"; + } + const m = mapping as ComposeServiceMapping; + if (typeof m.serviceName !== "string" || !SERVICE_NAME_RE.test(m.serviceName)) { + return "serviceName may only contain letters, numbers, underscores and hyphens"; + } + if (m.subdomain !== undefined && m.subdomain !== null) { + if (typeof m.subdomain !== "string" || !SUBDOMAIN_RE.test(m.subdomain)) { + return "subdomain must be a valid hostname label (lowercase letters, numbers, hyphens)"; + } + } + if (m.port !== undefined && m.port !== null) { + const port = Number(m.port); + if (!isPort(port)) { + return "port must be an integer between 1 and 65535"; + } + } + return null; +}; + +export const validateComposeServices = (value: unknown): { ok: true; services: ComposeServiceMapping[] } | { ok: false; error: string } => { + let parsed: unknown = value; + if (typeof value === "string") { + try { + parsed = JSON.parse(value); + } catch { + return { ok: false, error: "composeServices must be valid JSON" }; + } + } + if (!Array.isArray(parsed)) { + return { ok: false, error: "composeServices must be an array" }; + } + for (const mapping of parsed) { + const err = validateComposeServiceMapping(mapping); + if (err) return { ok: false, error: err }; + } + return { ok: true, services: parsed as ComposeServiceMapping[] }; +}; diff --git a/apps/docs/.astro/astro/content.d.ts b/apps/docs/.astro/astro/content.d.ts index 57c8503..2c97712 100644 --- a/apps/docs/.astro/astro/content.d.ts +++ b/apps/docs/.astro/astro/content.d.ts @@ -162,6 +162,13 @@ declare module 'astro:content' { collection: "changelogs"; data: any } & { render(): Render[".md"] }; +"v0.2.1.md": { + id: "v0.2.1.md"; + slug: "v021"; + body: string; + collection: "changelogs"; + data: any +} & { render(): Render[".md"] }; }; "docs": { "auth.md": { @@ -171,6 +178,13 @@ declare module 'astro:content' { collection: "docs"; data: any } & { render(): Render[".md"] }; +"changelog.md": { + id: "changelog.md"; + slug: "changelog"; + body: string; + collection: "docs"; + data: any +} & { render(): Render[".md"] }; "configuration.md": { id: "configuration.md"; slug: "configuration"; diff --git a/apps/docs/.astro/settings.json b/apps/docs/.astro/settings.json index be2c74f..b280c44 100644 --- a/apps/docs/.astro/settings.json +++ b/apps/docs/.astro/settings.json @@ -1,5 +1,5 @@ { "_variables": { - "lastUpdateCheck": 1784334507362 + "lastUpdateCheck": 1785406062618 } } \ No newline at end of file diff --git a/apps/docs/package.json b/apps/docs/package.json index 9091a24..0304a6e 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,7 +1,7 @@ { "name": "dequel-docs", "type": "module", - "version": "0.2.0", + "version": "0.2.1", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/apps/docs/src/components/CTA.astro b/apps/docs/src/components/CTA.astro index 826c9dc..1d1fcc6 100644 --- a/apps/docs/src/components/CTA.astro +++ b/apps/docs/src/components/CTA.astro @@ -1,29 +1,44 @@ --- --- -
- - - - - - - - - - - - -

Ready to deploy?

-

- Your infrastructure, your rules. Start deploying in minutes. -

-
- - Read the docs → - - - Install Dequel → - +
+
+
+
+ + + + + + + + + + + +
+ +

+ Ready to own your deployment platform? +

+

+ Spin up Dequel on your server in seconds with zero third-party dependencies. +

+ + +
+ curl -fsSL https://dequel.intrep.xyz/install.sh | bash + 1-Line Install +
+ +
diff --git a/apps/docs/src/components/Comparison.astro b/apps/docs/src/components/Comparison.astro new file mode 100644 index 0000000..d8a4a73 --- /dev/null +++ b/apps/docs/src/components/Comparison.astro @@ -0,0 +1,83 @@ +--- +--- +
+
+
PaaS Paradigm Shift
+

+ Stop fighting infrastructure.
+ Start shipping instantly. +

+

+ Dequel replaces fragmented DevOps tools with a unified self-hosted platform. +

+
+ +
+ +
+
+
+ TRADITIONAL DEVOPS +
+

+ Fighting infra with DIY scripts & expensive PaaS +

+
    +
  • + ✕ + Wrestling with complex Kubernetes manifests, ingress controllers, and Helm charts just to host an API. +
  • +
  • + ✕ + Hand-rolling certbot renewal scripts, nginx proxy configurations, and port forward rules on raw VPS boxes. +
  • +
  • + ✕ + Paying bloated monthly per-seat fees or unpredictable bandwidth egress markups on public cloud PaaS platforms. +
  • +
  • + ✕ + Scattered logs, missing metrics dashboards, and zero visibility into build errors or worker queues. +
  • +
+
+
+ Status: High overhead • Proprietary lock-in +
+
+ + +
+
+
+ DEQUEL PAAS +
+

+ Shipping apps with Dequel in seconds +

+
    +
  • + ✓ + One-command install: Spin up the entire PaaS control plane on any clean Linux VPS in under 60 seconds. +
  • +
  • + ✓ + Automated Railpack builds: Connect Git or upload a ZIP file — Dequel autodetects your runtime and builds via local Buildkit. +
  • +
  • + ✓ + Zero-config SSL & Domains: Built-in Caddy engine automatically handles Let's Encrypt certificates and domain routing. +
  • +
  • + ✓ + Complete Observability: Full Prometheus, cAdvisor, Loki log aggregation, and Grafana monitoring out-of-the-box. +
  • +
+
+
+ Status: 100% Owned by You + 0% Markups +
+
+
+
diff --git a/apps/docs/src/components/Features.astro b/apps/docs/src/components/Features.astro index bce1715..1b50145 100644 --- a/apps/docs/src/components/Features.astro +++ b/apps/docs/src/components/Features.astro @@ -1,60 +1,175 @@ --- -const features = [ +const tabs = [ { - num: "01", - name: "Instant Deployments", - desc: "Push code and watch it go live in seconds. Zero-downtime deployments with automatic container rollbacks.", - iconHtml: `` + id: "build", + label: "Git & Build Engine", + icon: ``, + title: "Railpack Compilation & BuildKit Daemon", + description: "Push code directly from GitHub or upload a project ZIP file. Dequel's internal Railpack CLI autodetects Node.js, Python, Go, Rust, and Dockerfiles, compiling zero-config production containers via BuildKit.", + highlights: [ + "Automatic framework detection (Next.js, Express, Fastify, FastAPI, Go, etc.)", + "Layer-cached builds with isolated workspace staging", + "Instant rollback to previous container SHA hashes", + "Custom environment variable injection & secret encryption" + ], + codePreview: `// dequel.config.json (Optional) +{ + "name": "my-api-service", + "build": { + "builder": "railpack", + "env": { + "NODE_ENV": "production" + } }, - { - num: "02", - name: "Custom Domains & SSL", - desc: "Automatic HTTPS with Let's Encrypt, custom domain routing, and wildcard certificates via Caddy reverse proxy.", - iconHtml: `` - }, - { - num: "03", - name: "Observability", - desc: "Real-time metrics, request monitoring, and log aggregation powered by Prometheus, Loki, and Grafana.", - iconHtml: `` + "routes": [ + { "domain": "api.mydomain.com", "port": 3001 } + ] +}` }, { - num: "04", - name: "Managed Databases", - desc: "One-click PostgreSQL instances with automatic backups, connection pooling, and private network attachment.", - iconHtml: `` + id: "ingress", + label: "Dynamic Ingress & Auto SSL", + icon: ``, + title: "Caddy Ingress & Free Let's Encrypt Certificates", + description: "Every deployed container is instantly routed through an internal high-performance Caddy reverse proxy. Dynamic route files are updated in real-time with zero server reboots.", + highlights: [ + "Automatic SSL certificate issuance & renewal via Let's Encrypt", + "Wildcard subdomains & custom apex domain attachments", + "HTTP/2 and HTTP/3 QUIC protocol support out-of-the-box", + "Zero-downtime dynamic routing reloading" + ], + codePreview: `# Dynamic Caddyfile (Auto-generated by Dequel) +api.mydomain.com { + tls admin@mydomain.com + reverse_proxy 10.0.0.12:3001 { + header_up Host {host} + header_up X-Real-IP {remote} + } +}` }, { - num: "05", - name: "Auto Scaling", - desc: "Scale horizontally based on CPU, memory, or request load. Define min/max replicas with zero-config policies.", - iconHtml: `` + id: "observability", + label: "Observability Stack", + icon: ``, + title: "Full Observability: Prometheus, cAdvisor & Loki Logs", + description: "Get production-grade monitoring without setting up third-party SaaS agents. Dequel collects container stats, memory usage, CPU load, network I/O, and stdout/stderr log streams in real-time.", + highlights: [ + "cAdvisor real-time per-container CPU & RAM metrics", + "Prometheus metric scrape target integration", + "Grafana dashboard & Loki log aggregation", + "Live terminal stdout stream viewer in dashboard" + ], + codePreview: `# Querying logs via Dequel CLI +$ dequel logs --app my-api-service --follow + +[INFO] 10:45:02 [express] Server listening on port 3001 +[INFO] 10:45:15 GET /api/v1/health 200 4.2ms +[METRIC] CPU: 1.2% | RAM: 42.8MB / 512MB` }, { - num: "06", - name: "Secure by Default", - desc: "Container isolation, encrypted environment variables, network policies, and audit logging built into every layer.", - iconHtml: `` + id: "scaling", + label: "Auto-Scaling & Resiliency", + icon: ``, + title: "CPU & Memory Scaling Engine", + description: "Define auto-scaling parameters for high-traffic endpoints. Dequel's background scaling engine evaluates resource thresholds and automatically scales container replicas up or down.", + highlights: [ + "Horizontal pod-like replica scaling based on CPU/RAM thresholds", + "Automated health checks & container auto-healing restart policies", + "Redis job queue integration for background worker scaling", + "Zero-downtime rolling updates" + ], + codePreview: `# Scaling policy rule evaluated by Dequel API +scaling_policy: + target_cpu_percent: 75 + min_replicas: 2 + max_replicas: 10 + cooldown_period_seconds: 60` } ]; --- -
-
-
Platform Capabilities
-

- Everything you need to ship.
- Nothing you don't. +
+
+
Platform Capabilities
+

+ Everything you need to ship.
+ Powered by your infrastructure.

+

+ Explore Dequel's core PaaS engines built into a single lightweight control plane. +

+
+ + +
+
+ {tabs.map((tab, idx) => ( + + ))} +
-
- {features.map((feature) => ( -
-
{feature.num}
-
-

{feature.name}

-

{feature.desc}

+ +
+ {tabs.map((tab, idx) => ( +
+
+ +
+
+
+ PaaS Module: + {tab.id} +
+

+ {tab.title} +

+

+ {tab.description} +

+
    + {tab.highlights.map((item) => ( +
  • + ✓ + {item} +
  • + ))} +
+
+ +
+ + +
+
+
+ + {tab.label} Preview +
+ Dequel v1.0 +
+
{tab.codePreview}
+
+ Status: Active + Zero Config Required +
+
+
))}
diff --git a/apps/docs/src/components/Hero.astro b/apps/docs/src/components/Hero.astro index 359b666..1e2d827 100644 --- a/apps/docs/src/components/Hero.astro +++ b/apps/docs/src/components/Hero.astro @@ -7,59 +7,65 @@ interface Props { } const { - tag = "Private Beta", - titleNormal = "Deploy anything.", - titleItalic = "Scale everything.", - subText = "The self-hosted cloud platform for developers who want Railway-grade deployment workflows on their own infrastructure. Container orchestration, observability, and scaling — all from one dashboard.", + tag = "Self-Hosted PaaS Platform • Open Source v1.0", + titleNormal = "Deploy apps from Git, ZIP, or Docker Compose", + titleItalic = "yours by default.", + subText = "The all-in-one self-hosted Platform as a Service (PaaS). Build container images via Railpack, route with dynamic Caddy auto-SSL, scale workloads, and monitor metrics — all on your own servers with zero vendor lock-in.", } = Astro.props; --- -
-
- - {tag} -
+
+
+ +
-

- {titleNormal}
- {titleItalic} -

+ + -

- {subText} -

+ +

+ Deploy apps from Git, ZIP, or Docker Compose — + yours by default. +

- + +

+ {subText} +

- -
diff --git a/apps/docs/src/components/HowItWorks.astro b/apps/docs/src/components/HowItWorks.astro index 5dcc486..9387d92 100644 --- a/apps/docs/src/components/HowItWorks.astro +++ b/apps/docs/src/components/HowItWorks.astro @@ -1,148 +1,147 @@ --- --- -
-
-
Self-Hosting Engine
-

- Your own PaaS, running on your own servers. +
+
+
Engine Architecture
+

+ Your own PaaS, running on your own servers.

-

- Dequel wraps raw Docker containers, Buildkit compilation, and Caddy reverse proxy routing in a single cohesive platform. No third-party APIs, no hidden fees, and zero vendor lock-in. +

+ Dequel orchestrates raw Docker containers, Buildkit compilation, Redis job queues, SQLite data, and dynamic Caddy routing into one unified platform.

-
+
-
-
-
STEP 01
-

Connect Your Server

+
+
+
+ STEP 01 + Debian / Ubuntu VPS +
+

Connect Your Server

- Run our installer script on any clean Debian/Ubuntu machine. Dequel boots its control plane, Redis task queue, and SQLite database inside a dedicated Docker network. + Run our 1-command installer. Dequel bootstraps its API control plane, Redis job queue, SQLite persistence, and cAdvisor monitoring inside a dedicated Docker network.

-
-
STEP 02
-

Push Code, Detect Stack

+
+
+ STEP 02 + Git / ZIP / Dockerfile +
+

Push Code & Build

- Dequel uses Railpack to analyze your source code (Git repo or ZIP upload), automatically detects the language, and compiles a containerized image via local Buildkit. + Dequel uses Railpack to analyze your repository, autodetects your language runtime, and compiles isolated container images locally via Buildkit.

-
-
STEP 03
-

Auto Routing & SSL

+
+
+ STEP 03 + Caddy Proxy + Auto-SSL +
+

Instant Auto Routing

- The internal Caddy engine dynamically reloads routes and automatically provisions SSL certificates via Let's Encrypt, mapping your domain directly to the app container. + The internal Caddy engine reloads dynamic routes without zero-downtime, automatically provisions Let's Encrypt certificates, and connects public traffic directly to your app.

-
+
- - HOST: vps-node-01 (ONLINE) + + NODE: vps-dequel-01 (ONLINE)
dequel-network (10.0.0.0/16)
-
+
-
-
- Client Request -
my-app.com
- -
- - +
+
+ HTTP Traffic +
app.domain.com
+
+ +
-
- Git Push -
Repo Webhook
-
- +
+ Git Webhook +
Push to main
+
+
-
+
-
+
- Caddy Ingress - Auto SSL / Proxy + Caddy Ingress Engine + Auto SSL / Reverse Proxy
- PORT 80/443 -
- - + PORT 80/443 +
+
-
- Dequel API - Docker Orchestrator -
- -
- -
- +
+ Dequel API Orchestrator + ElysiaJS + Bun Runtime +
+
-
- -
+
+
- deploy-my-app - Running (Replica 1) + web-app-prod + Replica #1 (Healthy)
- +
- -
+
buildkit-daemon Railpack compiling...
- +
- -
+
- db-postgres-1 - Private volume active + postgres-db-1 + Mounted Data Volume
- +
-
- SQLite: Local Persistence - Redis: Job Queue + SQLite: ./data/dequel.db + Redis: Task Queue
- 100% Owned by You + Zero Infrastructure Markup
diff --git a/apps/docs/src/components/Stats.astro b/apps/docs/src/components/Stats.astro index dbbb14c..ada148a 100644 --- a/apps/docs/src/components/Stats.astro +++ b/apps/docs/src/components/Stats.astro @@ -1,27 +1,28 @@ --- const stats = [ - { value: "15.2s", label: "Avg Deploy Time" }, - { value: "99.9%", label: "Uptime SLA" }, - { value: "0", label: "Vendor Lock-in" }, - { value: "∞", label: "Your Infrastructure" }, + { value: "< 10s", label: "Average Deploy Time", sub: "Railpack + Buildkit" }, + { value: "100%", label: "Self-Hosted Control", sub: "On your own VPS" }, + { value: "$0", label: "Third-Party SaaS Fees", sub: "Zero vendor lock-in" }, + { value: "0%", label: "Infrastructure Markup", sub: "Pay only for raw VPS" }, ]; --- -
-
-
- { - stats.map((stat) => ( -
-
- {stat.value} -
-
- {stat.label} -
-
- )) - } -
+
+
+ {stats.map((stat) => ( +
+
+
+ {stat.value} +
+
+ {stat.label} +
+
+
+ {stat.sub} +
+
+ ))} +
-
diff --git a/apps/docs/src/components/TerminalDemo.astro b/apps/docs/src/components/TerminalDemo.astro index 4c78aec..14085d3 100644 --- a/apps/docs/src/components/TerminalDemo.astro +++ b/apps/docs/src/components/TerminalDemo.astro @@ -1,28 +1,46 @@ --- --- -
-
-
-
Developer Experience
-

From git push to production
in under 4 seconds.

-

- Connect your repository, push to main, and Dequel handles the rest — - building your container image, routing traffic, provisioning SSL, - and serving your application at a custom domain. +

+
+
+
Developer Experience
+

+ From git push to production
+ in under 4 seconds. +

+

+ Connect your project, run dequel deploy, and watch Dequel handle the rest — building your container via Buildkit, reloaded Caddy dynamic proxy, automated SSL issuance, and live status reporting.

+
+
+ + Zero Downtime +
+
+ + Auto Certs +
+
-
-
-
- - - + + +
+
+
+ + +
- dequel — deploy + dequel cli v1.0.0 + bash
-
-
$
+
+
+ $ + + +
diff --git a/apps/docs/src/content/changelogs/v0.2.1.md b/apps/docs/src/content/changelogs/v0.2.1.md new file mode 100644 index 0000000..4b053ef --- /dev/null +++ b/apps/docs/src/content/changelogs/v0.2.1.md @@ -0,0 +1,18 @@ +--- +version: 0.2.1 +date: "2026-07-27" +--- + +## What's Changed +### Improvements +- Caddy reverse proxy now passes the Host header correctly to upstream containers ([f20e3fa](https://github.com/Lftobs/dequel/commit/f20e3fa)) +- Runtime reconciliation skips non-existent containers ([dc4593c](https://github.com/Lftobs/dequel/commit/dc4593c)) +- Docker-compose configuration updated for local builds ([dc4593c](https://github.com/Lftobs/dequel/commit/dc4593c)) +- Add `project_type`, `buildCommand`, and `startCommand` fields to projects ([f20e3fa](https://github.com/Lftobs/dequel/commit/f20e3fa)) +- Implement dynamic `railpack.json` generation for build/runtime compatibility ([f20e3fa](https://github.com/Lftobs/dequel/commit/f20e3fa)) +- Add `ProjectSettingsTab` to the dashboard for managing build settings ([f20e3fa](https://github.com/Lftobs/dequel/commit/f20e3fa)) +- Track deployment completion time with `finished_at` column ([dc4593c](https://github.com/Lftobs/dequel/commit/dc4593c)) +- Implement status-based timestamping for deployment lifecycle ([dc4593c](https://github.com/Lftobs/dequel/commit/dc4593c)) + +### Bug Fixes +- Ko-fi popup mounting fixed in dashboard sidebar ([dc4593c](https://github.com/Lftobs/dequel/commit/dc4593c)) diff --git a/apps/docs/src/content/docs/installation.md b/apps/docs/src/content/docs/installation.md index 41c2a61..72edfde 100644 --- a/apps/docs/src/content/docs/installation.md +++ b/apps/docs/src/content/docs/installation.md @@ -15,7 +15,7 @@ slug: installation The quickest way to get Dequel running: ```bash -curl -fsSL https://raw.githubusercontent.com/Lftobs/dequel/main/scripts/install.sh | sh +curl -fsSL https://github.com/Lftobs/dequel/releases/latest/download/install.sh | bash ``` This will: diff --git a/apps/docs/src/pages/index.astro b/apps/docs/src/pages/index.astro index 8f89f0d..5863a8e 100644 --- a/apps/docs/src/pages/index.astro +++ b/apps/docs/src/pages/index.astro @@ -2,10 +2,10 @@ import "../styles/global.css"; import Hero from "../components/Hero.astro"; import Stats from "../components/Stats.astro"; +import Comparison from "../components/Comparison.astro"; import Features from "../components/Features.astro"; import HowItWorks from "../components/HowItWorks.astro"; import TerminalDemo from "../components/TerminalDemo.astro"; -import TechStack from "../components/TechStack.astro"; import CTA from "../components/CTA.astro"; --- @@ -15,11 +15,11 @@ import CTA from "../components/CTA.astro"; - Dequel — Self-Hosted Container - Platform + Dequel — Self-Hosted PaaS Engine & Container Platform - + + - - - - - - - - -
- - +
+ + + + + + + +
- - -