From 027e32da825df9f520e4d8a36b15abdcde7bb438 Mon Sep 17 00:00:00 2001 From: Lftobs Date: Sat, 1 Aug 2026 00:39:04 +0100 Subject: [PATCH 01/11] feat(orchestrator): add support for Docker Compose project deployments - Update database schema to store compose-specific metadata - Enhance Caddy dynamic ingress to route traffic to specific services - Extend project/domain APIs to support service target configuration - Refactor documentation landing page and UI components --- apps/api/src/api/domains/index.ts | 12 + apps/api/src/api/projects/index.ts | 36 + apps/api/src/databases/manager.ts | 94 +- .../src/db/migrations/0004_early_sunfire.sql | 3 + .../db/migrations/0005_famous_grandmaster.sql | 2 + .../migrations/0006_cheerful_vance_astro.sql | 1 + .../src/db/migrations/meta/0004_snapshot.json | 1347 ++++++++++++++++ .../src/db/migrations/meta/0005_snapshot.json | 1361 ++++++++++++++++ .../src/db/migrations/meta/0006_snapshot.json | 1368 +++++++++++++++++ apps/api/src/db/migrations/meta/_journal.json | 21 + apps/api/src/db/repo/domains.ts | 4 + apps/api/src/db/repo/projects.ts | 12 + apps/api/src/db/schema.ts | 6 + .../orchestrator/__tests__/compose.test.ts | 69 + apps/api/src/orchestrator/compose.ts | 365 +++++ apps/api/src/orchestrator/pipeline.ts | 287 +++- apps/api/src/types.ts | 12 + .../utils/__tests__/domain-verifier.test.ts | 4 +- apps/api/src/utils/__tests__/validate.test.ts | 63 + apps/api/src/utils/domain-verifier.ts | 24 +- apps/api/src/utils/validate.ts | 53 + apps/docs/.astro/astro/content.d.ts | 7 + apps/docs/.astro/settings.json | 2 +- apps/docs/src/components/CTA.astro | 63 +- apps/docs/src/components/Comparison.astro | 83 + apps/docs/src/components/Features.astro | 197 ++- apps/docs/src/components/Hero.astro | 100 +- apps/docs/src/components/HowItWorks.astro | 145 +- apps/docs/src/components/Stats.astro | 43 +- apps/docs/src/components/TerminalDemo.astro | 54 +- apps/docs/src/pages/index.astro | 303 ++-- apps/docs/src/styles/global.css | 165 +- apps/web/src/api/client.ts | 16 +- .../components/project/domains/DomainsTab.tsx | 38 +- .../project/settings/ProjectSettingsTab.tsx | 178 ++- apps/web/src/routes/CreateProjectPage.tsx | 1110 +++++++++++++ apps/web/src/routes/Dashboard.tsx | 15 +- apps/web/src/routes/index.tsx | 9 +- apps/web/src/types/index.ts | 3 + 39 files changed, 7124 insertions(+), 551 deletions(-) create mode 100644 apps/api/src/db/migrations/0004_early_sunfire.sql create mode 100644 apps/api/src/db/migrations/0005_famous_grandmaster.sql create mode 100644 apps/api/src/db/migrations/0006_cheerful_vance_astro.sql create mode 100644 apps/api/src/db/migrations/meta/0004_snapshot.json create mode 100644 apps/api/src/db/migrations/meta/0005_snapshot.json create mode 100644 apps/api/src/db/migrations/meta/0006_snapshot.json create mode 100644 apps/api/src/orchestrator/__tests__/compose.test.ts create mode 100644 apps/api/src/orchestrator/compose.ts create mode 100644 apps/api/src/utils/__tests__/validate.test.ts create mode 100644 apps/api/src/utils/validate.ts create mode 100644 apps/docs/src/components/Comparison.astro create mode 100644 apps/web/src/routes/CreateProjectPage.tsx 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 92fad65..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,6 +52,11 @@ export const projectsRoutes = new Elysia() set.status = 400; return { error: "name is required" }; } + const validationError = validateComposeFields(body); + if (validationError) { + set.status = 400; + return { error: validationError }; + } const project = await createProject({ name: body.name, description: body.description, @@ -46,6 +69,10 @@ export const projectsRoutes = new Elysia() 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, }); @@ -55,6 +82,11 @@ export const projectsRoutes = new Elysia() .patch( "/projects/:id", async ({ params: { id }, body, set }: any) => { + const validationError = validateComposeFields(body); + if (validationError) { + set.status = 400; + return { error: validationError }; + } const project = await updateProject(id, { name: body?.name, description: body?.description, @@ -66,6 +98,10 @@ export const projectsRoutes = new Elysia() 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, }); 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/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/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 f2fdd56..038b6a3 100644 --- a/apps/api/src/db/migrations/meta/_journal.json +++ b/apps/api/src/db/migrations/meta/_journal.json @@ -29,6 +29,27 @@ "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/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 a7086a2..850c8a1 100644 --- a/apps/api/src/db/repo/projects.ts +++ b/apps/api/src/db/repo/projects.ts @@ -29,6 +29,10 @@ const mapProject = (row: typeof projects.$inferSelect): Project => ({ 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, @@ -55,6 +59,10 @@ export const createProject = async (input: CreateProjectInput): Promise 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, @@ -95,6 +103,10 @@ 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 a757519..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, @@ -29,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() @@ -146,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( @@ -389,27 +409,45 @@ export class PipelineOrchestrator { deployment.projectId || deploymentId; const envVars = deployment.projectId ? await listEnvironmentVariablesForDeploy(deployment.projectId, "production") : []; - 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 - }, - ); + 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, @@ -483,6 +521,9 @@ export class PipelineOrchestrator { let oldContainerName: | string | undefined; + let oldDeploymentId: + | string + | undefined; if (deployment.projectId) { const all = await listDeployments( deployment.projectId, @@ -497,6 +538,7 @@ export class PipelineOrchestrator { if (prev?.containerName) { oldContainerName = prev.containerName; + oldDeploymentId = prev.id; await emitLog( deploymentId, "deploy", @@ -527,29 +569,135 @@ export class PipelineOrchestrator { 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; @@ -558,9 +706,8 @@ export class PipelineOrchestrator { "running", { imageTag, - containerName: - runtime.containerName, - liveUrl: runtime.liveUrl, + containerName: runtimeContainerName, + liveUrl: runtimeLiveUrl, failureReason: null, }, ); @@ -649,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/types.ts b/apps/api/src/types.ts index 46e65e2..330aa6c 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -23,6 +23,10 @@ export interface Project { 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; @@ -53,6 +57,10 @@ export interface CreateProjectInput { sourceDir?: string | null; sourceType?: string; projectType?: string; + buildType?: string; + composeService?: string | null; + composePort?: number | null; + composeServices?: string | null; buildCommand?: string | null; startCommand?: string | null; } @@ -122,6 +130,8 @@ export interface Domain { type: DomainType; validationStatus: DomainValidationStatus; sslStatus: SslStatus; + targetService: string | null; + targetPort: number | null; createdAt: string; updatedAt: string; } @@ -130,6 +140,8 @@ export interface CreateDomainInput { projectId: string; domain: string; type: DomainType; + targetService?: string | null; + targetPort?: number | null; } export interface ScalingPolicy { diff --git a/apps/api/src/utils/__tests__/domain-verifier.test.ts b/apps/api/src/utils/__tests__/domain-verifier.test.ts index c86fad6..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()), })); 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 62bb1cd..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 header_up Host {upstream_hostport}\n }\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/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 e2e9502..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": { 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/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/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 - + + - - - - - - - - -
- - +
+ + + + + + + +
- - -