UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

142 lines 6.12 kB
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}".`]; };