@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
451 lines • 24.4 kB
JavaScript
import { isBoolean, isString } from 'es-toolkit/compat';
import { isPlainObject } from 'es-toolkit/predicate';
import { isStringArray } from '../../../utils/value.js';
import { SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES, validateSearchSourceBindingConfig, validateSearchSourceConfig, validateSearchSourceDefinitionConfig } from './sources.js';
import { getSearchProvider, listSearchProviders } from '../providers/index.js';
const BASE_SEARCH_ROOT_SECTION_KEYS = ['deploy', 'projects', 'provider', 'release', 'workloads'];
const SEARCH_WORKLOAD_FILE_KEYS = ['index', 'mapper', 'source'];
const SEARCH_WORKLOAD_MAPPER_KEYS = ['export', 'name', 'source'];
const SEARCH_WORKLOAD_SOURCE_KEYS = ['description', 'enabled', 'firestore', 'http', 'name', 'routeTemplate', 'sql', 'syncClass', 'type'];
const SEARCH_WORKLOAD_FIRESTORE_KEYS = ['database', 'documentPathPattern', 'triggerRegion'];
const SUPPORTED_SEARCH_CONFIG_TRANSPORTS = ['artifact'];
const SUPPORTED_CLOUD_RUN_VPC_EGRESS_SETTINGS = ['all-traffic', 'private-ranges-only'];
const SEARCH_COLLECTION_PATH_SEGMENT_PATTERN = /^[a-z][a-z0-9_]*$/;
const SUPPORTED_SEARCH_FIELD_TYPES = ['auto', 'bool', 'float', 'geopoint', 'int32', 'int64', 'object', 'object[]', 'string', 'string[]'];
const getRegisteredSearchProviders = () => listSearchProviders().map(getSearchProvider);
export const getProviderSectionEntries = () => getRegisteredSearchProviders().filter(provider => isString(provider.configSectionKey)).map(provider => [provider.configSectionKey, provider]);
const SEARCH_ROOT_SECTION_KEYS = [...BASE_SEARCH_ROOT_SECTION_KEYS, ...getProviderSectionEntries().map(([sectionKey]) => sectionKey)];
const SEARCH_PROJECT_SECTION_KEYS = ['deploy', 'environment', 'release', 'sourceBindings', ...getProviderSectionEntries().map(([sectionKey]) => sectionKey)];
const throwSearchConfigError = message => {
throw new Error(`Invalid Atlas search config. ${message}`);
};
const assertObjectWhenProvided = (value, propertyPath) => {
if (value !== undefined && !isPlainObject(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be an object when provided.`);
}
};
const assertStringWhenProvided = (value, propertyPath) => {
if (value !== undefined && !isString(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be a string when provided.`);
}
};
const assertStringOrNullWhenProvided = (value, propertyPath) => {
if (value !== undefined && value !== null && !isString(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be a string or null when provided.`);
}
};
const assertSnakeCaseCollectionPathWhenProvided = (value, propertyPath) => {
if (!isString(value)) {
return;
}
const normalizedValue = value.trim().replace(/^\/+|\/+$/g, '');
if (!normalizedValue) {
return;
}
const segments = normalizedValue.split('/').filter(Boolean);
for (let index = 0; index < segments.length; index += 2) {
if (!SEARCH_COLLECTION_PATH_SEGMENT_PATTERN.test(segments[index])) {
throwSearchConfigError(`The "${propertyPath}" property must use snake_case collection names. Invalid collection segment: "${segments[index]}".`);
}
}
};
const assertPositiveIntegerWhenProvided = (value, propertyPath) => {
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
throwSearchConfigError(`The "${propertyPath}" property must be a positive integer when provided.`);
}
};
const assertAllowedKeys = (value, propertyPath, allowedKeys) => {
if (!isPlainObject(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be an object.`);
}
const invalidKeys = Object.keys(value).filter(key => !allowedKeys.includes(key));
if (invalidKeys.length > 0) {
throwSearchConfigError(`Unsupported properties found on "${propertyPath}": ${invalidKeys.join(', ')}.`);
}
};
const assertNonEmptyString = (value, propertyPath) => {
if (!isString(value) || value.trim().length === 0) {
throwSearchConfigError(`The "${propertyPath}" property must be a non-empty string.`);
}
};
const validateSearchRuntimeSettings = config => {
if (config.provider !== undefined && !isString(config.provider)) {
throwSearchConfigError('The "provider" property must be a string when provided.');
}
if (config.provider !== undefined) {
getSearchProvider(config.provider);
}
for (const [sectionKey, provider] of getProviderSectionEntries()) {
if (config[sectionKey] === undefined) {
continue;
}
if (provider.validateConfigSection) {
provider.validateConfigSection(config[sectionKey]);
continue;
}
assertObjectWhenProvided(config[sectionKey], sectionKey);
}
if (config.deploy !== undefined) {
assertObjectWhenProvided(config.deploy, 'deploy');
if (config.deploy.cloudRun !== undefined) {
assertObjectWhenProvided(config.deploy.cloudRun, 'deploy.cloudRun');
const cloudRunConfig = config.deploy.cloudRun;
assertStringWhenProvided(cloudRunConfig.artifactRegistryProject, 'deploy.cloudRun.artifactRegistryProject');
assertStringWhenProvided(cloudRunConfig.artifactRegistryLocation, 'deploy.cloudRun.artifactRegistryLocation');
assertStringOrNullWhenProvided(cloudRunConfig.mapperManifestUri, 'deploy.cloudRun.mapperManifestUri');
assertStringOrNullWhenProvided(cloudRunConfig.runtimeConfigUri, 'deploy.cloudRun.runtimeConfigUri');
if (cloudRunConfig.searchConfigTransport !== undefined && !SUPPORTED_SEARCH_CONFIG_TRANSPORTS.includes(cloudRunConfig.searchConfigTransport)) {
throwSearchConfigError('The "deploy.cloudRun.searchConfigTransport" property must be "artifact" when provided.');
}
assertStringWhenProvided(cloudRunConfig.tag, 'deploy.cloudRun.tag');
assertStringWhenProvided(cloudRunConfig.region, 'deploy.cloudRun.region');
assertStringWhenProvided(cloudRunConfig.repository, 'deploy.cloudRun.repository');
assertStringWhenProvided(cloudRunConfig.serviceImage, 'deploy.cloudRun.serviceImage');
assertStringWhenProvided(cloudRunConfig.jobImage, 'deploy.cloudRun.jobImage');
assertStringWhenProvided(cloudRunConfig.serviceAccountEmail, 'deploy.cloudRun.serviceAccountEmail');
if (cloudRunConfig.vpcAccess !== undefined) {
assertObjectWhenProvided(cloudRunConfig.vpcAccess, 'deploy.cloudRun.vpcAccess');
assertStringWhenProvided(cloudRunConfig.vpcAccess.network, 'deploy.cloudRun.vpcAccess.network');
assertStringOrNullWhenProvided(cloudRunConfig.vpcAccess.subnetwork, 'deploy.cloudRun.vpcAccess.subnetwork');
if (cloudRunConfig.vpcAccess.egress !== undefined && !SUPPORTED_CLOUD_RUN_VPC_EGRESS_SETTINGS.includes(cloudRunConfig.vpcAccess.egress)) {
throwSearchConfigError('The "deploy.cloudRun.vpcAccess.egress" property must be "all-traffic" or "private-ranges-only" when provided.');
}
}
}
if (config.deploy.terraform !== undefined) {
assertObjectWhenProvided(config.deploy.terraform, 'deploy.terraform');
assertStringWhenProvided(config.deploy.terraform.rootDir, 'deploy.terraform.rootDir');
assertStringWhenProvided(config.deploy.terraform.moduleSource, 'deploy.terraform.moduleSource');
}
if (config.deploy.syncState !== undefined) {
assertObjectWhenProvided(config.deploy.syncState, 'deploy.syncState');
assertStringOrNullWhenProvided(config.deploy.syncState.collectionPath, 'deploy.syncState.collectionPath');
assertSnakeCaseCollectionPathWhenProvided(config.deploy.syncState.collectionPath, 'deploy.syncState.collectionPath');
assertPositiveIntegerWhenProvided(config.deploy.syncState.recentLimit, 'deploy.syncState.recentLimit');
}
}
if (config.release !== undefined) {
assertObjectWhenProvided(config.release, 'release');
if (config.release.strategy !== undefined && !['direct', 'managed'].includes(config.release.strategy)) {
throwSearchConfigError('The "release.strategy" property must be "direct" or "managed" when provided.');
}
for (const key of ['active', 'candidate']) {
assertStringOrNullWhenProvided(config.release[key], `release.${key}`);
}
}
return config;
};
const validateSearchIndexFields = (indexName, indexConfig) => {
if (!Array.isArray(indexConfig.fields) || indexConfig.fields.length === 0) {
throwSearchConfigError(`The index "${indexName}.fields" property must be a non-empty array of field definitions.`);
}
const fieldNames = new Set();
for (const fieldConfig of indexConfig.fields) {
if (!isPlainObject(fieldConfig)) {
throwSearchConfigError(`The index "${indexName}.fields" entries must be objects.`);
}
if (!isString(fieldConfig.name) || fieldConfig.name.trim().length === 0) {
throwSearchConfigError(`The index "${indexName}.fields[].name" property must be a non-empty string.`);
}
if (!isString(fieldConfig.type) || fieldConfig.type.trim().length === 0) {
throwSearchConfigError(`The index "${indexName}.fields[].type" property must be a non-empty string.`);
}
if (!SUPPORTED_SEARCH_FIELD_TYPES.includes(fieldConfig.type)) {
throwSearchConfigError(`The index "${indexName}.fields[].type" value "${fieldConfig.type}" is not supported.`);
}
for (const boolField of ['optional', 'sort', 'facet', 'filter', 'group']) {
if (fieldConfig[boolField] !== undefined && !isBoolean(fieldConfig[boolField])) {
throwSearchConfigError(`The index "${indexName}.fields[].${boolField}" property must be a boolean when provided.`);
}
}
if (fieldNames.has(fieldConfig.name)) {
throwSearchConfigError(`The index "${indexName}" defines the field "${fieldConfig.name}" more than once.`);
}
fieldNames.add(fieldConfig.name);
}
for (const refProp of ['queryBy', 'facets', 'filterBy', 'groupBy']) {
if ((indexConfig[refProp] ?? []).some(fieldName => !fieldNames.has(fieldName))) {
throwSearchConfigError(`The index "${indexName}.${refProp}" properties must reference fields declared in "${indexName}.fields".`);
}
}
if ((indexConfig.defaultSort ?? []).some(entry => !isPlainObject(entry) || !fieldNames.has(entry.field))) {
throwSearchConfigError(`The index "${indexName}.defaultSort" properties must reference fields declared in "${indexName}.fields".`);
}
};
const validateSearchSourceBindingRegistry = (sourceBindings, propertyPath) => {
if (sourceBindings === undefined) {
return sourceBindings;
}
if (!isPlainObject(sourceBindings)) {
throwSearchConfigError(`The "${propertyPath}" property must be an object.`);
}
for (const [sourceName, sourceBinding] of Object.entries(sourceBindings)) {
validateSearchSourceBindingConfig(sourceName, sourceBinding, {
sourceRegistryPath: propertyPath
});
}
return sourceBindings;
};
const validateSearchWorkloadMapperConfig = (mapperConfig, propertyPath) => {
assertAllowedKeys(mapperConfig, propertyPath, SEARCH_WORKLOAD_MAPPER_KEYS);
assertNonEmptyString(mapperConfig.name, `${propertyPath}.name`);
if (mapperConfig.source !== undefined) {
assertStringWhenProvided(mapperConfig.source, `${propertyPath}.source`);
}
if (mapperConfig.export !== undefined) {
assertStringWhenProvided(mapperConfig.export, `${propertyPath}.export`);
}
return mapperConfig;
};
const validateSearchWorkloadIndexConfig = (indexConfig, propertyPath) => {
assertObjectWhenProvided(indexConfig, propertyPath);
assertNonEmptyString(indexConfig.name, `${propertyPath}.name`);
validateSearchIndexFields(indexConfig.name, indexConfig);
for (const arrayProp of ['queryBy', 'facets', 'filterBy', 'groupBy']) {
if (indexConfig[arrayProp] !== undefined && !isStringArray(indexConfig[arrayProp])) {
throwSearchConfigError(`The "${propertyPath}.${arrayProp}" property must be an array of strings when provided.`);
}
}
if (indexConfig.defaultSort !== undefined) {
if (!Array.isArray(indexConfig.defaultSort)) {
throwSearchConfigError(`The "${propertyPath}.defaultSort" property must be an array when provided.`);
}
for (const sortEntry of indexConfig.defaultSort) {
if (!isPlainObject(sortEntry)) {
throwSearchConfigError(`The "${propertyPath}.defaultSort" entries must be objects.`);
}
if (!isString(sortEntry.field) || sortEntry.field.trim().length === 0) {
throwSearchConfigError(`The "${propertyPath}.defaultSort[].field" property must be a non-empty string.`);
}
if (sortEntry.direction !== undefined && (!isString(sortEntry.direction) || !['asc', 'desc'].includes(sortEntry.direction.trim().toLowerCase()))) {
throwSearchConfigError(`The "${propertyPath}.defaultSort[].direction" property must be either "asc" or "desc" when provided.`);
}
}
}
return indexConfig;
};
const validateSearchFirestoreWorkloadSourceConfig = (sourceConfig, propertyPath) => {
assertAllowedKeys(sourceConfig, propertyPath, SEARCH_WORKLOAD_SOURCE_KEYS);
assertNonEmptyString(sourceConfig.name, `${propertyPath}.name`);
assertNonEmptyString(sourceConfig.type, `${propertyPath}.type`);
assertNonEmptyString(sourceConfig.syncClass, `${propertyPath}.syncClass`);
assertStringWhenProvided(sourceConfig.description, `${propertyPath}.description`);
assertStringWhenProvided(sourceConfig.routeTemplate, `${propertyPath}.routeTemplate`);
if (sourceConfig.enabled !== undefined && !isBoolean(sourceConfig.enabled)) {
throwSearchConfigError(`The "${propertyPath}.enabled" property must be a boolean when provided.`);
}
if (sourceConfig.type.trim().toLowerCase() !== 'firestore') {
throwSearchConfigError(`The "${propertyPath}.type" property must be "firestore" for Firestore workloads.`);
}
if (!SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.includes(sourceConfig.syncClass.trim().toLowerCase())) {
throwSearchConfigError(`The "${propertyPath}.syncClass" property must be one of: ${SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.join(', ')}.`);
}
if (sourceConfig.syncClass.trim().toLowerCase() !== 'delta-merge') {
throwSearchConfigError(`The "${propertyPath}" entry must use syncClass "delta-merge" when type is "firestore".`);
}
if (sourceConfig.http !== undefined || sourceConfig.sql !== undefined) {
throwSearchConfigError(`The "${propertyPath}" entry cannot define "http" or "sql" when type is "firestore".`);
}
assertObjectWhenProvided(sourceConfig.firestore, `${propertyPath}.firestore`);
if (sourceConfig.firestore === undefined) {
throwSearchConfigError(`The "${propertyPath}.firestore" property is required for Firestore workloads.`);
}
assertAllowedKeys(sourceConfig.firestore, `${propertyPath}.firestore`, SEARCH_WORKLOAD_FIRESTORE_KEYS);
assertNonEmptyString(sourceConfig.firestore.documentPathPattern, `${propertyPath}.firestore.documentPathPattern`);
assertStringWhenProvided(sourceConfig.firestore.database, `${propertyPath}.firestore.database`);
assertStringWhenProvided(sourceConfig.firestore.triggerRegion, `${propertyPath}.firestore.triggerRegion`);
return sourceConfig;
};
const validateSearchWorkloadSourceConfig = (sourceConfig, mapperConfig, indexConfig, propertyPath) => {
assertObjectWhenProvided(sourceConfig, propertyPath);
if (sourceConfig.type?.trim().toLowerCase() === 'firestore') {
return validateSearchFirestoreWorkloadSourceConfig(sourceConfig, propertyPath);
}
if (sourceConfig.enabled !== undefined) {
throwSearchConfigError(`The "${propertyPath}.enabled" property is only supported for Firestore workloads.`);
}
if (sourceConfig.firestore !== undefined) {
throwSearchConfigError(`The "${propertyPath}.firestore" property is only supported when type is "firestore".`);
}
const sharedSourceDefinition = {
...sourceConfig
};
delete sharedSourceDefinition.enabled;
delete sharedSourceDefinition.firestore;
delete sharedSourceDefinition.name;
validateSearchSourceDefinitionConfig(sourceConfig.name, {
...sharedSourceDefinition,
indexes: [indexConfig.name],
mapper: mapperConfig.name
}, {
sourceRegistryPath: propertyPath
});
return sourceConfig;
};
export const validateSearchWorkloadConfig = (config, filePath) => {
if (!isPlainObject(config)) {
throw new Error(`Invalid Atlas search workload: ${filePath}. Expected a JSON object.`);
}
const invalidKeys = Object.keys(config).filter(key => !SEARCH_WORKLOAD_FILE_KEYS.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid Atlas search workload: ${filePath}. Unsupported properties: ${invalidKeys.join(', ')}.`);
}
if (!isPlainObject(config.source)) {
throw new Error(`Invalid Atlas search workload: ${filePath}. The "source" property must be an object.`);
}
if (!isPlainObject(config.mapper)) {
throw new Error(`Invalid Atlas search workload: ${filePath}. The "mapper" property must be an object.`);
}
if (!isPlainObject(config.index)) {
throw new Error(`Invalid Atlas search workload: ${filePath}. The "index" property must be an object.`);
}
validateSearchWorkloadMapperConfig(config.mapper, `${filePath}.mapper`);
validateSearchWorkloadIndexConfig(config.index, `${filePath}.index`);
validateSearchWorkloadSourceConfig(config.source, config.mapper, config.index, `${filePath}.source`);
return config;
};
const validateSearchRegistries = (config, options = {}) => {
const {
requireCollections = false,
requireIndexes = false,
validateSources = false
} = options;
if (config.collections === undefined ? requireCollections : !isPlainObject(config.collections)) {
throwSearchConfigError('The "collections" property must be an object.');
}
if (config.indexes === undefined ? requireIndexes : !isPlainObject(config.indexes)) {
throwSearchConfigError('The "indexes" property must be an object.');
}
if (config.mappers !== undefined && !isPlainObject(config.mappers)) {
throwSearchConfigError('The "mappers" property must be an object.');
}
if (validateSources) {
if (config.sources !== undefined && !isPlainObject(config.sources)) {
throwSearchConfigError('The "sources" property must be an object.');
}
for (const [sourceName, sourceConfig] of Object.entries(config.sources ?? {})) {
validateSearchSourceConfig(sourceName, sourceConfig);
}
}
for (const [mapperName, mapperConfig] of Object.entries(config.mappers ?? {})) {
if (!isPlainObject(mapperConfig)) {
throwSearchConfigError(`The mapper "${mapperName}" must be an object.`);
}
if (mapperConfig.source !== undefined && !isString(mapperConfig.source)) {
throwSearchConfigError(`The mapper "${mapperName}.source" property must be a string when provided.`);
}
if (mapperConfig.export !== undefined && !isString(mapperConfig.export)) {
throwSearchConfigError(`The mapper "${mapperName}.export" property must be a string when provided.`);
}
}
for (const [collectionName, collectionConfig] of Object.entries(config.collections ?? {})) {
if (!isPlainObject(collectionConfig)) {
throwSearchConfigError(`The collection "${collectionName}" must be an object.`);
}
if (collectionConfig.enabled !== undefined && !isBoolean(collectionConfig.enabled)) {
throwSearchConfigError(`The collection "${collectionName}.enabled" property must be a boolean when provided.`);
}
if (collectionConfig.indexes !== undefined && !isStringArray(collectionConfig.indexes)) {
throwSearchConfigError(`The collection "${collectionName}.indexes" property must be an array of strings when provided.`);
}
if (collectionConfig.mapper !== undefined && !isString(collectionConfig.mapper)) {
throwSearchConfigError(`The collection "${collectionName}.mapper" property must be a string when provided.`);
}
if (collectionConfig.sourcePathPattern !== undefined && !isString(collectionConfig.sourcePathPattern)) {
throwSearchConfigError(`The collection "${collectionName}.sourcePathPattern" property must be a string when provided.`);
}
if (collectionConfig.routeTemplate !== undefined && !isString(collectionConfig.routeTemplate)) {
throwSearchConfigError(`The collection "${collectionName}.routeTemplate" property must be a string when provided.`);
}
}
for (const [indexName, indexConfig] of Object.entries(config.indexes ?? {})) {
if (!isPlainObject(indexConfig)) {
throwSearchConfigError(`The index "${indexName}" must be an object.`);
}
validateSearchIndexFields(indexName, indexConfig);
for (const arrayProp of ['queryBy', 'facets', 'filterBy', 'groupBy']) {
if (indexConfig[arrayProp] !== undefined && !isStringArray(indexConfig[arrayProp])) {
throwSearchConfigError(`The index "${indexName}.${arrayProp}" property must be an array of strings when provided.`);
}
}
if (indexConfig.defaultSort !== undefined) {
if (!Array.isArray(indexConfig.defaultSort)) {
throwSearchConfigError(`The index "${indexName}.defaultSort" property must be an array when provided.`);
}
for (const sortEntry of indexConfig.defaultSort) {
if (!isPlainObject(sortEntry)) {
throwSearchConfigError(`The index "${indexName}.defaultSort" entries must be objects.`);
}
if (!isString(sortEntry.field) || sortEntry.field.trim().length === 0) {
throwSearchConfigError(`The index "${indexName}.defaultSort[].field" property must be a non-empty string.`);
}
if (sortEntry.direction !== undefined && (!isString(sortEntry.direction) || !['asc', 'desc'].includes(sortEntry.direction.trim().toLowerCase()))) {
throwSearchConfigError(`The index "${indexName}.defaultSort[].direction" property must be either "asc" or "desc" when provided.`);
}
}
}
}
return config;
};
export const validateSearchRootSection = config => {
if (!isPlainObject(config)) {
throw new Error('Invalid Atlas config. The "search" section must be an object.');
}
const invalidKeys = Object.keys(config).filter(key => !SEARCH_ROOT_SECTION_KEYS.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid Atlas search config. Unsupported root search properties: ${invalidKeys.join(', ')}.`);
}
if (!isStringArray(config.workloads) || config.workloads.length === 0) {
throw new Error('Invalid Atlas search config. The "workloads" property must be a non-empty array of strings.');
}
if (config.projects !== undefined) {
assertObjectWhenProvided(config.projects, 'projects');
for (const [projectId, projectConfig] of Object.entries(config.projects ?? {})) {
validateSearchProjectSection(projectConfig, projectId);
}
}
return validateSearchRuntimeSettings(config);
};
export const validateSearchProjectSection = (config, projectId = 'project') => {
if (!isPlainObject(config)) {
throw new Error(`Invalid Atlas search config. The "projects.${projectId}" property must be an object.`);
}
const invalidKeys = Object.keys(config).filter(key => !SEARCH_PROJECT_SECTION_KEYS.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid Atlas search config. Unsupported project search properties for "${projectId}": ${invalidKeys.join(', ')}.`);
}
if (config.environment !== undefined && !isString(config.environment)) {
throw new Error(`Invalid Atlas search config. The "projects.${projectId}.environment" property must be a string when provided.`);
}
validateSearchSourceBindingRegistry(config.sourceBindings, `projects.${projectId}.sourceBindings`);
const {
environment,
sourceBindings,
...runtimeConfig
} = config;
return {
...(environment !== undefined ? {
environment
} : {}),
...(sourceBindings !== undefined ? {
sourceBindings
} : {}),
...validateSearchRuntimeSettings(runtimeConfig)
};
};
export const validateResolvedSearchConfig = config => {
if (!isPlainObject(config)) {
throw new Error('Invalid Atlas search config. Expected a resolved search config object.');
}
if (!Number.isInteger(config.version) || config.version < 1) {
throw new Error('Invalid Atlas search config. The resolved "version" property must be a positive integer.');
}
validateSearchRuntimeSettings(config);
validateSearchRegistries(config, {
requireCollections: true,
requireIndexes: true,
validateSources: true
});
return config;
};
export const validateSearchFragmentConfig = validateSearchWorkloadConfig;