@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
700 lines • 47.7 kB
JavaScript
import { isBoolean, isString } from 'es-toolkit/compat';
import { cloneDeep as clone } from 'es-toolkit/object';
import { isPlainObject } from 'es-toolkit/predicate';
import { isStringArray } from '../../../utils/value.js';
export const SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES = ['append-only', 'delta-merge', 'snapshot-replace'];
export const SUPPORTED_SEARCH_SOURCE_TYPES = ['firestore', 'http', 'sql'];
export const SUPPORTED_PROJECT_SCOPED_SEARCH_SOURCE_TYPES = ['http', 'sql'];
const SUPPORTED_HTTP_INCREMENTAL_STRATEGIES = ['cursor'];
const SUPPORTED_HTTP_REQUEST_METHODS = ['get', 'post'];
const SHARED_HTTP_SOURCE_DEFINITION_ALLOWED_KEYS = ['body', 'headers', 'incremental', 'method', 'query'];
const SHARED_SEARCH_SOURCE_DEFINITION_ALLOWED_KEYS = ['description', 'http', 'indexes', 'mapper', 'routeTemplate', 'sql', 'syncClass', 'type'];
const SUPPORTED_SEARCH_SOURCE_PROMOTION_MODES = ['automatic', 'manual'];
const SHARED_SQL_SOURCE_DEFINITION_ALLOWED_KEYS = ['driver', 'incremental', 'promotion', 'snapshot'];
const PROJECT_HTTP_SOURCE_BINDING_ALLOWED_KEYS = ['connectionSecret', 'url'];
const PROJECT_SEARCH_SOURCE_BINDING_ALLOWED_KEYS = ['enabled', 'http', 'schedule', 'sql'];
const PROJECT_SQL_SOURCE_BINDING_ALLOWED_KEYS = ['connectionSecret'];
const SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES = ['none', 'soft-delete'];
const SUPPORTED_SQL_INCREMENTAL_STRATEGIES = ['watermark'];
const throwSearchConfigError = message => {
throw new Error(`Invalid Atlas search config. ${message}`);
};
const assertAllowedKeys = (value, propertyPath, allowedKeys) => {
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 assertAllowedStringValue = (value, propertyPath, allowedValues) => {
if (value === undefined) {
return;
}
if (!isString(value) || !allowedValues.includes(value.trim().toLowerCase())) {
throwSearchConfigError(`The "${propertyPath}" property must be one of: ${allowedValues.join(', ')}.`);
}
};
const assertObjectWhenProvided = (value, propertyPath) => {
if (value !== undefined && !isPlainObject(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be an object when provided.`);
}
};
const assertPositiveIntegerWhenProvided = (value, propertyPath) => {
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
throwSearchConfigError(`The "${propertyPath}" property must be a positive integer when provided.`);
}
};
const assertStringArrayWhenProvided = (value, propertyPath) => {
if (value !== undefined && !isStringArray(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be an array of strings 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 assertStringRecordWhenProvided = (value, propertyPath) => {
if (value === undefined) {
return;
}
if (!isPlainObject(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be an object when provided.`);
}
for (const [entryKey, entryValue] of Object.entries(value)) {
if (!isString(entryValue)) {
throwSearchConfigError(`The "${propertyPath}.${entryKey}" property must be a string when provided.`);
}
}
};
const assertStringWhenProvided = (value, propertyPath) => {
if (value !== undefined && !isString(value)) {
throwSearchConfigError(`The "${propertyPath}" property must be a string when provided.`);
}
};
const getNormalizedOptionalString = value => isString(value) && value.trim().length > 0 ? value.trim().toLowerCase() : null;
const toCamelCase = value => isString(value) ? value.trim().toLowerCase().replace(/[_-]+([a-z0-9])/g, (_, character) => character.toUpperCase()) : null;
const createSqlParameterAliases = (fieldName, genericAliases = []) => [...new Set([fieldName, `cursor_${fieldName}`, ...genericAliases, toCamelCase(fieldName), toCamelCase(`cursor_${fieldName}`)].filter(Boolean))];
const containsAnySqlPlaceholder = (sql, aliases) => aliases.some(alias => new RegExp(`@${alias}(?![A-Za-z0-9_])`, 'u').test(sql));
const cloneWhenProvided = value => value === undefined ? undefined : clone(value);
const createCanonicalSqlIncrementalConfig = incrementalConfig => {
if (!isPlainObject(incrementalConfig)) {
return undefined;
}
const resolvedIncrementalConfig = clone(incrementalConfig);
if (resolvedIncrementalConfig.query !== undefined) {
resolvedIncrementalConfig.sql = resolvedIncrementalConfig.query;
delete resolvedIncrementalConfig.query;
}
return resolvedIncrementalConfig;
};
const createCanonicalSqlSnapshotConfig = snapshotConfig => {
if (!isPlainObject(snapshotConfig)) {
return undefined;
}
const resolvedSnapshotConfig = clone(snapshotConfig);
if (resolvedSnapshotConfig.query !== undefined) {
resolvedSnapshotConfig.sql = resolvedSnapshotConfig.query;
delete resolvedSnapshotConfig.query;
}
return resolvedSnapshotConfig;
};
const resolveSearchSourceBindingType = (sourcePath, sourceBinding, sourceType = null) => {
const configuredBindingTypes = SUPPORTED_PROJECT_SCOPED_SEARCH_SOURCE_TYPES.filter(bindingType => sourceBinding?.[bindingType] !== undefined);
if (configuredBindingTypes.length !== 1) {
throwSearchConfigError(`The "${sourcePath}" entry must define exactly one type-specific binding via "http" or "sql".`);
}
const resolvedBindingType = configuredBindingTypes[0];
if (sourceType && resolvedBindingType !== sourceType) {
throwSearchConfigError(`The "${sourcePath}" entry must use the "${sourceType}" binding shape because its shared source definition is type "${sourceType}".`);
}
return resolvedBindingType;
};
const validateSqlDefinitionIncrementalConfig = (sourcePath, sourceDefinition) => {
const incrementalConfig = sourceDefinition?.sql?.incremental;
if (incrementalConfig === undefined) {
return;
}
assertObjectWhenProvided(incrementalConfig, `${sourcePath}.sql.incremental`);
assertStringWhenProvided(incrementalConfig.strategy, `${sourcePath}.sql.incremental.strategy`);
assertStringWhenProvided(incrementalConfig.query, `${sourcePath}.sql.incremental.query`);
assertAllowedStringValue(incrementalConfig.deleteStrategy, `${sourcePath}.sql.incremental.deleteStrategy`, SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES);
assertStringWhenProvided(incrementalConfig.deletedColumn, `${sourcePath}.sql.incremental.deletedColumn`);
assertStringWhenProvided(incrementalConfig.versionColumn, `${sourcePath}.sql.incremental.versionColumn`);
assertStringWhenProvided(incrementalConfig.idColumn, `${sourcePath}.sql.incremental.idColumn`);
assertStringWhenProvided(incrementalConfig.safetyLag, `${sourcePath}.sql.incremental.safetyLag`);
assertPositiveIntegerWhenProvided(incrementalConfig.batchSize, `${sourcePath}.sql.incremental.batchSize`);
if (incrementalConfig.cursor !== undefined) {
assertObjectWhenProvided(incrementalConfig.cursor, `${sourcePath}.sql.incremental.cursor`);
assertStringWhenProvided(incrementalConfig.cursor.versionColumn, `${sourcePath}.sql.incremental.cursor.versionColumn`);
assertStringWhenProvided(incrementalConfig.cursor.idColumn, `${sourcePath}.sql.incremental.cursor.idColumn`);
}
};
const validateSqlDefinitionSnapshotConfig = (sourcePath, sourceDefinition) => {
const snapshotConfig = sourceDefinition?.sql?.snapshot;
if (snapshotConfig === undefined) {
return;
}
assertObjectWhenProvided(snapshotConfig, `${sourcePath}.sql.snapshot`);
assertStringWhenProvided(snapshotConfig.query, `${sourcePath}.sql.snapshot.query`);
if (snapshotConfig.pagination !== undefined) {
assertObjectWhenProvided(snapshotConfig.pagination, `${sourcePath}.sql.snapshot.pagination`);
assertStringWhenProvided(snapshotConfig.pagination.mode, `${sourcePath}.sql.snapshot.pagination.mode`);
assertStringWhenProvided(snapshotConfig.pagination.keyColumn, `${sourcePath}.sql.snapshot.pagination.keyColumn`);
assertPositiveIntegerWhenProvided(snapshotConfig.pagination.batchSize, `${sourcePath}.sql.snapshot.pagination.batchSize`);
}
};
const validateSqlDefinitionPromotionConfig = (sourcePath, sourceDefinition) => {
const promotionConfig = sourceDefinition?.sql?.promotion;
if (promotionConfig === undefined) {
return;
}
assertObjectWhenProvided(promotionConfig, `${sourcePath}.sql.promotion`);
assertAllowedStringValue(promotionConfig.mode, `${sourcePath}.sql.promotion.mode`, SUPPORTED_SEARCH_SOURCE_PROMOTION_MODES);
assertStringWhenProvided(promotionConfig.rollbackGrace, `${sourcePath}.sql.promotion.rollbackGrace`);
};
const validateHttpDefinitionConfig = (sourcePath, sourceDefinition) => {
const httpConfig = sourceDefinition?.http;
if (httpConfig === undefined) {
throwSearchConfigError(`The "${sourcePath}.http" property is required for http source definitions.`);
}
assertObjectWhenProvided(httpConfig, `${sourcePath}.http`);
assertAllowedKeys(httpConfig, `${sourcePath}.http`, SHARED_HTTP_SOURCE_DEFINITION_ALLOWED_KEYS);
assertAllowedStringValue(httpConfig.method, `${sourcePath}.http.method`, SUPPORTED_HTTP_REQUEST_METHODS);
assertStringRecordWhenProvided(httpConfig.headers, `${sourcePath}.http.headers`);
assertStringRecordWhenProvided(httpConfig.query, `${sourcePath}.http.query`);
if (httpConfig.body !== undefined && !isPlainObject(httpConfig.body)) {
throwSearchConfigError(`The "${sourcePath}.http.body" property must be an object when provided.`);
}
if (!getNormalizedOptionalString(httpConfig.method)) {
throwSearchConfigError(`The "${sourcePath}.http.method" property must be a non-empty string for http source definitions.`);
}
if (getNormalizedOptionalString(httpConfig.method) === 'get' && httpConfig.body !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http.body" property is not supported when http.method is "GET".`);
}
};
const validateHttpDefinitionIncrementalConfig = (sourcePath, sourceDefinition) => {
const httpConfig = sourceDefinition?.http;
const incrementalConfig = httpConfig?.incremental;
const requestMethod = getNormalizedOptionalString(httpConfig?.method);
const syncClass = getNormalizedOptionalString(sourceDefinition?.syncClass);
if (incrementalConfig === undefined) {
throwSearchConfigError(`The "${sourcePath}.http.incremental" property is required for http source definitions.`);
}
assertObjectWhenProvided(incrementalConfig, `${sourcePath}.http.incremental`);
assertAllowedStringValue(incrementalConfig.strategy, `${sourcePath}.http.incremental.strategy`, SUPPORTED_HTTP_INCREMENTAL_STRATEGIES);
assertObjectWhenProvided(incrementalConfig.cursor, `${sourcePath}.http.incremental.cursor`);
assertObjectWhenProvided(incrementalConfig.cursor?.request, `${sourcePath}.http.incremental.cursor.request`);
assertObjectWhenProvided(incrementalConfig.cursor?.response, `${sourcePath}.http.incremental.cursor.response`);
assertObjectWhenProvided(incrementalConfig.response, `${sourcePath}.http.incremental.response`);
assertObjectWhenProvided(incrementalConfig.mapping, `${sourcePath}.http.incremental.mapping`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.bodyPath, `${sourcePath}.http.incremental.cursor.request.bodyPath`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.headerName, `${sourcePath}.http.incremental.cursor.request.headerName`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.queryParam, `${sourcePath}.http.incremental.cursor.request.queryParam`);
assertStringWhenProvided(incrementalConfig.cursor?.response?.hasMorePath, `${sourcePath}.http.incremental.cursor.response.hasMorePath`);
assertStringWhenProvided(incrementalConfig.cursor?.response?.nextCursorPath, `${sourcePath}.http.incremental.cursor.response.nextCursorPath`);
assertStringWhenProvided(incrementalConfig.response?.itemsPath, `${sourcePath}.http.incremental.response.itemsPath`);
assertStringWhenProvided(incrementalConfig.mapping?.afterPath, `${sourcePath}.http.incremental.mapping.afterPath`);
assertStringWhenProvided(incrementalConfig.mapping?.beforePath, `${sourcePath}.http.incremental.mapping.beforePath`);
assertStringWhenProvided(incrementalConfig.mapping?.deletedPath, `${sourcePath}.http.incremental.mapping.deletedPath`);
assertStringWhenProvided(incrementalConfig.mapping?.idPath, `${sourcePath}.http.incremental.mapping.idPath`);
assertStringWhenProvided(incrementalConfig.mapping?.versionPath, `${sourcePath}.http.incremental.mapping.versionPath`);
for (const unsupportedIncrementalProperty of ['batchSize', 'deleteStrategy', 'deletedColumn', 'idColumn', 'sql', 'versionColumn']) {
if (incrementalConfig[unsupportedIncrementalProperty] !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.${unsupportedIncrementalProperty}" property is not supported for http source definitions.`);
}
}
if (!getNormalizedOptionalString(incrementalConfig.strategy)) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.strategy" property must be a non-empty string for http source definitions.`);
}
const cursorTransportCount = [incrementalConfig.cursor?.request?.queryParam, incrementalConfig.cursor?.request?.headerName, incrementalConfig.cursor?.request?.bodyPath].filter(value => getNormalizedOptionalString(value) !== null).length;
if (cursorTransportCount !== 1) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.cursor.request" property must define exactly one cursor transport via queryParam, headerName, or bodyPath.`);
}
if (requestMethod === 'get' && getNormalizedOptionalString(incrementalConfig.cursor?.request?.bodyPath) !== null) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.cursor.request.bodyPath" property is not supported when http.method is "GET".`);
}
for (const [requiredPath, requiredValue] of [['http.incremental.cursor.response.nextCursorPath', incrementalConfig.cursor?.response?.nextCursorPath], ['http.incremental.response.itemsPath', incrementalConfig.response?.itemsPath], ['http.incremental.mapping.afterPath', incrementalConfig.mapping?.afterPath], ['http.incremental.mapping.idPath', incrementalConfig.mapping?.idPath], ['http.incremental.mapping.versionPath', incrementalConfig.mapping?.versionPath]]) {
if (!getNormalizedOptionalString(requiredValue)) {
throwSearchConfigError(`The "${sourcePath}.${requiredPath}" property must be a non-empty string for http source definitions.`);
}
}
if (syncClass === 'append-only') {
if (incrementalConfig.mapping?.beforePath !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.mapping.beforePath" property is not supported when syncClass is "append-only".`);
}
if (incrementalConfig.mapping?.deletedPath !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http.incremental.mapping.deletedPath" property is not supported when syncClass is "append-only".`);
}
}
};
export const validateSearchSourceDefinitionConfig = (sourceName, sourceDefinition, options = {}) => {
const sourceRegistryPath = options.sourceRegistryPath ?? 'workload.source';
const sourcePath = `${sourceRegistryPath}.${sourceName}`;
if (!isPlainObject(sourceDefinition)) {
throwSearchConfigError(`The "${sourcePath}" entry must be an object.`);
}
assertAllowedKeys(sourceDefinition, sourcePath, SHARED_SEARCH_SOURCE_DEFINITION_ALLOWED_KEYS);
assertNonEmptyString(sourceDefinition.type, `${sourcePath}.type`);
assertNonEmptyString(sourceDefinition.syncClass, `${sourcePath}.syncClass`);
const sourceType = sourceDefinition.type.trim().toLowerCase();
const syncClass = sourceDefinition.syncClass.trim().toLowerCase();
if (!SUPPORTED_PROJECT_SCOPED_SEARCH_SOURCE_TYPES.includes(sourceType)) {
throwSearchConfigError(`The "${sourcePath}.type" property must be one of: ${SUPPORTED_PROJECT_SCOPED_SEARCH_SOURCE_TYPES.join(', ')}.`);
}
if (!SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.includes(syncClass)) {
throwSearchConfigError(`The "${sourcePath}.syncClass" property must be one of: ${SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.join(', ')}.`);
}
if (!isStringArray(sourceDefinition.indexes) || sourceDefinition.indexes.length === 0) {
throwSearchConfigError(`The "${sourcePath}.indexes" property must be a non-empty array of strings.`);
}
assertNonEmptyString(sourceDefinition.mapper, `${sourcePath}.mapper`);
assertStringWhenProvided(sourceDefinition.description, `${sourcePath}.description`);
assertStringWhenProvided(sourceDefinition.routeTemplate, `${sourcePath}.routeTemplate`);
if (sourceType === 'sql') {
if (sourceDefinition.http !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http" property is not supported when type is "sql".`);
}
assertObjectWhenProvided(sourceDefinition.sql, `${sourcePath}.sql`);
if (sourceDefinition.sql === undefined) {
throwSearchConfigError(`The "${sourcePath}.sql" property is required for sql source definitions.`);
}
assertAllowedKeys(sourceDefinition.sql, `${sourcePath}.sql`, SHARED_SQL_SOURCE_DEFINITION_ALLOWED_KEYS);
assertNonEmptyString(sourceDefinition.sql.driver, `${sourcePath}.sql.driver`);
validateSqlDefinitionIncrementalConfig(sourcePath, sourceDefinition);
validateSqlDefinitionSnapshotConfig(sourcePath, sourceDefinition);
validateSqlDefinitionPromotionConfig(sourcePath, sourceDefinition);
if (syncClass === 'snapshot-replace') {
if (sourceDefinition.sql.incremental !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental" property is not supported when syncClass is "snapshot-replace".`);
}
if (sourceDefinition.sql.snapshot === undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.snapshot" property is required when syncClass is "snapshot-replace".`);
}
} else {
if (sourceDefinition.sql.snapshot !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.snapshot" property is only supported when syncClass is "snapshot-replace".`);
}
if (sourceDefinition.sql.incremental === undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental" property is required for sql source definitions unless syncClass is "snapshot-replace".`);
}
const cursorConfig = isPlainObject(sourceDefinition.sql.incremental?.cursor) ? sourceDefinition.sql.incremental.cursor : sourceDefinition.sql.incremental;
const deleteStrategy = getNormalizedOptionalString(sourceDefinition.sql.incremental?.deleteStrategy) ?? 'none';
const incrementalStrategy = getNormalizedOptionalString(sourceDefinition.sql.incremental?.strategy) ?? null;
const deletedColumn = isString(sourceDefinition.sql.incremental?.deletedColumn) && sourceDefinition.sql.incremental.deletedColumn.trim().length > 0 ? sourceDefinition.sql.incremental.deletedColumn.trim() : null;
if (!isString(cursorConfig.versionColumn) || cursorConfig.versionColumn.trim().length === 0) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.cursor.versionColumn" property must be a non-empty string for incremental sql source definitions.`);
}
if (!isString(cursorConfig.idColumn) || cursorConfig.idColumn.trim().length === 0) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.cursor.idColumn" property must be a non-empty string for incremental sql source definitions.`);
}
const sqlQuery = sourceDefinition.sql.incremental.query;
const versionAliases = createSqlParameterAliases(cursorConfig.versionColumn, ['version', 'cursor_version']);
const idAliases = createSqlParameterAliases(cursorConfig.idColumn, ['id', 'cursor_id']);
if (!containsAnySqlPlaceholder(sqlQuery, versionAliases)) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.query" property must reference the configured version cursor placeholder.`);
}
if (!containsAnySqlPlaceholder(sqlQuery, idAliases)) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.query" property must reference the configured id cursor placeholder.`);
}
if (!containsAnySqlPlaceholder(sqlQuery, ['batchSize', 'batch_size', 'limit'])) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.query" property must reference a batch-size placeholder such as @limit.`);
}
if (!SUPPORTED_SQL_INCREMENTAL_STRATEGIES.includes(incrementalStrategy)) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.strategy" property must be one of: ${SUPPORTED_SQL_INCREMENTAL_STRATEGIES.join(', ')}.`);
}
if (syncClass === 'append-only') {
if (sourceDefinition.sql.incremental.deleteStrategy !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.deleteStrategy" property is not supported when syncClass is "append-only".`);
}
if (sourceDefinition.sql.incremental.deletedColumn !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.deletedColumn" property is not supported when syncClass is "append-only".`);
}
}
if (syncClass === 'delta-merge') {
if (!SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES.includes(deleteStrategy)) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.deleteStrategy" property must be one of: ${SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES.join(', ')}.`);
}
if (deleteStrategy === 'soft-delete' && deletedColumn === null) {
throwSearchConfigError(`The "${sourcePath}.sql.incremental.deletedColumn" property is required when deleteStrategy is "soft-delete".`);
}
}
}
if (sourceDefinition.sql.promotion !== undefined && syncClass !== 'snapshot-replace') {
throwSearchConfigError(`The "${sourcePath}.sql.promotion" property is only supported when syncClass is "snapshot-replace".`);
}
return sourceDefinition;
}
if (sourceDefinition.sql !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql" property is not supported when type is "http".`);
}
validateHttpDefinitionConfig(sourcePath, sourceDefinition);
validateHttpDefinitionIncrementalConfig(sourcePath, sourceDefinition);
if (syncClass !== 'delta-merge' && syncClass !== 'append-only') {
throwSearchConfigError(`The "${sourcePath}" entry must use syncClass "delta-merge" or "append-only" when type is "http".`);
}
return sourceDefinition;
};
export const validateSearchSourceBindingConfig = (sourceName, sourceBinding, options = {}) => {
const sourceRegistryPath = options.sourceRegistryPath ?? 'projects.project.sourceBindings';
const sourcePath = `${sourceRegistryPath}.${sourceName}`;
const resolvedSourceType = getNormalizedOptionalString(options.sourceType);
if (!isPlainObject(sourceBinding)) {
throwSearchConfigError(`The "${sourcePath}" entry must be an object.`);
}
assertAllowedKeys(sourceBinding, sourcePath, PROJECT_SEARCH_SOURCE_BINDING_ALLOWED_KEYS);
if (sourceBinding.enabled !== undefined && !isBoolean(sourceBinding.enabled)) {
throwSearchConfigError(`The "${sourcePath}.enabled" property must be a boolean when provided.`);
}
assertNonEmptyString(sourceBinding.schedule, `${sourcePath}.schedule`);
const sourceType = resolveSearchSourceBindingType(sourcePath, sourceBinding, resolvedSourceType);
if (sourceType === 'sql') {
assertObjectWhenProvided(sourceBinding.sql, `${sourcePath}.sql`);
if (sourceBinding.sql === undefined) {
throwSearchConfigError(`The "${sourcePath}.sql" property is required for sql source bindings.`);
}
assertAllowedKeys(sourceBinding.sql, `${sourcePath}.sql`, PROJECT_SQL_SOURCE_BINDING_ALLOWED_KEYS);
assertNonEmptyString(sourceBinding.sql.connectionSecret, `${sourcePath}.sql.connectionSecret`);
if (sourceBinding.http !== undefined) {
throwSearchConfigError(`The "${sourcePath}.http" property is not supported when the binding type is "sql".`);
}
return sourceBinding;
}
assertObjectWhenProvided(sourceBinding.http, `${sourcePath}.http`);
if (sourceBinding.http === undefined) {
throwSearchConfigError(`The "${sourcePath}.http" property is required for http source bindings.`);
}
assertAllowedKeys(sourceBinding.http, `${sourcePath}.http`, PROJECT_HTTP_SOURCE_BINDING_ALLOWED_KEYS);
assertNonEmptyString(sourceBinding.http.url, `${sourcePath}.http.url`);
assertStringWhenProvided(sourceBinding.http.connectionSecret, `${sourcePath}.http.connectionSecret`);
if (sourceBinding.sql !== undefined) {
throwSearchConfigError(`The "${sourcePath}.sql" property is not supported when the binding type is "http".`);
}
return sourceBinding;
};
export const resolveProjectScopedSearchSourceConfig = (sourceName, sourceDefinition, sourceBinding) => {
validateSearchSourceDefinitionConfig(sourceName, sourceDefinition);
validateSearchSourceBindingConfig(sourceName, sourceBinding, {
sourceRegistryPath: 'projects.project.sourceBindings',
sourceType: sourceDefinition?.type
});
const sourceType = getNormalizedOptionalString(sourceDefinition.type);
if (sourceType === 'sql') {
const canonicalSourceConfig = {
connectionSecret: sourceBinding.sql.connectionSecret,
description: sourceDefinition.description,
driver: sourceDefinition.sql.driver,
enabled: sourceBinding.enabled !== false,
incremental: createCanonicalSqlIncrementalConfig(sourceDefinition.sql.incremental),
indexes: clone(sourceDefinition.indexes ?? []),
mapper: sourceDefinition.mapper,
promotion: cloneWhenProvided(sourceDefinition.sql.promotion),
routeTemplate: sourceDefinition.routeTemplate,
schedule: sourceBinding.schedule,
snapshot: createCanonicalSqlSnapshotConfig(sourceDefinition.sql.snapshot),
syncClass: sourceDefinition.syncClass,
type: sourceDefinition.type
};
validateSearchSourceConfig(sourceName, canonicalSourceConfig);
return canonicalSourceConfig;
}
const canonicalSourceConfig = {
connectionSecret: sourceBinding.http.connectionSecret,
description: sourceDefinition.description,
enabled: sourceBinding.enabled !== false,
incremental: cloneWhenProvided(sourceDefinition.http.incremental),
indexes: clone(sourceDefinition.indexes ?? []),
mapper: sourceDefinition.mapper,
request: {
body: cloneWhenProvided(sourceDefinition.http.body),
headers: cloneWhenProvided(sourceDefinition.http.headers),
method: sourceDefinition.http.method,
query: cloneWhenProvided(sourceDefinition.http.query),
url: sourceBinding.http.url
},
routeTemplate: sourceDefinition.routeTemplate,
schedule: sourceBinding.schedule,
syncClass: sourceDefinition.syncClass,
type: sourceDefinition.type
};
validateSearchSourceConfig(sourceName, canonicalSourceConfig);
return canonicalSourceConfig;
};
const validateSearchSourceEventarcConfig = (sourceName, sourceConfig) => {
if (sourceConfig.eventarc === undefined) {
return;
}
assertObjectWhenProvided(sourceConfig.eventarc, `sources.${sourceName}.eventarc`);
assertStringWhenProvided(sourceConfig.eventarc.documentPathPattern, `sources.${sourceName}.eventarc.documentPathPattern`);
assertStringOrNullWhenProvided(sourceConfig.eventarc.database, `sources.${sourceName}.eventarc.database`);
assertStringOrNullWhenProvided(sourceConfig.eventarc.triggerRegion, `sources.${sourceName}.eventarc.triggerRegion`);
};
const validateSqlSearchSourceIncrementalConfig = (sourceName, sourceConfig) => {
if (sourceConfig.incremental === undefined) {
return;
}
assertObjectWhenProvided(sourceConfig.incremental, `sources.${sourceName}.incremental`);
assertStringWhenProvided(sourceConfig.incremental.strategy, `sources.${sourceName}.incremental.strategy`);
assertStringWhenProvided(sourceConfig.incremental.sql, `sources.${sourceName}.incremental.sql`);
assertAllowedStringValue(sourceConfig.incremental.deleteStrategy, `sources.${sourceName}.incremental.deleteStrategy`, SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES);
assertStringWhenProvided(sourceConfig.incremental.deletedColumn, `sources.${sourceName}.incremental.deletedColumn`);
assertStringWhenProvided(sourceConfig.incremental.versionColumn, `sources.${sourceName}.incremental.versionColumn`);
assertStringWhenProvided(sourceConfig.incremental.idColumn, `sources.${sourceName}.incremental.idColumn`);
assertStringWhenProvided(sourceConfig.incremental.safetyLag, `sources.${sourceName}.incremental.safetyLag`);
assertPositiveIntegerWhenProvided(sourceConfig.incremental.batchSize, `sources.${sourceName}.incremental.batchSize`);
if (sourceConfig.incremental.cursor !== undefined) {
assertObjectWhenProvided(sourceConfig.incremental.cursor, `sources.${sourceName}.incremental.cursor`);
assertStringWhenProvided(sourceConfig.incremental.cursor.versionColumn, `sources.${sourceName}.incremental.cursor.versionColumn`);
assertStringWhenProvided(sourceConfig.incremental.cursor.idColumn, `sources.${sourceName}.incremental.cursor.idColumn`);
}
};
const validateSearchSourceSnapshotConfig = (sourceName, sourceConfig) => {
if (sourceConfig.snapshot === undefined) {
return;
}
assertObjectWhenProvided(sourceConfig.snapshot, `sources.${sourceName}.snapshot`);
assertStringWhenProvided(sourceConfig.snapshot.sql, `sources.${sourceName}.snapshot.sql`);
if (sourceConfig.snapshot.pagination !== undefined) {
assertObjectWhenProvided(sourceConfig.snapshot.pagination, `sources.${sourceName}.snapshot.pagination`);
assertStringWhenProvided(sourceConfig.snapshot.pagination.mode, `sources.${sourceName}.snapshot.pagination.mode`);
assertStringWhenProvided(sourceConfig.snapshot.pagination.keyColumn, `sources.${sourceName}.snapshot.pagination.keyColumn`);
assertPositiveIntegerWhenProvided(sourceConfig.snapshot.pagination.batchSize, `sources.${sourceName}.snapshot.pagination.batchSize`);
}
};
const validateSearchSourcePromotionConfig = (sourceName, sourceConfig) => {
if (sourceConfig.promotion === undefined) {
return;
}
assertObjectWhenProvided(sourceConfig.promotion, `sources.${sourceName}.promotion`);
assertAllowedStringValue(sourceConfig.promotion.mode, `sources.${sourceName}.promotion.mode`, SUPPORTED_SEARCH_SOURCE_PROMOTION_MODES);
assertStringWhenProvided(sourceConfig.promotion.rollbackGrace, `sources.${sourceName}.promotion.rollbackGrace`);
};
const validateHttpSearchSourceRequestConfig = (sourceName, sourceConfig) => {
if (sourceConfig.request === undefined) {
throwSearchConfigError(`The source "${sourceName}.request" property is required for http sources.`);
}
assertObjectWhenProvided(sourceConfig.request, `sources.${sourceName}.request`);
assertAllowedStringValue(sourceConfig.request.method, `sources.${sourceName}.request.method`, SUPPORTED_HTTP_REQUEST_METHODS);
assertStringWhenProvided(sourceConfig.request.url, `sources.${sourceName}.request.url`);
assertStringRecordWhenProvided(sourceConfig.request.headers, `sources.${sourceName}.request.headers`);
assertStringRecordWhenProvided(sourceConfig.request.query, `sources.${sourceName}.request.query`);
if (sourceConfig.request.body !== undefined && !isPlainObject(sourceConfig.request.body)) {
throwSearchConfigError(`The "sources.${sourceName}.request.body" property must be an object when provided.`);
}
if (!getNormalizedOptionalString(sourceConfig.request.method)) {
throwSearchConfigError(`The source "${sourceName}.request.method" property must be a non-empty string for http sources.`);
}
if (!getNormalizedOptionalString(sourceConfig.request.url)) {
throwSearchConfigError(`The source "${sourceName}.request.url" property must be a non-empty string for http sources.`);
}
if (getNormalizedOptionalString(sourceConfig.request.method) === 'get' && sourceConfig.request.body !== undefined) {
throwSearchConfigError(`The source "${sourceName}.request.body" property is not supported when request.method is "GET".`);
}
};
const validateHttpSearchSourceIncrementalConfig = (sourceName, sourceConfig) => {
if (sourceConfig.incremental === undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental" property is required for http sources.`);
}
const requestMethod = getNormalizedOptionalString(sourceConfig.request?.method);
const syncClass = getNormalizedOptionalString(sourceConfig.syncClass);
const incrementalConfig = sourceConfig.incremental;
assertObjectWhenProvided(incrementalConfig, `sources.${sourceName}.incremental`);
assertAllowedStringValue(incrementalConfig.strategy, `sources.${sourceName}.incremental.strategy`, SUPPORTED_HTTP_INCREMENTAL_STRATEGIES);
assertObjectWhenProvided(incrementalConfig.cursor, `sources.${sourceName}.incremental.cursor`);
assertObjectWhenProvided(incrementalConfig.cursor?.request, `sources.${sourceName}.incremental.cursor.request`);
assertObjectWhenProvided(incrementalConfig.cursor?.response, `sources.${sourceName}.incremental.cursor.response`);
assertObjectWhenProvided(incrementalConfig.response, `sources.${sourceName}.incremental.response`);
assertObjectWhenProvided(incrementalConfig.mapping, `sources.${sourceName}.incremental.mapping`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.bodyPath, `sources.${sourceName}.incremental.cursor.request.bodyPath`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.headerName, `sources.${sourceName}.incremental.cursor.request.headerName`);
assertStringWhenProvided(incrementalConfig.cursor?.request?.queryParam, `sources.${sourceName}.incremental.cursor.request.queryParam`);
assertStringWhenProvided(incrementalConfig.cursor?.response?.hasMorePath, `sources.${sourceName}.incremental.cursor.response.hasMorePath`);
assertStringWhenProvided(incrementalConfig.cursor?.response?.nextCursorPath, `sources.${sourceName}.incremental.cursor.response.nextCursorPath`);
assertStringWhenProvided(incrementalConfig.response?.itemsPath, `sources.${sourceName}.incremental.response.itemsPath`);
assertStringWhenProvided(incrementalConfig.mapping?.afterPath, `sources.${sourceName}.incremental.mapping.afterPath`);
assertStringWhenProvided(incrementalConfig.mapping?.beforePath, `sources.${sourceName}.incremental.mapping.beforePath`);
assertStringWhenProvided(incrementalConfig.mapping?.deletedPath, `sources.${sourceName}.incremental.mapping.deletedPath`);
assertStringWhenProvided(incrementalConfig.mapping?.idPath, `sources.${sourceName}.incremental.mapping.idPath`);
assertStringWhenProvided(incrementalConfig.mapping?.versionPath, `sources.${sourceName}.incremental.mapping.versionPath`);
for (const unsupportedIncrementalProperty of ['batchSize', 'deleteStrategy', 'deletedColumn', 'idColumn', 'sql', 'versionColumn']) {
if (incrementalConfig[unsupportedIncrementalProperty] !== undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental.${unsupportedIncrementalProperty}" property is not supported for http sources.`);
}
}
if (!getNormalizedOptionalString(incrementalConfig.strategy)) {
throwSearchConfigError(`The source "${sourceName}.incremental.strategy" property must be a non-empty string for http sources.`);
}
const cursorTransportCount = [incrementalConfig.cursor?.request?.queryParam, incrementalConfig.cursor?.request?.headerName, incrementalConfig.cursor?.request?.bodyPath].filter(value => getNormalizedOptionalString(value) !== null).length;
if (cursorTransportCount !== 1) {
throwSearchConfigError(`The source "${sourceName}.incremental.cursor.request" property must define exactly one cursor transport via queryParam, headerName, or bodyPath.`);
}
if (requestMethod === 'get' && getNormalizedOptionalString(incrementalConfig.cursor?.request?.bodyPath) !== null) {
throwSearchConfigError(`The source "${sourceName}.incremental.cursor.request.bodyPath" property is not supported when request.method is "GET".`);
}
for (const requiredPropertyPath of [['incremental.cursor.response.nextCursorPath', incrementalConfig.cursor?.response?.nextCursorPath], ['incremental.response.itemsPath', incrementalConfig.response?.itemsPath], ['incremental.mapping.afterPath', incrementalConfig.mapping?.afterPath], ['incremental.mapping.idPath', incrementalConfig.mapping?.idPath], ['incremental.mapping.versionPath', incrementalConfig.mapping?.versionPath]]) {
if (!getNormalizedOptionalString(requiredPropertyPath[1])) {
throwSearchConfigError(`The source "${sourceName}.${requiredPropertyPath[0]}" property must be a non-empty string for http sources.`);
}
}
if (syncClass === 'append-only') {
if (incrementalConfig.mapping?.beforePath !== undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental.mapping.beforePath" property is not supported when syncClass is "append-only".`);
}
if (incrementalConfig.mapping?.deletedPath !== undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental.mapping.deletedPath" property is not supported when syncClass is "append-only".`);
}
}
};
export const validateSearchSourceConfig = (sourceName, sourceConfig) => {
if (!isPlainObject(sourceConfig)) {
throwSearchConfigError(`The source "${sourceName}" must be an object.`);
}
const allowedKeys = ['collection', 'connectionSecret', 'description', 'driver', 'enabled', 'eventarc', 'incremental', 'indexes', 'mapper', 'promotion', 'request', 'routeTemplate', 'schedule', 'snapshot', 'syncClass', 'type'];
const invalidKeys = Object.keys(sourceConfig).filter(key => !allowedKeys.includes(key));
if (invalidKeys.length > 0) {
throwSearchConfigError(`Unsupported properties found on source "${sourceName}": ${invalidKeys.join(', ')}.`);
}
if (!isString(sourceConfig.type) || sourceConfig.type.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.type" property must be a non-empty string.`);
}
const sourceType = sourceConfig.type.trim().toLowerCase();
if (!SUPPORTED_SEARCH_SOURCE_TYPES.includes(sourceType)) {
throwSearchConfigError(`The source "${sourceName}.type" property must be one of: ${SUPPORTED_SEARCH_SOURCE_TYPES.join(', ')}.`);
}
if (!isString(sourceConfig.syncClass) || sourceConfig.syncClass.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.syncClass" property must be a non-empty string.`);
}
const syncClass = sourceConfig.syncClass.trim().toLowerCase();
if (!SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.includes(syncClass)) {
throwSearchConfigError(`The source "${sourceName}.syncClass" property must be one of: ${SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES.join(', ')}.`);
}
if (sourceConfig.enabled !== undefined && !isBoolean(sourceConfig.enabled)) {
throwSearchConfigError(`The source "${sourceName}.enabled" property must be a boolean when provided.`);
}
assertStringWhenProvided(sourceConfig.connectionSecret, `sources.${sourceName}.connectionSecret`);
assertStringWhenProvided(sourceConfig.description, `sources.${sourceName}.description`);
assertStringArrayWhenProvided(sourceConfig.indexes, `sources.${sourceName}.indexes`);
assertStringWhenProvided(sourceConfig.mapper, `sources.${sourceName}.mapper`);
assertStringWhenProvided(sourceConfig.routeTemplate, `sources.${sourceName}.routeTemplate`);
assertStringWhenProvided(sourceConfig.schedule, `sources.${sourceName}.schedule`);
if (sourceType === 'firestore') {
if (!isString(sourceConfig.collection) || sourceConfig.collection.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.collection" property must be a non-empty string for firestore sources.`);
}
if (syncClass !== 'delta-merge') {
throwSearchConfigError(`The source "${sourceName}" must use syncClass "delta-merge" when type is "firestore".`);
}
if (sourceConfig.schedule !== undefined) {
throwSearchConfigError(`The source "${sourceName}.schedule" property is not supported for firestore sources.`);
}
if (sourceConfig.connectionSecret !== undefined || sourceConfig.driver !== undefined || sourceConfig.request !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define remote access settings when type is "firestore".`);
}
if (sourceConfig.incremental !== undefined || sourceConfig.snapshot !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define incremental or snapshot settings when type is "firestore".`);
}
validateSearchSourceEventarcConfig(sourceName, sourceConfig);
return;
}
if (sourceType === 'sql') {
if (!isString(sourceConfig.driver) || sourceConfig.driver.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.driver" property must be a non-empty string for sql sources.`);
}
if (!isString(sourceConfig.connectionSecret) || sourceConfig.connectionSecret.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.connectionSecret" property must be a non-empty string for sql sources.`);
}
if (!isString(sourceConfig.schedule) || sourceConfig.schedule.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.schedule" property must be a non-empty string for sql sources.`);
}
if (sourceConfig.collection !== undefined || sourceConfig.eventarc !== undefined || sourceConfig.request !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define firestore-only or http-only settings when type is "sql".`);
}
validateSqlSearchSourceIncrementalConfig(sourceName, sourceConfig);
validateSearchSourceSnapshotConfig(sourceName, sourceConfig);
validateSearchSourcePromotionConfig(sourceName, sourceConfig);
if (syncClass === 'snapshot-replace') {
if (sourceConfig.incremental !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define incremental settings when syncClass is "snapshot-replace".`);
}
if (sourceConfig.snapshot === undefined) {
throwSearchConfigError(`The source "${sourceName}.snapshot" property is required when syncClass is "snapshot-replace".`);
}
return;
}
if (sourceConfig.snapshot !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define snapshot settings unless syncClass is "snapshot-replace".`);
}
if (sourceConfig.incremental === undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental" property is required for sql sources unless syncClass is "snapshot-replace".`);
}
const cursorConfig = isPlainObject(sourceConfig.incremental.cursor) ? sourceConfig.incremental.cursor : sourceConfig.incremental;
const deleteStrategy = getNormalizedOptionalString(sourceConfig.incremental.deleteStrategy) ?? 'none';
const incrementalStrategy = getNormalizedOptionalString(sourceConfig.incremental.strategy) ?? null;
const deletedColumn = isString(sourceConfig.incremental.deletedColumn) && sourceConfig.incremental.deletedColumn.trim().length > 0 ? sourceConfig.incremental.deletedColumn.trim() : null;
if (!isString(cursorConfig.versionColumn) || cursorConfig.versionColumn.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.incremental.cursor.versionColumn" property must be a non-empty string for incremental sql sources.`);
}
if (!isString(cursorConfig.idColumn) || cursorConfig.idColumn.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.incremental.cursor.idColumn" property must be a non-empty string for incremental sql sources.`);
}
const sqlQuery = sourceConfig.incremental.sql;
const versionAliases = createSqlParameterAliases(cursorConfig.versionColumn, ['version', 'cursor_version']);
const idAliases = createSqlParameterAliases(cursorConfig.idColumn, ['id', 'cursor_id']);
if (!containsAnySqlPlaceholder(sqlQuery, versionAliases)) {
throwSearchConfigError(`The source "${sourceName}.incremental.sql" property must reference the configured version cursor placeholder.`);
}
if (!containsAnySqlPlaceholder(sqlQuery, idAliases)) {
throwSearchConfigError(`The source "${sourceName}.incremental.sql" property must reference the configured id cursor placeholder.`);
}
if (!containsAnySqlPlaceholder(sqlQuery, ['batchSize', 'batch_size', 'limit'])) {
throwSearchConfigError(`The source "${sourceName}.incremental.sql" property must reference a batch-size placeholder such as @limit.`);
}
if (!SUPPORTED_SQL_INCREMENTAL_STRATEGIES.includes(incrementalStrategy)) {
throwSearchConfigError(`The source "${sourceName}.incremental.strategy" property must be one of: ${SUPPORTED_SQL_INCREMENTAL_STRATEGIES.join(', ')}.`);
}
if (syncClass === 'append-only') {
if (sourceConfig.incremental.deleteStrategy !== undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental.deleteStrategy" property is not supported when syncClass is "append-only".`);
}
if (sourceConfig.incremental.deletedColumn !== undefined) {
throwSearchConfigError(`The source "${sourceName}.incremental.deletedColumn" property is not supported when syncClass is "append-only".`);
}
}
if (syncClass === 'delta-merge') {
if (!SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES.includes(deleteStrategy)) {
throwSearchConfigError(`The source "${sourceName}.incremental.deleteStrategy" property must be one of: ${SUPPORTED_SQL_INCREMENTAL_DELETE_STRATEGIES.join(', ')}.`);
}
if (deleteStrategy === 'soft-delete' && deletedColumn === null) {
throwSearchConfigError(`The source "${sourceName}.incremental.deletedColumn" property is required when deleteStrategy is "soft-delete".`);
}
}
if (sourceConfig.promotion !== undefined) {
throwSearchConfigError(`The source "${sourceName}.promotion" property is only supported when syncClass is "snapshot-replace".`);
}
return;
}
if (!isString(sourceConfig.schedule) || sourceConfig.schedule.trim().length === 0) {
throwSearchConfigError(`The source "${sourceName}.schedule" property must be a non-empty string for http sources.`);
}
if (syncClass !== 'delta-merge' && syncClass !== 'append-only') {
throwSearchConfigError(`The source "${sourceName}" must use syncClass "delta-merge" or "append-only" when type is "http".`);
}
if (sourceConfig.collection !== undefined || sourceConfig.driver !== undefined || sourceConfig.eventarc !== undefined || sourceConfig.snapshot !== undefined || sourceConfig.promotion !== undefined) {
throwSearchConfigError(`The source "${sourceName}" cannot define firestore-only, sql-only, or snapshot-only settings when type is "http".`);
}
validateHttpSearchSourceRequestConfig(sourceName, sourceConfig);
validateHttpSearchSourceIncrementalConfig(sourceName, sourceConfig);
};
export default {
resolveProjectScopedSearchSourceConfig,
SUPPORTED_PROJECT_SCOPED_SEARCH_SOURCE_TYPES,
SUPPORTED_SEARCH_SOURCE_SYNC_CLASSES,
SUPPORTED_SEARCH_SOURCE_TYPES,
validateSearchSourceBindingConfig,
validateSearchSourceDefinitionConfig,
validateSearchSourceConfig
};