mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-09 05:48:20 +00:00
WIP
This commit is contained in:
parent
1f6c6900e2
commit
9c15bb33e7
13
pom.xml
13
pom.xml
@ -13,10 +13,17 @@
|
||||
<artifactId>spring-boot-db-admin</artifactId>
|
||||
<version>0.1.2</version>
|
||||
<name>spring-boot-db-admin</name>
|
||||
<description>Srping Boot DB Admin Dashboard</description>
|
||||
<description>Srping Boot Database Admin is an auto-generated CRUD admin panel for Spring Boot apps</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>GPL-v3.0</name>
|
||||
<url>http://www.gnu.org/licenses/gpl-3.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
@ -38,8 +45,8 @@
|
||||
<maven-central>
|
||||
<active>ALWAYS</active>
|
||||
<url>https://s01.oss.sonatype.org/service/local</url>
|
||||
<closeRepository>false</closeRepository>
|
||||
<releaseRepository>false</releaseRepository>
|
||||
<closeRepository>true</closeRepository>
|
||||
<releaseRepository>true</releaseRepository>
|
||||
<stagingRepositories>target/staging-deploy</stagingRepositories>
|
||||
</maven-central>
|
||||
</nexus2>
|
||||
|
@ -53,6 +53,12 @@ public class DbAdmin {
|
||||
|
||||
private String modelsPackage;
|
||||
|
||||
/**
|
||||
* Builds the DbAdmin instance by scanning the `@Entity` beans and loading
|
||||
* the schemas.
|
||||
* @param entityManager the entity manager
|
||||
* @param properties the configuration properties
|
||||
*/
|
||||
public DbAdmin(@Autowired EntityManager entityManager, @Autowired DbAdminProperties properties) {
|
||||
this.modelsPackage = properties.getModelsPackage();
|
||||
this.entityManager = entityManager;
|
||||
@ -71,7 +77,7 @@ public class DbAdmin {
|
||||
|
||||
/**
|
||||
* Returns all the loaded schemas (i.e. entity classes)
|
||||
* @return
|
||||
* @return the list of loaded schemas from the `@Entity` classes
|
||||
*/
|
||||
public List<DbObjectSchema> getSchemas() {
|
||||
return Collections.unmodifiableList(schemas);
|
||||
@ -80,7 +86,7 @@ public class DbAdmin {
|
||||
/**
|
||||
* Finds a schema by its full class name
|
||||
* @param className qualified class name
|
||||
* @return
|
||||
* @return the schema with this class name
|
||||
* @throws DbAdminException if corresponding schema not found
|
||||
*/
|
||||
public DbObjectSchema findSchemaByClassName(String className) {
|
||||
@ -92,7 +98,7 @@ public class DbAdmin {
|
||||
/**
|
||||
* Finds a schema by its table name
|
||||
* @param tableName the table name on the database
|
||||
* @return
|
||||
* @return the schema with this table name
|
||||
* @throws DbAdminException if corresponding schema not found
|
||||
*/
|
||||
public DbObjectSchema findSchemaByTableName(String tableName) {
|
||||
@ -102,9 +108,9 @@ public class DbAdmin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a schema by its class
|
||||
* @param klass
|
||||
* @return
|
||||
* Finds a schema by its class object
|
||||
* @param the `@Entity` class you want to find the schema for
|
||||
* @return the schema for the `@Entity` class
|
||||
* @throws DbAdminException if corresponding schema not found
|
||||
*/
|
||||
public DbObjectSchema findSchemaByClass(Class<?> klass) {
|
||||
@ -118,7 +124,7 @@ public class DbAdmin {
|
||||
*
|
||||
* If any field is not mappable, the method will throw an exception.
|
||||
* @param bd
|
||||
* @return
|
||||
* @return a schema derived from the `@Entity` class
|
||||
*/
|
||||
private DbObjectSchema processBeanDefinition(BeanDefinition bd) {
|
||||
String fullClassName = bd.getBeanClassName();
|
||||
|
@ -21,6 +21,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import tech.ailef.dbadmin.internal.InternalDbAdminConfiguration;
|
||||
|
||||
/**
|
||||
* The configuration class that adds and configures the "internal" data source.
|
||||
*
|
||||
*/
|
||||
@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true)
|
||||
@ComponentScan
|
||||
@EnableConfigurationProperties(DbAdminProperties.class)
|
||||
@ -56,7 +60,7 @@ public class DbAdminAutoConfiguration {
|
||||
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
|
||||
factoryBean.setDataSource(internalDataSource());
|
||||
factoryBean.setPersistenceUnitName("internal");
|
||||
factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); // , "tech.ailef.dbadmin.repository");
|
||||
factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model");
|
||||
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
|
||||
|
@ -26,6 +26,9 @@ public class DbAdminProperties {
|
||||
*/
|
||||
private String modelsPackage;
|
||||
|
||||
/**
|
||||
* Set to true when running the tests to configure the "internal" data source as in memory
|
||||
*/
|
||||
private boolean testMode = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
|
@ -13,5 +13,9 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface DisplayFormat {
|
||||
/**
|
||||
* The format to apply to the field's value
|
||||
* @return
|
||||
*/
|
||||
public String format() default "";
|
||||
}
|
@ -16,5 +16,9 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Filterable {
|
||||
/**
|
||||
* The type of filter (DEFAULT or CATEGORICAL)
|
||||
* @return
|
||||
*/
|
||||
public FilterableType type() default FilterableType.DEFAULT;
|
||||
}
|
@ -1,5 +1,23 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
/**
|
||||
* Type of filters that can be used in the faceted search.
|
||||
*
|
||||
*/
|
||||
public enum FilterableType {
|
||||
DEFAULT, CATEGORICAL;
|
||||
/**
|
||||
* The default filter provides a list of standard operators
|
||||
* customized to the field type (e.g. greater than/less than/equals for numbers,
|
||||
* after/before/equals for dates, contains/equals for strings, etc...), with,
|
||||
* if applicable, an autocomplete form if the field references a foreign key.
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* The categorical filter provides the full list of possible values
|
||||
* for the field, rendered as a list of clickable items (that will
|
||||
* filter for equality). This provides a better UX if the field can take
|
||||
* a limited number of values and it's more convenient to have them all
|
||||
* on screen rather than typing them.
|
||||
*/
|
||||
CATEGORICAL;
|
||||
}
|
||||
|
@ -37,6 +37,13 @@ public class DownloadController {
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
|
||||
/**
|
||||
* Serve a binary field as an image
|
||||
* @param className
|
||||
* @param fieldName
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value="/{className}/{fieldName}/{id}/image", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> serveImage(@PathVariable String className,
|
||||
@ -58,6 +65,16 @@ public class DownloadController {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a binary field as a file. This tries to detect the file type using Tika
|
||||
* in order to serve the file with a plausible extension, since we don't have
|
||||
* any meta-data about what was originally uploaded and it is not feasible to
|
||||
* store it (it could be modified on another end and we wouldn't be aware of it).
|
||||
* @param className
|
||||
* @param fieldName
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/{className}/{fieldName}/{id}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> serveFile(@PathVariable String className,
|
||||
|
@ -43,11 +43,21 @@ public class GlobalController {
|
||||
return props.getBaseUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* The full request URL, not including the query string
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@ModelAttribute("requestUrl")
|
||||
public String getRequestUrl(HttpServletRequest request) {
|
||||
return request.getRequestURI();
|
||||
}
|
||||
|
||||
/**
|
||||
* The UserConfiguration object used to retrieve values specified
|
||||
* in the settings table.
|
||||
* @return
|
||||
*/
|
||||
@ModelAttribute("userConf")
|
||||
public UserConfiguration getUserConf() {
|
||||
return userConf;
|
||||
|
@ -28,6 +28,12 @@ public class AutocompleteController {
|
||||
@Autowired
|
||||
private DbAdminRepository repository;
|
||||
|
||||
/**
|
||||
* Returns a list of entities from a given table that match an input query.
|
||||
* @param className
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/{className}")
|
||||
public ResponseEntity<?> autocomplete(@PathVariable String className, @RequestParam String query) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
@ -80,6 +80,51 @@ public class CustomJpaRepository extends SimpleJpaRepository {
|
||||
.setFirstResult((page - 1) * pageSize).getResultList();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
|
||||
CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass());
|
||||
|
||||
Root root = update.from(schema.getJavaClass());
|
||||
|
||||
for (DbField field : schema.getSortedFields()) {
|
||||
if (field.isPrimaryKey()) continue;
|
||||
|
||||
boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on");
|
||||
if (keepValue) continue;
|
||||
|
||||
String stringValue = params.get(field.getName());
|
||||
Object value = null;
|
||||
if (stringValue != null && stringValue.isBlank()) stringValue = null;
|
||||
if (stringValue != null) {
|
||||
value = field.getType().parseValue(stringValue);
|
||||
} else {
|
||||
try {
|
||||
MultipartFile file = files.get(field.getName());
|
||||
if (file != null) {
|
||||
if (file.isEmpty()) value = null;
|
||||
else value = file.getBytes();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.getConnectedSchema() != null)
|
||||
value = field.getConnectedSchema().getJpaRepository().findById(value).get();
|
||||
|
||||
update.set(root.get(field.getJavaName()), value);
|
||||
}
|
||||
|
||||
String pkName = schema.getPrimaryKey().getJavaName();
|
||||
update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName())));
|
||||
|
||||
Query query = entityManager.createQuery(update);
|
||||
return query.executeUpdate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
|
||||
CriteriaBuilder cb, Path root) {
|
||||
@ -155,48 +200,4 @@ public class CustomJpaRepository extends SimpleJpaRepository {
|
||||
}
|
||||
return finalPredicates;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
|
||||
CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass());
|
||||
|
||||
Root root = update.from(schema.getJavaClass());
|
||||
|
||||
for (DbField field : schema.getSortedFields()) {
|
||||
if (field.isPrimaryKey()) continue;
|
||||
|
||||
boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on");
|
||||
if (keepValue) continue;
|
||||
|
||||
String stringValue = params.get(field.getName());
|
||||
Object value = null;
|
||||
if (stringValue != null && stringValue.isBlank()) stringValue = null;
|
||||
if (stringValue != null) {
|
||||
value = field.getType().parseValue(stringValue);
|
||||
} else {
|
||||
try {
|
||||
MultipartFile file = files.get(field.getName());
|
||||
if (file != null) {
|
||||
if (file.isEmpty()) value = null;
|
||||
else value = file.getBytes();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.getConnectedSchema() != null)
|
||||
value = field.getConnectedSchema().getJpaRepository().findById(value).get();
|
||||
|
||||
update.set(root.get(field.getJavaName()), value);
|
||||
}
|
||||
|
||||
String pkName = schema.getPrimaryKey().getJavaName();
|
||||
update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName())));
|
||||
|
||||
Query query = entityManager.createQuery(update);
|
||||
return query.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,25 @@ import tech.ailef.dbadmin.external.annotations.Filterable;
|
||||
import tech.ailef.dbadmin.external.annotations.FilterableType;
|
||||
|
||||
public class DbField {
|
||||
/**
|
||||
* The inferred name of this field on the database
|
||||
*/
|
||||
protected String dbName;
|
||||
|
||||
/**
|
||||
* The name of this field in the Java code (instance variable)
|
||||
*/
|
||||
protected String javaName;
|
||||
|
||||
/**
|
||||
* The type of this field
|
||||
*/
|
||||
protected DbFieldType type;
|
||||
|
||||
@JsonIgnore
|
||||
/**
|
||||
* The primitive Field object from the Class
|
||||
*/
|
||||
protected Field field;
|
||||
|
||||
/**
|
||||
@ -29,12 +41,25 @@ public class DbField {
|
||||
@JsonIgnore
|
||||
private Class<?> connectedType;
|
||||
|
||||
/**
|
||||
* Whether this field is a primary key
|
||||
*/
|
||||
private boolean primaryKey;
|
||||
|
||||
/**
|
||||
* Whether this field is nullable
|
||||
*/
|
||||
private boolean nullable;
|
||||
|
||||
/**
|
||||
* The optional format to apply to this field, if the `@DisplayFormat`
|
||||
* annotation has been applied.
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* The schema this field belongs to
|
||||
*/
|
||||
@JsonIgnore
|
||||
private DbObjectSchema schema;
|
||||
|
||||
|
@ -14,6 +14,9 @@ import jakarta.persistence.OneToOne;
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
/**
|
||||
* The list of supported field types
|
||||
*/
|
||||
public enum DbFieldType {
|
||||
INTEGER {
|
||||
@Override
|
||||
@ -27,7 +30,6 @@ public enum DbFieldType {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public Class<?> getJavaClass() {
|
||||
return Integer.class;
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Wrapper for the value of a field
|
||||
*
|
||||
*/
|
||||
public class DbFieldValue {
|
||||
private Object value;
|
||||
|
||||
|
@ -15,9 +15,19 @@ import tech.ailef.dbadmin.external.annotations.DisplayName;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
/**
|
||||
* Wrapper for all objects retrieved from the database.
|
||||
*
|
||||
*/
|
||||
public class DbObject {
|
||||
/**
|
||||
* The instance of the object, i.e. an instance of the `@Entity` class
|
||||
*/
|
||||
private Object instance;
|
||||
|
||||
/**
|
||||
* The schema this object belongs to
|
||||
*/
|
||||
private DbObjectSchema schema;
|
||||
|
||||
public DbObject(Object instance, DbObjectSchema schema) {
|
||||
|
@ -22,6 +22,11 @@ import tech.ailef.dbadmin.external.annotations.ComputedColumn;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
/**
|
||||
* A class that represents a table/`@Entity` as reconstructed from the
|
||||
* JPA annotations found on its fields.
|
||||
*
|
||||
*/
|
||||
public class DbObjectSchema {
|
||||
/**
|
||||
* All the fields in this table. The fields include all the
|
||||
|
@ -2,6 +2,11 @@ package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObject;
|
||||
|
||||
/**
|
||||
* An object to hold autocomplete results returned from the
|
||||
* respective AutocompleteController
|
||||
*
|
||||
*/
|
||||
public class AutocompleteSearchResult {
|
||||
private Object id;
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
/**
|
||||
* A list of operators that are used in faceted search.
|
||||
*
|
||||
*/
|
||||
public enum CompareOperator {
|
||||
GT {
|
||||
@Override
|
||||
|
@ -3,19 +3,45 @@ package tech.ailef.dbadmin.external.dto;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
/**
|
||||
* A client request for the Action logs page where
|
||||
* several filtering parameters are present
|
||||
*
|
||||
*/
|
||||
public class LogsSearchRequest {
|
||||
/**
|
||||
* The table name to filter on
|
||||
*/
|
||||
private String table;
|
||||
|
||||
/**
|
||||
* The action type to filter on (EDIT, CREATE, DELETE, ANY)
|
||||
*/
|
||||
private String actionType;
|
||||
|
||||
/**
|
||||
* The item id to filter on.
|
||||
*/
|
||||
private String itemId;
|
||||
|
||||
/**
|
||||
* The requested page
|
||||
*/
|
||||
private int page;
|
||||
|
||||
/**
|
||||
* The requested page size
|
||||
*/
|
||||
private int pageSize;
|
||||
|
||||
/**
|
||||
* The requested sort key
|
||||
*/
|
||||
private String sortKey;
|
||||
|
||||
/**
|
||||
* The requested sort order
|
||||
*/
|
||||
private String sortOrder;
|
||||
|
||||
public String getTable() {
|
||||
@ -80,6 +106,10 @@ public class LogsSearchRequest {
|
||||
+ page + ", pageSize=" + pageSize + ", sortKey=" + sortKey + ", sortOrder=" + sortOrder + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Spring PageRequest object from the parameters in this request
|
||||
* @return a Spring PageRequest object
|
||||
*/
|
||||
public PageRequest toPageRequest() {
|
||||
int actualPage = page - 1 < 0 ? 0 : page - 1;
|
||||
int actualPageSize = pageSize <= 0 ? 50 : pageSize;
|
||||
|
@ -2,9 +2,19 @@ package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A wrapper class that holds info about the current pagination and one page
|
||||
* of returned result.
|
||||
*/
|
||||
public class PaginatedResult<T> {
|
||||
/**
|
||||
* The pagination settings used to produce this output
|
||||
*/
|
||||
private PaginationInfo pagination;
|
||||
|
||||
/**
|
||||
* The list of results in the current page
|
||||
*/
|
||||
private List<T> results;
|
||||
|
||||
public PaginatedResult(PaginationInfo pagination, List<T> page) {
|
||||
|
@ -29,6 +29,10 @@ public class QueryFilter {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a readable version of this query filter, customized
|
||||
* based on field type and/or operator.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (value != null && !value.toString().isBlank()) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user