Some rewrites + packages

~4h work
This commit is contained in:
Benedikt Galbavy 2024-01-05 16:32:46 +01:00
parent 872172cf76
commit 756d299e2c
10 changed files with 248 additions and 146 deletions

View File

@ -1,5 +1,4 @@
import at.nanopenguin.mtcg.application.service.TestService; import at.nanopenguin.mtcg.application.service.*;
import at.nanopenguin.mtcg.application.service.UserService;
import at.nanopenguin.mtcg.http.HttpMethod; import at.nanopenguin.mtcg.http.HttpMethod;
import at.nanopenguin.mtcg.http.Router; import at.nanopenguin.mtcg.http.Router;
import at.nanopenguin.mtcg.http.Server; import at.nanopenguin.mtcg.http.Server;
@ -18,24 +17,24 @@ public class Main {
router.addRoute(HttpMethod.POST, "/sessions", new UserService(), new int[]{}); router.addRoute(HttpMethod.POST, "/sessions", new UserService(), new int[]{});
/* packages */ /* packages */
router.addRoute(HttpMethod.POST, "/packages", new TestService(), new int[]{}); router.addRoute(HttpMethod.POST, "/packages", new PackagesService(), new int[]{});
router.addRoute(HttpMethod.POST, "/transaction/packages", new TestService(), new int[]{}); router.addRoute(HttpMethod.POST, "/transaction/packages", new PackagesService(), new int[]{});
/* cards */ /* cards */
router.addRoute(HttpMethod.GET, "/cards", new TestService(), new int[]{}); router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{});
router.addRoute(HttpMethod.GET, "/deck", new TestService(), new int[]{}); router.addRoute(HttpMethod.GET, "/deck", new CardsService(), new int[]{});
router.addRoute(HttpMethod.PUT, "/deck", new TestService(), new int[]{}); router.addRoute(HttpMethod.PUT, "/deck", new CardsService(), new int[]{});
/* game */ /* game */
router.addRoute(HttpMethod.GET, "/stats", new TestService(), new int[]{}); router.addRoute(HttpMethod.GET, "/stats", new GameService(), new int[]{});
router.addRoute(HttpMethod.GET, "/scoreboard", new TestService(), new int[]{}); router.addRoute(HttpMethod.GET, "/scoreboard", new GameService(), new int[]{});
router.addRoute(HttpMethod.POST, "/battles", new TestService(), new int[]{}); router.addRoute(HttpMethod.POST, "/battles", new GameService(), new int[]{});
/* trading */ /* trading */
router.addRoute(HttpMethod.GET, "/tradings", new TestService(), new int[]{}); router.addRoute(HttpMethod.GET, "/tradings", new TradingService(), new int[]{});
router.addRoute(HttpMethod.POST, "/tradings", new TestService(), new int[]{}); router.addRoute(HttpMethod.POST, "/tradings", new TradingService(), new int[]{});
router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TestService(), new int[]{2}); router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TradingService(), new int[]{2});
router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TestService(), new int[]{2}); router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TradingService(), new int[]{2});
Server server = new Server(10001, 10, router); Server server = new Server(10001, 10, router);
server.start(); server.start();

View File

@ -0,0 +1,44 @@
package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import at.nanopenguin.mtcg.db.DbQuery;
import at.nanopenguin.mtcg.db.SqlCommand;
import at.nanopenguin.mtcg.db.Table;
import lombok.val;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
public class Package {
public static boolean create(List<Card> cards) throws SQLException {
// TODO: assert package size == 5
val result = DbQuery.builder()
.command(SqlCommand.INSERT)
.table(Table.PACKAGES)
.parameter("cost", 5)
.returnColumn("uuid")
.executeQuery();
if (result.isEmpty()) throw new SQLException(); // maybe change to different exception
if (!result.get(0).containsKey("uuid")) throw new SQLException();
UUID packageUuid = (UUID) result.get(0).get("uuid");
for (val card : cards) {
val query = DbQuery.builder()
.command(SqlCommand.INSERT)
.table(Table.CARDS);
if (card.id() != null) query.parameter("uuid", card.id());
if (query.parameter("damage", card.damage())
.parameter("name", card.name())
.parameter("package", packageUuid)
.executeUpdate() != 1){
return false;
}
}
return true;
}
}

View File

@ -52,12 +52,16 @@ public final class SessionHandler {
return uuid; return uuid;
} }
public static UUID uuidFromHttpHeader(String headerValue) {
return headerValue == null ? null : UUID.fromString(headerValue.replaceFirst("^Bearer ", ""));
}
public boolean verifyUUID(UUID uuid) { public boolean verifyUUID(UUID uuid) {
return verifyUUID(uuid, false); return verifyUUID(uuid, false);
} }
public boolean verifyUUID(UUID uuid, boolean requireAdmin) { public boolean verifyUUID(UUID uuid, boolean requireAdmin) {
return Sessions.containsKey(uuid) && (!requireAdmin || Sessions.get(uuid).admin()); return uuid != null && Sessions.containsKey(uuid) && (!requireAdmin || Sessions.get(uuid).admin());
} }
public boolean verifyUUID(UUID uuid, String username) { public boolean verifyUUID(UUID uuid, String username) {
@ -65,6 +69,6 @@ public final class SessionHandler {
} }
public boolean verifyUUID(UUID uuid, String username, boolean allowAdmin) { public boolean verifyUUID(UUID uuid, String username, boolean allowAdmin) {
return Sessions.containsKey(uuid) && (username.equals(Sessions.get(uuid).username()) || (allowAdmin && Sessions.get(uuid).admin())); return uuid != null && Sessions.containsKey(uuid) && (username.equals(Sessions.get(uuid).username()) || (allowAdmin && Sessions.get(uuid).admin()));
} }
} }

View File

@ -1,20 +1,46 @@
package at.nanopenguin.mtcg.application; package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.application.service.schemas.UserCredentials; import at.nanopenguin.mtcg.application.service.schemas.UserCredentials;
import at.nanopenguin.mtcg.application.service.schemas.UserData;
import at.nanopenguin.mtcg.db.DbQuery; import at.nanopenguin.mtcg.db.DbQuery;
import at.nanopenguin.mtcg.db.SqlCommand; import at.nanopenguin.mtcg.db.SqlCommand;
import at.nanopenguin.mtcg.db.Table; import at.nanopenguin.mtcg.db.Table;
import at.nanopenguin.mtcg.http.HttpStatus;
import at.nanopenguin.mtcg.http.Response;
import lombok.val;
import java.sql.SQLException; import java.sql.SQLException;
public class User { public class User {
public static int create(UserCredentials userCredentials) throws SQLException { public static boolean create(UserCredentials userCredentials) throws SQLException {
return DbQuery.builder() return DbQuery.builder()
.command(SqlCommand.INSERT) .command(SqlCommand.INSERT)
.table(Table.USERS) .table(Table.USERS)
.parameter("username", userCredentials.username()) .parameter("username", userCredentials.username())
.parameter("password", userCredentials.password()) .parameter("password", userCredentials.password())
.executeUpdate(); .executeUpdate() == 1;
} }
public static boolean update(String username, UserData userData) throws SQLException {
return DbQuery.builder()
.command(SqlCommand.UPDATE)
.table(Table.USERS)
.parameter("name", userData.name())
.parameter("bio", userData.bio())
.parameter("image", userData.image())
.condition("username", username)
.executeUpdate() == 1;
}
public static UserData retrieve(String username) throws SQLException {
val result = DbQuery.builder()
.command(SqlCommand.SELECT)
.table(Table.USERS)
.condition("username", username)
.executeQuery();
if (result.isEmpty()) return null;
val row1 = result.get(0);
return new UserData((String) row1.get("name"), (String) row1.get("bio"), (String) row1.get("image"));
}
} }

View File

@ -1,15 +0,0 @@
package at.nanopenguin.mtcg.application.service;
import at.nanopenguin.mtcg.application.service.Service;
import at.nanopenguin.mtcg.http.HttpStatus;
import at.nanopenguin.mtcg.http.HttpRequest;
import at.nanopenguin.mtcg.http.Response;
public class InternalErrorService implements Service {
/* For error in http server */
@Override
public Response handleRequest(HttpRequest request) {
return new Response(HttpStatus.INTERNAL, "application/json", "");
}
}

View File

@ -1,12 +1,22 @@
package at.nanopenguin.mtcg.application.service; package at.nanopenguin.mtcg.application.service;
import at.nanopenguin.mtcg.application.Package;
import at.nanopenguin.mtcg.application.SessionHandler;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import at.nanopenguin.mtcg.http.HttpMethod; import at.nanopenguin.mtcg.http.HttpMethod;
import at.nanopenguin.mtcg.http.HttpRequest; import at.nanopenguin.mtcg.http.HttpRequest;
import at.nanopenguin.mtcg.http.HttpStatus; import at.nanopenguin.mtcg.http.HttpStatus;
import at.nanopenguin.mtcg.http.Response; import at.nanopenguin.mtcg.http.Response;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.val;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class PackagesService implements Service { public class PackagesService implements Service {
@ -14,11 +24,21 @@ public class PackagesService implements Service {
public Response handleRequest(HttpRequest request) throws JsonProcessingException { public Response handleRequest(HttpRequest request) throws JsonProcessingException {
try { try {
if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) { if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) {
return new Response(HttpStatus.NOT_IMPLEMENTED); // new ObjectMapper().readValue(request.getBody(), Card.class); UUID uuid;
if ((uuid = SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) == null)
return new Response(HttpStatus.UNAUTHORIZED); // TODO: unauthorized for invalid token
if (!SessionHandler.getInstance().verifyUUID(uuid, true))
return new Response(HttpStatus.FORBIDDEN);
return new Response(Package.create(new ObjectMapper().readValue(request.getBody(), new TypeReference<List<Card>>() {})) ?
HttpStatus.CREATED :
HttpStatus.CONFLICT);
} }
if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 2)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) { if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 2)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) {
return new Response(HttpStatus.NOT_IMPLEMENTED); return new Response(!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) ?
HttpStatus.UNAUTHORIZED :
HttpStatus.NOT_IMPLEMENTED);
} }
return new Response(HttpStatus.NOT_FOUND); return new Response(HttpStatus.NOT_FOUND);
@ -26,5 +46,9 @@ public class PackagesService implements Service {
catch (ArrayIndexOutOfBoundsException e) { catch (ArrayIndexOutOfBoundsException e) {
return new Response(HttpStatus.BAD_REQUEST); return new Response(HttpStatus.BAD_REQUEST);
} }
catch (SQLException e) {
System.out.println(e.getMessage());
return new Response(HttpStatus.INTERNAL);
}
} }
} }

View File

@ -35,38 +35,23 @@ public class UserService implements Service {
return switch (request.getMethod()) { return switch (request.getMethod()) {
case GET -> { case GET -> {
String username = request.getPath().split("/")[2]; String username = request.getPath().split("/")[2];
if (request.getHttpHeader("Authorization") == null || !SessionHandler.getInstance().verifyUUID(UUID.fromString(request.getHttpHeader("Authorization").replaceFirst("^Bearer ", "")), username, true)) if (!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true))
yield new Response(HttpStatus.UNAUTHORIZED); yield new Response(HttpStatus.UNAUTHORIZED);
val result = DbQuery.builder() val userData = User.retrieve(username);
.command(SqlCommand.SELECT) yield userData != null ?
.table(Table.USERS) new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(userData)) :
.condition("username", username) new Response(HttpStatus.NOT_FOUND);
.executeQuery();
if (result.isEmpty()) yield new Response(HttpStatus.NOT_FOUND);
val row1 = result.get(0);
UserData userData = new UserData((String) row1.get("name"), (String) row1.get("bio"), (String) row1.get("image"));
yield new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(userData));
} }
case POST -> { // register new user case POST -> { // register new user
int success = User.create(new ObjectMapper().readValue(request.getBody(), UserCredentials.class)); UserCredentials userCredentials = new ObjectMapper().readValue(request.getBody(), UserCredentials.class);
yield new Response(success > 0 ? HttpStatus.CREATED : HttpStatus.CONFLICT); yield new Response(User.create(userCredentials) ? HttpStatus.CREATED : HttpStatus.CONFLICT);
} }
case PUT -> { case PUT -> {
String username = request.getPath().split("/")[2]; String username = request.getPath().split("/")[2];
UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class); UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class);
if (request.getHttpHeader("Authorization") == null || !SessionHandler.getInstance().verifyUUID(UUID.fromString(request.getHttpHeader("Authorization").replaceFirst("^Bearer ", "")), username, true)) if (!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true))
yield new Response(HttpStatus.UNAUTHORIZED); yield new Response(HttpStatus.UNAUTHORIZED);
if (DbQuery.builder() yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND);
.command(SqlCommand.UPDATE)
.table(Table.USERS)
.parameter("name", userData.name())
.parameter("bio", userData.bio())
.parameter("image", userData.image())
.condition("username", username)
.executeUpdate() == 1) {
yield new Response(HttpStatus.OK);
}
yield new Response(HttpStatus.NOT_FOUND);
} }
default -> new Response(HttpStatus.NOT_FOUND); default -> new Response(HttpStatus.NOT_FOUND);
}; };

View File

@ -2,6 +2,8 @@ package at.nanopenguin.mtcg.application.service.schemas;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public record Card(String id, String name, Float damage) { public record Card(UUID id, String name, Float damage) {
} }

View File

@ -8,6 +8,7 @@ import lombok.val;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
@Builder @Builder
public final class DbQuery { public final class DbQuery {
@ -23,15 +24,24 @@ public final class DbQuery {
private SortedMap<String, Object> parameters; private SortedMap<String, Object> parameters;
@Singular @Singular
private SortedMap<String, Object> conditions; private SortedMap<String, Object> conditions;
private String returnColumn;
public static class DbQueryBuilder { public static class DbQueryBuilder {
public List<Map<String, Object>> executeQuery() throws SQLException { public List<Map<String, Object>> executeQuery() throws SQLException {
DbQuery dbQuery = this.build(); DbQuery dbQuery = this.build();
if (dbQuery.command != SqlCommand.SELECT) throw new SQLException(); if (dbQuery.command != SqlCommand.SELECT && dbQuery.returnColumn == null) throw new SQLException();
return dbQuery.read(); return switch (dbQuery.command) {
case INSERT -> dbQuery.create(true);
case SELECT -> dbQuery.read();
case UPDATE -> dbQuery.update(true);
default -> throw new SQLException();
};
} }
public int executeUpdate() throws SQLException { public int executeUpdate() throws SQLException {
if (this.returnColumn != null) throw new SQLException();
DbQuery dbQuery = this.build(); DbQuery dbQuery = this.build();
return switch (dbQuery.command) { return switch (dbQuery.command) {
case INSERT -> dbQuery.create(); case INSERT -> dbQuery.create();
@ -47,104 +57,125 @@ public final class DbQuery {
} }
private String buildParameterizedString(@NonNull SortedMap<String, Object> parameters) { private String buildParameterizedString(@NonNull SortedMap<String, Object> parameters) {
if (!parameters.isEmpty()) { if (parameters.isEmpty()) {
return String.join(" = ?, ", parameters.keySet()) + " = ?"; return "";
} }
return ""; return String.join(" = ?, ", parameters.keySet()) + " = ?";
} }
private int create() throws SQLException { private static int executeUpdate(String sql, List<Object> parameterValues) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement.");
try (Connection connection = connect()) { try (Connection connection = connect()) {
String columns = this.parameters.keySet().stream()
.filter(columnName -> columnName.matches("[a-zA-Z0-9_]+"))
.collect(Collectors.joining(", "));
String sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING;", table.table, columns, String.join(", ", Collections.nCopies(this.parameters.size(), "?")));
// on conflict return int equals 0
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
int i = 1;
for (val entry : this.parameters.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
}
return preparedStatement.executeUpdate();
}
}
}
private List<Map<String, Object>> read() throws SQLException {
try (Connection connection = connect()) {
StringJoiner columnJoiner = new StringJoiner(", ");
if (this.columns.isEmpty()) {
columnJoiner.add("*");
}
this.columns.forEach(columnJoiner::add);
String sql = String.format("SELECT %s FROM %s%s;", columnJoiner, table.table,
this.conditions.isEmpty() ? "" : " WHERE (" + this.buildParameterizedString(this.conditions) + ")");
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
int i = 1;
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
}
ResultSet resultSet = preparedStatement.executeQuery();
List<Map<String, Object>> result = new ArrayList<>();
while (resultSet.next()) {
Map<String, Object> row = new HashMap<>();
for (i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
row.put(resultSet.getMetaData().getColumnName(i), resultSet.getObject(i));
}
result.add(row);
}
return result;
}
}
}
private int update() throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException();
if (this.conditions.isEmpty()) throw new SQLException();
try (Connection connection = connect()) {
String sql = String.format("UPDATE %s SET %s WHERE (%s);", table.table,
this.buildParameterizedString(this.parameters),
this.buildParameterizedString(this.conditions));
PreparedStatement preparedStatement = connection.prepareStatement(sql); PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1; int i = 1;
for (val entry : this.parameters.entrySet()) { for (val value : parameterValues) {
preparedStatement.setObject(i++, entry.getValue()); preparedStatement.setObject(i++, value);
}
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
} }
return preparedStatement.executeUpdate(); return preparedStatement.executeUpdate();
} }
} }
private int delete() throws SQLException { private static List<Map<String, Object>> executeQuery(String sql, List<Object> parameterValues) throws SQLException {
if (this.conditions.isEmpty()) throw new SQLException();
try (Connection connection = connect()) { try (Connection connection = connect()) {
String sql = String.format("DELETE FROM %s WHERE (%s);", table.table, this.buildParameterizedString(this.conditions)); PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { for (val value : parameterValues) {
int i = 1; preparedStatement.setObject(i++, value);
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
}
return preparedStatement.executeUpdate();
} }
ResultSet resultSet = preparedStatement.executeQuery();
List<Map<String, Object>> result = new ArrayList<>();
while (resultSet.next()) {
Map<String, Object> row = new HashMap<>();
for (i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
row.put(resultSet.getMetaData().getColumnName(i), resultSet.getObject(i));
}
result.add(row);
}
return result;
} }
} }
private String getInsertStatement(boolean hasReturn) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement.");
String columns = this.parameters.keySet().stream()
.filter(columnName -> columnName.matches("[a-zA-Z0-9_]+"))
.collect(Collectors.joining(", "));
return String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING%s;",
table.table,
columns,
String.join(", ", Collections.nCopies(this.parameters.size(), "?")),
hasReturn ? " RETURNING " + this.returnColumn : "");
}
private int create() throws SQLException {
return executeUpdate(getInsertStatement(false), new ArrayList<>(this.parameters.values()));
}
private List<Map<String, Object>> create(boolean hasReturn) throws SQLException {
if (!hasReturn) {
// throw dev-is-stupid error
}
return executeQuery(getInsertStatement(true), new ArrayList<>(this.parameters.values()));
}
private List<Map<String, Object>> read() throws SQLException {
StringJoiner columnJoiner = new StringJoiner(", ");
if (this.columns.isEmpty()) {
columnJoiner.add("*");
}
this.columns.forEach(columnJoiner::add);
String sql = String.format("SELECT %s FROM %s%s;",
columnJoiner,
table.table,
this.conditions.isEmpty() ? "" : " WHERE (" + this.buildParameterizedString(this.conditions) + ")");
return executeQuery(sql, new ArrayList<>(this.conditions.values()));
}
private String getUpdateStatement(boolean hasReturn) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException();
if (this.conditions.isEmpty()) throw new SQLException();
return String.format("UPDATE %s SET %s WHERE (%s)%s;",
table.table,
this.buildParameterizedString(this.parameters),
this.buildParameterizedString(this.conditions),
hasReturn ? " RETURNING " + this.returnColumn : "");
}
private int update() throws SQLException {
return executeUpdate(getUpdateStatement(false), new ArrayList<>(
Stream.concat(
this.parameters.values().stream(),
this.conditions.values().stream()
).collect(Collectors.toList())));
}
private List<Map<String, Object>> update(boolean hasReturn) throws SQLException {
if (!hasReturn) {
// throw dev-is-stupid error
}
return executeQuery(getUpdateStatement(true), new ArrayList<>(
Stream.concat(
this.parameters.values().stream(),
this.conditions.values().stream()
).collect(Collectors.toList())));
}
private int delete() throws SQLException {
if (this.conditions.isEmpty()) throw new SQLException();
String sql = String.format("DELETE FROM %s WHERE (%s);",
table.table,
this.buildParameterizedString(this.conditions));
return executeUpdate(sql, new ArrayList<>(this.conditions.values()));
}
} }

View File

@ -4,7 +4,9 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public enum Table { public enum Table {
USERS("users"); USERS("users"),
CARDS("cards"),
PACKAGES("packages");
public final String table; public final String table;
} }