Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C2-2432: implement create namespace (database) command #88

Merged
merged 4 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ Note that this project uses Java 17, please ensure that you have the target JDK

You can run your application in dev mode that enables live coding using:
```shell script
docker run -d --rm -e CLUSTER_NAME=dse-cluster -e CLUSTER_VERSION=6.8 -e ENABLE_AUTH=true -e DEVELOPER_MODE=true -e DS_LICENSE=accept -e DSE=true -p 8081:8081 -p 8091:8091 -p 9042:9042 stargateio/coordinator-dse-68:v2

./mvnw compile quarkus:dev
```

The command above will first spin the single Stargate DSE coordinator in dev that the API would communicate to.

> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/stargate/dev/.

#### Debugging
Expand Down
38 changes: 37 additions & 1 deletion src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

@OpenAPIDefinition(
// note that info is defined via the properties
info = @Info(title = "", version = ""),
tags = {
@Tag(name = "General", description = "Executes general commands."),
@Tag(name = "Databases", description = "Executes database commands."),
@Tag(
name = "Documents",
description = "Executes document commands against a single collection."),
},
components =
@Components(

Expand Down Expand Up @@ -153,6 +161,34 @@
}
}
"""),
@ExampleObject(
name = "createDatabase",
summary = "`CreateDatabase` command",
value =
"""
{
"createDatabase": {
"name": "cycling"
}
}
"""),
@ExampleObject(
name = "createDatabaseWithReplication",
summary = "`CreateDatabase` command with replication",
value =
"""
{
"createDatabase": {
"name": "cycling",
"options": {
"replication": {
"class": "SimpleStrategy",
"replication_factor": 3
}
}
}
}
"""),
@ExampleObject(
name = "createCollection",
summary = "`CreateCollection` command",
Expand Down Expand Up @@ -266,7 +302,7 @@
}
"""),
@ExampleObject(
name = "resultCreateCollection",
name = "resultCreate",
summary = "Create result",
value =
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.stargate.sgv2.jsonapi.api.model.command;

/** Interface for all commands executed against a collection in a namespace (database). */
public interface CollectionCommand extends Command {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateCollectionCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateDatabaseCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand;
Expand Down Expand Up @@ -33,6 +34,7 @@
include = JsonTypeInfo.As.WRAPPER_OBJECT,
property = "commandName")
@JsonSubTypes({
@JsonSubTypes.Type(value = CreateDatabaseCommand.class),
@JsonSubTypes.Type(value = CreateCollectionCommand.class),
@JsonSubTypes.Type(value = DeleteOneCommand.class),
@JsonSubTypes.Type(value = FindCommand.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@
* @param database The name of the database.
* @param collection The name of the collection.
*/
public record CommandContext(String database, String collection) {}
public record CommandContext(String database, String collection) {

private static final CommandContext EMPTY = new CommandContext(null, null);

/**
* @return Returns empty command context, having both {@link #database} and {@link #collection} as
* <code>null</code>.
*/
public static CommandContext empty() {
return EMPTY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.stargate.sgv2.jsonapi.api.model.command;

/** Interface for all commands executed against a namespace (database). */
public interface DatabaseCommand extends Command {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.stargate.sgv2.jsonapi.api.model.command;

/**
* Interface for all general commands, that are not executed against a namespace (database) nor a
* collection .
*/
public interface GeneralCommand extends Command {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.stargate.sgv2.jsonapi.api.model.command;

/** Base for any commands that modify, such as insert, delete, update, etc. */
public interface ModifyCommand extends Command {}
public interface ModifyCommand extends CollectionCommand {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.stargate.sgv2.jsonapi.api.model.command;

/** Basic interface for all read commands. */
public interface ReadCommand extends Command {}
public interface ReadCommand extends CollectionCommand {}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.stargate.sgv2.jsonapi.api.model.command.impl;

import com.fasterxml.jackson.annotation.JsonTypeName;
import io.stargate.sgv2.jsonapi.api.model.command.SchemaChangeCommand;
import io.stargate.sgv2.jsonapi.api.model.command.DatabaseCommand;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
Expand All @@ -17,6 +17,6 @@ public record CreateCollectionCommand(
type = SchemaType.OBJECT)
String name,
@Nullable Options options)
implements SchemaChangeCommand {
implements DatabaseCommand {
public record Options() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.stargate.sgv2.jsonapi.api.model.command.impl;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.stargate.sgv2.jsonapi.api.model.command.GeneralCommand;
import java.util.Map;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema(description = "Command that creates a namespace (database).")
@JsonTypeName("createDatabase")
public record CreateDatabaseCommand(
@NotBlank @Size(min = 1, max = 48) @Schema(description = "Name of the database") String name,
@Nullable @Valid CreateDatabaseCommand.Options options)
implements GeneralCommand {

@Schema(
name = "CreateDatabaseCommand.Options",
description = "Options for creating a new database.")
public record Options(@Nullable @Valid Replication replication) {}

/**
* Replication options for the create namespace.
*
* @param strategy Cassandra keyspace strategy class name to use (SimpleStrategy or
* NetworkTopologyStrategy).
* @param strategyOptions Options for each strategy. For <code>SimpleStrategy</code>,
* `replication_factor` is optional. For the <code>NetworkTopologyStrategy</code> each data
* center with replication.
*/
@Schema(description = "Cassandra based replication settings.")
// no record due to the @JsonAnySetter, see
// https://github.com/FasterXML/jackson-databind/issues/562
public static class Replication {
@NotNull()
@Pattern(regexp = "SimpleStrategy|NetworkTopologyStrategy")
@JsonProperty("class")
private String strategy;

@JsonAnySetter
@Schema(hidden = true)
private Map<String, Integer> strategyOptions;

public String strategy() {
return strategy;
}

public Map<String, Integer> strategyOptions() {
return strategyOptions;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.stargate.sgv2.jsonapi.api.v1;

import io.smallrye.mutiny.Uni;
import io.stargate.sgv2.jsonapi.api.model.command.Command;
import io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand;
Expand Down Expand Up @@ -39,7 +39,7 @@
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@SecurityRequirement(name = OpenApiConstants.SecuritySchemes.TOKEN)
@Tag(name = "Documents", description = "Executes document commands against a single collection.")
@Tag(ref = "Documents")
public class CollectionResource {

public static final String BASE_PATH = "/v1/{database}/{collection}";
Expand Down Expand Up @@ -102,7 +102,7 @@ public CollectionResource(CommandProcessor commandProcessor) {
})))
@POST
public Uni<RestResponse<CommandResult>> postCommand(
@NotNull @Valid Command command,
@NotNull @Valid CollectionCommand command,
@PathParam("database") String database,
@PathParam("collection") String collection) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import io.smallrye.mutiny.Uni;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.api.model.command.SchemaChangeCommand;
import io.stargate.sgv2.jsonapi.api.model.command.DatabaseCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateCollectionCommand;
import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants;
import io.stargate.sgv2.jsonapi.service.processor.CommandProcessor;
Expand Down Expand Up @@ -33,7 +33,7 @@
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@SecurityRequirement(name = OpenApiConstants.SecuritySchemes.TOKEN)
@Tag(name = "Databases", description = "Executes database commands.")
@Tag(ref = "Databases")
public class DatabaseResource {

public static final String BASE_PATH = "/v1/{database}";
Expand Down Expand Up @@ -67,12 +67,12 @@ public DatabaseResource(CommandProcessor commandProcessor) {
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CommandResult.class),
examples = {
@ExampleObject(ref = "resultCreateCollection"),
@ExampleObject(ref = "resultCreate"),
@ExampleObject(ref = "resultError"),
})))
@POST
public Uni<RestResponse<CommandResult>> postCommand(
@NotNull @Valid SchemaChangeCommand command, @PathParam("database") String database) {
@NotNull @Valid DatabaseCommand command, @PathParam("database") String database) {

// create context
CommandContext commandContext = new CommandContext(database, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.stargate.sgv2.jsonapi.api.v1;

import io.smallrye.mutiny.Uni;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.api.model.command.GeneralCommand;
import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateDatabaseCommand;
import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants;
import io.stargate.sgv2.jsonapi.service.processor.CommandProcessor;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.RestResponse;

@Path(GeneralResource.BASE_PATH)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@SecurityRequirement(name = OpenApiConstants.SecuritySchemes.TOKEN)
@Tag(ref = "General")
public class GeneralResource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this as NamespaceResource? General resource doesn't convey what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not fit, we already have a DatabaseResource I followed the logic:

  • /v1 - GeneralResurce
  • /v1/{database} - DatabaseResource
  • /v1/{database}/{collection} - CollectionResource

That would mean that we would have database resource and namespace resource, but they are both actually the same..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be

/v1 - GeneralResource
/v1/{namespace} - NamespaceResource
/v1/{namespace}/{collection} - CollectionResource

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK but this database to namespace change is huge and needs a separate PR imo.. WIll create a ticket..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#93


public static final String BASE_PATH = "/v1";

private final CommandProcessor commandProcessor;

@Inject
public GeneralResource(CommandProcessor commandProcessor) {
this.commandProcessor = commandProcessor;
}

@Operation(summary = "Execute command", description = "Executes a single general command.")
@RequestBody(
content =
@Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(anyOf = {CreateDatabaseCommand.class}),
examples = {
@ExampleObject(ref = "createDatabase"),
@ExampleObject(ref = "createDatabaseWithReplication"),
}))
@APIResponses(
@APIResponse(
responseCode = "200",
description =
"Call successful. Returns result of the command execution. Note that in case of errors, response code remains `HTTP 200`.",
content =
@Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = CommandResult.class),
examples = {
@ExampleObject(ref = "resultCreate"),
@ExampleObject(ref = "resultError"),
})))
@POST
public Uni<RestResponse<CommandResult>> postCommand(@NotNull @Valid GeneralCommand command) {

// call processor
return commandProcessor
.processCommand(CommandContext.empty(), command)
// map to 2xx always
.map(RestResponse::ok);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.stargate.sgv2.jsonapi.service.operation.model.impl;

import io.smallrye.mutiny.Uni;
import io.stargate.bridge.proto.QueryOuterClass;
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor;
import io.stargate.sgv2.jsonapi.service.operation.model.Operation;
import java.util.function.Supplier;

/**
* Operation that creates a new Cassandra keyspace that serves as a namespace (database) for the
* JSON API.
*
* @param name Name of the namespace to create.
* @param replicationMap A replication json, see
* https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/cqlCreateKeyspace.html#Table2.Replicationstrategyclassandfactorsettings.
*/
public record CreateDatabaseOperation(String name, String replicationMap) implements Operation {

// simple pattern for the cql
private static final String CREATE_KEYSPACE_CQL =
"CREATE KEYSPACE IF NOT EXISTS \"%s\" WITH REPLICATION = %s;\n";

/** {@inheritDoc} */
@Override
public Uni<Supplier<CommandResult>> execute(QueryExecutor queryExecutor) {
QueryOuterClass.Query query =
QueryOuterClass.Query.newBuilder()
.setCql(String.format(CREATE_KEYSPACE_CQL, name, replicationMap))
.build();

// execute
return queryExecutor
.executeSchemaChange(query)

// if we have a result always respond positively
.map(any -> new SchemaChangeResult(true));
}
}
Loading