@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
142 lines • 6.12 kB
JavaScript
import { Firestore } from '@google-cloud/firestore';
import { normalizeFirestoreCollectionPath } from '../../utils/firebase.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { resolveSearchSyncFailureInspectionConfig } from './planning.js';
const FAILED_ITEM_PREVIEW_LIMIT = 3;
const normalizeFailureTimestamp = value => {
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value?.toDate === 'function') {
return value.toDate().toISOString();
}
return normalizeOptionalString(value);
};
const createSearchSyncFailureInspectionResult = (status, overrides = {}) => ({
collectionPath: null,
deadLetterFailures: null,
recentFailures: [],
status,
terminalFailures: null,
totalFailures: null,
warning: null,
...overrides
});
const createNotConfiguredInspectionResult = () => createSearchSyncFailureInspectionResult('not-configured');
const createUnavailableInspectionResult = (collectionPath, error) => createSearchSyncFailureInspectionResult('unavailable', {
collectionPath,
warning: `Could not inspect Firestore sync failure state in ${collectionPath}: ` + `${error?.message ?? String(error)}`
});
const readAggregateCount = async aggregateQuery => {
const snapshot = await aggregateQuery.get();
return Number(snapshot?.data?.().count ?? 0);
};
const createFailedSyncStateQuery = collection => collection.where('failedOn', '!=', null);
const normalizeFailureDocument = documentSnapshot => {
const data = documentSnapshot.data() ?? {};
return {
collection: normalizeOptionalString(data.collection),
failureCategory: normalizeOptionalString(data.failureCategory) ?? 'unknown',
failedOn: normalizeFailureTimestamp(data.failedOn),
id: documentSnapshot.id,
lastErrorSummary: normalizeOptionalString(data.lastErrorSummary),
objectPath: normalizeOptionalString(data.dlqPointer?.objectPath),
path: normalizeOptionalString(documentSnapshot.ref?.path),
sourceDocumentId: normalizeOptionalString(data.sourceDocumentId),
syncKey: normalizeOptionalString(data.syncKey),
syncVersion: normalizeOptionalString(data.syncVersion)
};
};
export const inspectSearchSyncFailures = async (context, dependencies = {}) => {
const inspectionConfig = resolveSearchSyncFailureInspectionConfig(context);
if (!inspectionConfig.collectionPath) {
return createNotConfiguredInspectionResult();
}
const collectionPath = normalizeFirestoreCollectionPath(inspectionConfig.collectionPath);
const createFirestoreClient = dependencies.createFirestoreClient ?? (options => new Firestore(options));
const firestoreClient = dependencies.firestoreClient ?? createFirestoreClient({
projectId: context.projectId
});
try {
const collection = firestoreClient.collection(collectionPath);
const failedSyncStateQuery = createFailedSyncStateQuery(collection);
const [totalFailures, deadLetterFailures, terminalFailures, recentFailureSnapshot] = await Promise.all([readAggregateCount(failedSyncStateQuery.count()), readAggregateCount(failedSyncStateQuery.where('failureCategory', '==', 'dead-letter').count()), readAggregateCount(failedSyncStateQuery.where('failureCategory', '==', 'terminal').count()), failedSyncStateQuery.orderBy('failedOn', 'desc').limit(inspectionConfig.recentLimit).get()]);
return createSearchSyncFailureInspectionResult('available', {
collectionPath,
deadLetterFailures,
recentFailures: recentFailureSnapshot.docs.map(normalizeFailureDocument),
terminalFailures,
totalFailures
});
} catch (error) {
return createUnavailableInspectionResult(collectionPath, error);
}
};
export const createSearchSyncFailureInspectionSummaryRows = inspection => {
if (inspection.status === 'not-configured') {
return [{
label: 'Failure backlog',
tone: 'muted',
value: 'not inspected (set deploy.syncState.collectionPath to inspect Firestore failure state).'
}];
}
if (inspection.status === 'unavailable') {
return [{
label: 'Failure backlog',
tone: 'warning',
value: 'could not be inspected from Firestore sync failure state.'
}];
}
const latestFailure = inspection.recentFailures[0] ?? null;
const totalFailures = inspection.totalFailures ?? 0;
return [{
label: 'Failure backlog',
tone: totalFailures === 0 ? 'success' : 'warning',
value: `${totalFailures} operator-visible sync failure item${totalFailures === 1 ? '' : 's'}.`
}, {
label: 'Dead-letter failures',
value: String(inspection.deadLetterFailures ?? 0)
}, {
label: 'Terminal failures',
value: String(inspection.terminalFailures ?? 0)
}, latestFailure ? {
label: 'Latest failure',
value: `${latestFailure.failureCategory} ${latestFailure.syncKey ?? latestFailure.id}` + `${latestFailure.failedOn ? ` @ ${latestFailure.failedOn}` : ''}`
} : null];
};
export const createSearchSyncFailureInspectionDetailEntries = inspection => {
if (inspection.status !== 'available') {
return [];
}
return inspection.recentFailures.slice(0, FAILED_ITEM_PREVIEW_LIMIT).map(failure => {
const summaryParts = [failure.failureCategory];
if (failure.syncKey) {
summaryParts.push(failure.syncKey);
}
if (failure.syncVersion) {
summaryParts.push(`v=${failure.syncVersion}`);
}
if (failure.failedOn) {
summaryParts.push(failure.failedOn);
}
if (failure.lastErrorSummary) {
summaryParts.push(failure.lastErrorSummary);
}
if (failure.objectPath) {
summaryParts.push(`dlq=${failure.objectPath}`);
}
return summaryParts.join(' | ');
});
};
export const collectSearchSyncFailureInspectionIssues = inspection => {
if (inspection.status === 'not-configured') {
return [];
}
if (inspection.status === 'unavailable') {
return [inspection.warning];
}
if ((inspection.totalFailures ?? 0) === 0) {
return [];
}
return [`Firestore sync failure state: ${inspection.totalFailures} operator-visible ` + `failed sync item${inspection.totalFailures === 1 ? '' : 's'} remain in "${inspection.collectionPath}".`];
};