UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

150 lines 6.62 kB
import { execFileSync as nodeExecFileSync } from 'child_process'; import { isPlainObject } from 'es-toolkit/compat'; import { resolveSearchMapperManifestUri } from './artifactBucket.js'; import { DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS, fetchWithTimeout } from './providers/request.js'; import { runGcloudFileCommand } from '../../utils/gcloud.js'; import { normalizeOptionalString } from '../../utils/value.js'; export const SEARCH_MAPPER_MANIFEST_VERSION = 1; const isSupportedManifestUri = value => { const normalized = normalizeOptionalString(value); return normalized !== null && (normalized.startsWith('gs://') || normalized.startsWith('https://')); }; const readGsManifest = (manifestUri, runCommand = nodeExecFileSync) => runGcloudFileCommand(['storage', 'cat', manifestUri], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, runCommand); const readHttpManifest = async (manifestUri, options = {}) => { const { fetchImpl = fetch, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS } = options; const response = await fetchWithTimeout(manifestUri, {}, timeoutMs, { fetchImpl }); if (!response.ok) { throw new Error(`Atlas search mapper manifest fetch failed with status ${response.status}.`); } return response.text(); }; const readManifestText = async (manifestUri, options = {}) => { const { fetchImpl = fetch, runCommand = nodeExecFileSync, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS } = options; if (!isSupportedManifestUri(manifestUri)) { throw new Error('Atlas search mapper manifest validation supports only gs:// and https:// destinations.'); } if (manifestUri.startsWith('gs://')) { return readGsManifest(manifestUri, runCommand); } return readHttpManifest(manifestUri, { fetchImpl, timeoutMs }); }; const isMissingManifestError = error => { const message = `${error?.stderr?.toString?.() ?? ''}\n${error?.message ?? ''}`; return /404|not[ -]?found|no urls matched|matched no objects or files|bucket.+not found|object.+not found|status 404/i.test(message); }; const getReadableManifestReadError = error => { const stderr = error?.stderr?.toString?.().trim(); if (stderr) { return stderr; } return String(error?.message ?? 'Unknown error').replace(/^Command failed:[\s\S]*?\r?\n/, '').trim(); }; const formatManifestReadIssue = (manifestUri, error) => { if (isMissingManifestError(error)) { return `Atlas search mapper manifest is missing at ${manifestUri}.`; } return `Could not read Atlas search mapper manifest from ${manifestUri}. ${getReadableManifestReadError(error)}`.trim(); }; const parseManifestJson = manifestText => { try { return JSON.parse(manifestText); } catch (error) { throw new Error(`Atlas search mapper manifest contains invalid JSON. ${error.message}`); } }; export const validateSearchMapperManifest = (context, manifest, manifestUri) => { const issues = []; const warnings = []; if (!isPlainObject(manifest)) { return { issues: ['Atlas search mapper manifest must resolve to a JSON object.'], warnings }; } if (manifest.version !== SEARCH_MAPPER_MANIFEST_VERSION) { issues.push(`Atlas search mapper manifest version ${manifest.version ?? 'unknown'} is not supported. ` + `Expected ${SEARCH_MAPPER_MANIFEST_VERSION}.`); } if (!manifest.mappers || !isPlainObject(manifest.mappers)) { issues.push('Atlas search mapper manifest must define a "mappers" object.'); return { issues, warnings }; } if (manifest.projectId && manifest.projectId !== context.projectId) { issues.push(`Atlas search mapper manifest projectId ${manifest.projectId} does not match the selected project ${context.projectId}.`); } if (manifest.manifestUri && manifest.manifestUri !== manifestUri) { warnings.push(`Atlas search mapper manifest declares manifestUri ${manifest.manifestUri}, ` + `but the active Atlas search config points to ${manifestUri}.`); } if (manifest.manifestUri && !isSupportedManifestUri(manifest.manifestUri)) { issues.push(`Atlas search mapper manifest declares an unsupported manifestUri ${manifest.manifestUri}. ` + 'Only gs:// and https:// URIs are allowed.'); } for (const [mapperName, mapperConfig] of Object.entries(context.config.mappers ?? {})) { const manifestMapper = manifest.mappers[mapperName] ?? null; if (!manifestMapper) { issues.push(`Atlas search mapper manifest is missing mapper ${mapperName} required by the current search config.`); continue; } if (manifestMapper.export !== mapperConfig.export) { issues.push(`Atlas search mapper manifest export mismatch for ${mapperName}. ` + `Expected ${mapperConfig.export}, received ${manifestMapper.export ?? 'undefined'}.`); } if (manifestMapper.source !== mapperConfig.source) { issues.push(`Atlas search mapper manifest source mismatch for ${mapperName}. ` + `Expected ${mapperConfig.source}, received ${manifestMapper.source ?? 'undefined'}.`); } if (!normalizeOptionalString(manifestMapper.modulePath)) { issues.push(`Atlas search mapper manifest entry ${mapperName} must define a non-empty modulePath.`); } if (!normalizeOptionalString(manifestMapper.moduleUri)) { issues.push(`Atlas search mapper manifest entry ${mapperName} must define a non-empty moduleUri.`); } else if (!isSupportedManifestUri(manifestMapper.moduleUri)) { issues.push(`Atlas search mapper manifest entry ${mapperName} must use a gs:// or https:// moduleUri.`); } } for (const manifestMapperName of Object.keys(manifest.mappers)) { if (!context.config.mappers?.[manifestMapperName]) { warnings.push(`Atlas search mapper manifest contains extra mapper ${manifestMapperName} ` + 'that is not referenced by the current search config.'); } } return { issues, warnings }; }; export const inspectSearchMapperManifest = async (context, options = {}) => { const manifestUri = resolveSearchMapperManifestUri(context); try { const manifestText = await readManifestText(manifestUri, options); const manifest = parseManifestJson(manifestText); const validation = validateSearchMapperManifest(context, manifest, manifestUri); return { ...validation, manifest, manifestUri, status: validation.issues.length > 0 ? 'invalid' : 'ok' }; } catch (error) { return { issues: [formatManifestReadIssue(manifestUri, error)], manifest: null, manifestUri, status: isMissingManifestError(error) ? 'missing' : 'unreachable', warnings: [] }; } };