add: login working

This commit is contained in:
Jose
2025-12-21 08:33:58 +01:00
parent 5136a67fba
commit af675a32b8
24 changed files with 628 additions and 929 deletions

View File

@@ -124,6 +124,11 @@ public class ConfigManager {
return Boolean.parseBoolean(System.getenv("RUNNING_IN_DOCKER"));
}
public String getApiPrefix(String domain) {
return getStringProperty("api." + domain + ".prefix").replace("${app.version}",
String.valueOf(getStringProperty("app.version")));
}
public String getStringProperty(String key) {
return config.getProperty(key);
}

View File

@@ -1,67 +1,45 @@
package net.miarma.api.backlib.db;
import java.lang.reflect.Field;
import java.util.UUID;
import com.google.gson.annotations.SerializedName;
import org.slf4j.Logger;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.annotations.APIDontReturn;
import net.miarma.api.backlib.interfaces.IValuableEnum;
import net.miarma.api.backlib.log.LoggerProvider;
import net.miarma.api.backlib.util.BufferUtil;
/**
* Clase base para todas las entidades persistentes del sistema.
* <p>
* Proporciona utilidades para:
* <ul>
* <li>Construir una entidad a partir de una fila de base de datos ({@link Row})</li>
* <li>Serializar una entidad a {@link JsonObject}</li>
* <li>Generar una representación en texto</li>
* </ul>
*
* Los campos se mapean por reflexión, lo que permite extender fácilmente las entidades
* sin necesidad de escribir lógica de parsing repetitiva.
*
* @author José Manuel Amador Gallardo
*/
public abstract class AbstractEntity {
private final Logger LOGGER = LoggerProvider.getLogger();
private final Logger LOGGER = LoggerProvider.getLogger();
/**
* Constructor por defecto. Requerido para instanciación sin datos.
*/
public AbstractEntity() {}
public AbstractEntity() {}
/**
* Constructor que inicializa los campos de la entidad a partir de una fila de base de datos.
*
* @param row Fila SQL proporcionada por Vert.x.
*/
public AbstractEntity(Row row) {
populateFromRow(row);
}
/**
* Rellena los campos del objeto usando reflexión a partir de una {@link Row} de Vert.x.
* Se soportan tipos básicos (String, int, boolean, etc.), enums con método estático {@code fromInt(int)},
* y {@link java.math.BigDecimal} (a través del tipo {@code Numeric} de Vert.x).
* <p>
* Si un tipo no está soportado, se registra un error en el log y se ignora ese campo.
*
* @param row Fila de datos de la que extraer los valores.
*/
private String getColumnName(Field field) {
if (field.isAnnotationPresent(SerializedName.class)) {
return field.getAnnotation(SerializedName.class).value();
}
return field.getName();
}
private void populateFromRow(Row row) {
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
Class<?> type = field.getType();
String name = field.getName();
String columnName = getColumnName(field);
Object value;
if (type.isEnum()) {
Integer intValue = row.getInteger(name);
Integer intValue = row.getInteger(columnName);
if (intValue != null) {
try {
var method = type.getMethod("fromInt", int.class);
@@ -73,43 +51,44 @@ public abstract class AbstractEntity {
value = null;
}
} else {
value = switch (type.getSimpleName()) {
case "Integer", "int" -> row.getInteger(name);
case "String" -> row.getString(name);
case "double", "Double" -> row.getDouble(name);
case "long", "Long" -> row.getLong(name);
case "boolean", "Boolean" -> row.getBoolean(name);
case "LocalDateTime" -> row.getLocalDateTime(name);
case "BigDecimal" -> {
try {
var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(name));
yield numeric != null ? numeric.bigDecimalValue() : null;
} catch (Exception e) {
yield null;
}
}
default -> {
LOGGER.error("Type not supported yet: {} for field {}", type.getName(), name);
yield null;
}
};
value = switch (type.getSimpleName()) {
case "Integer", "int" -> row.getInteger(columnName);
case "String" -> row.getString(columnName);
case "double", "Double" -> row.getDouble(columnName);
case "long", "Long" -> row.getLong(columnName);
case "boolean", "Boolean" -> row.getBoolean(columnName);
case "LocalDateTime" -> row.getLocalDateTime(columnName);
case "UUID" -> {
Object raw = row.getValue(columnName);
if (raw instanceof UUID) yield raw;
if (raw instanceof String s) yield UUID.fromString(s);
if (raw instanceof Buffer b) {
yield BufferUtil.uuidFromBuffer(b);
}
yield null;
}
case "BigDecimal" -> {
try {
var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(columnName));
yield numeric != null ? numeric.bigDecimalValue() : null;
} catch (Exception e) { yield null; }
}
default -> {
LOGGER.error("Type not supported yet: {} for field {}", type.getName(), field.getName());
yield null;
}
};
}
field.set(this, value);
if (value != null) {
field.set(this, value);
}
} catch (Exception e) {
LOGGER.error("Error populating field {}: {}", field.getName(), e.getMessage());
}
}
}
/**
* Codifica esta entidad como un objeto JSON, omitiendo los campos anotados con {@link APIDontReturn}.
*
* <p>Si un campo implementa {@link IValuableEnum}, se usará su valor en lugar del nombre del enum.</p>
*
* @return Representación JSON de esta entidad.
*/
public String encode() {
JsonObject json = new JsonObject();
Class<?> clazz = this.getClass();
@@ -117,13 +96,13 @@ public abstract class AbstractEntity {
while (clazz != null) {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(APIDontReturn.class)) continue;
field.setAccessible(true);
try {
Object value = field.get(this);
if (value instanceof IValuableEnum ve) {
json.put(field.getName(), ve.getValue());
} else if (value instanceof UUID u) {
json.put(field.getName(), u.toString());
} else {
json.put(field.getName(), value);
}
@@ -133,31 +112,22 @@ public abstract class AbstractEntity {
}
clazz = clazz.getSuperclass();
}
return json.encode();
}
/**
* Devuelve una representación en texto de la entidad, mostrando todos los campos y sus valores.
*
* <p>Útil para logs y debugging.</p>
*
* @return Cadena de texto con el nombre de la clase y todos los campos.
*/
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName()).append(" [ ");
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
sb.append(field.getName()).append("= ").append(field.get(this)).append(", ");
} catch (IllegalAccessException e) {
LOGGER.error("Error stringing field {}: {}", field.getName(), e.getMessage());
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName()).append(" [ ");
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
sb.append(field.getName()).append("=").append(field.get(this)).append(", ");
} catch (IllegalAccessException e) {
LOGGER.error("Error stringing field {}: {}", field.getName(), e.getMessage());
}
}
sb.append("]");
return sb.toString();
}
sb.append("]");
return sb.toString();
}
}
}

View File

@@ -3,73 +3,40 @@ package net.miarma.api.backlib.db;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.*;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import com.google.gson.annotations.SerializedName;
import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.log.LoggerProvider;
/**
* Clase utilitaria para construir queries SQL dinámicamente mediante reflexión,
* usando entidades anotadas con {@link Table}.
* <p>
* Soporta operaciones SELECT, INSERT, UPDATE (con y sin valores nulos), y UPSERT.
* También permite aplicar filtros desde un mapa o directamente desde un objeto.
* <p>
* ¡Ojo! No ejecuta la query, solo la construye.
*
* @author José Manuel Amador Gallardo
*/
public class QueryBuilder {
private final static Logger LOGGER = LoggerProvider.getLogger();
private static final Logger LOGGER = LoggerProvider.getLogger();
private final StringBuilder query;
private String sort;
private String order;
private String limit;
private String orderByClause;
private String limitClause;
private String offsetClause;
private Class<?> entityClass;
public QueryBuilder() {
this.query = new StringBuilder();
}
/**
* Obtiene el nombre de la tabla desde la anotación @Table de la clase dada.
*/
private static <T> String getTableName(Class<T> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Class cannot be null");
}
if (clazz.isAnnotationPresent(Table.class)) {
Table annotation = clazz.getAnnotation(Table.class);
return annotation.value();
}
throw new IllegalArgumentException("Class does not have @Table annotation");
if (clazz == null) throw new IllegalArgumentException("Class cannot be null");
if (!clazz.isAnnotationPresent(Table.class)) throw new IllegalArgumentException("Class does not have @Table annotation");
return clazz.getAnnotation(Table.class).value();
}
/**
* Devuelve la consulta SQL construida hasta el momento.
*/
public String getQuery() {
return query.toString();
private static String getColumnName(Field field) {
SerializedName annotation = field.getAnnotation(SerializedName.class);
return annotation != null ? annotation.value() : field.getName();
}
/**
* Extrae el valor de un campo, manejando enums y tipos especiales.
* Si es un Enum y tiene getValue(), lo usa; si no, devuelve el name().
* Si es un LocalDateTime, lo convierte a String en formato SQL.
*/
private static Object extractValue(Object fieldValue) {
if (fieldValue == null) return null;
if (fieldValue instanceof Enum<?>) {
try {
var method = fieldValue.getClass().getMethod("getValue");
@@ -78,199 +45,137 @@ public class QueryBuilder {
return ((Enum<?>) fieldValue).name();
}
}
if (fieldValue instanceof LocalDateTime ldt) {
return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
return fieldValue;
}
/**
* Escapa los caracteres especiales en una cadena para evitar inyecciones SQL.
* @param value the string value to escape
* @return the escaped string
*/
private String formatValue(Object value) {
if (value == null) return "NULL";
if (value instanceof UUID uuid) {
return "UNHEX(REPLACE('" + uuid.toString() + "','-',''))";
}
if (value instanceof String || value instanceof LocalDateTime) {
return "'" + escapeSql(value.toString()) + "'";
}
return value.toString();
}
private static String escapeSql(String value) {
return value.replace("'", "''");
}
/**
* Construye una consulta SELECT para la clase dada, con columnas opcionales.
* @param clazz the entity class to query
* @param columns optional columns to select; if empty, selects all columns
* @return the current QueryBuilder instance
* @param <T> the type of the entity class
* @param clazz La clase de la entidad a seleccionar
* @param columns Columnas a seleccionar, si no se pasan selecciona "*"
*/
public static <T> QueryBuilder select(Class<T> clazz, String... columns) {
if (clazz == null) {
throw new IllegalArgumentException("Class cannot be null");
}
QueryBuilder qb = new QueryBuilder();
qb.entityClass = clazz;
String tableName = getTableName(clazz);
qb.query.append("SELECT ");
if (columns.length == 0) {
qb.query.append("* ");
} else {
StringJoiner joiner = new StringJoiner(", ");
for (String column : columns) {
if (column != null) {
joiner.add(column);
}
}
qb.query.append(joiner).append(" ");
}
qb.query.append("FROM ").append(tableName).append(" ");
if (columns.length == 0) qb.query.append("* ");
else qb.query.append(String.join(", ", columns)).append(" ");
qb.query.append("FROM ").append(getTableName(clazz)).append(" ");
return qb;
}
/**
* Añade una cláusula WHERE a la consulta actual, filtrando por los campos del mapa.
* Los valores pueden ser números o cadenas, y se manejan adecuadamente.
*
* @param filters un mapa de filtros donde la clave es el nombre del campo y el valor es el valor a filtrar
* @return el QueryBuilder actual para encadenar más métodos
* @param filters Mapa clave = valor para el WHERE. Detecta UUID y Strings tipo IN
*/
public QueryBuilder where(Map<String, String> filters) {
if (filters == null || filters.isEmpty()) {
return this;
public QueryBuilder where(Map<String, Object> filters) {
if (filters == null || filters.isEmpty() || entityClass == null) return this;
Map<String, String> javaToSql = new HashMap<>();
Map<String, String> sqlToSql = new HashMap<>();
for (Field f : entityClass.getDeclaredFields()) {
String sqlCol = getColumnName(f);
javaToSql.put(f.getName(), sqlCol);
sqlToSql.put(sqlCol, sqlCol);
}
Set<String> validFields = entityClass != null
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
: Collections.emptySet();
List<String> conditions = new ArrayList<>();
for (Map.Entry<String, String> entry : filters.entrySet()) {
for (Map.Entry<String, Object> entry : filters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (!validFields.contains(key)) {
String sqlCol = javaToSql.getOrDefault(key, sqlToSql.get(key));
if (sqlCol == null) {
LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key);
continue;
}
Object val = entry.getValue();
if (val == null) continue;
if (val instanceof String s && s.startsWith("(") && s.endsWith(")")) conditions.add(sqlCol + " IN " + s);
else conditions.add(sqlCol + " = " + formatValue(val));
}
if (value.startsWith("(") && value.endsWith(")")) {
conditions.add(key + " IN " + value);
} else if (value.matches("-?\\d+(\\.\\d+)?")) {
conditions.add(key + " = " + value);
} else {
conditions.add(key + " = '" + value + "'");
if (!conditions.isEmpty()) {
String prefix = query.toString().contains("WHERE") ? "AND " : "WHERE ";
query.append(prefix).append(String.join(" AND ", conditions)).append(" ");
}
return this;
}
/**
* @param object Objeto con campos para WHERE. Soporta UUID y LocalDateTime
*/
public <T> QueryBuilder where(T object) {
if (object == null) throw new IllegalArgumentException("Object cannot be null");
if (entityClass == null) return this;
Set<String> validColumns = Arrays.stream(entityClass.getDeclaredFields())
.map(QueryBuilder::getColumnName)
.collect(Collectors.toSet());
List<String> conditions = new ArrayList<>();
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object value = field.get(object);
if (value != null) {
String col = getColumnName(field);
if (!validColumns.contains(col)) continue;
Object extracted = extractValue(value);
conditions.add(col + " = " + formatValue(extracted));
}
} catch (IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
if (!conditions.isEmpty()) {
query.append("WHERE ").append(String.join(" AND ", conditions)).append(" ");
String prefix = query.toString().contains("WHERE") ? "AND " : "WHERE ";
query.append(prefix).append(String.join(" AND ", conditions)).append(" ");
}
return this;
}
/**
* Añade una cláusula WHERE a la consulta actual, filtrando por los campos del objeto.
* Los valores se extraen mediante reflexión y se manejan adecuadamente.
*
* @param object el objeto del cual se extraerán los campos para filtrar
* @return el QueryBuilder actual para encadenar más métodos
*/
public <T> QueryBuilder where(T object) {
if (object == null) {
throw new IllegalArgumentException("Object cannot be null");
}
Set<String> validFields = entityClass != null
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
: Collections.emptySet();
this.query.append("WHERE ");
StringJoiner joiner = new StringJoiner(" AND ");
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(object);
if (fieldValue != null) {
String key = field.getName();
if (!validFields.contains(key)) {
LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key);
continue;
}
Object value = extractValue(fieldValue);
if (value instanceof String || value instanceof LocalDateTime) {
joiner.add(key + " = '" + value + "'");
} else {
joiner.add(key + " = " + value.toString());
}
}
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
this.query.append(joiner).append(" ");
return this;
}
/**
* Construye una consulta INSERT para el objeto dado, insertando todos sus campos.
* Los valores se extraen mediante reflexión y se manejan adecuadamente.
*
* @param object el objeto a insertar
* @return el QueryBuilder actual para encadenar más métodos
* @param <T> el tipo del objeto a insertar
*/
public static <T> QueryBuilder insert(T object) {
if (object == null) {
throw new IllegalArgumentException("Object cannot be null");
}
if (object == null) throw new IllegalArgumentException("Object cannot be null");
QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass());
qb.query.append("INSERT INTO ").append(table).append(" ");
qb.query.append("(");
StringJoiner columns = new StringJoiner(", ");
StringJoiner values = new StringJoiner(", ");
StringJoiner cols = new StringJoiner(", ");
StringJoiner vals = new StringJoiner(", ");
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
columns.add(field.getName());
Object fieldValue = field.get(object);
if (fieldValue != null) {
Object value = extractValue(fieldValue);
if (value instanceof String || value instanceof LocalDateTime) {
values.add("'" + escapeSql((String) value) + "'");
} else {
values.add(value.toString());
}
} else {
values.add("NULL");
}
} catch (IllegalArgumentException | IllegalAccessException e) {
cols.add(getColumnName(field));
Object value = extractValue(field.get(object));
vals.add(qb.formatValue(value));
} catch (IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
qb.query.append(columns).append(") ");
qb.query.append("VALUES (").append(values).append(") RETURNING * ");
qb.query.append("(").append(cols).append(") VALUES (").append(vals).append(") RETURNING * ");
return qb;
}
/**
* Construye una consulta UPDATE para el objeto dado, actualizando todos sus campos.
* Los valores se extraen mediante reflexión y se manejan adecuadamente.
* Requiere que el objeto tenga un campo ID (terminado en _id) para la cláusula WHERE.
*
* @param object el objeto a actualizar
* @return el QueryBuilder actual para encadenar más métodos
* @param <T> el tipo del objeto a actualizar
*/
public static <T> QueryBuilder update(T object) {
if (object == null) {
throw new IllegalArgumentException("Object cannot be null");
}
private static <T> QueryBuilder buildUpdate(T object, boolean includeNulls) {
if (object == null) throw new IllegalArgumentException("Object cannot be null");
QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass());
@@ -278,103 +183,36 @@ public class QueryBuilder {
StringJoiner setJoiner = new StringJoiner(", ");
StringJoiner whereJoiner = new StringJoiner(" AND ");
Field idField = null;
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(object);
if (fieldValue == null) continue;
String fieldName = field.getName();
Object value = extractValue(fieldValue);
if (fieldName.endsWith("_id")) {
String col = getColumnName(field);
Object value = extractValue(field.get(object));
if (col.endsWith("_id")) {
idField = field;
whereJoiner.add(fieldName + " = " + (value instanceof String
|| value instanceof LocalDateTime ? "'" + value + "'" : value));
if (value != null) whereJoiner.add(col + " = " + qb.formatValue(value));
else throw new IllegalArgumentException("ID field cannot be null");
continue;
}
setJoiner.add(fieldName + " = " + (value instanceof String
|| value instanceof LocalDateTime ? "'" + value + "'" : value));
} catch (Exception e) {
if (value != null) setJoiner.add(col + " = " + qb.formatValue(value));
else if (includeNulls) setJoiner.add(col + " = NULL");
} catch (IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
if (idField == null) {
throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
}
if (idField == null) throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner).append(" ");
return qb;
}
/**
* Construye una consulta UPDATE que establece los campos a NULL si son nulos.
* Requiere que el objeto tenga un campo ID (terminado en _id) para la cláusula WHERE.
*
* @param object el objeto a actualizar
* @return el QueryBuilder actual para encadenar más métodos
* @param <T> el tipo del objeto a actualizar
*/
public static <T> QueryBuilder updateWithNulls(T object) {
if (object == null) {
throw new IllegalArgumentException("Object cannot be null");
}
public static <T> QueryBuilder update(T object) { return buildUpdate(object, false); }
QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass());
qb.query.append("UPDATE ").append(table).append(" SET ");
public static <T> QueryBuilder updateWithNulls(T object) { return buildUpdate(object, true); }
StringJoiner setJoiner = new StringJoiner(", ");
StringJoiner whereJoiner = new StringJoiner(" AND ");
Field idField = null;
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
String fieldName = field.getName();
Object fieldValue = field.get(object);
if (fieldName.endsWith("_id")) {
idField = field;
Object value = extractValue(fieldValue);
whereJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
continue;
}
if (fieldValue == null) {
setJoiner.add(fieldName + " = NULL");
} else {
Object value = extractValue(fieldValue);
setJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
}
} catch (Exception e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
if (idField == null) {
throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
}
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
return qb;
}
/**
* Construye una consulta UPSERT (INSERT o UPDATE) para el objeto dado.
* Si hay claves de conflicto, se actualizan los campos excepto las claves duplicadas.
*
* @param object el objeto a insertar o actualizar
* @param conflictKeys las claves que causan conflictos y no deben actualizarse
* @return el QueryBuilder actual para encadenar más métodos
* @param <T> el tipo del objeto a insertar o actualizar
*/
public static <T> QueryBuilder upsert(T object, String... conflictKeys) {
if (object == null) throw new IllegalArgumentException("Object cannot be null");
@@ -382,146 +220,75 @@ public class QueryBuilder {
String table = getTableName(object.getClass());
qb.query.append("INSERT INTO ").append(table).append(" ");
StringJoiner columns = new StringJoiner(", ");
StringJoiner values = new StringJoiner(", ");
StringJoiner cols = new StringJoiner(", ");
StringJoiner vals = new StringJoiner(", ");
Map<String, String> updates = new HashMap<>();
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(object);
String columnName = field.getName();
columns.add(columnName);
Object value = extractValue(fieldValue);
String valueStr = value == null ? "NULL"
: (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value.toString());
values.add(valueStr);
// no actualizamos la clave duplicada
boolean isConflictKey = Arrays.asList(conflictKeys).contains(columnName);
if (!isConflictKey) {
updates.put(columnName, valueStr);
}
} catch (Exception e) {
String col = getColumnName(field);
Object value = extractValue(field.get(object));
String formatted = qb.formatValue(value);
cols.add(col);
vals.add(formatted);
if (!Arrays.asList(conflictKeys).contains(col)) updates.put(col, formatted);
} catch (IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
qb.query.append("(").append(columns).append(") VALUES (").append(values).append(")");
if (conflictKeys.length > 0 && !updates.isEmpty()) {
qb.query.append("(").append(cols).append(") VALUES (").append(vals).append(")");
if (!updates.isEmpty() && conflictKeys.length > 0) {
qb.query.append(" ON DUPLICATE KEY UPDATE ");
StringJoiner updateSet = new StringJoiner(", ");
updates.forEach((k, v) -> updateSet.add(k + " = " + v));
qb.query.append(updateSet);
qb.query.append(updates.entrySet().stream()
.map(e -> e.getKey() + " = " + e.getValue())
.collect(Collectors.joining(", ")));
}
return qb;
}
/**
* Construye una consulta DELETE para el objeto dado, eliminando registros que coincidan con sus campos.
* Los valores se extraen mediante reflexión y se manejan adecuadamente.
*
* @param object el objeto a eliminar
* @return el QueryBuilder actual para encadenar más métodos
* @param <T> el tipo del objeto a eliminar
*/
public static <T> QueryBuilder delete(T object) {
if (object == null) throw new IllegalArgumentException("Object cannot be null");
QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass());
qb.query.append("DELETE FROM ").append(table).append(" WHERE ");
StringJoiner joiner = new StringJoiner(" AND ");
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(object);
if (fieldValue != null) {
Object value = extractValue(fieldValue);
joiner.add(field.getName() + " = " + (value instanceof String
|| value instanceof LocalDateTime ? "'" + value + "'" : value.toString()));
}
} catch (Exception e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
}
}
qb.query.append(joiner).append(" ");
return qb;
qb.entityClass = object.getClass();
String table = getTableName(qb.entityClass);
qb.query.append("DELETE FROM ").append(table).append(" ");
return qb.where(object);
}
/**
* Añade una cláusula ORDER BY a la consulta actual, ordenando por la columna y el orden especificados.
* Si la columna no es válida, se ignora.
*
* @param column la columna por la que ordenar
* @param order el orden (ASC o DESC); si no se especifica, se asume ASC
* @return el QueryBuilder actual para encadenar más métodos
*/
public QueryBuilder orderBy(Optional<String> column, Optional<String> order) {
column.ifPresent(c -> {
if (entityClass != null) {
boolean isValid = Arrays.stream(entityClass.getDeclaredFields())
.map(Field::getName)
.anyMatch(f -> f.equals(c));
if (!isValid) {
boolean valid = Arrays.stream(entityClass.getDeclaredFields())
.map(QueryBuilder::getColumnName)
.anyMatch(f -> f.equals(c));
if (!valid) {
LOGGER.warn("[QueryBuilder] Ignorando campo invalido en ORDER BY: {}", c);
return;
}
}
sort = "ORDER BY " + c + " ";
order.ifPresent(o -> sort += o.equalsIgnoreCase("asc") ? "ASC" : "DESC" + " ");
orderByClause = "ORDER BY " + c + " " + order.orElse("ASC") + " ";
});
return this;
}
/**
* Añade una cláusula LIMIT a la consulta actual, limitando el número de resultados.
* Si se especifica un offset, se añade también.
*
* @param limitParam el número máximo de resultados a devolver; si no se especifica, no se aplica límite
* @return el QueryBuilder actual para encadenar más métodos
*/
public QueryBuilder limit(Optional<Integer> limitParam) {
limitParam.ifPresent(param -> limit = "LIMIT " + param + " ");
limitParam.ifPresent(l -> limitClause = "LIMIT " + l + " ");
return this;
}
/**
* Añade una cláusula OFFSET a la consulta actual, desplazando el inicio de los resultados.
* Si se especifica un offset, se añade también.
*
* @param offsetParam el número de resultados a omitir antes de empezar a devolver resultados; si no se especifica, no se aplica offset
* @return el QueryBuilder actual para encadenar más métodos
*/
public QueryBuilder offset(Optional<Integer> offsetParam) {
offsetParam.ifPresent(param -> limit += "OFFSET " + param + " ");
offsetParam.ifPresent(o -> offsetClause = "OFFSET " + o + " ");
return this;
}
/**
* Construye y devuelve la consulta SQL completa.
* Si no se han añadido cláusulas ORDER BY, LIMIT o OFFSET, las omite.
*
* @return la consulta SQL construida
*/
public String build() {
if (order != null && !order.isEmpty()) {
query.append(order);
}
if (sort != null && !sort.isEmpty()) {
query.append(sort);
}
if (limit != null && !limit.isEmpty()) {
query.append(limit);
}
if (orderByClause != null) query.append(orderByClause);
if (limitClause != null) query.append(limitClause);
if (offsetClause != null) query.append(offsetClause);
return query.toString().trim() + ";";
}
}
}

View File

@@ -32,7 +32,7 @@ public class JWTManager {
* Genera un token JWT usando UUID para el usuario.
*/
public String generateToken(
String userName,
String displayName,
UUID userId,
IUserRole role,
Integer serviceId,
@@ -45,7 +45,7 @@ public class JWTManager {
);
return JWT.create()
.withSubject(userName)
.withSubject(displayName)
.withClaim("userId", userId.toString())
.withClaim("serviceId", serviceId)
.withClaim("role", role.name())
@@ -54,6 +54,10 @@ public class JWTManager {
.sign(algorithm);
}
public DecodedJWT decode(String token) {
return JWT.decode(token);
}
public UUID extractUserId(String token) {
try {
DecodedJWT jwt = verifier.verify(token);

View File

@@ -0,0 +1,16 @@
package net.miarma.api.backlib.util;
import java.util.UUID;
import io.vertx.core.buffer.Buffer;
public class BufferUtil {
public static UUID uuidFromBuffer(Buffer b) {
if (b == null || b.length() != 16) {
return null;
}
long mostSigBits = b.getLong(0);
long leastSigBits = b.getLong(8);
return new UUID(mostSigBits, leastSigBits);
}
}