EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
digitalOceanSyncWorker.js
Go to the documentation of this file.
1/**
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
5 */
6
7const pool = require('./db');
8const DigitalOceanService = require('./services/digitalOceanService');
9
10const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
11
12/**
13 * Sync DO resources for a single tenant
14 * @param tenantId
15 */
16async function syncTenantResources(tenantId) {
17 try {
18 console.log(`[DO Sync] Starting sync for tenant ${tenantId}`);
19
20 const doService = new DigitalOceanService(tenantId);
21 const resources = await doService.getAllResources();
22
23 // Sync Apps
24 const activeAppIds = [];
25 if (resources.apps && resources.apps.length > 0) {
26 for (const app of resources.apps) {
27 activeAppIds.push(app.id);
28 await pool.query(
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,
40 updated_at = NOW()`,
41 [
42 tenantId,
43 app.id,
44 app.spec?.name || 'Unnamed App',
45 app.spec?.services?.[0]?.build_command ? 'nodejs' : 'static',
46 app.phase || 'unknown',
47 app.region?.slug || null,
48 app.live_url || null,
49 JSON.stringify(app)
50 ]
51 );
52 }
53 console.log(`[DO Sync] Synced ${resources.apps.length} apps for tenant ${tenantId}`);
54 }
55
56 // Mark apps as deleted if they no longer exist in DO
57 if (activeAppIds.length > 0) {
58 const deleteResult = await pool.query(
59 `UPDATE hosting_apps
60 SET status = 'deleted', updated_at = NOW()
61 WHERE tenant_id = $1
62 AND do_app_id NOT IN (${activeAppIds.map((_, i) => `$${i + 2}`).join(',')})
63 AND status != 'deleted'`,
64 [tenantId, ...activeAppIds]
65 );
66 if (deleteResult.rowCount > 0) {
67 console.log(`[DO Sync] Marked ${deleteResult.rowCount} apps as deleted for tenant ${tenantId}`);
68 }
69 } else {
70 // If no apps in DO, mark all as deleted
71 const deleteResult = await pool.query(
72 `UPDATE hosting_apps
73 SET status = 'deleted', updated_at = NOW()
74 WHERE tenant_id = $1 AND status != 'deleted'`,
75 [tenantId]
76 );
77 if (deleteResult.rowCount > 0) {
78 console.log(`[DO Sync] Marked ${deleteResult.rowCount} apps as deleted (no active apps in DO)`);
79 }
80 }
81
82 // Sync Droplets
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;
89
90 await pool.query(
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,
109 updated_at = NOW()`,
110 [
111 tenantId,
112 droplet.id,
113 droplet.name,
114 droplet.status,
115 publicIp || null,
116 privateIp || null,
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)
125 ]
126 );
127 }
128 console.log(`[DO Sync] Synced ${resources.droplets.length} droplets for tenant ${tenantId}`);
129 }
130
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()
136 WHERE tenant_id = $1
137 AND do_droplet_id NOT IN (${activeDropletIds.map((_, i) => `$${i + 2}`).join(',')})
138 AND status != 'deleted'`,
139 [tenantId, ...activeDropletIds]
140 );
141 if (deleteResult.rowCount > 0) {
142 console.log(`[DO Sync] Marked ${deleteResult.rowCount} droplets as deleted for tenant ${tenantId}`);
143 }
144 } else {
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'`,
149 [tenantId]
150 );
151 if (deleteResult.rowCount > 0) {
152 console.log(`[DO Sync] Marked ${deleteResult.rowCount} droplets as deleted (no active droplets in DO)`);
153 }
154 }
155
156 // Sync Databases
157 const activeDatabaseIds = [];
158 if (resources.databases && resources.databases.length > 0) {
159 for (const db of resources.databases) {
160 activeDatabaseIds.push(db.id);
161 await pool.query(
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,
179 updated_at = NOW()`,
180 [
181 tenantId,
182 db.id,
183 db.name,
184 db.engine,
185 db.version || null,
186 db.status,
187 db.region || null,
188 db.size || null,
189 db.num_nodes || 1,
190 db.connection?.host || null,
191 db.connection?.port || null,
192 JSON.stringify(db.tags || []),
193 JSON.stringify(db)
194 ]
195 );
196 }
197 console.log(`[DO Sync] Synced ${resources.databases.length} databases for tenant ${tenantId}`);
198 }
199
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()
205 WHERE tenant_id = $1
206 AND do_database_id NOT IN (${activeDatabaseIds.map((_, i) => `$${i + 2}`).join(',')})
207 AND status != 'deleted'`,
208 [tenantId, ...activeDatabaseIds]
209 );
210 if (deleteResult.rowCount > 0) {
211 console.log(`[DO Sync] Marked ${deleteResult.rowCount} databases as deleted for tenant ${tenantId}`);
212 }
213 } else {
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'`,
218 [tenantId]
219 );
220 if (deleteResult.rowCount > 0) {
221 console.log(`[DO Sync] Marked ${deleteResult.rowCount} databases as deleted (no active databases in DO)`);
222 }
223 }
224
225 // Update last sync time
226 await pool.query(
227 `UPDATE digitalocean_config SET last_sync_at = NOW() WHERE tenant_id = $1`,
228 [tenantId]
229 );
230
231 console.log(`[DO Sync] Completed sync for tenant ${tenantId}`);
232 } catch (error) {
233 console.error(`[DO Sync] Error syncing tenant ${tenantId}:`, error.message);
234 }
235}
236
237/**
238 * Sync all active tenants
239 */
240async function syncAllTenants() {
241 try {
242 const result = await pool.query(
243 `SELECT tenant_id FROM digitalocean_config WHERE is_active = true`
244 );
245
246 if (result.rows.length === 0) {
247 console.log('[DO Sync] No active DO configurations found');
248 return;
249 }
250
251 console.log(`[DO Sync] Starting sync for ${result.rows.length} tenant(s)`);
252
253 for (const row of result.rows) {
254 await syncTenantResources(row.tenant_id);
255 }
256
257 console.log('[DO Sync] All tenants synced successfully');
258 } catch (error) {
259 console.error('[DO Sync] Error in sync cycle:', error);
260 }
261}
262
263/**
264 * Start the sync worker
265 */
266function startSyncWorker() {
267 console.log(`[DO Sync] Worker started - syncing every ${SYNC_INTERVAL / 1000 / 60} minutes`);
268
269 // Run immediately on start
270 syncAllTenants();
271
272 // Then run every 5 minutes
273 setInterval(syncAllTenants, SYNC_INTERVAL);
274}
275
276// Start the worker if this file is run directly
277if (require.main === module) {
278 startSyncWorker();
279}
280
281module.exports = { startSyncWorker, syncAllTenants, syncTenantResources };