UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

164 lines 8.27 kB
import * as features from '../../utils/feature.js'; import { logger } from '../../utils/index.js'; import { getSearchProvider } from './providers/index.js'; import { discoverSearchRemoteResources } from './discovery.js'; import { inspectSearchMapperManifest } from './manifestValidation.js'; import { createSearchFailureHandlingSummary, createSearchRemoteSummaryEntry, createSearchRemoteSummary, getSearchResourceGroups, logLimitedSearchRemoteDiscoveryNotice } from './planning.js'; import { collectSearchSchemaLifecycleVerificationIssues, createSearchSchemaLifecycleEntries, createSearchSchemaLifecycleGuidanceEntries, createSearchSchemaLifecycleSummaryRows, inspectSearchSchemasWithFallback, logSearchSchemaInspectionWarnings } from './schemaLifecycle.js'; import { collectSearchSyncFailureInspectionIssues, createSearchSyncFailureInspectionDetailEntries, createSearchSyncFailureInspectionSummaryRows, inspectSearchSyncFailures } from './syncFailureInspection.js'; const VERIFY_REMOTE_DISCOVERY_LIMITED_NOTICE = 'Remote discovery is currently limited from this machine, so unavailable checks are reported as verification gaps instead of raw warnings.'; const logSchemaMismatchDetails = (provider, schemaInspection, loggerImpl = logger) => { const mismatchDetails = schemaInspection.collections.filter(collection => collection.schemaStatus === 'mismatch').flatMap(collection => (collection.mismatchDetails ?? []).map(detail => `${collection.name}: ${detail}`)); if (mismatchDetails.length === 0) { return; } loggerImpl.section(`${provider.descriptor.labels.targetGroup} mismatch details`, mismatchDetails, { detailOnly: true }); }; const collectVerificationIssues = (context, provider, remoteResources, schemaInspection, manifestInspection, syncFailureInspection) => { const issues = []; for (const group of getSearchResourceGroups(context).filter(resourceGroup => (remoteResources[resourceGroup.key] ?? []).length > 0)) { for (const resource of remoteResources[group.key]) { if (resource.remoteStatus === 'found') { continue; } if (resource.remoteStatus === 'not-found') { issues.push(`${group.label}: required resource "${resource.name}" was not found.`); continue; } issues.push(`${group.label}: required resource "${resource.name}" could not be verified.`); } } issues.push(...collectSearchSchemaLifecycleVerificationIssues(provider, schemaInspection.collections, { compact: true, includeMismatchPreview: false })); issues.push(...manifestInspection.issues); issues.push(...collectSearchSyncFailureInspectionIssues(syncFailureInspection)); return issues; }; const logSearchVerificationSummary = (context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl = logger) => { loggerImpl.summary('Verification summary', [{ label: 'Project', value: context.projectId }, context.environment ? { label: 'Environment', value: context.environment } : null, ...remoteSummary.filter(resourceGroup => resourceGroup.total > 0).map(resourceGroup => createSearchRemoteSummaryEntry(resourceGroup, { successVerb: 'verified' }))]); loggerImpl.summary('Local files', [{ label: 'Config', value: context.configPath }]); loggerImpl.summary('Failure handling', createSearchFailureHandlingSummary(context)); loggerImpl.summary('Failure state', createSearchSyncFailureInspectionSummaryRows(syncFailureInspection)); logSchemaVerificationSummary(provider, schemaInspection, loggerImpl); loggerImpl.summary('Schema lifecycle', createSearchSchemaLifecycleSummaryRows(provider, schemaInspection.collections ?? [], { includeAffectedIndexNames: true })); loggerImpl.summary('Mapper manifest', [{ label: 'Status', tone: manifestInspection.status === 'ok' ? 'success' : manifestInspection.status === 'invalid' ? 'error' : 'warning', value: manifestInspection.status }, { label: 'URI', value: manifestInspection.manifestUri ?? 'not configured' }]); logSchemaMismatchDetails(provider, schemaInspection, loggerImpl); const lifecycleEntries = createSearchSchemaLifecycleEntries(provider, schemaInspection.collections ?? [], { compact: true, includeMismatchPreview: false, omittedStatuses: ['reindex-required'] }); if (lifecycleEntries.length > 0) { loggerImpl.summary('Schema lifecycle details', lifecycleEntries); } const guidanceEntries = createSearchSchemaLifecycleGuidanceEntries(schemaInspection.collections ?? [], { context }); if (guidanceEntries.length > 0) { loggerImpl.summary('Operator guidance', guidanceEntries); } const failureEntries = createSearchSyncFailureInspectionDetailEntries(syncFailureInspection); if (failureEntries.length > 0) { loggerImpl.section('Recent failed sync items', failureEntries, { detailOnly: true }); } }; const logSchemaVerificationSummary = (provider, schemaInspection, loggerImpl = logger) => { const totalCount = schemaInspection.collections.length; if (totalCount === 0) { return; } const unknownCount = schemaInspection.collections.filter(collection => collection.schemaStatus === 'unknown').length; if (unknownCount > 0) { loggerImpl.summary('Schema status', [{ label: provider.descriptor.labels.targetGroup, tone: 'warning', value: 'could not be verified from this machine.' }]); return; } const verifiedCount = schemaInspection.collections.filter(collection => collection.schemaStatus === 'match').length; loggerImpl.summary('Schema status', [{ label: provider.descriptor.labels.targetGroup, tone: verifiedCount === totalCount ? 'success' : 'warning', value: `${verifiedCount}/${totalCount} verified.` }]); }; export const verifySearch = async (options, { discoverSearchRemoteResourcesImpl = discoverSearchRemoteResources, exitImpl = code => process.exit(code), getSearchProviderImpl = getSearchProvider, inspectSearchSchemasDirectImpl = null, inspectSearchSchemasImpl, inspectSearchSyncFailuresImpl = inspectSearchSyncFailures, inspectSearchMapperManifestImpl = inspectSearchMapperManifest, loadFeatureContextImpl = features.loadFeatureContext, loggerImpl = logger, workingDirectory = process.cwd() } = {}) => { let spinner; try { const context = await loadFeatureContextImpl('search', options, { cwd: workingDirectory }); spinner = loggerImpl.spinner('Verifying Atlas search...'); const provider = getSearchProviderImpl(context.config.provider); const remoteResources = await discoverSearchRemoteResourcesImpl(context); const remoteSummary = createSearchRemoteSummary(context, remoteResources); const schemaInspection = await inspectSearchSchemasWithFallback(context, { getSearchProviderImpl, inspectSearchSchemasDirectImpl, inspectSearchSchemasViaSearchApiImpl: inspectSearchSchemasImpl, provider }); logSearchSchemaInspectionWarnings(schemaInspection, loggerImpl); const syncFailureInspection = await inspectSearchSyncFailuresImpl(context); if (syncFailureInspection.warning) { loggerImpl.warning(syncFailureInspection.warning); } const manifestInspection = await inspectSearchMapperManifestImpl(context); const issues = collectVerificationIssues(context, provider, remoteResources, schemaInspection, manifestInspection, syncFailureInspection); if (issues.length > 0) { spinner.fail('Atlas search verification failed.'); logSearchVerificationSummary(context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl); logLimitedSearchRemoteDiscoveryNotice(remoteSummary, loggerImpl, VERIFY_REMOTE_DISCOVERY_LIMITED_NOTICE); for (const issue of issues) { loggerImpl.error(issue, false); } exitImpl(1); return; } spinner.succeed('Atlas search verification passed.'); logSearchVerificationSummary(context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl); } catch (error) { spinner?.fail('Atlas search verification failed.'); loggerImpl.error(error.message, false); exitImpl(1); } }; export default verifySearch;