Skip to main content

โœ… Validate Domains

๐Ÿ“– Overviewโ€‹

The Validate Domains job synchronizes custom domain status from Cloudflare to the DashClicks Lightning Domains database. It runs every 5 minutes, fetching all custom hostnames from Cloudflare, comparing their status with local database records, and updating changed domains. When a domain becomes inactive or has SSL issues, it automatically reviews and updates the status of all funnels using that domain.

Complete Flow:

  1. Cron Initialization: queue-manager/crons/domains/validate.js
  2. Service Processing: queue-manager/services/domains/validate.js
  3. Queue Definition: queue-manager/queues/domains/update_validity.js

Execution Pattern: Scheduled polling (every 5 minutes)

Queue Name: domains_update_validity

Environment Flag: QM_DOMAINS_VALIDATE=true (in index.js)

๐Ÿ”„ Complete Processing Flowโ€‹

sequenceDiagram
participant CRON as Cron Schedule<br/>(every 5 min)
participant SERVICE as Validate Service
participant CF_API as Cloudflare API
participant QUEUE as Bull Queue
participant PROCESSOR as Job Processor
participant LD_DB as Lightning<br/>Domains DB
participant FUNNEL_DB as Funnels DB

CRON->>SERVICE: validate()
SERVICE->>CF_API: GET /custom_hostnames<br/>page 1
CF_API-->>SERVICE: {result[], result_info}

SERVICE->>SERVICE: Calculate total pages:<br/>result_info.total_pages

SERVICE->>QUEUE: Add page 1 job:<br/>{page_num:1, page_data}

loop For pages 2..totalPages
SERVICE->>QUEUE: Add job:<br/>{page_num: N}
end

loop Each queued page
QUEUE->>PROCESSOR: Process page job

alt Page data not provided
PROCESSOR->>CF_API: GET /custom_hostnames?page=N
CF_API-->>PROCESSOR: {result[]}
end

PROCESSOR->>PROCESSOR: Extract CF domain IDs
PROCESSOR->>LD_DB: Find domains:<br/>cf_id IN domain_ids
LD_DB-->>PROCESSOR: foundDomains[]

loop Each found domain
PROCESSOR->>PROCESSOR: Compare:<br/>CF status vs DB status<br/>CF SSL status vs DB SSL

alt Status changed
PROCESSOR->>LD_DB: updateOne:<br/>Update domain record

alt Domain not active
PROCESSOR->>FUNNEL_DB: reviewAndSetStatusForDomain:<br/>Set funnels to test mode
end
end
end

PROCESSOR-->>QUEUE: done()
end

๐Ÿ“ Source Filesโ€‹

1. Cron Initializationโ€‹

File: queue-manager/crons/domains/validate.js

Purpose: Schedule domain validation every 5 minutes

Cron Pattern: */5 * * * * (every 5 minutes)

Initialization:

const validate = require('../../services/domains/validate');
const cron = require('node-cron');
const logger = require('../../utilities/logger');

let inProgress = false;
exports.start = async () => {
try {
cron.schedule('*/5 * * * *', async () => {
if (!inProgress) {
inProgress = true;
await validate();
inProgress = false;
}
});
} catch (err) {
logger.error({ initiator: 'QM/domains/validate', error: err });
}
};

In-Progress Lock: Prevents overlapping validations if Cloudflare API is slow.

2. Service Processing (THE CLOUDFLARE PAGINATION LOGIC)โ€‹

File: queue-manager/services/domains/validate.js

Purpose: Fetch Cloudflare custom hostnames and queue paginated processing

Key Functions:

  • Fetch first page from Cloudflare API
  • Calculate total pages from result_info
  • Queue page 1 with embedded data (optimization)
  • Queue remaining pages without data (lazy load)

Main Processing Function:

const { default: axios } = require('axios');
const Queue = require('../../queues/domains/update_validity');
const logger = require('../../utilities/logger');

module.exports = async () => {
try {
// Handle the promise returned by axios.get
await axios
.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
)
.then(async page1 => {
const totalPages = page1.data.result_info.total_pages || 1;

const queue = await Queue.start();

// Queue page 1 with embedded data (avoids redundant API call)
queue.add(
{ page_num: 1, page_data: page1.data.result },
{
attempts: 3,
},
);

// Queue remaining pages without data (lazy load in processor)
for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
queue.add(
{ page_num: pageNum },
{
attempts: 3,
},
);
}

logger.log({
initiator: 'QM/domains/validate',
message: 'Domain Validation service processed.',
});
});
} catch (err) {
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
}
};

Pagination Optimization:

  • Page 1: Data embedded in job (avoids redundant API call)
  • Pages 2+: Only page number provided (processor fetches lazily)
  • Reduces memory usage for large datasets

3. Queue Processor (THE SYNC & UPDATE LOGIC)โ€‹

File: queue-manager/queues/domains/update_validity.js

Purpose: Process each page of Cloudflare domains and sync to database

Key Functions:

  • Lazy load page data if not provided
  • Fetch matching domains from database
  • Compare Cloudflare status with database status
  • Update changed domains
  • Review funnel status for inactive domains

Main Processor:

const mongoose = require('mongoose');
const { socketEmit } = require('../../utilities');
const { verifyBalance } = require('../../utilities/onebalance');
const QueueWrapper = require('../../common/queue-wrapper');
const LightningDomain = require('../../models/lightning-domain');
const logger = require('../../utilities/logger');
const { reviewAndSetStatusForDomain } = require('../../utilities/funnelDomainStatusUtility');
const { default: axios } = require('axios');

const processCb = async (job, done) => {
try {
let { page_num, page_data } = job.data;

// Lazy load page data if not provided
if (!page_data) {
const response = await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames?page=${page_num}`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);
page_data = response.data?.result;
}

// Get related records from db
const { foundDomains, notFoundDomains } = await fetchLightningDomains(page_data);

if (foundDomains.length) {
await updateLightningDomains(foundDomains, page_data);
}

// Commented out: Delete unwanted domains
// if (notFoundDomains.length) {
// await deleteUnwantedDomains(notFoundDomains);
// }

done();
} catch (err) {
logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
done(err);
}
};

let queue;

exports.start = async () => {
try {
if (!queue) queue = QueueWrapper(`domains_update_validity`, 'global', { processCb });
return Promise.resolve(queue)
.then(result => {
logger.log({
initiator: 'QM/domains/update_validity',
message: 'Queue initialized:' + result,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
error: 'Error initializing queue' + err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
throw err;
});
} catch (err) {
logger.error({
initiator: 'QM/domains/update_validity',
error: 'Error while initializing domain validity queue' + err.message + err.stack,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});
throw err; // Re-throw to ensure the error is properly propagated
}
};

const fetchLightningDomains = async domains => {
const domain_ids = domains.map(d => d.id);
const foundDomains = await LightningDomain.find({ cf_id: { $in: domain_ids } });
const notFoundDomains = domains.reduce((acc, curr) => {
if (!foundDomains.find(d => d?.cf_id == curr.id)) acc.push(curr);
return acc;
}, []);
return { foundDomains, notFoundDomains };
};

const updateLightningDomains = async (foundDomains, pageData) => {
const updatePromises = [];
for (let fd of foundDomains) {
const domain = pageData.find(({ id }) => id == fd.cf_id);
if (domain && (domain.status != fd.status || domain.ssl.status != fd.ssl.status)) {
// Update domain record
updatePromises.push(
LightningDomain.updateOne({ _id: fd._id }, { ...domain })
.then(result => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Updated domain ${fd._id}:${result}`,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Error updating domain ${fd._id}:${err}`,
});
throw err;
}),
);

// If domain not active, review and update funnel status
if (domain.status !== 'active') {
await reviewAndSetStatusForDomain({ domain_id: fd._id })
.then(result => {
logger.log({
initiator: 'QM/domains/update_validity',
message: `Status reviewed for domain ${fd._id}: ` + result,
});
return result;
})
.catch(err => {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Error reviewing status for domain ${fd._id}:${err}`,
});
throw err;
});
}
}
}

await Promise.allSettled(updatePromises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
logger.log({
initiator: 'QM/domains/update_validity',
message: `Update fulfilled:': ` + result.value,
});
} else {
logger.error({
initiator: 'QM/domains/update_validity',
message: `Update rejected:${result?.reason}`,
});
}
});
});
};

๐Ÿ—„๏ธ Collections Usedโ€‹

lightning_domainsโ€‹

  • Operations: Find (query by cf_id), Update (sync status from Cloudflare)
  • Model: shared/models/lightning-domain.js
  • Usage Context: Store and sync custom domain status

Query Criteria (Fetch Matching Domains):

{
cf_id: {
$in: domain_ids;
} // Cloudflare custom hostname IDs
}

Update Operation:

await LightningDomain.updateOne(
{ _id: fd._id },
{ ...domain }, // Spread Cloudflare domain object
);

Key Fields:

  • cf_id: Cloudflare custom hostname ID (e.g., 'a1b2c3d4-5678-90ab-cdef-1234567890ab')
  • status: Domain status ('active', 'pending', 'pending_validation', 'blocked')
  • ssl.status: SSL certificate status ('active', 'pending', 'pending_validation')

Cloudflare Domain Object Structure:

{
id: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', // cf_id
hostname: 'custom.example.com',
status: 'active', // Domain status
ssl: {
status: 'active', // SSL status
method: 'http',
type: 'dv',
certificate_authority: 'lets_encrypt',
validation_errors: []
},
ownership_verification: {
type: 'txt',
name: '_cf-custom-hostname.custom.example.com',
value: 'abc123...'
},
created_at: '2024-01-15T10:30:00Z'
}

funnels (Indirect via Utility)โ€‹

  • Operations: Update (via reviewAndSetStatusForDomain utility)
  • Model: shared/models/funnel.js
  • Usage Context: Set funnels to test mode when domain becomes inactive

Utility Function: utilities/funnelDomainStatusUtility.js

Purpose: Updates all funnels using an inactive domain to prevent broken live funnels

๐Ÿ”ง Job Configurationโ€‹

Cron Scheduleโ€‹

'*/5 * * * *'; // Every 5 minutes

Why 5-Minute Interval?

  • Balances timely status updates with API rate limits
  • Cloudflare status changes are typically not immediate
  • Avoids excessive API calls

Job Settingsโ€‹

queue.add(
{ page_num: 1, page_data: page1.data.result }, // Page 1 with data
{ attempts: 3 }, // Retry up to 3 times
);

queue.add(
{ page_num: 2 }, // Page 2+ without data
{ attempts: 3 }, // Retry up to 3 times
);

Job Data:

  • page_num: Page number (1-based)
  • page_data: Optional pre-fetched Cloudflare domain array (only page 1)

Queue Configurationโ€‹

QueueWrapper(`domains_update_validity`, 'global', { processCb });

Queue Name: domains_update_validity

Redis Scope: global (shared across queue manager instances)

๐Ÿ“‹ Processing Logic - Detailed Flowโ€‹

1. Cloudflare API Paginationโ€‹

First Request (Service Layer):

await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);

Response Structure:

{
result: [
{ id: 'cf123...', hostname: 'domain1.com', status: 'active', ssl: {...} },
{ id: 'cf456...', hostname: 'domain2.com', status: 'pending', ssl: {...} },
// ... up to 50 domains per page (Cloudflare default)
],
result_info: {
page: 1,
per_page: 50,
count: 50,
total_count: 150,
total_pages: 3 // Calculate from this
},
success: true,
errors: [],
messages: []
}

Calculate Total Pages:

const totalPages = page1.data.result_info.total_pages || 1;

2. Job Queuing Strategyโ€‹

Optimization for Page 1:

queue.add(
{ page_num: 1, page_data: page1.data.result }, // Embed data
{ attempts: 3 },
);

Why Embed Page 1 Data?

  • Already fetched in service layer
  • Avoids redundant API call in processor
  • Reduces Cloudflare API usage

Lazy Load for Pages 2+:

for (let pageNum = 2; pageNum <= totalPages; pageNum++) {
queue.add(
{ page_num: pageNum }, // No data, fetch in processor
{ attempts: 3 },
);
}

Why Not Embed All Pages?

  • Memory efficiency (each page ~50 domains ร— 1KB = 50KB)
  • Parallel processing (Bull queue handles concurrency)
  • API rate limit distribution

3. Lazy Data Loading (Processor Layer)โ€‹

Conditional Fetch:

let { page_num, page_data } = job.data;
if (!page_data) {
const response = await axios.get(
`https://api.cloudflare.com/client/v4/zones/${process.env.LD_CLOUDFLARE_ZONE_ID}/custom_hostnames?page=${page_num}`,
{ headers: { Authorization: 'Bearer ' + process.env.CLOUDFLARE_API_KEY } },
);
page_data = response.data?.result;
}

Request URL Example:

GET https://api.cloudflare.com/client/v4/zones/abc123.../custom_hostnames?page=2

4. Domain Matchingโ€‹

Extract Cloudflare IDs:

const domain_ids = domains.map(d => d.id);
// ['cf123...', 'cf456...', 'cf789...']

Query Database:

const foundDomains = await LightningDomain.find({ cf_id: { $in: domain_ids } });

Identify Orphaned Domains (Commented Out):

const notFoundDomains = domains.reduce((acc, curr) => {
if (!foundDomains.find(d => d?.cf_id == curr.id)) acc.push(curr);
return acc;
}, []);

Why Track Not Found Domains?

  • Cloudflare domains without database records
  • Possible orphaned domains from deleted funnels
  • Cleanup logic currently disabled

5. Status Comparison & Updateโ€‹

Detect Changes:

const domain = pageData.find(({ id }) => id == fd.cf_id);
if (domain && (domain.status != fd.status || domain.ssl.status != fd.ssl.status)) {
// Status changed - update required
}

Comparison Logic:

Cloudflare StatusDB StatusUpdate Needed?Action
activeactiveโŒ NoSkip
activependingโœ… YesUpdate + Review Funnels
pendingactiveโœ… YesUpdate + Set Funnels to Test
pending_validationpendingโœ… YesUpdate

SSL Status Comparison (Separate Check):

domain.ssl.status != fd.ssl.status;

SSL Statuses:

  • active: Certificate valid and installed
  • pending: Certificate issuance in progress
  • pending_validation: Awaiting domain validation
  • failed: Certificate issuance failed

6. Database Updateโ€‹

Spread Operator Pattern:

await LightningDomain.updateOne({ _id: fd._id }, { ...domain });

What Gets Updated (Cloudflare Object Spread):

{
id: 'cf123...', // cf_id
hostname: 'custom.example.com',
status: 'active', // Main status
ssl: { // SSL object
status: 'active',
method: 'http',
type: 'dv',
certificate_authority: 'lets_encrypt',
validation_errors: []
},
ownership_verification: {...},
created_at: '2024-01-15T10:30:00Z',
// ... other Cloudflare fields
}

Warning: This spreads the entire Cloudflare object, which may include fields not in the Mongoose schema. Mongoose will ignore undefined schema fields, but this pattern can lead to data inconsistencies.

7. Funnel Status Reviewโ€‹

Triggered When Domain Becomes Inactive:

if (domain.status !== 'active') {
await reviewAndSetStatusForDomain({ domain_id: fd._id });
}

Utility Function (utilities/funnelDomainStatusUtility.js):

Purpose: Set all funnels using this domain to test mode

Logic (inferred):

// Find all funnels using this domain
const funnels = await Funnel.find({ domain_id: fd._id, status: 'live' });

// Set to test mode to prevent broken live funnels
await Funnel.updateMany({ domain_id: fd._id, status: 'live' }, { status: 'test' });

Why Set to Test Mode?

  • Prevents users from accessing broken funnels
  • Domain may have SSL errors or be blocked
  • Test mode allows fixing without affecting live traffic

8. Promise Settlement Patternโ€‹

Using Promise.allSettled (Not Promise.all):

await Promise.allSettled(updatePromises).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
logger.log({ message: `Update fulfilled: ${result.value}` });
} else {
logger.error({ message: `Update rejected: ${result.reason}` });
}
});
});

Why allSettled Instead of all?

  • Promise.all: Fails fast on first rejection (stops other updates)
  • Promise.allSettled: Waits for all promises, logs each success/failure
  • Better for batch operations where partial success is acceptable

๐Ÿšจ Error Handlingโ€‹

Common Error Scenariosโ€‹

Cloudflare API Rate Limitโ€‹

Scenario: Too many requests to Cloudflare API

Error:

{
errors: [{ code: 10000, message: 'rate limit exceeded' }];
}

Result: Job fails, retries up to 3 times with backoff.

Recovery: Exponential backoff allows rate limit to reset.

Invalid Cloudflare Zone IDโ€‹

Error:

{
errors: [{ code: 1001, message: 'Invalid zone_id' }];
}

Result: Service-level error, logged but no retries (configuration issue).

Database Connection Errorโ€‹

Scenario: MongoDB connection lost during domain update

Result: Job fails, retries up to 3 times.

Recovery: Mongoose reconnection logic handles transient failures.

Domain Update Conflictโ€‹

Scenario: Concurrent updates to same domain (race condition)

Result: One update succeeds, others may fail with version conflict.

Recovery: Retry handles eventual consistency.

Commented-Out Cleanup Logicโ€‹

Orphaned Domain Deletion (Disabled):

// if (notFoundDomains.length) {
// await deleteUnwantedDomains(notFoundDomains);
// }

Why Disabled?

  • Risky operation (permanent deletion)
  • May delete domains still in use
  • Requires careful validation logic before re-enabling

๐Ÿ“Š Monitoring & Loggingโ€‹

Success Loggingโ€‹

Service-Level:

logger.log({
initiator: 'QM/domains/validate',
message: 'Domain Validation service processed.',
});

Update Success:

logger.log({
initiator: 'QM/domains/update_validity',
message: `Updated domain ${fd._id}:${result}`,
});

Funnel Review Success:

logger.log({
initiator: 'QM/domains/update_validity',
message: `Status reviewed for domain ${fd._id}: ${result}`,
});

Error Loggingโ€‹

Service-Level Errors:

logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});

Processor Errors:

logger.error({
initiator: 'QM/domains/validate',
error: err,
api_error: err?.response?.data?.errors?.[0]?.message || undefined,
});

Individual Update Errors:

logger.error({
initiator: 'QM/domains/update_validity',
message: `Error updating domain ${fd._id}:${err}`,
});

Performance Metricsโ€‹

  • Service Query Time: 1-3 seconds (Cloudflare API latency)
  • Job Processing Time: 100-500ms per page (50 domains)
  • Total Job Time: 5-15 seconds for 150 domains (3 pages)
  • Typical Updates: 0-10 domains per run (status changes are infrequent)

๐Ÿ”— Integration Pointsโ€‹

Triggers This Jobโ€‹

  • Cron Schedule: Every 5 minutes automatically
  • Manual Trigger: Via API endpoint (if QM_HOOKS=true)

External Dependenciesโ€‹

  • Cloudflare API: Custom Hostnames API
    • Zone ID: process.env.LD_CLOUDFLARE_ZONE_ID
    • API Key: process.env.CLOUDFLARE_API_KEY
    • Rate Limit: 1,200 requests per 5 minutes (default)

Jobs That Depend On Thisโ€‹

  • Domain Renewal: Checks domain status before renewal
  • Domain Cancellation: Validates status before cancellation
  • Funnel Publishing: Verifies domain is active before publishing

Utilities Usedโ€‹

  • reviewAndSetStatusForDomain: Updates funnel status when domain becomes inactive
  • socketEmit: (Imported but unused) Potential real-time status updates
  • verifyBalance: (Imported but unused) Potential balance checks for domain renewals

โš ๏ธ Important Notesโ€‹

Side Effectsโ€‹

  • โš ๏ธ Domain Record Update: Overwrites entire domain record with Cloudflare data
  • โš ๏ธ Funnel Status Change: Sets funnels to test mode when domain becomes inactive
  • โš ๏ธ SSL Status Tracking: Updates SSL certificate status independently

Performance Considerationsโ€‹

  • Pagination: Processes 50 domains per page (Cloudflare default)
  • Concurrent Processing: Bull queue handles multiple pages in parallel
  • API Rate Limits: Monitor Cloudflare rate limits (1,200 req/5min)
  • Database Load: Batch $in queries reduce database round trips

Business Logicโ€‹

Why Validate Every 5 Minutes?

  • Domain status changes are infrequent but time-sensitive
  • SSL validation can take 1-10 minutes
  • 5-minute interval balances timeliness with API costs

Why Update Funnels on Inactive Domains?

  • Prevents users from accessing broken funnels
  • Maintains user trust (no broken experiences)
  • Allows admin intervention before re-enabling

Why Embed Page 1 Data?

  • Optimization: Avoids redundant API call
  • Reduces API usage by ~33% (for 3-page dataset)
  • Negligible memory overhead (50KB)

Maintenance Notesโ€‹

  • Cloudflare API Keys: Rotate periodically, stored in environment variables
  • Zone ID: Tied to specific Cloudflare account, rarely changes
  • Orphaned Domain Cleanup: Currently disabled, requires careful validation before re-enabling
  • Schema Spread Pattern: Consider explicit field mapping instead of spreading entire Cloudflare object

Potential Issuesโ€‹

Spread Operator Risk:

await LightningDomain.updateOne({ _id: fd._id }, { ...domain });

Problem: Spreads entire Cloudflare object, may include fields not in schema.

Recommendation: Use explicit field mapping:

await LightningDomain.updateOne(
{ _id: fd._id },
{
status: domain.status,
ssl: domain.ssl,
ownership_verification: domain.ownership_verification,
// ... only fields defined in schema
},
);

๐Ÿงช Testingโ€‹

Manual Triggerโ€‹

# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/domains/validate

Simulate Cloudflare Status Changeโ€‹

Using Cloudflare Dashboard:

  1. Navigate to SSL/TLS โ†’ Custom Hostnames
  2. Select a domain
  3. Force re-validation or change status
  4. Wait up to 5 minutes for sync

Verify Update:

const domain = await LightningDomain.findOne({ hostname: 'custom.example.com' });
console.log('Status:', domain.status);
console.log('SSL Status:', domain.ssl.status);

Test Paginationโ€‹

// Create test domains in Cloudflare (use API or dashboard)
// Ensure > 50 domains to trigger pagination

// Run validation
await validate();

// Check logs for page processing
// Expected: "Domain Validation service processed."
// Expected: Multiple job logs for each page

Test Funnel Status Reviewโ€‹

// Setup: Create funnel with custom domain
const funnel = await Funnel.create({
name: 'Test Funnel',
domain_id: domain._id,
status: 'live',
});

// Simulate domain becoming inactive in Cloudflare
// (Use Cloudflare API to change domain status)

// Run validation
await validate();

// Verify funnel set to test mode
const updatedFunnel = await Funnel.findById(funnel._id);
console.log('Funnel status:', updatedFunnel.status); // Should be 'test'

Monitor Cloudflare API Usageโ€‹

// Track API calls
let apiCallCount = 0;

// Intercept axios requests
const originalGet = axios.get;
axios.get = async (...args) => {
if (args[0].includes('cloudflare')) {
apiCallCount++;
console.log(`Cloudflare API call #${apiCallCount}: ${args[0]}`);
}
return originalGet(...args);
};

// Run validation
await validate();

// Expected calls: 1 (service) + totalPages (processor) = 1 + 3 = 4 calls
console.log('Total Cloudflare API calls:', apiCallCount);

Verify Status Change Detectionโ€‹

// Setup: Domain with status change
const domain = await LightningDomain.findOne({});
const originalStatus = domain.status;

// Manually change status in Cloudflare
// (Use Cloudflare API)

// Run validation
await validate();

// Verify update
const updatedDomain = await LightningDomain.findById(domain._id);
console.log('Original status:', originalStatus);
console.log('Updated status:', updatedDomain.status);
console.log('Status changed:', originalStatus !== updatedDomain.status);

Job Type: Scheduled + Queued
Execution Frequency: Every 5 minutes
Average Duration: 5-15 seconds (150 domains across 3 pages)
Status: Active

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM