diff --git a/CONFIGURATION.md b/CONFIGURATION.md index bb0763d268..735351d0d0 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -71,10 +71,17 @@ Other Quarkus properties that are specifically relevant for the service: ## Command level logging configuration -*Configuration for command level logging, defined by [CommandLoggingConfig.java](src/main/java/io/stargate/sgv2/jsonapi/config/CommandLoggingConfig.java).* +*Configuration for command level logging, defined by [CommandLevelLoggingConfig.java](src/main/java/io/stargate/sgv2/jsonapi/config/CommandLoggingConfig.java).* | Property | Type | Default | Description | |-----------------------------------------------------|-----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `stargate.jsonapi.logging.enabled` | `boolean` | `false` | Setting it to `true` enables command level logging. | | `stargate.jsonapi.logging.only-results-with-errors` | `boolean` | `true` | Setting it to `true` prints the command level info only for the commands where the command result has errors. | | `stargate.jsonapi.logging.enabled-tenants` | `string` | `ALL` | Comma separated list of tenants for which command level logging should be enabled. Default is a special keyword called `ALL` which prints this log for all tenants | + +## API Feature enabling configuration +*Configuration for enabling Features, defined by [FeaturesConfig.java](src/main/java/io/stargate/sgv2/jsonapi/config/CommandLoggingConfig.java).* + +| Property | Type | Default | Description | +|---------------------------------|-----------|---------|---------------------------------------------------------------------------------------------------------------------| +| `stargate.feature.flags.tables` | `boolean` | `null` | Setting it to `true` enables Tables functionality; `false` disables; leaving as `null` allows per-request override.| diff --git a/pom.xml b/pom.xml index ab57200d78..51f3b6f5b6 100644 --- a/pom.xml +++ b/pom.xml @@ -238,7 +238,7 @@ org.junit-pioneer junit-pioneer - 2.0.0 + 2.2.0 test diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java index c7a65d91d4..b52055e192 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import io.stargate.sgv2.jsonapi.api.v1.metrics.JsonProcessingMetricsReporter; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.*; import io.stargate.sgv2.jsonapi.service.embedding.operation.EmbeddingProvider; @@ -16,7 +17,8 @@ public record CommandContext( T schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { // TODO: this is what the original EMPTY had, no idea why the name of the command is missing // this is used by the GeneralResource @@ -40,26 +42,31 @@ public static CommandContext forSchemaObject( T schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { // TODO: upgrade to use the modern switch statements // TODO: how to remove the unchecked cast ? Had to use unchecked cast to get back to the // CommandContext if (schemaObject instanceof CollectionSchemaObject cso) { return (CommandContext) - forSchemaObject(cso, embeddingProvider, commandName, jsonProcessingMetricsReporter); + forSchemaObject( + cso, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } if (schemaObject instanceof TableSchemaObject tso) { return (CommandContext) - forSchemaObject(tso, embeddingProvider, commandName, jsonProcessingMetricsReporter); + forSchemaObject( + tso, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } if (schemaObject instanceof KeyspaceSchemaObject kso) { return (CommandContext) - forSchemaObject(kso, embeddingProvider, commandName, jsonProcessingMetricsReporter); + forSchemaObject( + kso, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } if (schemaObject instanceof DatabaseSchemaObject dso) { return (CommandContext) - forSchemaObject(dso, embeddingProvider, commandName, jsonProcessingMetricsReporter); + forSchemaObject( + dso, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } throw ErrorCodeV1.SERVER_INTERNAL_ERROR.toApiException( "Unknown schema object type: %s", schemaObject.getClass().getName()); @@ -79,9 +86,10 @@ public static CommandContext forSchemaObject( CollectionSchemaObject schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { return new CommandContext<>( - schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter); + schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } /** @@ -98,9 +106,10 @@ public static CommandContext forSchemaObject( TableSchemaObject schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { return new CommandContext<>( - schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter); + schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } /** @@ -117,9 +126,10 @@ public static CommandContext forSchemaObject( KeyspaceSchemaObject schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { return new CommandContext<>( - schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter); + schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } /** @@ -136,9 +146,10 @@ public static CommandContext forSchemaObject( DatabaseSchemaObject schemaObject, EmbeddingProvider embeddingProvider, String commandName, - JsonProcessingMetricsReporter jsonProcessingMetricsReporter) { + JsonProcessingMetricsReporter jsonProcessingMetricsReporter, + ApiFeatures apiFeatures) { return new CommandContext<>( - schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter); + schemaObject, embeddingProvider, commandName, jsonProcessingMetricsReporter, apiFeatures); } @SuppressWarnings("unchecked") diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/request/DataApiRequestInfo.java b/src/main/java/io/stargate/sgv2/jsonapi/api/request/DataApiRequestInfo.java index 65cbd8d63d..4a17fba7fc 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/request/DataApiRequestInfo.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/request/DataApiRequestInfo.java @@ -18,6 +18,7 @@ public class DataApiRequestInfo { private final Optional tenantId; private final Optional cassandraToken; private final EmbeddingCredentials embeddingCredentials; + private final HttpHeaderAccess httpHeaders; /** * Constructor that will be useful in the offline library mode, where only the tenant will be set @@ -29,6 +30,7 @@ public DataApiRequestInfo(Optional tenantId) { this.tenantId = tenantId; this.cassandraToken = Optional.empty(); this.embeddingCredentials = null; + httpHeaders = null; } @Inject @@ -41,6 +43,7 @@ public DataApiRequestInfo( this.embeddingCredentials = apiKeysResolver.get().resolveEmbeddingCredentials(routingContext); this.tenantId = (tenantResolver.get()).resolve(routingContext, securityContext); this.cassandraToken = (tokenResolver.get()).resolve(routingContext, securityContext); + httpHeaders = new HttpHeaderAccess(routingContext.request().headers()); } public Optional getTenantId() { @@ -54,4 +57,40 @@ public Optional getCassandraToken() { public EmbeddingCredentials getEmbeddingCredentials() { return this.embeddingCredentials; } + + public HttpHeaderAccess getHttpHeaders() { + return this.httpHeaders; + } + + /** + * Simple wrapper around internal HTTP header container, providing safe(r) access to typed header + * values. Minimal API, currently mainly used for feature flags. + */ + public static class HttpHeaderAccess { + private final io.vertx.core.MultiMap headers; + + public HttpHeaderAccess(io.vertx.core.MultiMap headers) { + this.headers = headers; + } + + /** + * Accessor for getting value of given header, as {@code Boolean} if (and only if!) value is one + * of "true" or "false". Access by name is (and has to be) case-insensitive as per HTTP + * standard. + * + * @param headerName Name of header to check + * @return Boolean.TRUE if header value is "true", Boolean.FALSE if "false", or null if not + */ + public Boolean getHeaderAsBoolean(String headerName) { + String str = headers.get(headerName); + // Only consider strict "true" and "false"; ignore other values + if ("true".equals(str)) { + return Boolean.TRUE; + } + if ("false".equals(str)) { + return Boolean.FALSE; + } + return null; + } + } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java index 7de03448f3..ce0adca057 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java @@ -20,9 +20,14 @@ import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; import io.stargate.sgv2.jsonapi.api.v1.metrics.JsonProcessingMetricsReporter; import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeature; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; +import io.stargate.sgv2.jsonapi.config.feature.FeaturesConfig; +import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.exception.mappers.ThrowableCommandResultSupplier; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.SchemaCache; +import io.stargate.sgv2.jsonapi.service.cqldriver.executor.SchemaObject; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.VectorConfig; import io.stargate.sgv2.jsonapi.service.embedding.operation.EmbeddingProvider; import io.stargate.sgv2.jsonapi.service.embedding.operation.EmbeddingProviderFactory; @@ -68,6 +73,8 @@ public class CollectionResource { @Inject private DataApiRequestInfo dataApiRequestInfo; + @Inject FeaturesConfig apiFeatureConfig; + @Inject private JsonProcessingMetricsReporter jsonProcessingMetricsReporter; @Inject @@ -181,6 +188,14 @@ public Uni> postCommand( return Uni.createFrom().item(new ThrowableCommandResultSupplier(error)); } else { // TODO No need for the else clause here, simplify + final ApiFeatures apiFeatures = + ApiFeatures.fromConfigAndRequest( + apiFeatureConfig, dataApiRequestInfo.getHttpHeaders()); + if ((schemaObject.type == SchemaObject.SchemaObjectType.TABLE) + && !apiFeatures.isFeatureEnabled(ApiFeature.TABLES)) { + return Uni.createFrom() + .failure(ErrorCodeV1.TABLE_FEATURE_NOT_ENABLED.toApiException()); + } // TODO: refactor this code to be cleaner so it assigns on one line EmbeddingProvider embeddingProvider = null; final VectorConfig.VectorizeConfig vectorizeConfig = @@ -203,7 +218,8 @@ public Uni> postCommand( schemaObject, embeddingProvider, command.getClass().getSimpleName(), - jsonProcessingMetricsReporter); + jsonProcessingMetricsReporter, + apiFeatures); return meteredCommandProcessor.processCommand( dataApiRequestInfo, commandContext, command); diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/GeneralResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/GeneralResource.java index 384239b6e5..d928f2e354 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/GeneralResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/GeneralResource.java @@ -7,6 +7,8 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateNamespaceCommand; import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; +import io.stargate.sgv2.jsonapi.config.feature.FeaturesConfig; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.DatabaseSchemaObject; import io.stargate.sgv2.jsonapi.service.processor.MeteredCommandProcessor; import jakarta.inject.Inject; @@ -37,6 +39,8 @@ public class GeneralResource { @Inject private DataApiRequestInfo dataApiRequestInfo; + @Inject FeaturesConfig apiFeatureConfig; + public static final String BASE_PATH = "/v1"; private final MeteredCommandProcessor meteredCommandProcessor; @@ -75,10 +79,16 @@ public GeneralResource(MeteredCommandProcessor meteredCommandProcessor) { }))) @POST public Uni> postCommand(@NotNull @Valid GeneralCommand command) { + final ApiFeatures apiFeatures = + ApiFeatures.fromConfigAndRequest(apiFeatureConfig, dataApiRequestInfo.getHttpHeaders()); var commandContext = CommandContext.forSchemaObject( - new DatabaseSchemaObject(), null, command.getClass().getSimpleName(), null); + new DatabaseSchemaObject(), + null, + command.getClass().getSimpleName(), + null, + apiFeatures); return meteredCommandProcessor .processCommand(dataApiRequestInfo, commandContext, command) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java index a88d47adfc..a05936e42e 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java @@ -9,8 +9,10 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteCollectionCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCollectionsCommand; import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; -import io.stargate.sgv2.jsonapi.config.ApiTablesConfig; import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeature; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; +import io.stargate.sgv2.jsonapi.config.feature.FeaturesConfig; import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.exception.mappers.ThrowableCommandResultSupplier; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.KeyspaceSchemaObject; @@ -51,7 +53,7 @@ public class NamespaceResource { @Inject private DataApiRequestInfo dataApiRequestInfo; - @Inject ApiTablesConfig apiTablesConfig; + @Inject FeaturesConfig apiFeatureConfig; @Inject public NamespaceResource(MeteredCommandProcessor meteredCommandProcessor) { @@ -106,22 +108,26 @@ public Uni> postCommand( @Size(min = 1, max = 48) String namespace) { - if (command instanceof TableOnlyCommand && !apiTablesConfig.enabled()) { - return Uni.createFrom() - .item( - new ThrowableCommandResultSupplier( - ErrorCodeV1.TABLE_FEATURE_NOT_ENABLED.toApiException())) - .map(commandResult -> commandResult.toRestResponse()); - } + final ApiFeatures apiFeatures = + ApiFeatures.fromConfigAndRequest(apiFeatureConfig, dataApiRequestInfo.getHttpHeaders()); // create context // TODO: Aaron , left here to see what CTOR was used, there was a lot of different ones. // CommandContext commandContext = new CommandContext(namespace, null); // HACK TODO: The above did not set a command name on the command context, how did that work ? CommandContext commandContext = - new CommandContext<>(new KeyspaceSchemaObject(namespace), null, "", null); + new CommandContext<>(new KeyspaceSchemaObject(namespace), null, "", null, apiFeatures); + + // Need context first to check if feature is enabled + if (command instanceof TableOnlyCommand && !apiFeatures.isFeatureEnabled(ApiFeature.TABLES)) { + return Uni.createFrom() + .item( + new ThrowableCommandResultSupplier( + ErrorCodeV1.TABLE_FEATURE_NOT_ENABLED.toApiException())) + .map(commandResult -> commandResult.toRestResponse()); + } - // call processor + // call processor return meteredCommandProcessor .processCommand(dataApiRequestInfo, commandContext, command) // map to 2xx unless overridden by error diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/ApiTablesConfig.java b/src/main/java/io/stargate/sgv2/jsonapi/config/ApiTablesConfig.java deleted file mode 100644 index d4a423540e..0000000000 --- a/src/main/java/io/stargate/sgv2/jsonapi/config/ApiTablesConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.stargate.sgv2.jsonapi.config; - -import io.smallrye.config.ConfigMapping; -import io.smallrye.config.WithDefault; - -/** Configuration mapping for API Tables feature. */ -@ConfigMapping(prefix = "stargate.tables") -public interface ApiTablesConfig { - /** Setting that determines if the API Tables feature is enabled. */ - @WithDefault("false") - boolean enabled(); -} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeature.java b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeature.java new file mode 100644 index 0000000000..8d98f05091 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeature.java @@ -0,0 +1,62 @@ +package io.stargate.sgv2.jsonapi.config.feature; + +import com.fasterxml.jackson.annotation.JsonValue; +import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; + +/** + * Set of "Feature Flags" that can be used to enable/disable certain features in the Data API. + * Enumeration defines the key used to introspect state of feature. + * + *

NOTE: although flag names are in upper case (like {@code TABLES}), the actual configuration + * uses lower-case names (like {@code tables}) (with proper prefix). + * + *

Usage: Features may be enabled via configuration: see {@link FeaturesConfig}; if defined at + * that level, they are either enabled or disabled for all requests. If not defined (left as empty + * or {@code null}), HTTP Request headers can be used to enable/disable features on per-request + * basis. Finally, if neither configuration nor request headers are used, feature is disabled. + */ +public enum ApiFeature { + /** + * API Tables feature flag: if enabled, the API will expose table-specific Namespace resource + * commands, and support commands on Tables. If disabled, those operations will fail with {@link + * ErrorCodeV1#TABLE_FEATURE_NOT_ENABLED}. + */ + TABLES("tables"); + + /** + * Prefix for HTTP headers used to override feature flags for specific requests: prepended before + * {@link #featureName()}, so f.ex for {@link #TABLES} flag, the header name would be {@code + * Feature-Flag-tables}. + */ + public static final String HTTP_HEADER_PREFIX = "Feature-Flag-"; + + /** + * Feature flag name, in lower-case with hyphens (aka "kebab case"), used for as serialization + * (JSON and YAML config files) as well as for constructing HTTP header names. + */ + private final String featureName; + + /** + * HTTP header name to be used to override the feature flag for a specific request: lower-case, + * prefixed with "x-stargate-feature-"; lookup case-insensitive. + */ + private final String featureNameAsHeader; + + ApiFeature(String featureName) { + if (!featureName.equals(featureName.toLowerCase())) { + throw new IllegalStateException( + "Internal error: 'featureName' must be lower-case, was: \"" + featureName + "\""); + } + this.featureName = featureName; + featureNameAsHeader = HTTP_HEADER_PREFIX + featureName; + } + + @JsonValue // for Jackson to serialize as lower-case + public String featureName() { + return featureName; + } + + public String httpHeaderName() { + return featureNameAsHeader; + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java new file mode 100644 index 0000000000..f31bafdccf --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/ApiFeatures.java @@ -0,0 +1,49 @@ +package io.stargate.sgv2.jsonapi.config.feature; + +import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; +import java.util.Collections; +import java.util.Map; + +/** + * Accessor for combined state of feature flags; typically based on static configuration (with its + * overrides) and possible per-request settings. For code that wants to check whether given feature + * is enabled or not, method to use is {@link ApiFeatures#isFeatureEnabled(ApiFeature)}. For details + * on how configuration settings and request headers are combined, see {@link ApiFeature} and {@link + * FeaturesConfig} + */ +public class ApiFeatures { + private final Map fromConfig; + private final DataApiRequestInfo.HttpHeaderAccess httpHeaders; + + private ApiFeatures( + Map fromConfig, DataApiRequestInfo.HttpHeaderAccess httpHeaders) { + this.fromConfig = fromConfig; + this.httpHeaders = httpHeaders; + } + + public static ApiFeatures empty() { + return new ApiFeatures(Collections.emptyMap(), null); + } + + public static ApiFeatures fromConfigAndRequest( + FeaturesConfig config, DataApiRequestInfo.HttpHeaderAccess httpHeaders) { + Map fromConfig = config.flags(); + if (fromConfig == null) { + fromConfig = Collections.emptyMap(); + } + return new ApiFeatures(fromConfig, httpHeaders); + } + + public boolean isFeatureEnabled(ApiFeature flag) { + // First check if there is definition from configuration + Boolean b = fromConfig.get(flag); + + if (b == null) { + // and only if not, allow per-request specification + if (httpHeaders != null) { + b = httpHeaders.getHeaderAsBoolean(flag.httpHeaderName()); + } + } + return (b != null) && b.booleanValue(); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java new file mode 100644 index 0000000000..765f1d00c0 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/config/feature/FeaturesConfig.java @@ -0,0 +1,18 @@ +package io.stargate.sgv2.jsonapi.config.feature; + +import io.smallrye.config.ConfigMapping; +import java.util.Map; + +/** + * Configuration mapping for Data API Feature flags as read from main application configuration + * (with possible property / sysenv overrides). + * + *

NOTE: actual keys in YAML or similar configuration file would be {@code + * stargate.feature.}, where {@code } comes from {@link + * ApiFeature#featureName()} (which is always lower-case). So, for example, to enable {@link + * ApiFeature#TABLES} feature, one would use: {@code stargate.feature.flags.tables} as the key. + */ +@ConfigMapping(prefix = "stargate.feature") +public interface FeaturesConfig { + Map flags(); +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCache.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCache.java index f09241c1fa..6efe201530 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCache.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCache.java @@ -22,8 +22,6 @@ public class NamespaceCache { private final ObjectMapper objectMapper; - private final boolean apiTablesEnabled; - // TODO: move the settings to config // TODO: set the cache loader when creating the cache private static final long CACHE_TTL_SECONDS = 300; @@ -34,15 +32,10 @@ public class NamespaceCache { .maximumSize(CACHE_MAX_SIZE) .build(); - public NamespaceCache( - String namespace, - boolean apiTablesEnabled, - QueryExecutor queryExecutor, - ObjectMapper objectMapper) { + public NamespaceCache(String namespace, QueryExecutor queryExecutor, ObjectMapper objectMapper) { this.namespace = namespace; this.queryExecutor = queryExecutor; this.objectMapper = objectMapper; - this.apiTablesEnabled = apiTablesEnabled; } protected Uni getSchemaObject( @@ -109,13 +102,8 @@ private Uni loadSchemaObject( optionalTable.get(), objectMapper); } - if (apiTablesEnabled) { - return new TableSchemaObject(table); - } - - // Target is not a collection and we are not supporting tables - throw ErrorCodeV1.INVALID_JSONAPI_COLLECTION_SCHEMA.toApiException( - "%s", collectionName); + // 04-Sep-2024, tatu: Used to check that API Tables enabled; no longer checked here + return new TableSchemaObject(table); }); } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/SchemaCache.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/SchemaCache.java index b4f602833d..864087a9a8 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/SchemaCache.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/SchemaCache.java @@ -9,7 +9,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import io.smallrye.mutiny.Uni; import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; -import io.stargate.sgv2.jsonapi.config.ApiTablesConfig; import io.stargate.sgv2.jsonapi.config.OperationsConfig; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -26,8 +25,6 @@ public class SchemaCache { @Inject OperationsConfig operationsConfig; - @Inject ApiTablesConfig apiTablesConfig; - // TODO: The size of the cache should be in configuration. // TODO: set the cache loader when creating the cache private final Cache schemaCache = @@ -68,8 +65,7 @@ public void evictCollectionSettingCacheEntry( } private NamespaceCache addNamespaceCache(CacheKey cacheKey) { - return new NamespaceCache( - cacheKey.namespace(), apiTablesConfig.enabled(), queryExecutor, objectMapper); + return new NamespaceCache(cacheKey.namespace(), queryExecutor, objectMapper); } /** diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateTableOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateTableOperation.java index 2136d6ed9c..3d7af004fc 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateTableOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/CreateTableOperation.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import org.slf4j.Logger; @@ -47,7 +48,7 @@ public CreateTableOperation( String comment) { this.commandContext = commandContext; this.tableName = tableName; - this.columnTypes = columnTypes; + this.columnTypes = Objects.requireNonNull(columnTypes, "columnTypes must not be null"); this.partitionKeys = partitionKeys; this.clusteringKeys = clusteringKeys; this.comment = comment; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 11f37783b7..c67e7196cc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,6 +25,10 @@ stargate: exception-mappers: enabled: false + feature: # See io.stargate.sgv2.jsonapi.config.feature.FeaturesConfig + flags: # Ok to leave out features that have no default value (enabled or disabled) + tables: + # custom grpc settings grpc: @@ -48,10 +52,6 @@ stargate: multi-tenancy: enabled: false - # Is support for API Tables enabled? - tables: - enabled: false - quarkus: # general app properties application: diff --git a/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java b/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java index 31a5c4f62e..e50b656de2 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java @@ -1,6 +1,7 @@ package io.stargate.sgv2.jsonapi; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.*; import org.apache.commons.lang3.RandomStringUtils; @@ -40,15 +41,21 @@ public final class TestConstants { // CommandContext for working on the schema objects above + public static final ApiFeatures DEFAULT_API_FEATURES_FOR_TESTS = ApiFeatures.empty(); + public static final CommandContext COLLECTION_CONTEXT = - new CommandContext<>(COLLECTION_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null); + new CommandContext<>( + COLLECTION_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null, DEFAULT_API_FEATURES_FOR_TESTS); public static final CommandContext VECTOR_COLLECTION_CONTEXT = - new CommandContext<>(VECTOR_COLLECTION_SCHEMA_OBJECT, null, null, null); + new CommandContext<>( + VECTOR_COLLECTION_SCHEMA_OBJECT, null, null, null, DEFAULT_API_FEATURES_FOR_TESTS); public static final CommandContext KEYSPACE_CONTEXT = - new CommandContext<>(KEYSPACE_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null); + new CommandContext<>( + KEYSPACE_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null, DEFAULT_API_FEATURES_FOR_TESTS); public static final CommandContext DATABASE_CONTEXT = - new CommandContext<>(DATABASE_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null); + new CommandContext<>( + DATABASE_SCHEMA_OBJECT, null, TEST_COMMAND_NAME, null, DEFAULT_API_FEATURES_FOR_TESTS); } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AbstractTableIntegrationTestBase.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AbstractTableIntegrationTestBase.java index 39b6c01b30..f898f41092 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AbstractTableIntegrationTestBase.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/AbstractTableIntegrationTestBase.java @@ -10,6 +10,7 @@ import io.stargate.sgv2.jsonapi.api.v1.AbstractNamespaceIntegrationTestBase; import io.stargate.sgv2.jsonapi.api.v1.CollectionResource; import io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders; +import io.stargate.sgv2.jsonapi.api.v1.util.DataApiResponseValidator; import java.io.IOException; import java.util.Map; @@ -17,9 +18,9 @@ public class AbstractTableIntegrationTestBase extends AbstractNamespaceIntegrationTestBase { private static final ObjectMapper MAPPER = new ObjectMapper(); - protected void createTableWithColumns( + protected DataApiResponseValidator createTableWithColumns( String tableName, Map columns, Object primaryKeyDef) { - createTable( + return createTable( """ { "name": "%s", @@ -32,8 +33,8 @@ protected void createTableWithColumns( .formatted(tableName, asJSON(columns), asJSON(primaryKeyDef))); } - protected void createTable(String tableDefAsJSON) { - DataApiCommandSenders.assertNamespaceCommand(namespaceName) + protected DataApiResponseValidator createTable(String tableDefAsJSON) { + return DataApiCommandSenders.assertNamespaceCommand(namespaceName) .postCreateTable(tableDefAsJSON) .hasNoErrors() .body("status.ok", is(1)); diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIntegrationTest.java index 9b882e815e..c978f7847d 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/CreateTableIntegrationTest.java @@ -9,17 +9,16 @@ import io.stargate.sgv2.jsonapi.api.v1.AbstractNamespaceIntegrationTestBase; import io.stargate.sgv2.jsonapi.api.v1.NamespaceResource; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; -import org.junit.Test; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestClassOrder; @QuarkusIntegrationTest @WithTestResource(value = DseTestResource.class, restrictToAnnotatedClass = false) @TestClassOrder(ClassOrderer.OrderAnnotation.class) class CreateTableIntegrationTest extends AbstractNamespaceIntegrationTestBase { - @Nested @Order(1) class CreateTable { @@ -111,7 +110,7 @@ private void deleteTable(String tableName) { .body( """ { - "deleteTable": { + "deleteCollection": { "name": "%s" } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/TableFeatureDisabledIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/TableFeatureDisabledIntegrationTest.java new file mode 100644 index 0000000000..4337c832a3 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/TableFeatureDisabledIntegrationTest.java @@ -0,0 +1,95 @@ +package io.stargate.sgv2.jsonapi.api.v1.tables; + +import static org.hamcrest.Matchers.is; + +import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeature; +import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.testresource.DseTestResource; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; + +/** + * Basic testing to see what happens when "Tables" feature is disabled (other tests will have + * feature enabled) + */ +@QuarkusIntegrationTest +@WithTestResource( + value = TableFeatureDisabledIntegrationTest.TestResource.class, + restrictToAnnotatedClass = true) +@TestClassOrder(ClassOrderer.OrderAnnotation.class) +public class TableFeatureDisabledIntegrationTest extends AbstractTableIntegrationTestBase { + // Need to be able to enable/disable the TABLES feature + public static class TestResource extends DseTestResource { + public TestResource() {} + + @Override + public String getFeatureFlagTables() { + // return empty to leave feature "undefined" (disabled unless per-request header override) + // ("false" would be "disabled" for all tests, regardless of headers) + return ""; + } + } + + private static final String TABLE_TO_CREATE = "table_with_table_feature"; + + // By default, table creation should fail + @Order(1) + @Test + public void failCreateWithoutFeatureEnabled() { + DataApiCommandSenders.assertNamespaceCommand(namespaceName) + .postCreateTable(simpleTableDef(TABLE_TO_CREATE)) + .hasSingleApiError(ErrorCodeV1.TABLE_FEATURE_NOT_ENABLED); + } + + // But with header override, should succeed + @Order(2) + @Test + public void okCreateWithFeatureEnabledViaHeader() { + DataApiCommandSenders.assertNamespaceCommand(namespaceName) + .header(ApiFeature.TABLES.httpHeaderName(), "true") + .postCreateTable(simpleTableDef(TABLE_TO_CREATE)) + .hasNoErrors() + .body("status.ok", is(1)); + } + + // But even with table, find() should fail without Feature enabled + @Order(3) + @Test + public void failFindWithoutFeature() { + DataApiCommandSenders.assertTableCommand(namespaceName, TABLE_TO_CREATE) + .postFindOne("{}") + .hasSingleApiError(ErrorCodeV1.TABLE_FEATURE_NOT_ENABLED); + } + + // And finally, with header override, should succeed in findOne() + @Order(4) + @Test + public void okFindWithFeatureEnabledViaHeader() { + DataApiCommandSenders.assertTableCommand(namespaceName, TABLE_TO_CREATE) + .header(ApiFeature.TABLES.httpHeaderName(), "true") + .postFindOne("{}") + .hasNoErrors(); + } + + private static String simpleTableDef(String tableName) { + return + """ + { + "name": "%s", + "definition": { + "columns": { + "id": { "type": "text" }, + "name": { "type": "text" } + }, + "primaryKey": "id" + } + } + """ + .formatted(tableName); + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiCommandSenderBase.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiCommandSenderBase.java index 5450a93d5a..5bbab6a6b0 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiCommandSenderBase.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiCommandSenderBase.java @@ -12,6 +12,7 @@ import io.stargate.sgv2.jsonapi.service.embedding.operation.test.CustomITEmbeddingProvider; import jakarta.ws.rs.core.Response; import java.util.Base64; +import java.util.LinkedHashMap; import java.util.Map; import org.eclipse.microprofile.config.ConfigProvider; @@ -24,8 +25,11 @@ public abstract class DataApiCommandSenderBase headers; + protected DataApiCommandSenderBase(String namespace) { this.namespace = namespace; + headers = DefaultHeaders.mutableCopy(); } /** @@ -39,6 +43,22 @@ public T expectHttpStatus(Response.Status expectedHttpStatus) { return _this(); } + /** + * Fluent method for adding/overriding/removing a header in the request. + * + * @param name Name of header to set + * @param value Value of header to set; if null, header is removed, otherwise added or overridden + * @return Type-safe "this" sender for call chaining + */ + public T header(String name, String value) { + if (value == null) { + headers.remove(name); + } else { + headers.put(name, value); + } + return _this(); + } + protected T _this() { return (T) this; } @@ -50,11 +70,11 @@ protected T _this() { * @param jsonBody JSON body to POST * @return Response validator for further assertions */ - public final DataApiResponseValidator postRaw(String jsonBody) { + public final DataApiResponseValidator postJSON(String jsonBody) { RequestSpecification request = given() .port(getTestPort()) - .headers(getHeaders()) + .headers(headers) .contentType(ContentType.JSON) .body(jsonBody) .when(); @@ -64,41 +84,46 @@ public final DataApiResponseValidator postRaw(String jsonBody) { } public DataApiResponseValidator postCommand(String command, String commandClause) { - return postRaw("{ \"%s\": %s }".formatted(command, commandClause)); + return postJSON("{ \"%s\": %s }".formatted(command, commandClause)); } protected abstract io.restassured.response.Response postInternal(RequestSpecification request); - protected Map getHeaders() { - if (useCoordinator()) { - return Map.of( - HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, - getAuthToken(), - HttpConstants.EMBEDDING_AUTHENTICATION_TOKEN_HEADER_NAME, - CustomITEmbeddingProvider.TEST_API_KEY); - } else { - String credential = - "Cassandra:" - + Base64.getEncoder().encodeToString(getCassandraUsername().getBytes()) - + ":" - + Base64.getEncoder().encodeToString(getCassandraPassword().getBytes()); - return Map.of( - HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, - credential, - HttpConstants.EMBEDDING_AUTHENTICATION_TOKEN_HEADER_NAME, - CustomITEmbeddingProvider.TEST_API_KEY); - } - } - - private boolean useCoordinator() { - return Boolean.getBoolean("testing.containers.use-coordinator"); - } - - protected int getTestPort() { + protected static int getTestPort() { try { return ConfigProvider.getConfig().getValue("quarkus.http.test-port", Integer.class); } catch (Exception e) { return Integer.parseInt(System.getProperty("quarkus.http.test-port")); } } + + private static class DefaultHeaders { + private static final Map HEADERS = collectDefaultHeaders(); + + public static Map mutableCopy() { + return new LinkedHashMap<>(HEADERS); + } + + private static Map collectDefaultHeaders() { + final boolean useCoordinator = Boolean.getBoolean("testing.containers.use-coordinator"); + if (useCoordinator) { + return Map.of( + HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, + getAuthToken(), + HttpConstants.EMBEDDING_AUTHENTICATION_TOKEN_HEADER_NAME, + CustomITEmbeddingProvider.TEST_API_KEY); + } else { + String credential = + "Cassandra:" + + Base64.getEncoder().encodeToString(getCassandraUsername().getBytes()) + + ":" + + Base64.getEncoder().encodeToString(getCassandraPassword().getBytes()); + return Map.of( + HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, + credential, + HttpConstants.EMBEDDING_AUTHENTICATION_TOKEN_HEADER_NAME, + CustomITEmbeddingProvider.TEST_API_KEY); + } + } + } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCacheTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCacheTest.java index f3c488fe07..d7891cb379 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCacheTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/NamespaceCacheTest.java @@ -18,8 +18,6 @@ import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; import jakarta.inject.Inject; import java.util.HashMap; @@ -292,7 +290,7 @@ public void checkValidJsonApiTableWithIndexing() { .awaitItem() .getItem(); - assertThat(schemaObject instanceof CollectionSchemaObject); + assertThat(schemaObject).isInstanceOf(CollectionSchemaObject.class); var collectionSchemaObject = (CollectionSchemaObject) schemaObject; assertThat(collectionSchemaObject) .satisfies( @@ -304,7 +302,7 @@ public void checkValidJsonApiTableWithIndexing() { } @Test - public void checkInvalidJsonApiTable() { + public void checkNonCollectionJsonApiTable() { QueryExecutor queryExecutor = mock(QueryExecutor.class); when(queryExecutor.getSchema(any(), any(), any())) .then( @@ -350,28 +348,19 @@ public void checkInvalidJsonApiTable() { new HashMap<>()))); }); NamespaceCache namespaceCache = createNamespaceCache(queryExecutor); - Throwable error = + var schemaObject = namespaceCache .getSchemaObject(dataApiRequestInfo, "table") .subscribe() .withSubscriber(UniAssertSubscriber.create()) - .awaitFailure() - .getFailure(); + .awaitItem() + .getItem(); - assertThat(error) - .isInstanceOfSatisfying( - JsonApiException.class, - s -> { - assertThat(s.getErrorCode()) - .isEqualTo(ErrorCodeV1.INVALID_JSONAPI_COLLECTION_SCHEMA); - assertThat(s.getMessage()) - .isEqualTo( - ErrorCodeV1.INVALID_JSONAPI_COLLECTION_SCHEMA.getMessage() + ": table"); - }); + assertThat(schemaObject).isInstanceOf(TableSchemaObject.class); } } private NamespaceCache createNamespaceCache(QueryExecutor qe) { - return new NamespaceCache("ks", false, qe, objectMapper); + return new NamespaceCache("ks", qe, objectMapper); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/embedding/operation/TestEmbeddingProvider.java b/src/test/java/io/stargate/sgv2/jsonapi/service/embedding/operation/TestEmbeddingProvider.java index bfa8077236..eb7b1f78a7 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/embedding/operation/TestEmbeddingProvider.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/embedding/operation/TestEmbeddingProvider.java @@ -24,7 +24,8 @@ public class TestEmbeddingProvider extends EmbeddingProvider { null), new TestEmbeddingProvider(), "testCommand", - null); + null, + TestConstants.DEFAULT_API_FEATURES_FOR_TESTS); @Override public Uni vectorize( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/CreateCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/CreateCollectionOperationTest.java index 1934e33b2c..c3d58a3b9b 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/CreateCollectionOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/CreateCollectionOperationTest.java @@ -53,7 +53,12 @@ public class CreateCollectionOperationTest extends OperationTestBase { private CommandContext COMMAND_CONTEXT = - new CommandContext<>(COLLECTION_SCHEMA_OBJECT, null, "CreateCollectionCommand", null); + new CommandContext<>( + COLLECTION_SCHEMA_OBJECT, + null, + "CreateCollectionCommand", + null, + DEFAULT_API_FEATURES_FOR_TESTS); @Inject DatabaseLimitsConfig databaseLimitsConfig; diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java index f25dc792ae..9d78f32f69 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java @@ -85,7 +85,8 @@ public void init() { null), null, "testCommand", - jsonProcessingMetricsReporter); + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); } @Nested diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/InsertCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/InsertCollectionOperationTest.java index 2f4e569266..04b6c93f51 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/InsertCollectionOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/InsertCollectionOperationTest.java @@ -86,7 +86,8 @@ public void init() { null), null, "testCommand", - jsonProcessingMetricsReporter); + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); } @Nested diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/OperationTestBase.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/OperationTestBase.java index 3564a1cef0..30736c8d74 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/OperationTestBase.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/OperationTestBase.java @@ -19,6 +19,7 @@ import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; import io.stargate.sgv2.jsonapi.api.v1.metrics.JsonProcessingMetricsReporter; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.CollectionSchemaObject; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.KeyspaceSchemaObject; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.SchemaObjectName; @@ -37,6 +38,7 @@ public class OperationTestBase { protected final String COLLECTION_NAME = RandomStringUtils.randomAlphanumeric(16); protected final SchemaObjectName SCHEMA_OBJECT_NAME = new SchemaObjectName(KEYSPACE_NAME, COLLECTION_NAME); + protected final ApiFeatures DEFAULT_API_FEATURES_FOR_TESTS = ApiFeatures.empty(); protected final CollectionSchemaObject COLLECTION_SCHEMA_OBJECT = new CollectionSchemaObject( @@ -51,10 +53,18 @@ public class OperationTestBase { protected final CommandContext COLLECTION_CONTEXT = new CommandContext<>( - COLLECTION_SCHEMA_OBJECT, null, COMMAND_NAME, jsonProcessingMetricsReporter); + COLLECTION_SCHEMA_OBJECT, + null, + COMMAND_NAME, + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); protected final CommandContext KEYSPACE_CONTEXT = new CommandContext<>( - KEYSPACE_SCHEMA_OBJECT, null, COMMAND_NAME, jsonProcessingMetricsReporter); + KEYSPACE_SCHEMA_OBJECT, + null, + COMMAND_NAME, + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); @InjectMock protected DataApiRequestInfo dataApiRequestInfo; @@ -64,7 +74,11 @@ public class OperationTestBase { protected CommandContext createCommandContextWithCommandName( String commandName) { return new CommandContext<>( - COLLECTION_SCHEMA_OBJECT, null, commandName, jsonProcessingMetricsReporter); + COLLECTION_SCHEMA_OBJECT, + null, + commandName, + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); } protected ColumnDefinitions buildColumnDefs(TestColumn... columns) { diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/ReadAndUpdateCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/ReadAndUpdateCollectionOperationTest.java index 552001f2bf..b10a676b48 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/ReadAndUpdateCollectionOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/ReadAndUpdateCollectionOperationTest.java @@ -120,7 +120,8 @@ public void init() { null), null, "testCommand", - jsonProcessingMetricsReporter); + jsonProcessingMetricsReporter, + DEFAULT_API_FEATURES_FOR_TESTS); } private MockRow resultRow(ColumnDefinitions columnDefs, int index, Object... values) { diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/CommandResolverWithVectorizerTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/CommandResolverWithVectorizerTest.java index e57a40ee88..6c10d60f96 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/CommandResolverWithVectorizerTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/CommandResolverWithVectorizerTest.java @@ -19,6 +19,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; import io.stargate.sgv2.jsonapi.api.request.DataApiRequestInfo; import io.stargate.sgv2.jsonapi.config.OperationsConfig; +import io.stargate.sgv2.jsonapi.config.feature.ApiFeatures; import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.service.cqldriver.executor.CollectionSchemaObject; @@ -71,7 +72,7 @@ public class CommandResolverWithVectorizerTest { @Nested class Resolve { - // TODO: do these need to be uniqe to this test ? Can we use TestConstants ? + // TODO: do these need to be unique to this test ? Can we use TestConstants ? protected final String KEYSPACE_NAME = RandomStringUtils.randomAlphanumeric(16); protected final String COLLECTION_NAME = RandomStringUtils.randomAlphanumeric(16); private final CommandContext VECTOR_COMMAND_CONTEXT = @@ -83,7 +84,8 @@ class Resolve { null), null, null, - null); + null, + ApiFeatures.empty()); @Test public void find() throws Exception { diff --git a/src/test/java/io/stargate/sgv2/jsonapi/testresource/DseTestResource.java b/src/test/java/io/stargate/sgv2/jsonapi/testresource/DseTestResource.java index 9799f2f24b..be691958ad 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/testresource/DseTestResource.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/testresource/DseTestResource.java @@ -65,13 +65,24 @@ public Long getMaxDocumentSortCount() { return 100L; } + // By default, we enable the feature flag for tables + public String getFeatureFlagTables() { + return "true"; + } + @Override public Map start() { Map env = super.start(); ImmutableMap.Builder propsBuilder = ImmutableMap.builder(); propsBuilder.putAll(env); propsBuilder.put("stargate.jsonapi.custom.embedding.enabled", "true"); - propsBuilder.put("stargate.tables.enabled", "true"); + + // 04-Sep-2024, tatu: [data-api#1335] Enable Tables using new Feature Flag: + String tableFeatureSetting = getFeatureFlagTables(); + if (tableFeatureSetting != null) { + propsBuilder.put("stargate.feature.flags.tables", tableFeatureSetting); + } + propsBuilder.put( "stargate.jsonapi.custom.embedding.clazz", "io.stargate.sgv2.jsonapi.service.embedding.operation.test.CustomITEmbeddingProvider");