Files
vibn-frontend/lib/coolify.ts
Mark Henderson 62c52747f5 fix(coolify): list project databases across per-flavor arrays
GET /projects/{uuid}/{envName} returns databases split into
postgresqls/mysqls/mariadbs/mongodbs/redis/keydbs/dragonflies/clickhouses
sibling arrays instead of a unified `databases` list. Combine all of
them in listDatabasesInProject. Also normalize setApplicationDomains
to prepend https:// on bare hostnames (Coolify validates as URL).

Made-with: Cursor
2026-04-21 12:30:36 -07:00

815 lines
27 KiB
TypeScript

/**
* Coolify API client for Vibn project provisioning.
*
* Used server-side only. Credentials from env vars:
* COOLIFY_URL — e.g. https://coolify.vibnai.com
* COOLIFY_API_TOKEN — admin bearer token
* COOLIFY_DEFAULT_SERVER_UUID — which Coolify server workspaces deploy to
* COOLIFY_DEFAULT_DESTINATION_UUID — Docker destination on that server
*/
const COOLIFY_URL = process.env.COOLIFY_URL ?? 'https://coolify.vibnai.com';
const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? '';
const COOLIFY_DEFAULT_SERVER_UUID =
process.env.COOLIFY_DEFAULT_SERVER_UUID ?? 'jws4g4cgssss4cw48s488woc';
const COOLIFY_DEFAULT_DESTINATION_UUID =
process.env.COOLIFY_DEFAULT_DESTINATION_UUID ?? 'zkogkggkw0wg40gccks80oo0';
export interface CoolifyProject {
uuid: string;
name: string;
description?: string;
environments?: Array<{ id: number; uuid: string; name: string; project_id: number }>;
}
/** All database flavors Coolify v4 can provision. */
export type CoolifyDatabaseType =
| 'postgresql'
| 'mysql'
| 'mariadb'
| 'mongodb'
| 'redis'
| 'keydb'
| 'dragonfly'
| 'clickhouse';
export interface CoolifyDatabase {
uuid: string;
name: string;
type?: string;
status: string;
internal_db_url?: string;
external_db_url?: string;
is_public?: boolean;
public_port?: number;
project_uuid?: string;
environment_id?: number;
environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };
}
export interface CoolifyApplication {
uuid: string;
name: string;
status: string;
fqdn?: string;
domains?: string;
git_repository?: string;
git_branch?: string;
project_uuid?: string;
environment_id?: number;
environment_name?: string;
environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };
}
export interface CoolifyEnvVar {
uuid?: string;
key: string;
value: string;
is_preview?: boolean;
is_build_time?: boolean;
is_literal?: boolean;
is_multiline?: boolean;
is_shown_once?: boolean;
}
export interface CoolifyPrivateKey {
uuid: string;
name: string;
description?: string;
fingerprint?: string;
}
export interface CoolifyServer {
uuid: string;
name: string;
ip: string;
is_reachable?: boolean;
settings?: { wildcard_domain?: string | null };
}
export interface CoolifyDeployment {
uuid: string;
status: string;
created_at?: string;
finished_at?: string;
commit?: string;
}
async function coolifyFetch(path: string, options: RequestInit = {}) {
const url = `${COOLIFY_URL}/api/v1${path}`;
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${COOLIFY_API_TOKEN}`,
...(options.headers ?? {}),
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Coolify API error ${res.status} on ${path}: ${text}`);
}
if (res.status === 204) return null;
return res.json();
}
// ──────────────────────────────────────────────────
// Projects
// ──────────────────────────────────────────────────
export async function listProjects(): Promise<CoolifyProject[]> {
return coolifyFetch('/projects');
}
export async function createProject(name: string, description?: string): Promise<CoolifyProject> {
return coolifyFetch('/projects', {
method: 'POST',
body: JSON.stringify({ name, description }),
});
}
export async function getProject(uuid: string): Promise<CoolifyProject> {
return coolifyFetch(`/projects/${uuid}`);
}
export async function deleteProject(uuid: string): Promise<void> {
await coolifyFetch(`/projects/${uuid}`, { method: 'DELETE' });
}
// ──────────────────────────────────────────────────
// Servers
// ──────────────────────────────────────────────────
export async function listServers(): Promise<CoolifyServer[]> {
return coolifyFetch('/servers');
}
// ──────────────────────────────────────────────────
// Private keys (SSH deploy keys)
// ──────────────────────────────────────────────────
export async function listPrivateKeys(): Promise<CoolifyPrivateKey[]> {
return coolifyFetch('/security/keys');
}
export async function createPrivateKey(opts: {
name: string;
privateKeyPem: string;
description?: string;
}): Promise<CoolifyPrivateKey> {
return coolifyFetch('/security/keys', {
method: 'POST',
body: JSON.stringify({
name: opts.name,
private_key: opts.privateKeyPem,
description: opts.description ?? '',
}),
});
}
export async function deletePrivateKey(uuid: string): Promise<void> {
await coolifyFetch(`/security/keys/${uuid}`, { method: 'DELETE' });
}
// ──────────────────────────────────────────────────
// Databases (all 8 types)
// ──────────────────────────────────────────────────
/** Shared context used by every database-create call. */
export interface CoolifyDbCreateContext {
projectUuid: string;
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
instantDeploy?: boolean;
}
export interface CreateDatabaseOpts extends CoolifyDbCreateContext {
type: CoolifyDatabaseType;
name: string;
description?: string;
image?: string;
isPublic?: boolean;
publicPort?: number;
/** Type-specific credentials / config */
credentials?: Record<string, unknown>;
/** Resource limits */
limits?: { memory?: string; cpus?: string };
}
/**
* Create a database of the requested type. Dispatches to the right
* POST /databases/{type} endpoint and packs the right credential
* fields for each flavor.
*/
export async function createDatabase(opts: CreateDatabaseOpts): Promise<{ uuid: string }> {
const {
type, name,
projectUuid,
serverUuid = COOLIFY_DEFAULT_SERVER_UUID,
environmentName = 'production',
destinationUuid = COOLIFY_DEFAULT_DESTINATION_UUID,
instantDeploy = true,
description,
image,
isPublic,
publicPort,
credentials = {},
limits = {},
} = opts;
const common: Record<string, unknown> = {
project_uuid: projectUuid,
server_uuid: serverUuid,
environment_name: environmentName,
destination_uuid: destinationUuid,
name,
description,
image,
is_public: isPublic,
public_port: publicPort,
instant_deploy: instantDeploy,
limits_memory: limits.memory,
limits_cpus: limits.cpus,
};
return coolifyFetch(`/databases/${type}`, {
method: 'POST',
body: JSON.stringify(stripUndefined({ ...common, ...credentials })),
});
}
export async function listDatabases(): Promise<CoolifyDatabase[]> {
return coolifyFetch('/databases');
}
export async function getDatabase(uuid: string): Promise<CoolifyDatabase> {
return coolifyFetch(`/databases/${uuid}`);
}
export async function updateDatabase(uuid: string, patch: Record<string, unknown>): Promise<{ uuid: string }> {
return coolifyFetch(`/databases/${uuid}`, {
method: 'PATCH',
body: JSON.stringify(stripUndefined(patch)),
});
}
export async function deleteDatabase(
uuid: string,
opts: {
deleteConfigurations?: boolean;
deleteVolumes?: boolean;
dockerCleanup?: boolean;
deleteConnectedNetworks?: boolean;
} = {}
): Promise<void> {
const q = new URLSearchParams();
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
const qs = q.toString();
await coolifyFetch(`/databases/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
}
export async function startDatabase(uuid: string): Promise<void> {
await coolifyFetch(`/databases/${uuid}/start`, { method: 'POST' });
}
export async function stopDatabase(uuid: string): Promise<void> {
await coolifyFetch(`/databases/${uuid}/stop`, { method: 'POST' });
}
export async function restartDatabase(uuid: string): Promise<void> {
await coolifyFetch(`/databases/${uuid}/restart`, { method: 'POST' });
}
// ──────────────────────────────────────────────────
// Applications
// ──────────────────────────────────────────────────
export type CoolifyBuildPack = 'nixpacks' | 'static' | 'dockerfile' | 'dockercompose';
export interface CreatePrivateDeployKeyAppOpts {
projectUuid: string;
privateKeyUuid: string;
gitRepository: string; // SSH URL: git@git.vibnai.com:vibn-mark/repo.git
gitBranch?: string;
portsExposes: string; // "3000"
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
buildPack?: CoolifyBuildPack;
name?: string;
description?: string;
domains?: string; // comma-separated FQDNs
isAutoDeployEnabled?: boolean;
isForceHttpsEnabled?: boolean;
instantDeploy?: boolean;
installCommand?: string;
buildCommand?: string;
startCommand?: string;
baseDirectory?: string;
dockerfileLocation?: string;
manualWebhookSecretGitea?: string;
}
export async function createPrivateDeployKeyApp(
opts: CreatePrivateDeployKeyAppOpts
): Promise<{ uuid: string }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
environment_name: opts.environmentName ?? 'production',
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
private_key_uuid: opts.privateKeyUuid,
git_repository: opts.gitRepository,
git_branch: opts.gitBranch ?? 'main',
build_pack: opts.buildPack ?? 'nixpacks',
ports_exposes: opts.portsExposes,
name: opts.name,
description: opts.description,
domains: opts.domains,
is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true,
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
instant_deploy: opts.instantDeploy ?? true,
install_command: opts.installCommand,
build_command: opts.buildCommand,
start_command: opts.startCommand,
base_directory: opts.baseDirectory,
dockerfile_location: opts.dockerfileLocation,
manual_webhook_secret_gitea: opts.manualWebhookSecretGitea,
});
return coolifyFetch('/applications/private-deploy-key', {
method: 'POST',
body: JSON.stringify(body),
});
}
export interface CreatePublicAppOpts {
projectUuid: string;
gitRepository: string; // https URL
gitBranch?: string;
portsExposes: string;
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
buildPack?: CoolifyBuildPack;
name?: string;
description?: string;
domains?: string;
isAutoDeployEnabled?: boolean;
isForceHttpsEnabled?: boolean;
instantDeploy?: boolean;
}
export async function createPublicApp(opts: CreatePublicAppOpts): Promise<{ uuid: string }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
environment_name: opts.environmentName ?? 'production',
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
git_repository: opts.gitRepository,
git_branch: opts.gitBranch ?? 'main',
build_pack: opts.buildPack ?? 'nixpacks',
ports_exposes: opts.portsExposes,
name: opts.name,
description: opts.description,
domains: opts.domains,
is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true,
is_force_https_enabled: opts.isForceHttpsEnabled ?? true,
instant_deploy: opts.instantDeploy ?? true,
});
return coolifyFetch('/applications/public', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function updateApplication(
uuid: string,
patch: Record<string, unknown>
): Promise<{ uuid: string }> {
return coolifyFetch(`/applications/${uuid}`, {
method: 'PATCH',
body: JSON.stringify(stripUndefined(patch)),
});
}
export async function setApplicationDomains(
uuid: string,
domains: string[],
opts: { forceOverride?: boolean } = {}
): Promise<{ uuid: string }> {
// Coolify validates each entry as a URL, so bare hostnames need a scheme.
const normalized = domains.map(d => {
const trimmed = d.trim();
if (/^https?:\/\//i.test(trimmed)) return trimmed;
return `https://${trimmed}`;
});
return updateApplication(uuid, {
domains: normalized.join(','),
force_domain_override: opts.forceOverride ?? true,
is_force_https_enabled: true,
});
}
export async function deleteApplication(
uuid: string,
opts: {
deleteConfigurations?: boolean;
deleteVolumes?: boolean;
dockerCleanup?: boolean;
deleteConnectedNetworks?: boolean;
} = {}
): Promise<void> {
const q = new URLSearchParams();
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
const qs = q.toString();
await coolifyFetch(`/applications/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
}
export async function listApplications(): Promise<CoolifyApplication[]> {
return coolifyFetch('/applications');
}
export async function deployApplication(uuid: string, opts: { force?: boolean } = {}): Promise<{ deployment_uuid: string }> {
// Coolify v4 exposes deploy as POST /deploy?uuid=...&force=...
// The older /applications/{uuid}/deploy path is a 404 on current
// Coolify releases.
const q = new URLSearchParams({ uuid });
if (opts.force) q.set('force', 'true');
const res = await coolifyFetch(`/deploy?${q.toString()}`, { method: 'POST' });
// Response shape: { deployments: [{ deployment_uuid, ... }] } or direct object.
const first = Array.isArray(res?.deployments) ? res.deployments[0] : res;
return {
deployment_uuid: first?.deployment_uuid ?? first?.uuid ?? '',
};
}
export async function getApplication(uuid: string): Promise<CoolifyApplication> {
return coolifyFetch(`/applications/${uuid}`);
}
export async function startApplication(uuid: string): Promise<void> {
await coolifyFetch(`/applications/${uuid}/start`, { method: 'POST' });
}
export async function stopApplication(uuid: string): Promise<void> {
await coolifyFetch(`/applications/${uuid}/stop`, { method: 'POST' });
}
export async function restartApplication(uuid: string): Promise<void> {
await coolifyFetch(`/applications/${uuid}/restart`, { method: 'POST' });
}
export async function getDeploymentLogs(deploymentUuid: string): Promise<{ logs: string }> {
return coolifyFetch(`/deployments/${deploymentUuid}/logs`);
}
export async function listApplicationDeployments(uuid: string): Promise<CoolifyDeployment[]> {
// Coolify v4 nests this under /deployments/applications/{uuid}
// and returns { count, deployments }. Normalize to a flat array.
const raw = await coolifyFetch(`/deployments/applications/${uuid}?take=50`);
if (Array.isArray(raw)) return raw as CoolifyDeployment[];
return (raw?.deployments ?? []) as CoolifyDeployment[];
}
// ──────────────────────────────────────────────────
// Legacy monorepo helper (still used by older flows)
// ──────────────────────────────────────────────────
export async function createMonorepoAppService(opts: {
projectUuid: string;
appName: string;
gitRepo: string;
gitBranch?: string;
domain: string;
serverUuid?: string;
environmentName?: string;
}): Promise<CoolifyApplication> {
const {
projectUuid, appName, gitRepo,
gitBranch = 'main',
domain,
serverUuid = COOLIFY_DEFAULT_SERVER_UUID,
environmentName = 'production',
} = opts;
return coolifyFetch(`/applications`, {
method: 'POST',
body: JSON.stringify({
project_uuid: projectUuid,
name: appName,
git_repository: gitRepo,
git_branch: gitBranch,
server_uuid: serverUuid,
environment_name: environmentName,
build_pack: 'nixpacks',
build_command: `pnpm install && turbo run build --filter=${appName}`,
start_command: `turbo run start --filter=${appName}`,
ports_exposes: '3000',
fqdn: `https://${domain}`,
}),
});
}
// ──────────────────────────────────────────────────
// Environment variables
// ──────────────────────────────────────────────────
export async function listApplicationEnvs(uuid: string): Promise<CoolifyEnvVar[]> {
return coolifyFetch(`/applications/${uuid}/envs`);
}
export async function upsertApplicationEnv(
uuid: string,
env: CoolifyEnvVar & { is_preview?: boolean }
): Promise<CoolifyEnvVar> {
try {
return await coolifyFetch(`/applications/${uuid}/envs`, {
method: 'PATCH',
body: JSON.stringify(env),
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes('404') || msg.includes('405')) {
return coolifyFetch(`/applications/${uuid}/envs`, {
method: 'POST',
body: JSON.stringify(env),
});
}
throw err;
}
}
export async function deleteApplicationEnv(uuid: string, key: string): Promise<void> {
await coolifyFetch(`/applications/${uuid}/envs/${encodeURIComponent(key)}`, {
method: 'DELETE',
});
}
// ──────────────────────────────────────────────────
// Services (raw Coolify service resources)
// ──────────────────────────────────────────────────
export interface CoolifyService {
uuid: string;
name: string;
status?: string;
project_uuid?: string;
environment_id?: number;
environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };
}
export async function listServices(): Promise<CoolifyService[]> {
return coolifyFetch('/services');
}
export async function getService(uuid: string): Promise<CoolifyService> {
return coolifyFetch(`/services/${uuid}`);
}
export async function createService(opts: {
projectUuid: string;
type: string; // e.g. "pocketbase", "authentik", "zitadel"
name: string;
description?: string;
serverUuid?: string;
environmentName?: string;
destinationUuid?: string;
instantDeploy?: boolean;
}): Promise<{ uuid: string }> {
const body = stripUndefined({
project_uuid: opts.projectUuid,
server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID,
environment_name: opts.environmentName ?? 'production',
destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID,
type: opts.type,
name: opts.name,
description: opts.description,
instant_deploy: opts.instantDeploy ?? true,
});
return coolifyFetch('/services', { method: 'POST', body: JSON.stringify(body) });
}
export async function deleteService(
uuid: string,
opts: {
deleteConfigurations?: boolean;
deleteVolumes?: boolean;
dockerCleanup?: boolean;
deleteConnectedNetworks?: boolean;
} = {}
): Promise<void> {
const q = new URLSearchParams();
if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations));
if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes));
if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup));
if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks));
const qs = q.toString();
await coolifyFetch(`/services/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' });
}
// ──────────────────────────────────────────────────
// Tenant helpers — every endpoint that returns an app/db/service runs
// through one of these so cross-project access is impossible.
//
// Coolify v4's /applications, /databases and /services list endpoints do NOT
// include `project_uuid` on the returned resources. The authoritative link is
// `environment_id`, which we match against the project's environments (fetched
// via /projects/{uuid}).
//
// For efficient listing per project, we use `/projects/{uuid}/{envName}` which
// returns the resources scoped to that project+environment in one call.
// ──────────────────────────────────────────────────
export class TenantError extends Error {
status = 403 as const;
}
function envIdOf(
resource: CoolifyApplication | CoolifyDatabase | CoolifyService
): number | null {
return (
(typeof resource.environment_id === 'number' ? resource.environment_id : null) ??
(resource.environment && typeof resource.environment.id === 'number'
? resource.environment.id
: null)
);
}
function explicitProjectUuidOf(
resource: CoolifyApplication | CoolifyDatabase | CoolifyService
): string | null {
return (
resource.project_uuid ??
resource.environment?.project_uuid ??
resource.environment?.project?.uuid ??
null
);
}
/** Fetch the set of environment IDs that belong to a project. */
async function projectEnvIds(projectUuid: string): Promise<Set<number>> {
const project = await getProject(projectUuid);
const ids = new Set<number>();
for (const env of project.environments ?? []) {
if (typeof env.id === 'number') ids.add(env.id);
}
return ids;
}
async function ensureResourceInProject(
resource: CoolifyApplication | CoolifyDatabase | CoolifyService,
resourceKind: string,
expectedProjectUuid: string
): Promise<void> {
const explicit = explicitProjectUuidOf(resource);
if (explicit && explicit === expectedProjectUuid) return;
if (explicit && explicit !== expectedProjectUuid) {
throw new TenantError(
`${resourceKind} ${resource.uuid} does not belong to project ${expectedProjectUuid}`
);
}
const envId = envIdOf(resource);
if (envId == null) {
throw new TenantError(
`${resourceKind} ${resource.uuid} has no environment_id; cannot verify project ${expectedProjectUuid}`
);
}
const envIds = await projectEnvIds(expectedProjectUuid);
if (!envIds.has(envId)) {
throw new TenantError(
`${resourceKind} ${resource.uuid} does not belong to project ${expectedProjectUuid}`
);
}
}
export async function getApplicationInProject(
appUuid: string,
expectedProjectUuid: string
): Promise<CoolifyApplication> {
const app = await getApplication(appUuid);
await ensureResourceInProject(app, 'Application', expectedProjectUuid);
return app;
}
export async function getDatabaseInProject(
dbUuid: string,
expectedProjectUuid: string
): Promise<CoolifyDatabase> {
const db = await getDatabase(dbUuid);
await ensureResourceInProject(db, 'Database', expectedProjectUuid);
return db;
}
export async function getServiceInProject(
serviceUuid: string,
expectedProjectUuid: string
): Promise<CoolifyService> {
const svc = await getService(serviceUuid);
await ensureResourceInProject(svc, 'Service', expectedProjectUuid);
return svc;
}
/**
* Response shape of GET /projects/{uuid}/{envName}.
* Coolify splits databases by flavor across sibling arrays.
*/
interface CoolifyProjectEnvResources {
id: number;
uuid: string;
name: string;
applications?: CoolifyApplication[];
services?: CoolifyService[];
postgresqls?: CoolifyDatabase[];
mysqls?: CoolifyDatabase[];
mariadbs?: CoolifyDatabase[];
mongodbs?: CoolifyDatabase[];
redis?: CoolifyDatabase[];
keydbs?: CoolifyDatabase[];
dragonflies?: CoolifyDatabase[];
clickhouses?: CoolifyDatabase[];
}
const DB_ARRAY_KEYS: Array<keyof CoolifyProjectEnvResources> = [
'postgresqls',
'mysqls',
'mariadbs',
'mongodbs',
'redis',
'keydbs',
'dragonflies',
'clickhouses',
];
async function getProjectEnvResources(
projectUuid: string,
envName: string
): Promise<CoolifyProjectEnvResources> {
return coolifyFetch(`/projects/${projectUuid}/${encodeURIComponent(envName)}`);
}
async function forEachEnv<T>(
projectUuid: string,
collect: (envResources: CoolifyProjectEnvResources) => T[]
): Promise<T[]> {
const project = await getProject(projectUuid);
const out: T[] = [];
for (const env of project.environments ?? []) {
const envResources = await getProjectEnvResources(projectUuid, env.name);
out.push(...collect(envResources));
}
return out;
}
export async function listApplicationsInProject(
projectUuid: string
): Promise<CoolifyApplication[]> {
return forEachEnv(projectUuid, r => r.applications ?? []);
}
export async function listDatabasesInProject(
projectUuid: string
): Promise<CoolifyDatabase[]> {
return forEachEnv(projectUuid, r => {
const out: CoolifyDatabase[] = [];
for (const k of DB_ARRAY_KEYS) {
const arr = r[k];
if (Array.isArray(arr)) out.push(...(arr as CoolifyDatabase[]));
}
return out;
});
}
export async function listServicesInProject(
projectUuid: string
): Promise<CoolifyService[]> {
return forEachEnv(projectUuid, r => r.services ?? []);
}
/** @deprecated Use getApplicationInProject / ensureResourceInProject instead. */
export function projectUuidOf(
resource: CoolifyApplication | CoolifyDatabase | CoolifyService
): string | null {
return explicitProjectUuidOf(resource);
}
// ──────────────────────────────────────────────────
// util
// ──────────────────────────────────────────────────
function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
const out: Partial<T> = {};
for (const [k, v] of Object.entries(obj)) {
if (v !== undefined) (out as Record<string, unknown>)[k] = v;
}
return out;
}
export { COOLIFY_DEFAULT_SERVER_UUID, COOLIFY_DEFAULT_DESTINATION_UUID };