UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

142 lines 5.61 kB
import fs from 'fs'; import path from 'path'; import { runGcloudFileCommand } from '../../utils/gcloud.js'; import { writeJsonFile } from '../../utils/file.js'; import { getAtlasGeneratedArtifactsDir, resolveRootPath } from '../../utils/atlas.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { bundleSearchMapperEntry } from './mapperBundler.js'; import { resolveSearchCloudRunDeployConfig } from './planning.js'; import { resolveSearchReleaseTarget } from './release.js'; const SEARCH_MAPPER_ARTIFACT_VERSION = 1; const normalizePathSeparators = value => value.replaceAll('\\', '/'); const ensureDirectory = directoryPath => { if (!fs.existsSync(directoryPath)) { fs.mkdirSync(directoryPath, { recursive: true }); } }; const normalizeGsUri = value => { const normalized = normalizeOptionalString(value); if (!normalized) { return null; } if (!normalized.startsWith('gs://')) { throw new Error('Atlas search mapper upload currently supports only gs:// manifest destinations.'); } const withoutTrailingSlash = normalized.replace(/\/+$/, ''); const pathPart = withoutTrailingSlash.slice('gs://'.length); const firstSlashIndex = pathPart.indexOf('/'); if (firstSlashIndex <= 0 || firstSlashIndex === pathPart.length - 1) { throw new Error('Atlas search mapper manifest destination must include both a bucket and an object path.'); } return withoutTrailingSlash; }; const getGsUriDirectory = gsUri => gsUri.slice(0, gsUri.lastIndexOf('/')); const joinGsUri = (baseUri, relativePath) => `${baseUri}/${normalizePathSeparators(relativePath)}`; const getMapperArtifactRoot = (projectId, releaseId, cwd = process.cwd()) => { const artifactRoot = path.join(getAtlasGeneratedArtifactsDir(cwd), 'search', 'mappers', projectId); if (!releaseId) { return artifactRoot; } return path.join(artifactRoot, releaseId); }; const createUploadOperation = (localPath, remoteUri) => ({ command: 'gcloud storage cp', localPath, remoteUri }); export const buildSearchMapperArtifact = async (context, options = {}, cwd = process.cwd()) => { const releaseTarget = resolveSearchReleaseTarget(context.config?.release); const destinationUri = normalizeGsUri(options.destinationUri ?? resolveSearchCloudRunDeployConfig(context).mapperManifestUri); if (!destinationUri) { throw new Error('Atlas search mapper upload requires a gs:// manifest destination. ' + 'Set search.deploy.cloudRun.mapperManifestUri or provide --destination-uri.'); } const mapperEntries = Object.entries(context.config.mappers ?? {}); if (mapperEntries.length === 0) { throw new Error('Atlas search mapper upload requires at least one configured mapper.'); } const mapperSourceEntries = mapperEntries.map(([mapperName, mapperConfig]) => { if (!mapperConfig?.source || !mapperConfig?.export) { throw new Error(`Atlas search mapper ${mapperName} must define both source and export before it can be uploaded.`); } const sourcePath = normalizePathSeparators(mapperConfig.source); const sourceFilePath = resolveRootPath(sourcePath, cwd); if (!fs.existsSync(sourceFilePath)) { throw new Error(`Atlas search mapper source file does not exist: ${sourceFilePath}`); } return { exportName: mapperConfig.export, mapperName, sourceFilePath, sourcePath }; }); const artifactRoot = getMapperArtifactRoot(context.projectId, releaseTarget.releaseId, cwd); const filesRoot = path.join(artifactRoot, 'files'); const manifestFilePath = path.join(artifactRoot, 'manifest.json'); const remoteBaseUri = getGsUriDirectory(destinationUri); fs.rmSync(artifactRoot, { force: true, recursive: true }); ensureDirectory(filesRoot); const bundledEntriesBySourcePath = new Map(); const mappers = {}; const uploadOperations = []; for (const mapperEntry of mapperSourceEntries) { if (bundledEntriesBySourcePath.has(mapperEntry.sourceFilePath)) { continue; } const bundledEntry = await bundleSearchMapperEntry({ artifactRoot, cwd, mapperName: mapperEntry.mapperName, sourceFilePath: mapperEntry.sourceFilePath }); bundledEntriesBySourcePath.set(mapperEntry.sourceFilePath, bundledEntry); uploadOperations.push(createUploadOperation(bundledEntry.localModulePath, joinGsUri(remoteBaseUri, bundledEntry.modulePath))); } for (const mapperEntry of mapperSourceEntries) { const { modulePath } = bundledEntriesBySourcePath.get(mapperEntry.sourceFilePath); const moduleUri = joinGsUri(remoteBaseUri, modulePath); mappers[mapperEntry.mapperName] = { export: mapperEntry.exportName, modulePath, moduleUri, source: mapperEntry.sourcePath }; } const manifest = { mappers, createdAt: new Date().toISOString(), manifestUri: destinationUri, projectId: context.projectId, version: SEARCH_MAPPER_ARTIFACT_VERSION, release: { releaseId: releaseTarget.releaseId, strategy: releaseTarget.strategy, target: releaseTarget.target } }; writeJsonFile(manifestFilePath, manifest); uploadOperations.push(createUploadOperation(manifestFilePath, destinationUri)); return { artifactRoot, destinationUri, filesRoot, manifest, manifestFilePath, uploadOperations }; }; export const executeSearchMapperUpload = artifact => { for (const operation of artifact.uploadOperations) { runGcloudFileCommand(['storage', 'cp', operation.localPath, operation.remoteUri], { stdio: 'inherit' }); } return artifact; };