2 * Digital Ocean Sync Worker
3 * Syncs DO resources (apps, droplets, databases) to local database every 5 minutes
4 * This provides faster page loads and reduces API calls to DigitalOcean
7const pool = require('./db');
8const DigitalOceanService = require('./services/digitalOceanService');
10const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
13 * Sync DO resources for a single tenant
16async function syncTenantResources(tenantId) {
18 console.log(`[DO Sync] Starting sync for tenant ${tenantId}`);
20 const doService = new DigitalOceanService(tenantId);
21 const resources = await doService.getAllResources();
24 const activeAppIds = [];
25 if (resources.apps && resources.apps.length > 0) {
26 for (const app of resources.apps) {
27 activeAppIds.push(app.id);
29 `INSERT INTO hosting_apps (
30 tenant_id, do_app_id, app_name, app_type, status, region,
31 live_url, metadata, updated_at
32 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
33 ON CONFLICT (do_app_id) DO UPDATE SET
34 app_name = EXCLUDED.app_name,
35 app_type = EXCLUDED.app_type,
36 status = EXCLUDED.status,
37 region = EXCLUDED.region,
38 live_url = EXCLUDED.live_url,
39 metadata = EXCLUDED.metadata,
44 app.spec?.name || 'Unnamed App',
45 app.spec?.services?.[0]?.build_command ? 'nodejs' : 'static',
46 app.phase || 'unknown',
47 app.region?.slug || null,
53 console.log(`[DO Sync] Synced ${resources.apps.length} apps for tenant ${tenantId}`);
56 // Mark apps as deleted if they no longer exist in DO
57 if (activeAppIds.length > 0) {
58 const deleteResult = await pool.query(
60 SET status = 'deleted', updated_at = NOW()
62 AND do_app_id NOT IN (${activeAppIds.map((_, i) => `$${i + 2}`).join(',')})
63 AND status != 'deleted'`,
64 [tenantId, ...activeAppIds]
66 if (deleteResult.rowCount > 0) {
67 console.log(`[DO Sync] Marked ${deleteResult.rowCount} apps as deleted for tenant ${tenantId}`);
70 // If no apps in DO, mark all as deleted
71 const deleteResult = await pool.query(
73 SET status = 'deleted', updated_at = NOW()
74 WHERE tenant_id = $1 AND status != 'deleted'`,
77 if (deleteResult.rowCount > 0) {
78 console.log(`[DO Sync] Marked ${deleteResult.rowCount} apps as deleted (no active apps in DO)`);
83 const activeDropletIds = [];
84 if (resources.droplets && resources.droplets.length > 0) {
85 for (const droplet of resources.droplets) {
86 activeDropletIds.push(droplet.id);
87 const publicIp = droplet.networks?.v4?.find(n => n.type === 'public')?.ip_address;
88 const privateIp = droplet.networks?.v4?.find(n => n.type === 'private')?.ip_address;
91 `INSERT INTO hosting_droplets (
92 tenant_id, do_droplet_id, droplet_name, status, ip_address,
93 private_ip_address, region, size, image, vcpus, memory, disk,
94 tags, metadata, updated_at
95 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW())
96 ON CONFLICT (do_droplet_id) DO UPDATE SET
97 droplet_name = EXCLUDED.droplet_name,
98 status = EXCLUDED.status,
99 ip_address = EXCLUDED.ip_address,
100 private_ip_address = EXCLUDED.private_ip_address,
101 region = EXCLUDED.region,
102 size = EXCLUDED.size,
103 image = EXCLUDED.image,
104 vcpus = EXCLUDED.vcpus,
105 memory = EXCLUDED.memory,
106 disk = EXCLUDED.disk,
107 tags = EXCLUDED.tags,
108 metadata = EXCLUDED.metadata,
117 droplet.region?.slug || null,
118 droplet.size_slug || null,
119 droplet.image?.slug || droplet.image?.name || null,
120 droplet.vcpus || null,
121 droplet.memory || null,
122 droplet.disk || null,
123 JSON.stringify(droplet.tags || []),
124 JSON.stringify(droplet)
128 console.log(`[DO Sync] Synced ${resources.droplets.length} droplets for tenant ${tenantId}`);
131 // Mark droplets as deleted if they no longer exist in DO
132 if (activeDropletIds.length > 0) {
133 const deleteResult = await pool.query(
134 `UPDATE hosting_droplets
135 SET status = 'deleted', updated_at = NOW()
137 AND do_droplet_id NOT IN (${activeDropletIds.map((_, i) => `$${i + 2}`).join(',')})
138 AND status != 'deleted'`,
139 [tenantId, ...activeDropletIds]
141 if (deleteResult.rowCount > 0) {
142 console.log(`[DO Sync] Marked ${deleteResult.rowCount} droplets as deleted for tenant ${tenantId}`);
145 const deleteResult = await pool.query(
146 `UPDATE hosting_droplets
147 SET status = 'deleted', updated_at = NOW()
148 WHERE tenant_id = $1 AND status != 'deleted'`,
151 if (deleteResult.rowCount > 0) {
152 console.log(`[DO Sync] Marked ${deleteResult.rowCount} droplets as deleted (no active droplets in DO)`);
157 const activeDatabaseIds = [];
158 if (resources.databases && resources.databases.length > 0) {
159 for (const db of resources.databases) {
160 activeDatabaseIds.push(db.id);
162 `INSERT INTO hosting_databases (
163 tenant_id, do_database_id, database_name, engine, version, status,
164 region, size, num_nodes, connection_host, connection_port,
165 tags, metadata, updated_at
166 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW())
167 ON CONFLICT (do_database_id) DO UPDATE SET
168 database_name = EXCLUDED.database_name,
169 engine = EXCLUDED.engine,
170 version = EXCLUDED.version,
171 status = EXCLUDED.status,
172 region = EXCLUDED.region,
173 size = EXCLUDED.size,
174 num_nodes = EXCLUDED.num_nodes,
175 connection_host = EXCLUDED.connection_host,
176 connection_port = EXCLUDED.connection_port,
177 tags = EXCLUDED.tags,
178 metadata = EXCLUDED.metadata,
190 db.connection?.host || null,
191 db.connection?.port || null,
192 JSON.stringify(db.tags || []),
197 console.log(`[DO Sync] Synced ${resources.databases.length} databases for tenant ${tenantId}`);
200 // Mark databases as deleted if they no longer exist in DO
201 if (activeDatabaseIds.length > 0) {
202 const deleteResult = await pool.query(
203 `UPDATE hosting_databases
204 SET status = 'deleted', updated_at = NOW()
206 AND do_database_id NOT IN (${activeDatabaseIds.map((_, i) => `$${i + 2}`).join(',')})
207 AND status != 'deleted'`,
208 [tenantId, ...activeDatabaseIds]
210 if (deleteResult.rowCount > 0) {
211 console.log(`[DO Sync] Marked ${deleteResult.rowCount} databases as deleted for tenant ${tenantId}`);
214 const deleteResult = await pool.query(
215 `UPDATE hosting_databases
216 SET status = 'deleted', updated_at = NOW()
217 WHERE tenant_id = $1 AND status != 'deleted'`,
220 if (deleteResult.rowCount > 0) {
221 console.log(`[DO Sync] Marked ${deleteResult.rowCount} databases as deleted (no active databases in DO)`);
225 // Update last sync time
227 `UPDATE digitalocean_config SET last_sync_at = NOW() WHERE tenant_id = $1`,
231 console.log(`[DO Sync] Completed sync for tenant ${tenantId}`);
233 console.error(`[DO Sync] Error syncing tenant ${tenantId}:`, error.message);
238 * Sync all active tenants
240async function syncAllTenants() {
242 const result = await pool.query(
243 `SELECT tenant_id FROM digitalocean_config WHERE is_active = true`
246 if (result.rows.length === 0) {
247 console.log('[DO Sync] No active DO configurations found');
251 console.log(`[DO Sync] Starting sync for ${result.rows.length} tenant(s)`);
253 for (const row of result.rows) {
254 await syncTenantResources(row.tenant_id);
257 console.log('[DO Sync] All tenants synced successfully');
259 console.error('[DO Sync] Error in sync cycle:', error);
264 * Start the sync worker
266function startSyncWorker() {
267 console.log(`[DO Sync] Worker started - syncing every ${SYNC_INTERVAL / 1000 / 60} minutes`);
269 // Run immediately on start
272 // Then run every 5 minutes
273 setInterval(syncAllTenants, SYNC_INTERVAL);
276// Start the worker if this file is run directly
277if (require.main === module) {
281module.exports = { startSyncWorker, syncAllTenants, syncTenantResources };