├── .gitignore ├── LICENSE ├── README.md ├── core ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── tqdev │ │ └── CrudApiHandler.java │ └── resources │ ├── hikari.properties │ └── jetty.properties └── full ├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── tqdev │ │ └── crudapi │ │ ├── ApiApp.java │ │ ├── column │ │ ├── ColumnService.java │ │ ├── JooqColumnService.java │ │ ├── definition │ │ │ ├── ColumnDefinition.java │ │ │ ├── DatabaseDefinition.java │ │ │ ├── DatabaseDefinitionException.java │ │ │ └── TableDefinition.java │ │ └── reflection │ │ │ ├── DatabaseReflection.java │ │ │ ├── DynamicIdentity.java │ │ │ └── ReflectedTable.java │ │ ├── config │ │ └── CorsConfiguration.java │ │ ├── controller │ │ ├── BackupController.java │ │ ├── ColumnController.java │ │ ├── ExceptionHandlerController.java │ │ ├── OpenApiController.java │ │ ├── RecordController.java │ │ └── Responder.java │ │ ├── openapi │ │ ├── JooqOpenApiService.java │ │ ├── OpenApiBuilder.java │ │ ├── OpenApiDefinition.java │ │ └── OpenApiService.java │ │ └── record │ │ ├── BaseRecordService.java │ │ ├── ColumnSelector.java │ │ ├── CrudApiHandlers.java │ │ ├── ErrorCode.java │ │ ├── FilterInfo.java │ │ ├── HabtmValues.java │ │ ├── JooqRecordService.java │ │ ├── OrderingInfo.java │ │ ├── PaginationInfo.java │ │ ├── Params.java │ │ ├── PathTree.java │ │ ├── RecordService.java │ │ ├── RelationIncluder.java │ │ ├── container │ │ ├── DatabaseRecords.java │ │ ├── DatabaseRecordsException.java │ │ ├── Record.java │ │ └── TableRecords.java │ │ ├── document │ │ ├── ErrorDocument.java │ │ └── ListDocument.java │ │ └── spatial │ │ ├── AsText.java │ │ ├── Contains.java │ │ ├── Crosses.java │ │ ├── Disjoint.java │ │ ├── Equals.java │ │ ├── GeomFromText.java │ │ ├── Intersects.java │ │ ├── IsClosed.java │ │ ├── IsSimple.java │ │ ├── IsValid.java │ │ ├── Overlaps.java │ │ ├── SpatialDSL.java │ │ ├── Touches.java │ │ └── Within.java └── resources │ ├── application.yml │ └── openapi.json └── test ├── java └── com │ └── tqdev │ └── crudapi │ └── Test001Records.java └── resources ├── application.yml ├── columns-schema.json ├── columns.json ├── records-schema.json └── records.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .classpath 3 | .project 4 | .settings 5 | target 6 | server.jar 7 | .idea 8 | *.iml 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Maurits van der Schee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo contains a port of the [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api) project in Java. 2 | 3 | - **core**: only the basic functionality (for benchmarking purposes) 4 | - **full**: a full featured REST API based on your database structure 5 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | 2 | This is a Java port of the [php-crud-api](https://github.com/mevdschee/php-crud-api) project (single file REST API). It currently only implements the core functionality. 3 | 4 | ### Dependencies 5 | 6 | Install dependencies using: 7 | 8 | sudo apt-get install maven openjdk-8-jdk 9 | 10 | Then build the server. 11 | 12 | ### Configuring 13 | 14 | In the file "core/src/main/resources/jetty.properties" you can configure the listening host and port. 15 | 16 | In "core/src/main/resources/hikari.properties" you can configure the MySQL connection. 17 | 18 | ### Running 19 | 20 | To run the api (during development) type: 21 | 22 | mvn exec:java 23 | 24 | In production I recommend deploying the JAR file as described below. 25 | 26 | ### Building a executable JAR file 27 | 28 | To compile everything in a single executable JAR file, run: 29 | 30 | mvn compile assembly:single 31 | 32 | You can execute the JAR using: 33 | 34 | java -jar server.jar 35 | 36 | You can see the api at work at http://localhost:8080/posts/1. 37 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | jar 4 | com.tqdev.java-crud-api 5 | java-crud-api 6 | 1.0 7 | Java CRUD API 8 | https://github.com/mevdschee/java-crud-api 9 | 10 | UTF-8 11 | 1.8 12 | 1.8 13 | 11.0.10 14 | 3.4.1 15 | 1.7.28 16 | 2.8.9 17 | 8.0.17 18 | 19 | 20 | 21 | com.zaxxer 22 | HikariCP 23 | ${hikari.version} 24 | 25 | 26 | org.slf4j 27 | slf4j-nop 28 | ${slf4j.version} 29 | 30 | 31 | com.google.code.gson 32 | gson 33 | ${gson.version} 34 | 35 | 36 | org.eclipse.jetty 37 | jetty-server 38 | ${jetty.version} 39 | 40 | 41 | org.eclipse.jetty 42 | jetty-servlet 43 | ${jetty.version} 44 | 45 | 46 | org.eclipse.jetty 47 | jetty-servlets 48 | ${jetty.version} 49 | 50 | 51 | org.eclipse.jetty 52 | jetty-xml 53 | ${jetty.version} 54 | 55 | 56 | mysql 57 | mysql-connector-java 58 | ${mysql.version} 59 | 60 | 61 | 62 | 63 | 64 | org.codehaus.mojo 65 | exec-maven-plugin 66 | 1.5.0 67 | 68 | 69 | default-cli 70 | 71 | java 72 | 73 | 74 | com.tqdev.CrudApiHandler 75 | 76 | 77 | 78 | 79 | 80 | maven-assembly-plugin 81 | 82 | 83 | jar-with-dependencies 84 | 85 | ${basedir} 86 | server 87 | false 88 | false 89 | 90 | 91 | com.tqdev.CrudApiHandler 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /core/src/main/java/com/tqdev/CrudApiHandler.java: -------------------------------------------------------------------------------- 1 | package com.tqdev; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | import javax.servlet.ServletException; 6 | import org.eclipse.jetty.server.Connector; 7 | import org.eclipse.jetty.server.Request; 8 | import org.eclipse.jetty.server.Server; 9 | import org.eclipse.jetty.server.ServerConnector; 10 | import org.eclipse.jetty.server.handler.AbstractHandler; 11 | import org.eclipse.jetty.server.HttpConfiguration; 12 | import org.eclipse.jetty.server.HttpConnectionFactory; 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | import com.google.gson.Gson; 16 | import java.sql.Connection; 17 | import java.sql.PreparedStatement; 18 | import java.sql.ResultSet; 19 | import java.sql.ResultSetMetaData; 20 | import java.sql.SQLException; 21 | import java.util.Map; 22 | import com.zaxxer.hikari.HikariConfig; 23 | import com.zaxxer.hikari.HikariDataSource; 24 | import java.util.Properties; 25 | 26 | public class CrudApiHandler extends AbstractHandler 27 | { 28 | protected HikariDataSource dataSource; 29 | 30 | public static void main(String[] args) throws Exception 31 | { 32 | // jetty config 33 | Properties properties = new Properties(); 34 | properties.load(CrudApiHandler.class.getClassLoader().getResourceAsStream("jetty.properties")); 35 | HttpConfiguration config = new HttpConfiguration(); 36 | config.setSendServerVersion( false ); 37 | HttpConnectionFactory factory = new HttpConnectionFactory( config ); 38 | Server server = new Server(); 39 | ServerConnector connector = new ServerConnector(server,factory); 40 | server.setConnectors( new Connector[] { connector } ); 41 | connector.setHost(properties.getProperty("host")); 42 | connector.setPort(Integer.parseInt(properties.getProperty("port"))); 43 | server.addConnector(connector); 44 | server.setHandler(new CrudApiHandler()); 45 | server.start(); 46 | server.join(); 47 | } 48 | 49 | public CrudApiHandler() throws IOException 50 | { 51 | this.dataSource = this.getDataSource(); 52 | } 53 | 54 | protected HikariDataSource getDataSource() 55 | { 56 | HikariDataSource dataSource; 57 | try { 58 | Properties properties = new Properties(); 59 | properties.load(CrudApiHandler.class.getClassLoader().getResourceAsStream("hikari.properties")); 60 | dataSource = new HikariDataSource(new HikariConfig(properties)); 61 | } catch (Exception e) { 62 | System.out.println(e); 63 | dataSource = null; 64 | } 65 | return dataSource; 66 | } 67 | 68 | protected Connection getConnection() 69 | { 70 | Connection link; 71 | try { 72 | link = this.dataSource.getConnection(); 73 | } catch (SQLException e) { 74 | System.out.println(e); 75 | link = null; 76 | } 77 | return link; 78 | } 79 | 80 | public void handle(String target,Request baseReq,HttpServletRequest req,HttpServletResponse resp) 81 | throws IOException, ServletException 82 | { 83 | Gson gson = new Gson(); 84 | // get the HTTP method, path and body of the request 85 | String method = req.getMethod(); 86 | String[] request = req.getPathInfo().replaceAll("/$|^/","").split("/"); 87 | @SuppressWarnings("unchecked") 88 | Map input = gson.fromJson(req.getReader(), Map.class); 89 | // connect to the mysql database 90 | Connection link = this.getConnection(); 91 | // retrieve the table and key from the path 92 | String table = request[0].replaceAll("[^a-zA-Z0-9_]+",""); 93 | int key = (request.length>1?Integer.parseInt(request[1]):-1); 94 | // escape the columns and values from the input object 95 | String[] columns = input==null?(new String[0]):(String[])input.keySet().toArray(); 96 | for (int i=0;i0?",":"")+"`"+columns[i]+"`=?"; 103 | } 104 | // create SQL based on HTTP method 105 | String sql=""; 106 | if (method=="GET") { 107 | sql = "select * from `"+table+"`"+(key>0?" WHERE id="+key:""); 108 | } else if (method=="PUT") { 109 | sql = "update `"+table+"` set "+set+" where id="+key; 110 | } else if (method=="POST") { 111 | sql = "insert into `"+table+"` set "+set; 112 | } else if (method=="DELETE") { 113 | sql = "delete `"+table+"` where id="+key; 114 | } 115 | PreparedStatement statement=null; 116 | try { 117 | // execute SQL statement 118 | statement = link.prepareStatement(sql); 119 | for (int i=0;i0) w.print(','); 134 | w.print('{'); 135 | for (int col=1;col<=colCount;col++) { 136 | if (col>1) w.print(','); 137 | w.print(gson.toJson(meta.getColumnName(col))); 138 | w.print(':'); 139 | w.print(gson.toJson(result.getObject(col))); 140 | } 141 | w.print('}'); 142 | row++; 143 | } 144 | if (key<0) w.print(']'); 145 | } else if (method.equals("POST")) { 146 | statement.executeUpdate(sql); 147 | ResultSet result = statement.getGeneratedKeys(); 148 | result.next(); 149 | w.print(gson.toJson(result.getObject(1))); 150 | } else { 151 | w.print(gson.toJson(statement.executeUpdate(sql))); 152 | } 153 | } 154 | catch (SQLException e) { 155 | System.out.println("SQLException:"+e); 156 | } 157 | finally { 158 | // close mysql connection 159 | if (statement != null) try { statement.close(); } catch (SQLException ignore) {} 160 | if (link != null) try { link.close(); } catch (SQLException ignore) {} 161 | } 162 | baseReq.setHandled(true); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /core/src/main/resources/hikari.properties: -------------------------------------------------------------------------------- 1 | dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource 2 | dataSource.user=php-crud-api 3 | dataSource.password=php-crud-api 4 | dataSource.databaseName=php-crud-api 5 | dataSource.portNumber=3306 6 | dataSource.serverName=localhost 7 | dataSource.serverTimezone=UTC -------------------------------------------------------------------------------- /core/src/main/resources/jetty.properties: -------------------------------------------------------------------------------- 1 | host=localhost 2 | port=8080 -------------------------------------------------------------------------------- /full/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | 3 | 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff: 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/dictionaries 11 | 12 | # Sensitive or high-churn files: 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | cmake-build-release/ 27 | 28 | # Mongo Explorer plugin: 29 | .idea/**/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Cursive Clojure plugin 46 | .idea/replstate.xml 47 | 48 | # Crashlytics plugin (for Android Studio and IntelliJ) 49 | com_crashlytics_export_strings.xml 50 | crashlytics.properties 51 | crashlytics-build.properties 52 | fabric.properties -------------------------------------------------------------------------------- /full/README.md: -------------------------------------------------------------------------------- 1 | ### Limitations 2 | 3 | These limitation were also present in [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api). 4 | 5 | - Primary keys should either be auto-increment (from 1 to 2^53) or UUID 6 | - Composite primary or foreign keys are not supported 7 | - Complex writes (transactions) are not supported 8 | - Complex queries calling functions (like "concat" or "sum") are not supported 9 | - MySQL storage engine must be either InnoDB or XtraDB 10 | - Only MySQL, PostgreSQL and SQLServer support spatial/GIS functionality 11 | 12 | ### Features 13 | 14 | These features match features in PHP-CRUD-API. 15 | 16 | - [x] Supports POST variables as input (x-www-form-urlencoded) 17 | - [x] Supports a JSON object as input 18 | - [x] Supports a JSON array as input (batch insert) 19 | - [ ] ~~Supports file upload from web forms (multipart/form-data)~~ 20 | - [ ] ~~Optional condensed JSON: only first row contains field names~~ 21 | - [ ] Sanitize and validate input using callbacks 22 | - [ ] Permission system for databases, tables, columns and records 23 | - [ ] Multi-tenant database layouts are supported 24 | - [x] Multi-domain CORS support for cross-domain requests 25 | - [x] Combined requests with support for multiple table names 26 | - [x] Search support on multiple criteria 27 | - [x] Pagination, seeking, sorting and column selection 28 | - [x] Relation detection nested results (belongsTo, hasMany and HABTM) 29 | - [x] Atomic increment support via PATCH (for counters) 30 | - [x] Binary fields supported with base64 encoding 31 | - [x] Spatial/GIS fields and filters supported with WKT 32 | - [ ] ~~Unstructured data support through JSON/JSONB~~ 33 | - [x] Generate API documentation using OpenAPI tools 34 | - [ ] Authentication via JWT token or username/password 35 | 36 | ### Extra Features 37 | 38 | These features are new and where not included in PHP-CRUD-API. 39 | 40 | - [x] Does not reflect on every request (better performance) 41 | - [x] Complex filters (with both "and" & "or") are supported 42 | - [x] Support for input and output of database structure and records 43 | - [x] Support for all major database systems (thanks to jOOQ) 44 | - [x] Support for boolean and binary data in all database engines 45 | - [x] Support for relational data on read (not only on list operation) 46 | 47 | ## Features 48 | 49 | ### Multi-domain CORS support 50 | 51 | By default all cross-origin requests are allowed. Use the key "rest.cors.allowed-origins" and 52 | set it to one or multiple hosts in a comma separated list (e.g. "http://localhost:8080,http://localhost:9090"). 53 | 54 | ## Extra Features 55 | 56 | ### Input and output of database structure and records 57 | 58 | These are the supported types: 59 | 60 | character types: 61 | - varchar(length) 62 | - char(length) 63 | - longvarchar(length) 64 | - clob(length) 65 | 66 | unicode types: 67 | - nvarchar(length) 68 | - nchar(length) 69 | - longnvarchar(length) 70 | - nclob(length) 71 | 72 | boolean types: 73 | - boolean 74 | - bit 75 | 76 | integer types: 77 | - tinyint 78 | - smallint 79 | - integer 80 | - bigint 81 | 82 | floating point types: 83 | - double 84 | - float 85 | - real 86 | 87 | decimal types: 88 | - numeric(precision,scale) 89 | - decimal(precision,scale) 90 | 91 | date/time types: 92 | - date 93 | - time 94 | - timestamp 95 | 96 | binary types: 97 | - binary(length) 98 | - varbinary(length) 99 | - longvarbinary(length) 100 | - blob(length) 101 | 102 | other types: 103 | - other /* for JDBC unknown types */ 104 | - record /* for JDBC STRUCT type */ 105 | - result /* emulates REF CURSOR types and similar constructs */ 106 | - uuid /* non-jdbc type, limited support */ 107 | - geometry /* non-jdbc type, extension with limited support */ 108 | - xml /* non-jdbc type, extension with limited support */ 109 | - json /* non-jdbc type, extension with limited support */ 110 | 111 | The length parameter is always optional and not recommended on binary types and large objects (such as clob and nclob). 112 | -------------------------------------------------------------------------------- /full/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.tqdev.crudapi 6 | JavaCrudApi 7 | 1.0.0 8 | jar 9 | 10 | JavaCrudApi 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 2.1.3.RELEASE 16 | 17 | 18 | 19 | 1.8 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-jooq 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-test 34 | test 35 | 36 | 37 | mysql 38 | mysql-connector-java 39 | 40 | 41 | org.postgresql 42 | postgresql 43 | 44 | 45 | com.h2database 46 | h2 47 | test 48 | 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-maven-plugin 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/ApiApp.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi; 2 | 3 | import com.tqdev.crudapi.column.JooqColumnService; 4 | import com.tqdev.crudapi.column.ColumnService; 5 | import com.tqdev.crudapi.column.definition.DatabaseDefinitionException; 6 | import com.tqdev.crudapi.controller.Responder; 7 | import com.tqdev.crudapi.openapi.JooqOpenApiService; 8 | import com.tqdev.crudapi.openapi.OpenApiService; 9 | import com.tqdev.crudapi.record.RecordService; 10 | import com.tqdev.crudapi.record.container.DatabaseRecordsException; 11 | import com.tqdev.crudapi.record.JooqRecordService; 12 | import com.tqdev.crudapi.record.spatial.SpatialDSL; 13 | 14 | import org.jooq.DSLContext; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.SpringApplication; 17 | import org.springframework.boot.autoconfigure.SpringBootApplication; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.PropertySource; 20 | 21 | import java.io.IOException; 22 | 23 | @SpringBootApplication(scanBasePackages = { "com.tqdev.crudapi" }) 24 | @PropertySource("classpath:application.yml") 25 | public class ApiApp { 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(ApiApp.class, args); 29 | } 30 | 31 | @Bean 32 | @Autowired 33 | public Responder responder() { 34 | return new Responder(); 35 | } 36 | 37 | @Bean 38 | @Autowired 39 | public OpenApiService openApiService(DSLContext dsl, ColumnService columns) throws IOException { 40 | OpenApiService result; 41 | result = new JooqOpenApiService(dsl, columns); 42 | result.initialize("openapi.json"); 43 | return result; 44 | } 45 | 46 | @Bean 47 | @Autowired 48 | public ColumnService metaService(DSLContext dsl) throws IOException, 49 | DatabaseDefinitionException, DatabaseRecordsException { 50 | ColumnService result; 51 | SpatialDSL.registerDataTypes(dsl); 52 | result = new JooqColumnService(dsl); 53 | result.initialize("columns.json"); 54 | return result; 55 | } 56 | 57 | @Bean 58 | @Autowired 59 | public RecordService dataService(DSLContext dsl, ColumnService columns) throws 60 | IOException, DatabaseDefinitionException, DatabaseRecordsException { 61 | RecordService result; 62 | result = new JooqRecordService(dsl, columns); 63 | result.initialize("records.json"); 64 | return result; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/ColumnService.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column; 2 | 3 | import java.io.IOException; 4 | 5 | import com.tqdev.crudapi.column.definition.DatabaseDefinition; 6 | import com.tqdev.crudapi.column.definition.DatabaseDefinitionException; 7 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 8 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 9 | import com.tqdev.crudapi.record.container.DatabaseRecordsException; 10 | 11 | public interface ColumnService { 12 | 13 | // meta 14 | 15 | DatabaseReflection getDatabaseReflection(); 16 | 17 | DatabaseDefinition getDatabaseDefinition(); 18 | 19 | // initialization 20 | 21 | void initialize(String columnsFilename) 22 | throws IOException, DatabaseDefinitionException, DatabaseRecordsException; 23 | 24 | boolean hasTable(String tableName); 25 | 26 | ReflectedTable getTable(String tableName); 27 | } 28 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/JooqColumnService.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column; 2 | 3 | import com.tqdev.crudapi.column.definition.DatabaseDefinition; 4 | import com.tqdev.crudapi.column.definition.DatabaseDefinitionException; 5 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 6 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 7 | 8 | import org.jooq.DSLContext; 9 | 10 | import java.io.IOException; 11 | 12 | public class JooqColumnService implements ColumnService { 13 | 14 | private DSLContext dsl; 15 | 16 | private DatabaseReflection reflection; 17 | 18 | public JooqColumnService(DSLContext dsl) { 19 | this.dsl = dsl; 20 | this.reflection = new DatabaseReflection(dsl); 21 | } 22 | 23 | @Override 24 | public DatabaseReflection getDatabaseReflection() { 25 | return reflection; 26 | } 27 | 28 | @Override 29 | public DatabaseDefinition getDatabaseDefinition() { 30 | return new DatabaseDefinition(reflection); 31 | } 32 | 33 | @Override 34 | public void initialize(String columnsFilename) throws 35 | IOException, DatabaseDefinitionException { 36 | DatabaseDefinition.fromFile(columnsFilename).create(dsl); 37 | reflection.update(); 38 | } 39 | 40 | @Override 41 | public boolean hasTable(String tableName) { 42 | return this.reflection.hasTable(tableName); 43 | } 44 | 45 | @Override 46 | public ReflectedTable getTable(String tableName) { 47 | return this.reflection.getTable(tableName); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/definition/ColumnDefinition.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.definition; 2 | 3 | import org.jooq.DSLContext; 4 | import org.jooq.DataType; 5 | import org.jooq.Field; 6 | import org.jooq.SQLDialect; 7 | import org.jooq.impl.DefaultDataType; 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude; 10 | 11 | @JsonInclude(JsonInclude.Include.NON_DEFAULT) 12 | public class ColumnDefinition { 13 | 14 | private String name = null; 15 | private String type; 16 | private int length = -1; 17 | private int precision = -1; 18 | private int scale = -1; 19 | private boolean nullable = false; 20 | private boolean pk = false; 21 | private String fk = null; 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | public String getType() { 32 | return type; 33 | } 34 | 35 | public void setType(String type) { 36 | this.type = type; 37 | } 38 | 39 | public int getLength() { 40 | return length; 41 | } 42 | 43 | public void setLength(int length) { 44 | this.length = length; 45 | } 46 | 47 | public int getPrecision() { 48 | return precision; 49 | } 50 | 51 | public void setPrecision(int precision) { 52 | this.precision = precision; 53 | } 54 | 55 | public int getScale() { 56 | return scale; 57 | } 58 | 59 | public void setScale(int scale) { 60 | this.scale = scale; 61 | } 62 | 63 | public boolean getNullable() { 64 | return nullable; 65 | } 66 | 67 | public void setNullable(Boolean nullable) { 68 | this.nullable = nullable; 69 | } 70 | 71 | public boolean getPk() { 72 | return pk; 73 | } 74 | 75 | public void setPk(Boolean pk) { 76 | this.pk = pk; 77 | } 78 | 79 | public String getFk() { 80 | return fk; 81 | } 82 | 83 | public void setFk(String fk) { 84 | this.fk = fk; 85 | } 86 | 87 | // hackety hack 88 | private void override(DSLContext dsl) { 89 | if (dsl.dialect() == SQLDialect.H2) { 90 | if (type.equals("geometry")) { 91 | type = "nclob"; 92 | } 93 | } 94 | } 95 | 96 | public DataType getDataType(DSLContext dsl) { 97 | override(dsl); 98 | DataType result = DefaultDataType.getDataType(SQLDialect.DEFAULT, type); 99 | if (result.isNumeric() && !result.hasScale()) { 100 | result = result.identity(pk); 101 | } 102 | if (length >= 0) { 103 | result = result.length(length); 104 | } 105 | if (precision >= 0) { 106 | result = result.precision(precision); 107 | } 108 | if (scale >= 0) { 109 | result = result.scale(scale); 110 | } 111 | result = result.nullable(nullable); 112 | return result; 113 | } 114 | 115 | public ColumnDefinition() { 116 | // nothing 117 | } 118 | 119 | public ColumnDefinition(Field field) { 120 | DataType dataType = field.getDataType(); 121 | setPk(dataType.identity()); 122 | DataType defaultType = dataType.getSQLDataType(); 123 | if (defaultType == null) { 124 | defaultType = DefaultDataType.getDefaultDataType(dataType.getTypeName()); 125 | } 126 | setType(defaultType.getTypeName()); 127 | if (dataType.hasLength()) { 128 | setLength(dataType.length()); 129 | } 130 | if (dataType.hasPrecision()) { 131 | setPrecision(dataType.precision()); 132 | } 133 | if (dataType.hasScale()) { 134 | setScale(dataType.scale()); 135 | } 136 | setNullable(dataType.nullable()); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/definition/DatabaseDefinition.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.definition; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 5 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 6 | 7 | import org.jooq.*; 8 | import org.jooq.impl.DSL; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.core.io.ClassPathResource; 12 | 13 | import java.io.FileNotFoundException; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.LinkedHashMap; 18 | 19 | public class DatabaseDefinition { 20 | 21 | public static final Logger logger = LoggerFactory.getLogger(DatabaseDefinition.class); 22 | 23 | private LinkedHashMap tables; 24 | 25 | public Collection getTables() { 26 | return tables.values(); 27 | } 28 | 29 | public void setTables(Collection tables) { 30 | this.tables = new LinkedHashMap<>(); 31 | for (TableDefinition table : tables) { 32 | this.tables.put(table.getName(), table); 33 | } 34 | } 35 | 36 | public TableDefinition get(String tableName) { 37 | return tables.get(tableName); 38 | } 39 | 40 | public void create(DSLContext dsl) throws DatabaseDefinitionException { 41 | ArrayList created = new ArrayList<>(); 42 | for (String tableName : tables.keySet()) { 43 | TableDefinition table = tables.get(tableName); 44 | ArrayList> fields = table.getFields(dsl); 45 | ArrayList constraints = table.getPkConstraints(dsl, tableName); 46 | CreateTableConstraintStep query = dsl.createTable(DSL.name(tableName)).columns(fields) 47 | .constraints(constraints); 48 | logger.info("Executing SQL: " + query.getSQL()); 49 | query.execute(); 50 | created.add(tableName); 51 | } 52 | for (String tableName : created) { 53 | TableDefinition table = tables.get(tableName); 54 | for (Constraint constraint : table.getFkConstraints(dsl, tableName, this)) { 55 | AlterTableUsingIndexStep query = dsl.alterTable(DSL.name(tableName)).add(constraint); 56 | logger.info("Executing SQL: " + query.getSQL()); 57 | query.execute(); 58 | } 59 | } 60 | } 61 | 62 | public DatabaseDefinition() { 63 | tables = new LinkedHashMap<>(); 64 | } 65 | 66 | public DatabaseDefinition(DatabaseReflection database) { 67 | tables = new LinkedHashMap<>(); 68 | for (String tableName : database.getTableNames()) { 69 | ReflectedTable table = database.getTable(tableName); 70 | tables.put(tableName, new TableDefinition(table)); 71 | } 72 | } 73 | 74 | public static DatabaseDefinition fromFile(String filename) 75 | throws IOException { 76 | ObjectMapper mapper = new ObjectMapper(); 77 | ClassPathResource resource = new ClassPathResource(filename); 78 | DatabaseDefinition result; 79 | try { 80 | result = mapper.readValue(resource.getInputStream(), DatabaseDefinition.class); 81 | } catch (FileNotFoundException e) { 82 | result = new DatabaseDefinition(); 83 | } 84 | return result; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/definition/DatabaseDefinitionException.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.definition; 2 | 3 | public class DatabaseDefinitionException extends Exception { 4 | 5 | /** 6 | * 7 | */ 8 | private static final long serialVersionUID = 1L; 9 | 10 | public DatabaseDefinitionException(String format) { 11 | super(format); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/definition/TableDefinition.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.definition; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.LinkedHashMap; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 9 | 10 | import org.jooq.Constraint; 11 | import org.jooq.DSLContext; 12 | import org.jooq.Field; 13 | import org.jooq.impl.DSL; 14 | 15 | public class TableDefinition { 16 | 17 | private String name = null; 18 | private LinkedHashMap columns = new LinkedHashMap<>(); 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | public Collection getColumns() { 29 | return columns.values(); 30 | } 31 | 32 | public void setColumns(Collection columns) { 33 | this.columns = new LinkedHashMap<>(); 34 | for (ColumnDefinition column : columns) { 35 | this.columns.put(column.getName(), column); 36 | } 37 | } 38 | 39 | @JsonIgnore 40 | public ColumnDefinition getPk() { 41 | for (String key : columns.keySet()) { 42 | ColumnDefinition column = columns.get(key); 43 | if (column.getPk() == true) { 44 | return column; 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | public ArrayList> getFields(DSLContext dsl) { 51 | ArrayList> fields = new ArrayList<>(); 52 | for (String columnName : columns.keySet()) { 53 | ColumnDefinition column = columns.get(columnName); 54 | fields.add(DSL.field(DSL.name(columnName), column.getDataType(dsl))); 55 | } 56 | return fields; 57 | } 58 | 59 | public ArrayList getPkConstraints(DSLContext dsl, String tableName) { 60 | ArrayList constraints = new ArrayList<>(); 61 | ColumnDefinition pk = getPk(); 62 | if (pk != null) { 63 | constraints.add(DSL.constraint(DSL.name("pk_" + tableName)).primaryKey(DSL.field(DSL.name(pk.getName())))); 64 | } 65 | return constraints; 66 | } 67 | 68 | public ArrayList getFkConstraints(DSLContext dsl, String tableName, DatabaseDefinition definition) 69 | throws DatabaseDefinitionException { 70 | ArrayList constraints = new ArrayList<>(); 71 | for (String columnName : columns.keySet()) { 72 | ColumnDefinition column = columns.get(columnName); 73 | String fk = column.getFk(); 74 | if (fk != null) { 75 | String pk = definition.get(fk).getPk().getName(); 76 | if (pk == null) { 77 | throw new DatabaseDefinitionException(String.format( 78 | "Illegal 'fk' value for field '%s' of table '%s': Referenced table '%s' does not have a single field primary key", 79 | columnName, tableName, fk)); 80 | } 81 | constraints.add(DSL.constraint(DSL.name("fk_" + tableName + "_" + columnName)) 82 | .foreignKey(DSL.field(DSL.name(columnName))) 83 | .references(DSL.table(DSL.name(fk)), DSL.field(DSL.name(pk)))); 84 | } 85 | } 86 | return constraints; 87 | } 88 | 89 | public TableDefinition() { 90 | // nothing 91 | } 92 | 93 | public TableDefinition(ReflectedTable table) { 94 | setName(table.getName()); 95 | for (Field field : table.fields()) { 96 | String name = field.getName(); 97 | ColumnDefinition column = new ColumnDefinition(field); 98 | column.setName(name); 99 | column.setFk(table.getFk(name)); 100 | columns.put(name, column); 101 | } 102 | Field pk = table.getPk(); 103 | if (pk != null) { 104 | columns.get(pk.getName()).setPk(true); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/reflection/DatabaseReflection.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.reflection; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | import java.util.LinkedHashMap; 6 | import java.util.Set; 7 | 8 | import org.jooq.DSLContext; 9 | import org.jooq.Table; 10 | 11 | public class DatabaseReflection { 12 | 13 | private DSLContext dsl; 14 | 15 | private LinkedHashMap> tables; 16 | private LinkedHashMap cachedTables; 17 | 18 | private String tablePrefix; 19 | 20 | public DatabaseReflection(DSLContext dsl) { 21 | this.dsl = dsl; 22 | this.tablePrefix = findTablePrefix(); 23 | } 24 | 25 | public boolean hasTable(String tableName) { 26 | return tables.containsKey(tableName); 27 | } 28 | 29 | public ReflectedTable getTable(String tableName) { 30 | if (!tables.containsKey(tableName)) { 31 | return null; 32 | } 33 | if (!cachedTables.containsKey(tableName)) { 34 | cachedTables.put(tableName, new ReflectedTable(tables.get(tableName))); 35 | } 36 | return cachedTables.get(tableName); 37 | } 38 | 39 | private String findTablePrefix() { 40 | Connection connection = dsl.configuration().connectionProvider().acquire(); 41 | String catalog = null, schema = null; 42 | try { 43 | catalog = connection.getCatalog(); 44 | schema = connection.getSchema(); 45 | } catch (SQLException e) { 46 | // error on table prefix 47 | } finally { 48 | try { 49 | if (connection != null) { 50 | connection.close(); 51 | } 52 | } catch (SQLException e) { 53 | // ignore 54 | } 55 | } 56 | String prefix = ""; 57 | if (catalog != null) { 58 | prefix += "\"" + catalog + "\"."; 59 | } 60 | if (schema != null) { 61 | prefix += "\"" + schema + "\"."; 62 | } 63 | return prefix; 64 | } 65 | 66 | public void update() { 67 | tables = new LinkedHashMap<>(); 68 | cachedTables = new LinkedHashMap<>(); 69 | for (Table table : dsl.meta().getTables()) { 70 | if (!(table.toString().startsWith(tablePrefix))) { 71 | // table not in current catalog or schema 72 | continue; 73 | } 74 | tables.put(table.getName(), table); 75 | } 76 | } 77 | 78 | public Set getTableNames() { 79 | return tables.keySet(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/reflection/DynamicIdentity.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.reflection; 2 | 3 | import org.jooq.Identity; 4 | import org.jooq.Table; 5 | import org.jooq.TableField; 6 | 7 | @SuppressWarnings("rawtypes") 8 | class DynamicIdentity implements Identity { 9 | 10 | /** 11 | * 12 | */ 13 | private static final long serialVersionUID = 59304360964854237L; 14 | private Table table; 15 | private TableField pk; 16 | 17 | public DynamicIdentity(Table table, TableField pk) { 18 | this.table = table; 19 | this.pk = pk; 20 | } 21 | 22 | @Override 23 | public Table getTable() { 24 | return table; 25 | } 26 | 27 | @Override 28 | public TableField getField() { 29 | return pk; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/column/reflection/ReflectedTable.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.column.reflection; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import org.jooq.DataType; 10 | import org.jooq.Field; 11 | import org.jooq.ForeignKey; 12 | import org.jooq.Identity; 13 | import org.jooq.Record; 14 | import org.jooq.Table; 15 | import org.jooq.TableField; 16 | import org.jooq.UniqueKey; 17 | import org.jooq.impl.CustomTable; 18 | 19 | @SuppressWarnings("rawtypes") 20 | public class ReflectedTable extends CustomTable { 21 | 22 | /** 23 | * 24 | */ 25 | private static final long serialVersionUID = -3688698737591901523L; 26 | private Table table; 27 | private HashMap> fields = new LinkedHashMap<>(); 28 | private HashMap fks = new LinkedHashMap<>(); 29 | private TableField pk = null; 30 | 31 | @SuppressWarnings("unchecked") 32 | public ReflectedTable(Table table) { 33 | super(table.getQualifiedName()); 34 | this.table = table; 35 | for (Field field : table.fields()) { 36 | String name = field.getName(); 37 | DataType dataType = (DataType) field.getDataType(); 38 | TableField newField = createField(name, dataType); 39 | fields.put(name, newField); 40 | } 41 | UniqueKey primaryKey = table.getPrimaryKey(); 42 | if (primaryKey != null) { 43 | if (primaryKey.getFields().size() == 1) { 44 | pk = primaryKey.getFields().get(0); 45 | } 46 | } 47 | for (ForeignKey fk : table.getReferences()) { 48 | fks.put(findForeignKeyFieldName(fk), findForeignKeyReference(fk)); 49 | } 50 | } 51 | 52 | @Override 53 | public Class getRecordType() { 54 | return Record.class; 55 | } 56 | 57 | private TableField findPrimaryKey(Table table) { 58 | UniqueKey pk = table.getPrimaryKey(); 59 | if (pk != null) { 60 | TableField[] pks = pk.getFieldsArray(); 61 | if (pks.length == 1) { 62 | return pks[0]; 63 | } 64 | } 65 | return null; 66 | } 67 | 68 | private String findForeignKeyFieldName(ForeignKey fk) { 69 | TableField[] pks = fk.getFieldsArray(); 70 | if (pks.length == 1) { 71 | return pks[0].getName(); 72 | } 73 | return null; 74 | } 75 | 76 | private String findForeignKeyReference(ForeignKey fk) { 77 | UniqueKey pk = fk.getKey(); 78 | if (pk != null) { 79 | Field[] pks = pk.getFieldsArray(); 80 | if (pks.length == 1) { 81 | return pk.getTable().getName(); 82 | } 83 | } 84 | return null; 85 | } 86 | 87 | @Override 88 | public Identity getIdentity() { 89 | TableField pk = findPrimaryKey(table); 90 | if (pk == null) { 91 | return null; 92 | } 93 | return new DynamicIdentity(table, pk); 94 | } 95 | 96 | @SuppressWarnings("unchecked") 97 | public Field get(String field) { 98 | return (Field) fields.get(field); 99 | } 100 | 101 | @SuppressWarnings("unchecked") 102 | public Field getPk() { 103 | return (Field) pk; 104 | } 105 | 106 | public List> getFks() { 107 | return getFksTo(null); 108 | } 109 | 110 | @SuppressWarnings("unchecked") 111 | public List> getFksTo(String table) { 112 | List> result = new ArrayList<>(); 113 | for (String key : fks.keySet()) { 114 | if (table == null || fks.get(key).equals(table)) { 115 | result.add((Field) fields.get(key)); 116 | } 117 | } 118 | return result; 119 | } 120 | 121 | public String getFk(String field) { 122 | return fks.get(field); 123 | } 124 | 125 | // TODO: detect views 126 | public String getType() { 127 | return "table"; 128 | } 129 | 130 | public Set fieldNames() { 131 | return fields.keySet(); 132 | } 133 | 134 | public boolean exists(String key) { 135 | return fields.containsKey(key); 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/config/CorsConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration 10 | public class CorsConfiguration { 11 | 12 | @Value("${rest.cors.allowed-origins:*}") 13 | private String[] allowedOrigins; 14 | 15 | @Bean 16 | public WebMvcConfigurer corsConfigurer() { 17 | return new WebMvcConfigurer() { 18 | @Override 19 | public void addCorsMappings(CorsRegistry registry) { 20 | registry.addMapping("/**").allowedMethods("OPTIONS", "GET", "PUT", "POST", "DELETE", "PATCH") 21 | .allowedHeaders("Content-Type", "X-XSRF-TOKEN").allowedOrigins(allowedOrigins) 22 | .allowCredentials(true).maxAge(1728000); 23 | } 24 | }; 25 | } 26 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/BackupController.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import com.tqdev.crudapi.record.RecordService; 12 | 13 | @RestController 14 | @RequestMapping("/backup") 15 | public class BackupController { 16 | 17 | public static final Logger logger = LoggerFactory.getLogger(BackupController.class); 18 | 19 | @Autowired 20 | Responder responder; 21 | 22 | @Autowired 23 | RecordService service; 24 | 25 | @RequestMapping(value = "/dump", method = RequestMethod.GET) 26 | public ResponseEntity dump() { 27 | logger.info("Dumping records for backup"); 28 | return responder.success(service.getDatabaseRecords()); 29 | } 30 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/ColumnController.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import com.tqdev.crudapi.column.ColumnService; 4 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 5 | import com.tqdev.crudapi.record.ErrorCode; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | @RestController 14 | @RequestMapping("/columns") 15 | public class ColumnController { 16 | 17 | public static final Logger logger = LoggerFactory.getLogger(ColumnController.class); 18 | 19 | @Autowired 20 | Responder responder; 21 | 22 | @Autowired 23 | ColumnService service; 24 | 25 | @RequestMapping(value = "/", method = RequestMethod.GET) 26 | public ResponseEntity getDatabase() { 27 | logger.info("Requesting columns meta data"); 28 | return responder.success(service.getDatabaseDefinition()); 29 | } 30 | 31 | @RequestMapping(value = "/{table}", method = RequestMethod.GET) 32 | public ResponseEntity getTable(@PathVariable("table") String tableName) { 33 | logger.info("Requesting columns meta data"); 34 | if (!service.hasTable(tableName)) { 35 | return responder.error(ErrorCode.TABLE_NOT_FOUND, tableName); 36 | } 37 | return responder.success(service.getDatabaseDefinition().get(tableName)); 38 | } 39 | 40 | @RequestMapping(value = "/{table}/{column}", method = RequestMethod.GET) 41 | public ResponseEntity getColumn(@PathVariable("table") String tableName, @PathVariable("column") String columnName) { 42 | logger.info("Requesting columns meta data"); 43 | if (!service.hasTable(tableName)) { 44 | return responder.error(ErrorCode.TABLE_NOT_FOUND, tableName); 45 | } 46 | ReflectedTable table = service.getTable(tableName); 47 | if (!table.exists(columnName)) { 48 | return responder.error(ErrorCode.COLUMN_NOT_FOUND, columnName); 49 | } 50 | return responder.success(table.get(columnName)); 51 | } 52 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/ExceptionHandlerController.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | import org.springframework.web.servlet.HandlerMapping; 12 | 13 | import com.tqdev.crudapi.record.ErrorCode; 14 | 15 | @RestController 16 | @RestControllerAdvice 17 | public class ExceptionHandlerController { 18 | 19 | @Autowired 20 | Responder responder; 21 | 22 | @ExceptionHandler(Exception.class) 23 | public ResponseEntity exceptionHandler(Exception ex, HttpServletRequest request) { 24 | ErrorCode error = ErrorCode.ERROR_NOT_FOUND; 25 | String argument = ex.getClass().getSimpleName(); 26 | switch (argument) { 27 | case "HttpMessageNotReadableException": 28 | error = ErrorCode.HTTP_MESSAGE_NOT_READABLE; 29 | argument = null; 30 | break; 31 | case "DuplicateKeyException": 32 | error = ErrorCode.DUPLICATE_KEY_EXCEPTION; 33 | argument = null; 34 | break; 35 | case "DataIntegrityViolationException": 36 | error = ErrorCode.DATA_INTEGRITY_VIOLATION; 37 | argument = null; 38 | break; 39 | default: 40 | ex.printStackTrace(); 41 | } 42 | return responder.error(error, argument); 43 | } 44 | 45 | @RequestMapping(value = "/**") 46 | public ResponseEntity fallbackHandler(HttpServletRequest request) throws Exception { 47 | return responder.error(ErrorCode.ROUTE_NOT_FOUND, 48 | (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)); 49 | } 50 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/OpenApiController.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import com.tqdev.crudapi.openapi.OpenApiService; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/openapi") 14 | public class OpenApiController { 15 | 16 | public static final Logger logger = LoggerFactory.getLogger(OpenApiController.class); 17 | 18 | @Autowired 19 | Responder responder; 20 | 21 | @Autowired 22 | OpenApiService service; 23 | 24 | @RequestMapping(value = "/", method = RequestMethod.GET) 25 | public ResponseEntity openapi() { 26 | logger.info("Requesting openapi meta data"); 27 | return responder.success(service.getOpenApiDefinition()); 28 | } 29 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/RecordController.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.util.LinkedMultiValueMap; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import com.tqdev.crudapi.record.RecordService; 20 | import com.tqdev.crudapi.record.container.Record; 21 | import com.tqdev.crudapi.record.ErrorCode; 22 | import com.tqdev.crudapi.record.Params; 23 | 24 | @RestController 25 | @RequestMapping("/records") 26 | public class RecordController { 27 | 28 | public static final Logger logger = LoggerFactory.getLogger(RecordController.class); 29 | 30 | @Autowired 31 | Responder responder; 32 | 33 | @Autowired 34 | RecordService service; 35 | 36 | @RequestMapping(value = "/{table}", method = RequestMethod.GET) 37 | public ResponseEntity list(@PathVariable("table") String table, 38 | @RequestParam LinkedMultiValueMap params) { 39 | logger.info("Listing table with name {} and parameters {}", table, params); 40 | if (!service.exists(table)) { 41 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 42 | } 43 | return responder.success(service.list(table, new Params(params))); 44 | } 45 | 46 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.GET) 47 | public ResponseEntity read(@PathVariable("table") String table, @PathVariable("id") String id, 48 | @RequestParam LinkedMultiValueMap params) { 49 | logger.info("Reading record from {} with id {} and parameters {}", table, id, params); 50 | if (!service.exists(table)) { 51 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 52 | } 53 | if (id.indexOf(',') >= 0) { 54 | String[] ids = id.split(","); 55 | ArrayList result = new ArrayList<>(); 56 | for (int i = 0; i < ids.length; i++) { 57 | result.add(service.read(table, ids[i], new Params(params))); 58 | } 59 | return responder.success(result); 60 | } else { 61 | Object response = service.read(table, id, new Params(params)); 62 | if (response == null) { 63 | return responder.error(ErrorCode.RECORD_NOT_FOUND, id); 64 | } 65 | return responder.success(response); 66 | } 67 | } 68 | 69 | @RequestMapping(value = "/{table}", method = RequestMethod.POST, headers = "Content-Type=application/x-www-form-urlencoded") 70 | public ResponseEntity create(@PathVariable("table") String table, 71 | @RequestBody LinkedMultiValueMap record, 72 | @RequestParam LinkedMultiValueMap params) { 73 | ObjectMapper mapper = new ObjectMapper(); 74 | Object pojo = mapper.convertValue(convertToSingleValueMap(record), Object.class); 75 | return create(table, pojo, params); 76 | } 77 | 78 | @RequestMapping(value = "/{table}", method = RequestMethod.POST, headers = "Content-Type=application/json") 79 | public ResponseEntity create(@PathVariable("table") String table, @RequestBody Object record, 80 | @RequestParam LinkedMultiValueMap params) { 81 | logger.info("Creating record in {} with properties {}", table, record); 82 | if (!service.exists(table)) { 83 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 84 | } 85 | if (record instanceof ArrayList) { 86 | ArrayList records = (ArrayList) record; 87 | ArrayList result = new ArrayList<>(); 88 | for (int i = 0; i < records.size(); i++) { 89 | result.add(service.create(table, Record.valueOf(records.get(i)), new Params(params))); 90 | } 91 | return responder.success(result); 92 | } else { 93 | return responder.success(service.create(table, Record.valueOf(record), new Params(params))); 94 | } 95 | } 96 | 97 | @SuppressWarnings("unchecked") 98 | private LinkedHashMap convertToSingleValueMap(LinkedMultiValueMap map) { 99 | LinkedHashMap result = new LinkedHashMap<>(); 100 | for (String key : map.keySet()) { 101 | for (String v : map.get(key)) { 102 | Object value = v; 103 | if (key.endsWith("__is_null")) { 104 | key = key.substring(0, key.indexOf("__is_null")); 105 | value = null; 106 | } 107 | if (result.containsKey(key)) { 108 | Object current = result.get(key); 109 | if (current.getClass().isArray()) { 110 | ((ArrayList) current).add(value); 111 | } else { 112 | ArrayList arr = new ArrayList<>(); 113 | arr.add(current); 114 | arr.add(v); 115 | value = arr; 116 | } 117 | } 118 | result.put(key, value); 119 | } 120 | } 121 | return result; 122 | } 123 | 124 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.PUT, headers = "Content-Type=application/x-www-form-urlencoded") 125 | public ResponseEntity update(@PathVariable("table") String table, @PathVariable("id") String id, 126 | @RequestBody LinkedMultiValueMap record, 127 | @RequestParam LinkedMultiValueMap params) { 128 | ObjectMapper mapper = new ObjectMapper(); 129 | Object pojo = mapper.convertValue(convertToSingleValueMap(record), Object.class); 130 | return update(table, id, pojo, params); 131 | } 132 | 133 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.PUT, headers = "Content-Type=application/json") 134 | public ResponseEntity update(@PathVariable("table") String table, @PathVariable("id") String id, 135 | @RequestBody Object record, @RequestParam LinkedMultiValueMap params) { 136 | logger.info("Inrementing record in {} with id {} and properties {}", table, id, record); 137 | if (!service.exists(table)) { 138 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 139 | } 140 | String[] ids = id.split(","); 141 | if (record instanceof ArrayList) { 142 | ArrayList records = (ArrayList) record; 143 | if (ids.length != records.size()) { 144 | return responder.error(ErrorCode.ARGUMENT_COUNT_MISMATCH, id); 145 | } 146 | ArrayList result = new ArrayList<>(); 147 | for (int i = 0; i < ids.length; i++) { 148 | result.add(service.update(table, ids[i], Record.valueOf(records.get(i)), new Params(params))); 149 | } 150 | return responder.success(result); 151 | } else { 152 | if (ids.length != 1) { 153 | return responder.error(ErrorCode.ARGUMENT_COUNT_MISMATCH, id); 154 | } 155 | return responder.success(service.update(table, id, Record.valueOf(record), new Params(params))); 156 | } 157 | } 158 | 159 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.PATCH, headers = "Content-Type=application/x-www-form-urlencoded") 160 | public ResponseEntity increment(@PathVariable("table") String table, @PathVariable("id") String id, 161 | @RequestBody LinkedMultiValueMap record, 162 | @RequestParam LinkedMultiValueMap params) { 163 | ObjectMapper mapper = new ObjectMapper(); 164 | Object pojo = mapper.convertValue(convertToSingleValueMap(record), Object.class); 165 | return update(table, id, pojo, params); 166 | } 167 | 168 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.PATCH, headers = "Content-Type=application/json") 169 | public ResponseEntity increment(@PathVariable("table") String table, @PathVariable("id") String id, 170 | @RequestBody Object record, @RequestParam LinkedMultiValueMap params) { 171 | logger.info("Updating record in {} with id {} and properties {}", table, id, record); 172 | if (!service.exists(table)) { 173 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 174 | } 175 | String[] ids = id.split(","); 176 | if (record instanceof ArrayList) { 177 | ArrayList records = (ArrayList) record; 178 | if (ids.length != records.size()) { 179 | return responder.error(ErrorCode.ARGUMENT_COUNT_MISMATCH, id); 180 | } 181 | ArrayList result = new ArrayList<>(); 182 | for (int i = 0; i < ids.length; i++) { 183 | result.add(service.increment(table, ids[i], Record.valueOf(records.get(i)), new Params(params))); 184 | } 185 | return responder.success(result); 186 | } else { 187 | if (ids.length != 1) { 188 | return responder.error(ErrorCode.ARGUMENT_COUNT_MISMATCH, id); 189 | } 190 | return responder.success(service.increment(table, id, Record.valueOf(record), new Params(params))); 191 | } 192 | } 193 | 194 | @RequestMapping(value = "/{table}/{id}", method = RequestMethod.DELETE) 195 | public ResponseEntity delete(@PathVariable("table") String table, @PathVariable("id") String id, 196 | @RequestParam LinkedMultiValueMap params) { 197 | logger.info("Deleting record from {} with id {}", table, id); 198 | if (!service.exists(table)) { 199 | return responder.error(ErrorCode.TABLE_NOT_FOUND, table); 200 | } 201 | String[] ids = id.split(","); 202 | if (ids.length > 1) { 203 | ArrayList result = new ArrayList<>(); 204 | for (int i = 0; i < ids.length; i++) { 205 | result.add(service.delete(table, ids[i], new Params(params))); 206 | } 207 | return responder.success(result); 208 | } else { 209 | return responder.success(service.delete(table, id, new Params(params))); 210 | } 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/controller/Responder.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.controller; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | import com.tqdev.crudapi.record.ErrorCode; 7 | import com.tqdev.crudapi.record.document.ErrorDocument; 8 | 9 | public class Responder { 10 | 11 | protected ResponseEntity error(ErrorCode error, String argument) { 12 | return new ResponseEntity<>(new ErrorDocument(error, argument), error.getStatus()); 13 | } 14 | 15 | protected ResponseEntity success(Object result) { 16 | return new ResponseEntity<>(result, HttpStatus.OK); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/openapi/JooqOpenApiService.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.openapi; 2 | 3 | import com.tqdev.crudapi.column.ColumnService; 4 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 5 | import org.jooq.DSLContext; 6 | 7 | import java.io.IOException; 8 | 9 | public class JooqOpenApiService implements OpenApiService { 10 | 11 | private DSLContext dsl; 12 | private DatabaseReflection reflection; 13 | private OpenApiDefinition baseOpenApiDefinition; 14 | 15 | public JooqOpenApiService(DSLContext dsl, ColumnService columns) { 16 | this.dsl = dsl; 17 | reflection = columns.getDatabaseReflection(); 18 | baseOpenApiDefinition = null; 19 | } 20 | 21 | @Override 22 | public OpenApiDefinition getOpenApiDefinition() { 23 | OpenApiBuilder builder = new OpenApiBuilder(reflection,baseOpenApiDefinition); 24 | return builder.build(); 25 | } 26 | 27 | @Override 28 | public void initialize(String openApiFilename) throws IOException { 29 | baseOpenApiDefinition = OpenApiDefinition.fromFile(openApiFilename); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/openapi/OpenApiBuilder.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.openapi; 2 | 3 | import com.fasterxml.jackson.databind.node.ObjectNode; 4 | import com.tqdev.crudapi.column.definition.ColumnDefinition; 5 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 6 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 7 | import org.jooq.DataType; 8 | import org.jooq.Field; 9 | import org.jooq.impl.DefaultDataType; 10 | 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.URLEncoder; 13 | import java.util.LinkedHashMap; 14 | import java.util.Set; 15 | 16 | public class OpenApiBuilder { 17 | private OpenApiDefinition openapi; 18 | private DatabaseReflection reflection; 19 | private LinkedHashMap operations; 20 | private LinkedHashMap> types; 21 | 22 | private LinkedHashMap createType(String type, String format) { 23 | LinkedHashMap item = new LinkedHashMap<>(); 24 | item.put("type",type); 25 | if (format!=null) { 26 | item.put("format", format); 27 | } 28 | return item; 29 | } 30 | 31 | public OpenApiBuilder(DatabaseReflection reflection, OpenApiDefinition base) 32 | { 33 | operations = new LinkedHashMap<>(); 34 | operations.put("list", "getTable"); 35 | operations.put("create", "post"); 36 | operations.put("read", "getTable"); 37 | operations.put("update", "put"); 38 | operations.put("delete", "delete"); 39 | operations.put("increment", "patch"); 40 | types = new LinkedHashMap<>(); 41 | types.put("integer",createType("integer","int32")); 42 | types.put("bigint",createType("integer","int64")); 43 | types.put("varchar",createType("string",null)); 44 | types.put("clob",createType("string",null)); 45 | types.put("varbinary",createType("string","byte")); 46 | types.put("blob",createType("string","byte")); 47 | types.put("decimal",createType("string",null)); 48 | types.put("float",createType("number","float")); 49 | types.put("double",createType("number","double")); 50 | types.put("time",createType("string","date-time")); 51 | types.put("timestamp",createType("string","date-time")); 52 | types.put("geometry",createType("string",null)); 53 | types.put("boolean",createType("boolean",null)); 54 | this.reflection = reflection; 55 | openapi = new OpenApiDefinition(base); 56 | } 57 | 58 | public OpenApiDefinition build() 59 | { 60 | openapi.set("openapi", "3.0.0"); 61 | Set tableNames = reflection.getTableNames(); 62 | for (String tableName: tableNames) { 63 | setPath(tableName); 64 | } 65 | openapi.set("components|responses|pk_integer|description", "inserted primary key value (integer)"); 66 | openapi.set("components|responses|pk_integer|content|application/json|schema|type", "integer"); 67 | openapi.set("components|responses|pk_integer|content|application/json|schema|format", "int64"); 68 | openapi.set("components|responses|pk_string|description", "inserted primary key value (string)"); 69 | openapi.set("components|responses|pk_string|content|application/json|schema|type", "string"); 70 | openapi.set("components|responses|pk_string|content|application/json|schema|format", "uuid"); 71 | openapi.set("components|responses|rows_affected|description", "number of rows affected (integer)"); 72 | openapi.set("components|responses|rows_affected|content|application/json|schema|type", "integer"); 73 | openapi.set("components|responses|rows_affected|content|application/json|schema|format", "int64"); 74 | for (String tableName: tableNames) { 75 | setComponentSchema(tableName); 76 | setComponentResponse(tableName); 77 | setComponentRequestBody(tableName); 78 | } 79 | setComponentParameters(); 80 | int i=0; 81 | for (String tableName: tableNames) { 82 | setTag(i, tableName); 83 | i++; 84 | } 85 | return openapi; 86 | } 87 | 88 | private boolean isOperationOnTableAllowed(String operation, String tableName) 89 | { 90 | /*tableHandler = VariableStore.getTable("authorization.tableHandler"); 91 | if (tableHandler) { 92 | return true; 93 | } 94 | return (bool) call_user_func($tableHandler, $operation, $tableName);*/ 95 | return true; 96 | } 97 | 98 | private boolean isOperationOnColumnAllowed(String operation, String tableName, String columnName) 99 | { 100 | /*$columnHandler = VariableStore::getTable("authorization.columnHandler"); 101 | if (!$columnHandler) { 102 | return true; 103 | } 104 | return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);*/ 105 | return true; 106 | } 107 | 108 | private String urlencode(String str){ 109 | try { 110 | return URLEncoder.encode(str, "UTF-8"); 111 | } catch (UnsupportedEncodingException e) { 112 | return str; 113 | } 114 | } 115 | 116 | private void setPath(String tableName) 117 | { 118 | ReflectedTable table = reflection.getTable(tableName); 119 | String type = table.getType(); 120 | Field pk = table.getPk(); 121 | String pkName = pk!=null ? pk.getName() : null; 122 | String path; 123 | for (String operation : operations.keySet()) { 124 | String method = operations.get(operation); 125 | if (pkName==null && !operation.equals("list")) { 126 | continue; 127 | } 128 | if (!type.equals("table") && !operation.equals("list")) { 129 | continue; 130 | } 131 | if (!isOperationOnTableAllowed(operation, tableName)) { 132 | continue; 133 | } 134 | if (operation.equals("list") || operation.equals("create")) { 135 | path = String.format("/records/%s", tableName); 136 | } else { 137 | path = String.format("/records/%s/{%s}", tableName, pkName); 138 | openapi.set(String.format("paths|%s|%s|parameters|0|\\$ref",path,method), "#/components/parameters/pk"); 139 | } 140 | if (operation.equals("create") || operation.equals("update") || operation.equals("increment")) { 141 | openapi.set(String.format("paths|%s|%s|requestBody|\\$ref",path,method), String.format("#/components/requestBodies/%s-%s",operation,urlencode(tableName))); 142 | } 143 | openapi.set(String.format("paths|%s|%s|tags|0",path,method), tableName); 144 | openapi.set(String.format("paths|%s|%s|description",path,method), String.format("%s %s",operation, tableName)); 145 | switch (operation) { 146 | case "list": 147 | openapi.set(String.format("paths|%s|%s|responses|200|\\$ref",path,method), String.format("#/components/responses/%s-%s",operation,urlencode(tableName))); 148 | break; 149 | case "create": 150 | if (pk.getType().equals("integer")) { 151 | openapi.set(String.format("paths|%s|%s|responses|200|\\$ref",path,method), "#/components/responses/pk_integer"); 152 | } else { 153 | openapi.set(String.format("paths|%s|%s|responses|200|\\$ref",path,method), "#/components/responses/pk_string"); 154 | } 155 | break; 156 | case "read": 157 | openapi.set(String.format("paths|%s|%s|responses|200|\\$ref",path,method), String.format("#/components/responses/%s-%s",operation,urlencode(tableName))); 158 | break; 159 | case "update": 160 | case "delete": 161 | case "increment": 162 | openapi.set(String.format("paths|%s|%s|responses|200|\\$ref",path,method), "#/components/responses/rows_affected"); 163 | break; 164 | } 165 | } 166 | } 167 | 168 | private void setComponentSchema(String tableName) { 169 | ReflectedTable table = reflection.getTable(tableName); 170 | String type = table.getType(); 171 | Field pk = table.getPk(); 172 | String pkName = pk != null ? pk.getName() : null; 173 | String prefix; 174 | for (String operation : operations.keySet()) { 175 | String method = operations.get(operation); 176 | if (pkName==null && !operation.equals("list")) { 177 | continue; 178 | } 179 | if (!type.equals("table") && !operation.equals("list")) { 180 | continue; 181 | } 182 | if (operation.equals("delete")) { 183 | continue; 184 | } 185 | if (!isOperationOnTableAllowed(operation, tableName)) { 186 | continue; 187 | } 188 | if (operation.equals("list")) { 189 | openapi.set(String.format("components|schemas|%s-%s|type",operation,tableName), "object"); 190 | openapi.set(String.format("components|schemas|%s-%s|properties|results|type",operation,tableName), "integer"); 191 | openapi.set(String.format("components|schemas|%s-%s|properties|results|format",operation,tableName), "int64"); 192 | openapi.set(String.format("components|schemas|%s-%s|properties|records|type",operation,tableName), "array"); 193 | prefix = String.format("components|schemas|%s-%s|properties|records|items",operation,tableName); 194 | } else { 195 | prefix = String.format("components|schemas|%s-%s",operation,tableName); 196 | } 197 | openapi.set(String.format("%s|type",prefix), "object"); 198 | for(String columnName : table.fieldNames()) { 199 | if (!isOperationOnColumnAllowed(operation, tableName, columnName)) { 200 | continue; 201 | } 202 | ColumnDefinition column = new ColumnDefinition(table.get(columnName)); 203 | LinkedHashMap properties = types.get(column.getType()); 204 | if (properties==null) { 205 | properties = new LinkedHashMap<>(); 206 | properties.put("type","string"); 207 | } 208 | for (String key : properties.keySet()) { 209 | String value = properties.get(key); 210 | openapi.set(String.format("%s|properties|%s|%s",prefix,columnName,key), value); 211 | } 212 | } 213 | } 214 | } 215 | 216 | private void setComponentResponse(String tableName) 217 | { 218 | ReflectedTable table = reflection.getTable(tableName); 219 | String type = table.getType(); 220 | Field pk = table.getPk(); 221 | String pkName = pk != null ? pk.getName() : null; 222 | for (String operation : new String[]{"list", "read"}) { 223 | if (pkName==null && !operation.equals("list")) { 224 | continue; 225 | } 226 | if (!type.equals("table") && !operation.equals("list")) { 227 | continue; 228 | } 229 | if (!isOperationOnTableAllowed(operation, tableName)) { 230 | continue; 231 | } 232 | if (operation.equals("list")) { 233 | openapi.set(String.format("components|responses|%s-%s|description",operation,tableName), String.format("list of %s records",tableName)); 234 | } else { 235 | openapi.set(String.format("components|responses|%s-%s|description",operation,tableName), String.format("single %s record",tableName)); 236 | } 237 | openapi.set(String.format("components|responses|%s-%s|content|application/json|schema|\\$ref",operation,tableName), String.format("#/components/schemas/%s-%s",operation,urlencode(tableName))); 238 | } 239 | } 240 | 241 | private void setComponentRequestBody(String tableName) 242 | { 243 | ReflectedTable table = reflection.getTable(tableName); 244 | String type = table.getType(); 245 | Field pk = table.getPk(); 246 | String pkName = pk != null ? pk.getName() : null; 247 | if (pkName!=null && type.equals("table")) { 248 | for (String operation : new String[]{"create", "update", "increment"}) { 249 | if (!isOperationOnTableAllowed(operation, tableName)) { 250 | continue; 251 | } 252 | openapi.set(String.format("components|requestBodies|%s-%s|description",operation,tableName), String.format("single %s record",tableName)); 253 | openapi.set(String.format("components|requestBodies|%s-%s|content|application/json|schema|\\$ref",operation,tableName), String.format("#/components/schemas/%s-%s",operation,urlencode(tableName))); 254 | } 255 | } 256 | } 257 | 258 | private void setComponentParameters() 259 | { 260 | openapi.set("components|parameters|pk|name", "id"); 261 | openapi.set("components|parameters|pk|in", "path"); 262 | openapi.set("components|parameters|pk|schema|type", "string"); 263 | openapi.set("components|parameters|pk|description", "primary key value"); 264 | openapi.set("components|parameters|pk|required", true); 265 | } 266 | 267 | private void setTag(int index, String tableName) 268 | { 269 | openapi.set(String.format("tags|%d|name",index), tableName); 270 | openapi.set(String.format("tags|%d|description",index), String.format("%s operations",tableName)); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/openapi/OpenApiDefinition.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.openapi; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 4 | import com.fasterxml.jackson.annotation.JsonAnySetter; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.node.ObjectNode; 8 | import org.springframework.core.io.ClassPathResource; 9 | 10 | import java.io.FileNotFoundException; 11 | import java.io.IOException; 12 | import java.util.Iterator; 13 | import java.util.LinkedHashMap; 14 | import java.util.Map; 15 | 16 | public class OpenApiDefinition { 17 | 18 | private ObjectNode root; 19 | 20 | public ObjectNode getRoot() 21 | { 22 | return root; 23 | } 24 | 25 | public OpenApiDefinition() 26 | { 27 | ObjectMapper mapper = new ObjectMapper(); 28 | root = mapper.createObjectNode(); 29 | } 30 | 31 | public OpenApiDefinition(OpenApiDefinition copy) 32 | { 33 | root = copy.getRoot().deepCopy(); 34 | } 35 | 36 | @JsonAnyGetter 37 | public Map jsonAnyGet() { 38 | LinkedHashMap result = new LinkedHashMap<>(); 39 | Iterator> fields = root.fields(); 40 | while (fields.hasNext()) { 41 | Map.Entry field = fields.next(); 42 | result.put(field.getKey(), field.getValue()); 43 | } 44 | return result; 45 | } 46 | 47 | @JsonAnySetter 48 | public void jsonAnySet(String name, JsonNode value) { 49 | root.set(name, value); 50 | } 51 | 52 | public static OpenApiDefinition fromFile(String filename) throws IOException { 53 | ObjectMapper mapper = new ObjectMapper(); 54 | ClassPathResource resource = new ClassPathResource(filename); 55 | OpenApiDefinition result; 56 | try { 57 | result = mapper.readValue(resource.getInputStream(), OpenApiDefinition.class); 58 | } catch (FileNotFoundException e) { 59 | result = null; 60 | } 61 | return result; 62 | } 63 | 64 | public void set(String path, Object value) 65 | { 66 | String[] parts = path.replaceAll("\\|$|^\\|", "").split("\\|"); 67 | ObjectNode current = root; 68 | for (int i=0;i pk = reflection.getTable(table).getPk(); 24 | for (String key : reflection.getTable(table).fieldNames()) { 25 | Field field = reflection.getTable(table).get(key); 26 | if (field.getName().equals(pk.getName())) { 27 | record.remove(key); 28 | } 29 | } 30 | } 31 | } 32 | 33 | @Override 34 | public boolean exists(String table) { 35 | return reflection.hasTable(table); 36 | } 37 | 38 | @Override 39 | public DatabaseRecords getDatabaseRecords() { 40 | DatabaseRecords db = new DatabaseRecords(); 41 | for (String table : reflection.getTableNames()) { 42 | ArrayList records = new ArrayList<>(); 43 | for (Record record : list(table, new Params()).getRecords()) { 44 | records.add(record); 45 | } 46 | db.put(table, records); 47 | } 48 | return db; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/ColumnSelector.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Base64; 5 | import java.util.HashMap; 6 | import java.util.LinkedHashMap; 7 | import java.util.LinkedHashSet; 8 | import java.util.Set; 9 | 10 | import org.jooq.Field; 11 | import org.jooq.impl.DSL; 12 | 13 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 14 | import com.tqdev.crudapi.record.container.Record; 15 | import com.tqdev.crudapi.record.spatial.SpatialDSL; 16 | 17 | public class ColumnSelector { 18 | 19 | private boolean isMandatoryField(String tableName, String fieldName, Params params) { 20 | return params.containsKey("mandatory") && params.get("mandatory").contains(tableName + "." + fieldName); 21 | } 22 | 23 | private Set select(String tableName, boolean primaryTable, Params params, String paramName, 24 | Set fieldNames, boolean include) { 25 | if (!params.containsKey(paramName)) { 26 | return fieldNames; 27 | } 28 | HashMap columns = new HashMap<>(); 29 | for (String key : params.get(paramName).get(0).split(",")) { 30 | columns.put(key, true); 31 | } 32 | LinkedHashSet result = new LinkedHashSet<>(); 33 | for (String key : fieldNames) { 34 | boolean match = columns.containsKey("*.*"); 35 | if (!match) { 36 | match = columns.containsKey(tableName + ".*") || columns.containsKey(tableName + "." + key); 37 | } 38 | if (primaryTable && !match) { 39 | match = columns.containsKey("*") || columns.containsKey(key); 40 | } 41 | if (match) { 42 | if (include || isMandatoryField(tableName, key, params)) { 43 | result.add(key); 44 | } 45 | } else { 46 | if (!include || isMandatoryField(tableName, key, params)) { 47 | result.add(key); 48 | } 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | private Set columns(ReflectedTable table, boolean primaryTable, Params params) { 55 | String tableName = table.getName(); 56 | Set results = table.fieldNames(); 57 | results = select(tableName, primaryTable, params, "columns", results, true); 58 | results = select(tableName, primaryTable, params, "exclude", results, false); 59 | return results; 60 | } 61 | 62 | public LinkedHashMap, Object> getValues(ReflectedTable table, boolean primaryTable, Record record, 63 | Params params) { 64 | LinkedHashMap, Object> columns = new LinkedHashMap<>(); 65 | Set cols = columns(table, primaryTable, params); 66 | for (String key : cols) { 67 | if (record.containsKey(key)) { 68 | Field field = table.get(key); 69 | if (field.getDataType().getTypeName().equals("geometry")) { 70 | columns.put(field, SpatialDSL.geomFromText(DSL.val(record.get(key)))); 71 | } else if (field.getDataType().isBinary() && record.get(key) != null) { 72 | columns.put(field, Base64.getDecoder().decode((String) record.get(key))); 73 | } else { 74 | columns.put(field, record.get(key)); 75 | } 76 | } 77 | } 78 | return columns; 79 | } 80 | 81 | public LinkedHashMap, Object> getIncrements(ReflectedTable table, boolean primaryTable, 82 | Record record, Params params) { 83 | LinkedHashMap, Object> columns = new LinkedHashMap<>(); 84 | Set cols = columns(table, primaryTable, params); 85 | for (String key : cols) { 86 | if (record.containsKey(key)) { 87 | Field field = table.get(key); 88 | Object value = record.get(key); 89 | if (value instanceof Number) { 90 | columns.put(field, field.add((Number) value)); 91 | } 92 | } 93 | } 94 | return columns; 95 | } 96 | 97 | public ArrayList> getNames(ReflectedTable table, boolean primaryTable, Params params) { 98 | ArrayList> columns = new ArrayList<>(); 99 | for (String key : columns(table, primaryTable, params)) { 100 | Field field = table.get(key); 101 | if (field.getDataType().getTypeName().equals("geometry")) { 102 | columns.add(SpatialDSL.asText(field).as(key)); 103 | } else { 104 | columns.add(field); 105 | } 106 | } 107 | return columns; 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/CrudApiHandlers.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | public interface CrudApiHandlers { 4 | 5 | default boolean tableAuthorizer(String command, String database, String table) { 6 | return true; 7 | } 8 | 9 | default boolean recordFilter(String command, String database, String table) { 10 | return false; 11 | } 12 | 13 | default boolean columnAuthorizer(String command, String database, String table, String column) { 14 | return true; 15 | } 16 | 17 | default Object tenancyFunction(String command, String database, String table, String column) { 18 | return null; 19 | } 20 | 21 | default Object inputSanitizer(String command, String database, String table, String column, String type, 22 | String value) { 23 | return null; 24 | } 25 | 26 | default boolean inputValidator(String command, String database, String table, String column, String type, 27 | String value, Object context) { 28 | return true; 29 | } 30 | 31 | default void before(String command, String database, String table, String id, Object in) { 32 | 33 | } 34 | 35 | default void after(String command, String database, String table, String id, Object in, Object out) { 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public enum ErrorCode { 6 | 7 | ERROR_NOT_FOUND(9999, "%s", HttpStatus.INTERNAL_SERVER_ERROR), 8 | 9 | ROUTE_NOT_FOUND(1000, "Route '%s' not found", HttpStatus.NOT_FOUND), 10 | 11 | TABLE_NOT_FOUND(1001, "Table '%s' not found", HttpStatus.NOT_FOUND), 12 | 13 | ARGUMENT_COUNT_MISMATCH(1002, "Argument count mismatch in '%s'", HttpStatus.NOT_ACCEPTABLE), 14 | 15 | RECORD_NOT_FOUND(1003, "Record '%s' not found", HttpStatus.NOT_FOUND), 16 | 17 | ORIGIN_FORBIDDEN(1004, "Origin '%s' is forbidden", HttpStatus.FORBIDDEN), 18 | 19 | COLUMN_NOT_FOUND(1005, "Column '%s' not found", HttpStatus.NOT_FOUND), 20 | 21 | HTTP_MESSAGE_NOT_READABLE(1008, "Cannot read HTTP message", HttpStatus.NOT_ACCEPTABLE), 22 | 23 | DUPLICATE_KEY_EXCEPTION(1009, "Duplicate key exception", HttpStatus.NOT_ACCEPTABLE), 24 | 25 | DATA_INTEGRITY_VIOLATION(1010, "Data integrity violation", HttpStatus.NOT_ACCEPTABLE); 26 | 27 | private final int code; 28 | 29 | private final String message; 30 | 31 | private final HttpStatus status; 32 | 33 | ErrorCode(int code, String message, HttpStatus status) { 34 | this.code = code; 35 | this.message = message; 36 | this.status = status; 37 | } 38 | 39 | /** 40 | * Return the integer value of this error code. 41 | */ 42 | public int value() { 43 | return this.code; 44 | } 45 | 46 | /** 47 | * Return the message of this error code. 48 | */ 49 | public String getMessage(String argument) { 50 | return String.format(this.message, argument); 51 | } 52 | 53 | /** 54 | * Return the status of this error code. 55 | */ 56 | public HttpStatus getStatus() { 57 | return status; 58 | } 59 | 60 | /** 61 | * Return a string representation of this error code. 62 | */ 63 | @Override 64 | public String toString() { 65 | return Integer.toString(this.code); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/FilterInfo.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedList; 5 | 6 | import org.jooq.Condition; 7 | import org.jooq.Field; 8 | import org.jooq.impl.DSL; 9 | 10 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 11 | import com.tqdev.crudapi.record.spatial.SpatialDSL; 12 | 13 | public class FilterInfo { 14 | 15 | private void addConditionFromFilteraPath(PathTree conditions, LinkedList path, 16 | ReflectedTable table, Params params) { 17 | String key = "filter"; 18 | for (Character c : path) { 19 | key += c; 20 | } 21 | if (params.containsKey(key)) { 22 | for (String value : params.get(key)) { 23 | Condition condition = getConditionFromString(table, value); 24 | if (condition != null) { 25 | conditions.put(path, condition); 26 | } 27 | } 28 | } 29 | } 30 | 31 | private PathTree getConditionsAsPathTree(ReflectedTable table, Params params) { 32 | PathTree conditions = new PathTree<>(); 33 | LinkedList path0 = new LinkedList<>(); 34 | addConditionFromFilteraPath(conditions, path0, table, params); 35 | for (char n = '0'; n <= '9'; n++) { 36 | LinkedList path1 = new LinkedList<>(); 37 | path1.add(n); 38 | addConditionFromFilteraPath(conditions, path1, table, params); 39 | for (char l = 'a'; l <= 'f'; l++) { 40 | LinkedList path2 = new LinkedList<>(); 41 | path2.add(n); 42 | path2.add(l); 43 | addConditionFromFilteraPath(conditions, path2, table, params); 44 | } 45 | } 46 | return conditions; 47 | } 48 | 49 | private Condition combinePathTreeOfConditions(PathTree tree) { 50 | ArrayList conditions = tree.getValues(); 51 | Condition and = null; 52 | for (Condition condition : conditions) { 53 | if (and == null) { 54 | and = condition; 55 | } else { 56 | and = and.and(condition); 57 | } 58 | } 59 | if (tree.getKeys().size() == 0) { 60 | return and; 61 | } 62 | Condition or = null; 63 | for (Character p : tree.getKeys()) { 64 | Condition condition = combinePathTreeOfConditions(tree.get(p)); 65 | if (or == null) { 66 | or = condition; 67 | } else { 68 | or = or.or(condition); 69 | } 70 | } 71 | if (and == null) { 72 | and = or; 73 | } else { 74 | if (or != null) { 75 | and = and.and(or); 76 | } 77 | } 78 | return and; 79 | } 80 | 81 | public Condition getCombinedConditions(ReflectedTable table, Params params) { 82 | return combinePathTreeOfConditions(getConditionsAsPathTree(table, params)); 83 | } 84 | 85 | private Condition getConditionFromString(ReflectedTable table, String value) { 86 | Condition condition = null; 87 | String[] parts2; 88 | String[] parts = value.split(",", 3); 89 | if (parts.length < 2) { 90 | return null; 91 | } 92 | String command = parts[1]; 93 | Boolean negate = false; 94 | Boolean spatial = false; 95 | if (command.length() > 2) { 96 | if (command.charAt(0) == 'n') { 97 | negate = true; 98 | command = command.substring(1); 99 | } 100 | if (command.charAt(0) == 's') { 101 | spatial = true; 102 | command = command.substring(1); 103 | } 104 | } 105 | Field field = table.get(parts[0]); 106 | if (parts.length == 3 107 | || (parts.length == 2 && (command.equals("ic") || command.equals("is") || command.equals("iv")))) { 108 | if (spatial) { 109 | switch (command) { 110 | case "co": 111 | condition = SpatialDSL.contains(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 112 | break; 113 | case "cr": 114 | condition = SpatialDSL.crosses(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 115 | break; 116 | case "di": 117 | condition = SpatialDSL.disjoint(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 118 | break; 119 | case "eq": 120 | condition = SpatialDSL.equals(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 121 | break; 122 | case "in": 123 | condition = SpatialDSL.intersects(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 124 | break; 125 | case "ov": 126 | condition = SpatialDSL.overlaps(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 127 | break; 128 | case "to": 129 | condition = SpatialDSL.touches(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 130 | break; 131 | case "wi": 132 | condition = SpatialDSL.within(field, SpatialDSL.geomFromText(DSL.val(parts[2]))); 133 | break; 134 | case "ic": 135 | condition = SpatialDSL.isClosed(field); 136 | break; 137 | case "is": 138 | condition = SpatialDSL.isSimple(field); 139 | break; 140 | case "iv": 141 | condition = SpatialDSL.isValid(field); 142 | break; 143 | } 144 | } else { 145 | switch (command) { 146 | case "cs": 147 | condition = field.contains(parts[2]); 148 | break; 149 | case "sw": 150 | condition = field.startsWith(parts[2]); 151 | break; 152 | case "ew": 153 | condition = field.endsWith(parts[2]); 154 | break; 155 | case "eq": 156 | condition = field.eq(parts[2]); 157 | break; 158 | case "lt": 159 | condition = field.lt(parts[2]); 160 | break; 161 | case "le": 162 | condition = field.le(parts[2]); 163 | break; 164 | case "ge": 165 | condition = field.ge(parts[2]); 166 | break; 167 | case "gt": 168 | condition = field.gt(parts[2]); 169 | break; 170 | case "bt": 171 | parts2 = parts[2].split(",", 2); 172 | condition = field.between(parts2[0], parts2[1]); 173 | break; 174 | case "in": 175 | parts2 = parts[2].split(","); 176 | condition = field.in((Object[]) parts2); 177 | break; 178 | case "is": 179 | condition = field.isNull(); 180 | break; 181 | } 182 | } 183 | } 184 | if (condition != null) { 185 | if (negate) { 186 | condition = DSL.not(condition); 187 | } 188 | } 189 | return condition; 190 | } 191 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/HabtmValues.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | 6 | public class HabtmValues { 7 | 8 | public HashMap> pkValues; 9 | public HashMap fkValues; 10 | 11 | public HabtmValues(HashMap> pkValues, HashMap fkValues) { 12 | this.pkValues = pkValues; 13 | this.fkValues = fkValues; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/JooqRecordService.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.LinkedHashMap; 7 | 8 | import org.jooq.Condition; 9 | import org.jooq.DSLContext; 10 | import org.jooq.Field; 11 | import org.jooq.ResultQuery; 12 | import org.jooq.SelectLimitStep; 13 | import org.jooq.SortField; 14 | import org.jooq.Table; 15 | import org.jooq.impl.DSL; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import com.tqdev.crudapi.column.ColumnService; 20 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 21 | import com.tqdev.crudapi.record.container.DatabaseRecords; 22 | import com.tqdev.crudapi.record.container.DatabaseRecordsException; 23 | import com.tqdev.crudapi.record.container.Record; 24 | import com.tqdev.crudapi.record.document.ListDocument; 25 | 26 | public class JooqRecordService extends BaseRecordService implements RecordService { 27 | 28 | public static final Logger logger = LoggerFactory.getLogger(JooqRecordService.class); 29 | 30 | private DSLContext dsl; 31 | 32 | private ColumnSelector columns; 33 | private RelationIncluder includer; 34 | private FilterInfo filters; 35 | private OrderingInfo ordering; 36 | private PaginationInfo pagination; 37 | 38 | public JooqRecordService(DSLContext dsl, ColumnService columns) { 39 | this.dsl = dsl; 40 | reflection = columns.getDatabaseReflection(); 41 | this.columns = new ColumnSelector(); 42 | includer = new RelationIncluder(this.columns); 43 | filters = new FilterInfo(); 44 | ordering = new OrderingInfo(); 45 | pagination = new PaginationInfo(); 46 | } 47 | 48 | @SuppressWarnings("unchecked") 49 | @Override 50 | public String create(String tableName, Record record, Params params) { 51 | sanitizeRecord(tableName, record, null); 52 | ReflectedTable table = reflection.getTable(tableName); 53 | LinkedHashMap, Object> columnValues = columns.getValues(table, true, record, params); 54 | Field pk = reflection.getTable(tableName).getPk(); 55 | org.jooq.Record result = dsl.insertInto(table).set(columnValues).returning(pk).fetchOne(); 56 | if (result==null) { 57 | return String.valueOf(columnValues.get(pk)); 58 | } 59 | return String.valueOf(result.get(0)); 60 | } 61 | 62 | @Override 63 | public Record read(String tableName, String id, Params params) { 64 | ReflectedTable table = reflection.getTable(tableName); 65 | includer.addMandatoryColumns(table, reflection, params); 66 | ArrayList> columnNames = columns.getNames(table, true, params); 67 | Field pk = reflection.getTable(tableName).getPk(); 68 | org.jooq.Record record = dsl.select(columnNames).from(table).where(pk.eq(id)).fetchOne(); 69 | if (record == null) { 70 | return null; 71 | } 72 | Record r = Record.valueOf(record.intoMap()); 73 | ArrayList records = new ArrayList<>(Arrays.asList(r)); 74 | includer.addIncludes(tableName, records, reflection, params, dsl); 75 | return r; 76 | } 77 | 78 | @SuppressWarnings("unchecked") 79 | @Override 80 | public int update(String tableName, String id, Record record, Params params) { 81 | sanitizeRecord(tableName, record, id); 82 | ReflectedTable table = reflection.getTable(tableName); 83 | LinkedHashMap, Object> columnValues = columns.getValues(table, true, record, params); 84 | Field pk = reflection.getTable(tableName).getPk(); 85 | return dsl.update(table).set(columnValues).where(pk.eq(id)).execute(); 86 | } 87 | 88 | @SuppressWarnings("unchecked") 89 | @Override 90 | public int increment(String tableName, String id, Record record, Params params) { 91 | sanitizeRecord(tableName, record, id); 92 | ReflectedTable table = reflection.getTable(tableName); 93 | LinkedHashMap, Object> columnValues = columns.getValues(table, true, record, params); 94 | Field pk = reflection.getTable(tableName).getPk(); 95 | return dsl.update(table).set(columnValues).where(pk.eq(id)).execute(); 96 | } 97 | 98 | @Override 99 | public int delete(String tableName, String id, Params params) { 100 | Table table = reflection.getTable(tableName); 101 | Field pk = reflection.getTable(tableName).getPk(); 102 | return dsl.deleteFrom(table).where(pk.eq(id)).execute(); 103 | } 104 | 105 | @Override 106 | public ListDocument list(String tableName, Params params) { 107 | ArrayList records = new ArrayList<>(); 108 | ReflectedTable table = reflection.getTable(tableName); 109 | includer.addMandatoryColumns(table, reflection, params); 110 | ArrayList> columnNames = columns.getNames(table, true, params); 111 | Condition condition= filters.getCombinedConditions(table, params); 112 | ArrayList> columnOrdering = ordering.getColumnOrdering(table, params); 113 | int count = 0; 114 | ResultQuery query; 115 | if (!pagination.hasPage(params)) { 116 | int size = pagination.getResultSize(params); 117 | query = dsl.select(columnNames).from(table).where(condition).orderBy(columnOrdering); 118 | if (size != -1) { 119 | query = ((SelectLimitStep) query).limit(size); 120 | } 121 | } else { 122 | int offset = pagination.getPageOffset(params); 123 | int limit = pagination.getPageSize(params); 124 | count = (int) dsl.select(DSL.count()).from(table).where(condition).fetchOne(0); 125 | query = dsl.select(columnNames).from(table).where(condition).orderBy(columnOrdering).limit(offset, limit); 126 | } 127 | for (org.jooq.Record record : query.fetch()) { 128 | records.add(Record.valueOf(record.intoMap())); 129 | } 130 | includer.addIncludes(tableName, records, reflection, params, dsl); 131 | return new ListDocument(records.toArray(new Record[records.size()]), count); 132 | } 133 | 134 | @Override 135 | public void update() { 136 | reflection.update(); 137 | } 138 | 139 | @Override 140 | public void initialize(String recordsFilename) throws IOException, DatabaseRecordsException { 141 | DatabaseRecords.fromFile(recordsFilename).create(this); 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/OrderingInfo.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.jooq.SortField; 6 | 7 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 8 | 9 | public class OrderingInfo { 10 | 11 | public ArrayList> getColumnOrdering(ReflectedTable table, Params params) { 12 | ArrayList> fields = new ArrayList<>(); 13 | if (params.containsKey("order")) { 14 | for (String key : params.get("order")) { 15 | String[] parts = key.split(",", 3); 16 | boolean ascending = true; 17 | if (parts.length > 1) { 18 | ascending = !parts[1].toLowerCase().startsWith("desc"); 19 | } 20 | if (ascending) { 21 | fields.add(table.get(parts[0]).asc()); 22 | } else { 23 | fields.add(table.get(parts[0]).desc()); 24 | } 25 | } 26 | } 27 | return fields; 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/PaginationInfo.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | public class PaginationInfo { 4 | 5 | public final int DEFAULT_PAGE_SIZE = 20; 6 | 7 | public boolean hasPage(Params params) { 8 | return params.containsKey("page"); 9 | } 10 | 11 | public int getPageOffset(Params params) { 12 | int offset = 0; 13 | int pageSize = getPageSize(params); 14 | if (params.containsKey("page")) { 15 | for (String key : params.get("page")) { 16 | String[] parts = key.split(",", 2); 17 | int page = Integer.valueOf(parts[0]) - 1; 18 | offset = page * pageSize; 19 | } 20 | } 21 | return offset; 22 | } 23 | 24 | public int getPageSize(Params params) { 25 | int pageSize = DEFAULT_PAGE_SIZE; 26 | if (params.containsKey("page")) { 27 | for (String key : params.get("page")) { 28 | String[] parts = key.split(",", 2); 29 | if (parts.length > 1) { 30 | pageSize = Integer.valueOf(parts[1]); 31 | } 32 | } 33 | } 34 | return pageSize; 35 | } 36 | 37 | public int getResultSize(Params params) { 38 | int numberOfRows = -1; 39 | if (params.containsKey("size")) { 40 | for (String key : params.get("size")) { 41 | numberOfRows = Integer.valueOf(key); 42 | } 43 | } 44 | return numberOfRows; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/Params.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import org.springframework.util.LinkedMultiValueMap; 4 | 5 | public class Params extends LinkedMultiValueMap { 6 | 7 | public Params() { 8 | super(); 9 | } 10 | 11 | public Params(LinkedMultiValueMap params) { 12 | super(params); 13 | } 14 | 15 | /** 16 | * 17 | */ 18 | private static final long serialVersionUID = 1L; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/PathTree.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.LinkedList; 6 | import java.util.Set; 7 | 8 | public class PathTree { 9 | 10 | private ArrayList values = new ArrayList<>(); 11 | 12 | private LinkedHashMap> branches = new LinkedHashMap<>(); 13 | 14 | public ArrayList getValues() { 15 | return values; 16 | } 17 | 18 | public void put(LinkedList

path, T value) { 19 | if (path.isEmpty()) { 20 | values.add(value); 21 | return; 22 | } 23 | P key = path.removeFirst(); 24 | PathTree val = branches.get(key); 25 | if (val == null) { 26 | val = new PathTree<>(); 27 | branches.put(key, val); 28 | } 29 | val.put(path, value); 30 | } 31 | 32 | public Set

getKeys() { 33 | return branches.keySet(); 34 | } 35 | 36 | public PathTree get(P p) { 37 | return branches.get(p); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/RecordService.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import com.tqdev.crudapi.column.definition.DatabaseDefinitionException; 4 | import com.tqdev.crudapi.record.container.DatabaseRecords; 5 | import com.tqdev.crudapi.record.container.DatabaseRecordsException; 6 | import com.tqdev.crudapi.record.container.Record; 7 | import com.tqdev.crudapi.record.document.ListDocument; 8 | 9 | import java.io.IOException; 10 | 11 | public interface RecordService { 12 | 13 | // crud 14 | 15 | boolean exists(String table); 16 | 17 | String create(String table, Record record, Params params); 18 | 19 | Record read(String table, String id, Params params); 20 | 21 | int update(String table, String id, Record record, Params params); 22 | 23 | int increment(String table, String id, Record record, Params params); 24 | 25 | int delete(String table, String id, Params params); 26 | 27 | ListDocument list(String table, Params params); 28 | 29 | // meta 30 | 31 | void update(); 32 | 33 | DatabaseRecords getDatabaseRecords(); 34 | 35 | // initialization 36 | 37 | void initialize(String recordsFilename) throws IOException, 38 | DatabaseDefinitionException, DatabaseRecordsException; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/RelationIncluder.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | import org.jooq.Condition; 10 | import org.jooq.DSLContext; 11 | import org.jooq.Field; 12 | import org.jooq.ResultQuery; 13 | import org.jooq.impl.DSL; 14 | 15 | import com.tqdev.crudapi.column.reflection.DatabaseReflection; 16 | import com.tqdev.crudapi.column.reflection.ReflectedTable; 17 | import com.tqdev.crudapi.record.container.Record; 18 | 19 | public class RelationIncluder { 20 | 21 | private ColumnSelector columns; 22 | 23 | public RelationIncluder(ColumnSelector columns) { 24 | this.columns = columns; 25 | } 26 | 27 | public void addMandatoryColumns(ReflectedTable table, DatabaseReflection reflection, Params params) { 28 | if (!params.containsKey("include") || !params.containsKey("columns")) { 29 | return; 30 | } 31 | for (String tableNames : params.get("include")) { 32 | ReflectedTable t1 = table; 33 | for (String tableName : tableNames.split(",")) { 34 | ReflectedTable t2 = reflection.getTable(tableName); 35 | if (t2 == null) { 36 | continue; 37 | } 38 | List> fks1 = t1.getFksTo(t2.getName()); 39 | ReflectedTable t3 = hasAndBelongsToMany(t1, t2, reflection); 40 | if (t3 != null || !fks1.isEmpty()) { 41 | params.add("mandatory", t2.getName() + "." + t2.getPk().getName()); 42 | } 43 | for (Field fk : fks1) { 44 | params.add("mandatory", t1.getName() + "." + fk.getName()); 45 | } 46 | List> fks2 = t2.getFksTo(t1.getName()); 47 | if (t3 != null || !fks2.isEmpty()) { 48 | params.add("mandatory", t1.getName() + "." + t1.getPk().getName()); 49 | } 50 | for (Field fk : fks2) { 51 | params.add("mandatory", t2.getName() + "." + fk.getName()); 52 | } 53 | t1 = t2; 54 | } 55 | } 56 | } 57 | 58 | private PathTree getIncludesAsPathTree(DatabaseReflection reflection, Params params) { 59 | PathTree includes = new PathTree<>(); 60 | if (params.containsKey("include")) { 61 | for (String includedTableNames : params.get("include")) { 62 | LinkedList path = new LinkedList<>(); 63 | for (String includedTableName : includedTableNames.split(",")) { 64 | ReflectedTable t = reflection.getTable(includedTableName); 65 | if (t != null) { 66 | path.add(t.getName()); 67 | } 68 | } 69 | includes.put(path, true); 70 | } 71 | } 72 | return includes; 73 | } 74 | 75 | public void addIncludes(String tableName, ArrayList records, DatabaseReflection reflection, Params params, 76 | DSLContext dsl) { 77 | 78 | PathTree includes = getIncludesAsPathTree(reflection, params); 79 | addIncludesForTables(reflection.getTable(tableName), includes, records, reflection, params, dsl); 80 | } 81 | 82 | private ReflectedTable hasAndBelongsToMany(ReflectedTable t1, ReflectedTable t2, DatabaseReflection reflection) { 83 | for (String tableName : reflection.getTableNames()) { 84 | ReflectedTable t3 = reflection.getTable(tableName); 85 | if (!t3.getFksTo(t1.getName()).isEmpty() && !t3.getFksTo(t2.getName()).isEmpty()) { 86 | return t3; 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | private void addIncludesForTables(ReflectedTable t1, PathTree includes, ArrayList records, 93 | DatabaseReflection reflection, Params params, DSLContext dsl) { 94 | for (String t2Name : includes.getKeys()) { 95 | 96 | ReflectedTable t2 = reflection.getTable(t2Name); 97 | 98 | boolean belongsTo = !t1.getFksTo(t2.getName()).isEmpty(); 99 | boolean hasMany = !t2.getFksTo(t1.getName()).isEmpty(); 100 | ReflectedTable t3 = hasAndBelongsToMany(t1, t2, reflection); 101 | boolean hasAndBelongsToMany = t3 != null; 102 | 103 | ArrayList newRecords = new ArrayList<>(); 104 | HashMap fkValues = null; 105 | HashMap> pkValues = null; 106 | HabtmValues habtmValues = null; 107 | 108 | if (belongsTo) { 109 | fkValues = getFkEmptyValues(t1, t2, records); 110 | addFkRecords(t2, fkValues, params, dsl, newRecords); 111 | } 112 | if (hasMany) { 113 | pkValues = getPkEmptyValues(t1, records); 114 | addPkRecords(t1, t2, pkValues, params, dsl, newRecords); 115 | } 116 | if (hasAndBelongsToMany) { 117 | habtmValues = getHabtmEmptyValues(t1, t2, t3, dsl, records); 118 | addFkRecords(t2, habtmValues.fkValues, params, dsl, newRecords); 119 | } 120 | 121 | addIncludesForTables(t2, includes.get(t2Name), newRecords, reflection, params, dsl); 122 | 123 | if (fkValues != null) { 124 | fillFkValues(t2, newRecords, fkValues); 125 | setFkValues(t1, t2, records, fkValues); 126 | } 127 | if (pkValues != null) { 128 | fillPkValues(t1, t2, newRecords, pkValues); 129 | setPkValues(t1, t2, records, pkValues); 130 | } 131 | if (habtmValues != null) { 132 | fillFkValues(t2, newRecords, habtmValues.fkValues); 133 | setHabtmValues(t1, t3, records, habtmValues); 134 | } 135 | } 136 | } 137 | 138 | private HashMap getFkEmptyValues(ReflectedTable t1, ReflectedTable t2, ArrayList records) { 139 | HashMap fkValues = new HashMap<>(); 140 | List> fks = t1.getFksTo(t2.getName()); 141 | for (Field fk : fks) { 142 | for (Record record : records) { 143 | Object fkValue = record.get(fk.getName()); 144 | if (fkValue == null) { 145 | continue; 146 | } 147 | fkValues.put(fkValue, null); 148 | } 149 | } 150 | return fkValues; 151 | } 152 | 153 | private void addFkRecords(ReflectedTable t2, HashMap fkValues, Params params, DSLContext dsl, 154 | ArrayList records) { 155 | Field pk = t2.getPk(); 156 | ArrayList> fields = columns.getNames(t2, false, params); 157 | ResultQuery query = dsl.select(fields).from(t2).where(pk.in(fkValues.keySet())); 158 | for (org.jooq.Record record : query.fetch()) { 159 | records.add(Record.valueOf(record.intoMap())); 160 | } 161 | } 162 | 163 | private void fillFkValues(ReflectedTable t2, ArrayList fkRecords, HashMap fkValues) { 164 | Field pk = t2.getPk(); 165 | for (Record fkRecord : fkRecords) { 166 | Object pkValue = fkRecord.get(pk.getName()); 167 | fkValues.put(pkValue, fkRecord); 168 | } 169 | } 170 | 171 | private void setFkValues(ReflectedTable t1, ReflectedTable t2, ArrayList records, 172 | HashMap fkValues) { 173 | List> fks = t1.getFksTo(t2.getName()); 174 | for (Field fk : fks) { 175 | for (Record record : records) { 176 | Object key = record.get(fk.getName()); 177 | if (key == null) { 178 | continue; 179 | } 180 | record.put(fk.getName(), fkValues.get(key)); 181 | } 182 | } 183 | } 184 | 185 | private HashMap> getPkEmptyValues(ReflectedTable t1, ArrayList records) { 186 | HashMap> pkValues = new HashMap<>(); 187 | for (Record record : records) { 188 | Object key = record.get(t1.getPk().getName()); 189 | pkValues.put(key, new ArrayList<>()); 190 | } 191 | return pkValues; 192 | } 193 | 194 | private void addPkRecords(ReflectedTable t1, ReflectedTable t2, HashMap> pkValues, 195 | Params params, DSLContext dsl, ArrayList records) { 196 | List> fks = t2.getFksTo(t1.getName()); 197 | ArrayList> fields = columns.getNames(t2, false, params); 198 | Condition condition = DSL.falseCondition(); 199 | for (Field fk : fks) { 200 | condition = condition.or(fk.in(pkValues.keySet())); 201 | } 202 | ResultQuery query = dsl.select(fields).from(t2).where(condition); 203 | for (org.jooq.Record record : query.fetch()) { 204 | records.add(Record.valueOf(record.intoMap())); 205 | } 206 | } 207 | 208 | private void fillPkValues(ReflectedTable t1, ReflectedTable t2, ArrayList pkRecords, 209 | HashMap> pkValues) { 210 | List> fks = t2.getFksTo(t1.getName()); 211 | for (Field fk : fks) { 212 | for (Record pkRecord : pkRecords) { 213 | Object key = pkRecord.get(fk.getName()); 214 | ArrayList records = pkValues.get(key); 215 | if (records != null) { 216 | records.add(pkRecord); 217 | } 218 | } 219 | } 220 | } 221 | 222 | private void setPkValues(ReflectedTable t1, ReflectedTable t2, ArrayList records, 223 | HashMap> pkValues) { 224 | for (Record record : records) { 225 | Object key = record.get(t1.getPk().getName()); 226 | record.put(t2.getName(), pkValues.get(key)); 227 | } 228 | } 229 | 230 | private HabtmValues getHabtmEmptyValues(ReflectedTable t1, ReflectedTable t2, ReflectedTable t3, DSLContext dsl, 231 | ArrayList records) { 232 | HashMap> pkValues = getPkEmptyValues(t1, records); 233 | HashMap fkValues = new HashMap<>(); 234 | 235 | Field fk1 = t3.getFksTo(t1.getName()).get(0); 236 | Field fk2 = t3.getFksTo(t2.getName()).get(0); 237 | List> fields = Arrays.asList(fk1, fk2); 238 | Condition condition = fk1.in(pkValues.keySet()); 239 | ResultQuery query = dsl.select(fields).from(t3).where(condition); 240 | for (org.jooq.Record record : query.fetch()) { 241 | Object val1 = record.get(fk1); 242 | Object val2 = record.get(fk2); 243 | pkValues.get(val1).add(val2); 244 | fkValues.put(val2, null); 245 | } 246 | 247 | return new HabtmValues(pkValues, fkValues); 248 | } 249 | 250 | private void setHabtmValues(ReflectedTable t1, ReflectedTable t3, ArrayList records, 251 | HabtmValues habtmValues) { 252 | for (Record record : records) { 253 | Object key = record.get(t1.getPk().getName()); 254 | ArrayList val = new ArrayList<>(); 255 | ArrayList fks = habtmValues.pkValues.get(key); 256 | for (Object fk : fks) { 257 | val.add(habtmValues.fkValues.get(fk)); 258 | } 259 | record.put(t3.getName(), val); 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/container/DatabaseRecords.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.container; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.LinkedHashMap; 8 | 9 | import org.springframework.core.io.ClassPathResource; 10 | 11 | import com.fasterxml.jackson.core.JsonParseException; 12 | import com.fasterxml.jackson.databind.JsonMappingException; 13 | import com.fasterxml.jackson.databind.ObjectMapper; 14 | import com.tqdev.crudapi.record.RecordService; 15 | 16 | public class DatabaseRecords { 17 | 18 | private LinkedHashMap tables = new LinkedHashMap<>(); 19 | 20 | public Collection getTables() { 21 | return tables.values(); 22 | } 23 | 24 | public void setTables(Collection tables) { 25 | this.tables = new LinkedHashMap<>(); 26 | for (TableRecords table : tables) { 27 | this.tables.put(table.getName(), table); 28 | } 29 | } 30 | 31 | public static DatabaseRecords fromFile(String filename) 32 | throws JsonParseException, JsonMappingException, IOException { 33 | ObjectMapper mapper = new ObjectMapper(); 34 | ClassPathResource resource = new ClassPathResource(filename); 35 | DatabaseRecords result; 36 | try { 37 | result = mapper.readValue(resource.getInputStream(), DatabaseRecords.class); 38 | } catch (FileNotFoundException e) { 39 | result = new DatabaseRecords(); 40 | } 41 | return result; 42 | } 43 | 44 | public void create(RecordService service) throws DatabaseRecordsException { 45 | for (String table : tables.keySet()) { 46 | tables.get(table).create(service); 47 | } 48 | } 49 | 50 | public void put(String table, ArrayList records) { 51 | tables.put(table, new TableRecords(table, records)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/container/DatabaseRecordsException.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.container; 2 | 3 | public class DatabaseRecordsException extends Exception { 4 | 5 | /** 6 | * 7 | */ 8 | private static final long serialVersionUID = 1L; 9 | 10 | public DatabaseRecordsException(String format) { 11 | super(format); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/container/Record.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.container; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | public class Record extends LinkedHashMap { 7 | 8 | public static Record valueOf(Object object) { 9 | if (object instanceof Map) { 10 | return Record.valueOf((Map) object); 11 | } 12 | return null; 13 | } 14 | 15 | public static Record valueOf(Map map) { 16 | if (map != null) { 17 | Record result = new Record(); 18 | for (Object key : map.keySet()) { 19 | result.put(key.toString(), map.get(key)); 20 | } 21 | return result; 22 | } 23 | return null; 24 | } 25 | 26 | /** 27 | * 28 | */ 29 | private static final long serialVersionUID = 1L; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/container/TableRecords.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.container; 2 | 3 | import java.util.ArrayList; 4 | 5 | import com.tqdev.crudapi.record.RecordService; 6 | import com.tqdev.crudapi.record.Params; 7 | 8 | public class TableRecords { 9 | 10 | private String name = null; 11 | private ArrayList records = new ArrayList<>(); 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public void setName(String name) { 18 | this.name = name; 19 | } 20 | 21 | public ArrayList getRecords() { 22 | return records; 23 | } 24 | 25 | public void setRecords(ArrayList records) { 26 | this.records = records; 27 | } 28 | 29 | public TableRecords() { 30 | // nothing 31 | } 32 | 33 | public TableRecords(String name, ArrayList records) { 34 | this.name = name; 35 | this.records = records; 36 | } 37 | 38 | public void create(RecordService service) throws DatabaseRecordsException { 39 | for (Record record : records) { 40 | if (!service.exists(name)) { 41 | throw new DatabaseRecordsException( 42 | String.format("Cannot insert into table '%s': Table does not exist", name)); 43 | } 44 | service.create(name, record, new Params()); 45 | } 46 | } 47 | 48 | public void put(ArrayList records) { 49 | this.records = records; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/document/ErrorDocument.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.document; 2 | 3 | import com.tqdev.crudapi.record.ErrorCode; 4 | 5 | public class ErrorDocument { 6 | 7 | private final int code; 8 | 9 | private final String message; 10 | 11 | public ErrorDocument(ErrorCode error, String argument) { 12 | this.code = error.value(); 13 | this.message = error.getMessage(argument); 14 | } 15 | 16 | /** 17 | * Return the code of this error. 18 | */ 19 | public int getCode() { 20 | return code; 21 | } 22 | 23 | /** 24 | * Return the message of this error. 25 | */ 26 | public String getMessage() { 27 | return message; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/document/ListDocument.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.document; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 5 | import com.tqdev.crudapi.record.container.Record; 6 | 7 | public class ListDocument { 8 | 9 | private Record[] records; 10 | 11 | @JsonInclude(Include.NON_DEFAULT) 12 | private int results; 13 | 14 | public ListDocument(Record[] records, int results) { 15 | this.records = records; 16 | this.results = results; 17 | } 18 | 19 | public Record[] getRecords() { 20 | return records; 21 | } 22 | 23 | public int getResults() { 24 | return results; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/AsText.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomField; 8 | import org.jooq.impl.DSL; 9 | import org.jooq.impl.SQLDataType; 10 | 11 | class AsText extends CustomField { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = 1L; 17 | 18 | final Field field; 19 | 20 | AsText(Field field) { 21 | super("st_astext", SQLDataType.VARCHAR); 22 | this.field = field; 23 | } 24 | 25 | @Override 26 | public void accept(Context context) { 27 | context.visit(delegate(context.configuration())); 28 | } 29 | 30 | private QueryPart delegate(Configuration configuration) { 31 | switch (configuration.dialect().family().toString()) { 32 | case "MYSQL": 33 | case "POSTGRES": 34 | return DSL.field("ST_AsText({0})", String.class, field); 35 | case "SQLSERVER": 36 | return DSL.field("{0}.STAsText(0)", String.class, field); 37 | default: 38 | throw new UnsupportedOperationException("Dialect not supported"); 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Contains.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Contains extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Contains(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Contains({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STContains({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Crosses.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Crosses extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Crosses(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Crosses({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STCrosses({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Disjoint.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Disjoint extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Disjoint(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Disjoint({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STDisjoint({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Equals.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Equals extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Equals(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Equals({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STEquals({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/GeomFromText.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomField; 8 | import org.jooq.impl.DSL; 9 | import org.jooq.impl.SQLDataType; 10 | 11 | class GeomFromText extends CustomField { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = 1L; 17 | 18 | final Field field; 19 | 20 | GeomFromText(Field field) { 21 | super("st_geomfromtext", SQLDataType.VARBINARY); 22 | this.field = field; 23 | } 24 | 25 | @Override 26 | public void accept(Context context) { 27 | context.visit(delegate(context.configuration())); 28 | } 29 | 30 | private QueryPart delegate(Configuration configuration) { 31 | switch (configuration.dialect().family().toString()) { 32 | case "MYSQL": 33 | case "POSTGRES": 34 | return DSL.field("ST_GeomFromText({0})", byte[].class, field); 35 | case "SQLSERVER": 36 | return DSL.field("{0}.STGeomFromText(0)", byte[].class, field); 37 | default: 38 | throw new UnsupportedOperationException("Dialect not supported"); 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Intersects.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Intersects extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Intersects(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Intersects({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STIntersects({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/IsClosed.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class IsClosed extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field; 18 | 19 | IsClosed(Field field) { 20 | super(); 21 | this.field = field; 22 | } 23 | 24 | @Override 25 | public void accept(Context context) { 26 | context.visit(delegate(context.configuration())); 27 | } 28 | 29 | private QueryPart delegate(Configuration configuration) { 30 | switch (configuration.dialect().family().toString()) { 31 | case "MYSQL": 32 | case "POSTGRES": 33 | return DSL.field("ST_IsClosed({0})", Boolean.class, field); 34 | case "SQLSERVER": 35 | return DSL.field("{0}.STIsClosed()", Boolean.class, field); 36 | default: 37 | throw new UnsupportedOperationException("Dialect not supported"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/IsSimple.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class IsSimple extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field; 18 | 19 | IsSimple(Field field) { 20 | super(); 21 | this.field = field; 22 | } 23 | 24 | @Override 25 | public void accept(Context context) { 26 | context.visit(delegate(context.configuration())); 27 | } 28 | 29 | private QueryPart delegate(Configuration configuration) { 30 | switch (configuration.dialect().family().toString()) { 31 | case "MYSQL": 32 | case "POSTGRES": 33 | return DSL.field("ST_IsSimple({0})", Boolean.class, field); 34 | case "SQLSERVER": 35 | return DSL.field("{0}.STIsSimple()", Boolean.class, field); 36 | default: 37 | throw new UnsupportedOperationException("Dialect not supported"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/IsValid.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class IsValid extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field; 18 | 19 | IsValid(Field field) { 20 | super(); 21 | this.field = field; 22 | } 23 | 24 | @Override 25 | public void accept(Context context) { 26 | context.visit(delegate(context.configuration())); 27 | } 28 | 29 | private QueryPart delegate(Configuration configuration) { 30 | switch (configuration.dialect().family().toString()) { 31 | case "MYSQL": 32 | case "POSTGRES": 33 | return DSL.field("ST_IsValid({0})", Boolean.class, field); 34 | case "SQLSERVER": 35 | return DSL.field("{0}.STIsValid()", Boolean.class, field); 36 | default: 37 | throw new UnsupportedOperationException("Dialect not supported"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Overlaps.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Overlaps extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Overlaps(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Overlaps({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STOverlaps({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/SpatialDSL.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Condition; 4 | import org.jooq.DSLContext; 5 | import org.jooq.Field; 6 | import org.jooq.SQLDialect; 7 | import org.jooq.impl.DefaultDataType; 8 | 9 | public class SpatialDSL { 10 | 11 | public static Field asText(Field field) { 12 | return new AsText(field); 13 | } 14 | 15 | public static Field geomFromText(Field field) { 16 | return new GeomFromText(field); 17 | } 18 | 19 | public static Condition contains(Field field1, Field field2) { 20 | return new Contains(field1, field2); 21 | } 22 | 23 | public static Condition crosses(Field field1, Field field2) { 24 | return new Crosses(field1, field2); 25 | } 26 | 27 | public static Condition disjoint(Field field1, Field field2) { 28 | return new Disjoint(field1, field2); 29 | } 30 | 31 | public static Condition equals(Field field1, Field field2) { 32 | return new Equals(field1, field2); 33 | } 34 | 35 | public static Condition intersects(Field field1, Field field2) { 36 | return new Intersects(field1, field2); 37 | } 38 | 39 | public static Condition overlaps(Field field1, Field field2) { 40 | return new Overlaps(field1, field2); 41 | } 42 | 43 | public static Condition touches(Field field1, Field field2) { 44 | return new Touches(field1, field2); 45 | } 46 | 47 | public static Condition within(Field field1, Field field2) { 48 | return new Within(field1, field2); 49 | } 50 | 51 | public static Condition isClosed(Field field) { 52 | return new IsClosed(field); 53 | } 54 | 55 | public static Condition isSimple(Field field) { 56 | return new IsSimple(field); 57 | } 58 | 59 | public static Condition isValid(Field field) { 60 | return new IsValid(field); 61 | } 62 | 63 | public static void registerDataTypes(DSLContext dsl) { 64 | SQLDialect dialect = dsl.dialect(); 65 | switch (dialect.family().toString()) { 66 | case "MYSQL": 67 | case "POSTGRES": 68 | case "SQLSERVER": 69 | DefaultDataType.getDefaultDataType(SQLDialect.DEFAULT, "geometry"); 70 | break; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Touches.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Touches extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Touches(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Touches({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STTouches({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/java/com/tqdev/crudapi/record/spatial/Within.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi.record.spatial; 2 | 3 | import org.jooq.Configuration; 4 | import org.jooq.Context; 5 | import org.jooq.Field; 6 | import org.jooq.QueryPart; 7 | import org.jooq.impl.CustomCondition; 8 | import org.jooq.impl.DSL; 9 | 10 | public class Within extends CustomCondition { 11 | 12 | /** 13 | * 14 | */ 15 | private static final long serialVersionUID = 1L; 16 | 17 | final Field field1; 18 | final Field field2; 19 | 20 | Within(Field field1, Field field2) { 21 | super(); 22 | this.field1 = field1; 23 | this.field2 = field2; 24 | } 25 | 26 | @Override 27 | public void accept(Context context) { 28 | context.visit(delegate(context.configuration())); 29 | } 30 | 31 | private QueryPart delegate(Configuration configuration) { 32 | switch (configuration.dialect().family().toString()) { 33 | case "MYSQL": 34 | case "POSTGRES": 35 | return DSL.field("ST_Within({0}, {1})", Boolean.class, field1, field2); 36 | case "SQLSERVER": 37 | return DSL.field("{0}.STWithin({1})", Boolean.class, field1, field2); 38 | default: 39 | throw new UnsupportedOperationException("Dialect not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /full/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | servlet: 4 | context-path: / 5 | 6 | rest: 7 | cors: 8 | allowed-origins: "*" 9 | 10 | #spring: 11 | # jooq: 12 | # sql-dialect: POSTGRES 13 | # datasource: 14 | # url: jdbc:postgresql://localhost/php-crud-data 15 | # username: php-crud-data 16 | # password: php-crud-data 17 | 18 | spring: 19 | jooq: 20 | sql-dialect: MYSQL 21 | datasource: 22 | url: jdbc:mysql://localhost/php-crud-api?useSSL=false 23 | username: php-crud-api 24 | password: php-crud-api 25 | 26 | #spring: 27 | # jooq: 28 | # sql-dialect: H2 29 | # datasource: 30 | # url: jdbc:h2:mem:test -------------------------------------------------------------------------------- /full/src/main/resources/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "title": "JAVA-CRUD-API", 4 | "version": "1.0.0" 5 | } 6 | } -------------------------------------------------------------------------------- /full/src/test/java/com/tqdev/crudapi/Test001Records.java: -------------------------------------------------------------------------------- 1 | package com.tqdev.crudapi; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; 13 | 14 | import java.net.URLEncoder; 15 | 16 | import org.apache.tomcat.util.codec.binary.Base64; 17 | import org.junit.Before; 18 | import org.junit.FixMethodOrder; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | import org.junit.runners.MethodSorters; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.boot.test.context.SpringBootContextLoader; 24 | import org.springframework.test.context.ContextConfiguration; 25 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 26 | import org.springframework.test.context.web.WebAppConfiguration; 27 | import org.springframework.test.web.servlet.MockMvc; 28 | import org.springframework.web.context.WebApplicationContext; 29 | 30 | @RunWith(SpringJUnit4ClassRunner.class) 31 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 32 | @WebAppConfiguration 33 | @ContextConfiguration(classes = ApiApp.class, loader = SpringBootContextLoader.class) 34 | public class Test001Records { 35 | 36 | @Autowired 37 | private WebApplicationContext wac; 38 | 39 | private MockMvc mockMvc; 40 | 41 | @Before 42 | public void setup() throws Exception { 43 | mockMvc = webAppContextSetup(this.wac).build(); 44 | } 45 | 46 | @Test 47 | public void test001ListPosts() throws Exception { 48 | mockMvc.perform(get("/records/posts")).andExpect(status().isOk()).andExpect(content().string( 49 | "{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"},{\"id\":2,\"user_id\":1,\"category_id\":2,\"content\":\"It works!\"}]}")); 50 | } 51 | 52 | @Test 53 | public void test002ListPostColumns() throws Exception { 54 | mockMvc.perform(get("/records/posts?columns=id,content")).andExpect(status().isOk()).andExpect(content().string( 55 | "{\"records\":[{\"id\":1,\"content\":\"blog started\"},{\"id\":2,\"content\":\"It works!\"}]}")); 56 | } 57 | 58 | @Test 59 | public void test003ReadPost() throws Exception { 60 | mockMvc.perform(get("/records/posts/2")).andExpect(status().isOk()) 61 | .andExpect(content().string("{\"id\":2,\"user_id\":1,\"category_id\":2,\"content\":\"It works!\"}")); 62 | } 63 | 64 | @Test 65 | public void test004ReadPosts() throws Exception { 66 | mockMvc.perform(get("/records/posts/1,2")).andExpect(status().isOk()).andExpect(content().string( 67 | "[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"},{\"id\":2,\"user_id\":1,\"category_id\":2,\"content\":\"It works!\"}]")); 68 | } 69 | 70 | @Test 71 | public void test005ReadPostColumns() throws Exception { 72 | mockMvc.perform(get("/records/posts/2?columns=id,content")).andExpect(status().isOk()) 73 | .andExpect(content().string("{\"id\":2,\"content\":\"It works!\"}")); 74 | } 75 | 76 | @Test 77 | public void test006AddPost() throws Exception { 78 | mockMvc.perform(post("/records/posts").contentType("application/json") 79 | .content("{\"user_id\": 1, \"category_id\": 1, \"content\": \"test\"}")).andExpect(status().isOk()) 80 | .andExpect(content().string("3")); 81 | } 82 | 83 | @Test 84 | public void test007EditPost() throws Exception { 85 | mockMvc.perform(put("/records/posts/3").contentType("application/json") 86 | .content("{\"user_id\":1,\"category_id\":1,\"content\":\"test (edited)\"}")).andExpect(status().isOk()) 87 | .andExpect(content().string("1")); 88 | mockMvc.perform(get("/records/posts/3")).andExpect(status().isOk()).andExpect( 89 | content().string("{\"id\":3,\"user_id\":1,\"category_id\":1,\"content\":\"test (edited)\"}")); 90 | } 91 | 92 | @Test 93 | public void test008EditPostColumnsMissingField() throws Exception { 94 | mockMvc.perform(put("/records/posts/3?columns=id,content").contentType("application/json") 95 | .content("{\"content\":\"test (edited 2)\"}")).andExpect(status().isOk()) 96 | .andExpect(content().string("1")); 97 | mockMvc.perform(get("/records/posts/3")).andExpect(status().isOk()).andExpect( 98 | content().string("{\"id\":3,\"user_id\":1,\"category_id\":1,\"content\":\"test (edited 2)\"}")); 99 | } 100 | 101 | @Test 102 | public void test009EditPostColumnsExtraField() throws Exception { 103 | mockMvc.perform(put("/records/posts/3?columns=id,content").contentType("application/json") 104 | .content("{\"user_id\":2,\"content\":\"test (edited 3)\"}")).andExpect(status().isOk()) 105 | .andExpect(content().string("1")); 106 | mockMvc.perform(get("/records/posts/3")).andExpect(status().isOk()).andExpect( 107 | content().string("{\"id\":3,\"user_id\":1,\"category_id\":1,\"content\":\"test (edited 3)\"}")); 108 | } 109 | 110 | @Test 111 | public void test010EditPostWithUtf8Content() throws Exception { 112 | String utf8 = "🤗 Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте"; 113 | mockMvc.perform(put("/records/posts/2").contentType("application/json").content("{\"content\":\"" + utf8 + "\"}")) 114 | .andExpect(status().isOk()).andExpect(content().string("1")); 115 | mockMvc.perform(get("/records/posts/2")).andExpect(status().isOk()) 116 | .andExpect(content().json("{\"id\":2,\"user_id\":1,\"category_id\":2,\"content\":\"" + utf8 + "\"}")); 117 | } 118 | 119 | @Test 120 | public void test011EditPostWithUtf8ContentWithPost() throws Exception { 121 | String utf8 = "🦀€ Grüßgott, Вiтаю, dobrý deň, hyvää päivää, გამარჯობა, Γεια σας, góðan dag, здравствуйте"; 122 | String urlenc = URLEncoder.encode(utf8, "UTF-8"); 123 | mockMvc.perform( 124 | put("/records/posts/2").contentType("application/x-www-form-urlencoded").content("content=" + urlenc)) 125 | .andExpect(status().isOk()).andExpect(content().string("1")); 126 | mockMvc.perform(get("/records/posts/2")).andExpect(status().isOk()) 127 | .andExpect(content().json("{\"id\":2,\"user_id\":1,\"category_id\":2,\"content\":\"" + utf8 + "\"}")); 128 | } 129 | 130 | @Test 131 | public void test012DeletePost() throws Exception { 132 | mockMvc.perform(delete("/records/posts/3")).andExpect(status().isOk()).andExpect(content().string("1")); 133 | mockMvc.perform(get("/records/posts/3")).andExpect(status().isNotFound()) 134 | .andExpect(content().string("{\"code\":1003,\"message\":\"Record '3' not found\"}")); 135 | } 136 | 137 | @Test 138 | public void test013AddPostWithPost() throws Exception { 139 | mockMvc.perform(post("/records/posts").contentType("application/x-www-form-urlencoded") 140 | .content("user_id=1&category_id=1&content=test")).andExpect(status().isOk()) 141 | .andExpect(content().string("4")); 142 | } 143 | 144 | @Test 145 | public void test014EditPostWithPost() throws Exception { 146 | mockMvc.perform(put("/records/posts/4").contentType("application/x-www-form-urlencoded") 147 | .content("user_id=1&category_id=1&content=test+(edited)")).andExpect(status().isOk()) 148 | .andExpect(content().string("1")); 149 | mockMvc.perform(get("/records/posts/4")).andExpect(status().isOk()).andExpect( 150 | content().string("{\"id\":4,\"user_id\":1,\"category_id\":1,\"content\":\"test (edited)\"}")); 151 | } 152 | 153 | @Test 154 | public void test015DeletePostIgnoreColumns() throws Exception { 155 | mockMvc.perform(delete("/records/posts/4?columns=id,content")).andExpect(status().isOk()) 156 | .andExpect(content().string("1")); 157 | mockMvc.perform(get("/records/posts/4")).andExpect(status().isNotFound()) 158 | .andExpect(content().string("{\"code\":1003,\"message\":\"Record '4' not found\"}")); 159 | } 160 | 161 | @Test 162 | public void test016ListWithPaginate() throws Exception { 163 | for (int i = 1; i <= 10; i++) { 164 | mockMvc.perform(post("/records/posts").contentType("application/json") 165 | .content("{\"user_id\":1,\"category_id\":1,\"content\":\"#" + i + "\"}")).andExpect(status().isOk()) 166 | .andExpect(content().string("" + (4 + i))); 167 | } 168 | mockMvc.perform(get("/records/posts?page=2,2&order=id")).andExpect(status().isOk()).andExpect(content().string( 169 | "{\"records\":[{\"id\":5,\"user_id\":1,\"category_id\":1,\"content\":\"#1\"},{\"id\":6,\"user_id\":1,\"category_id\":1,\"content\":\"#2\"}],\"results\":12}")); 170 | } 171 | 172 | @Test 173 | public void test019ListWithPaginateInMultipleOrder() throws Exception { 174 | mockMvc.perform(get("/records/posts?page=1,2&order=category_id,asc&order=id,desc").accept("application/json")) 175 | .andExpect(status().isOk()).andExpect(content().string( 176 | "{\"records\":[{\"id\":14,\"user_id\":1,\"category_id\":1,\"content\":\"#10\"},{\"id\":13,\"user_id\":1,\"category_id\":1,\"content\":\"#9\"}],\"results\":12}")); 177 | } 178 | 179 | @Test 180 | public void test020ListWithPaginateInDescendingOrder() throws Exception { 181 | mockMvc.perform(get("/records/posts?page=2,2&order=id,desc").accept("application/json")).andExpect(status().isOk()) 182 | .andExpect(content().string( 183 | "{\"records\":[{\"id\":12,\"user_id\":1,\"category_id\":1,\"content\":\"#8\"},{\"id\":11,\"user_id\":1,\"category_id\":1,\"content\":\"#7\"}],\"results\":12}")); 184 | } 185 | 186 | @Test 187 | public void test021ListWithSize() throws Exception { 188 | mockMvc.perform(get("/records/posts?order=id&size=1")).andExpect(status().isOk()).andExpect(content() 189 | .string("{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}]}")); 190 | } 191 | 192 | @Test 193 | public void test022ListWithZeroPageSize() throws Exception { 194 | mockMvc.perform(get("/records/posts?order=id&page=1,0")).andExpect(status().isOk()) 195 | .andExpect(content().string("{\"records\":[],\"results\":12}")); 196 | } 197 | 198 | @Test 199 | public void test023ListWithZeroSize() throws Exception { 200 | mockMvc.perform(get("/records/posts?order=id&size=0")).andExpect(status().isOk()) 201 | .andExpect(content().string("{\"records\":[]}")); 202 | } 203 | 204 | @Test 205 | public void test024ListWithPaginateLastPage() throws Exception { 206 | mockMvc.perform(get("/records/posts?page=3,5&order=id")).andExpect(status().isOk()).andExpect(content().string( 207 | "{\"records\":[{\"id\":13,\"user_id\":1,\"category_id\":1,\"content\":\"#9\"},{\"id\":14,\"user_id\":1,\"category_id\":1,\"content\":\"#10\"}],\"results\":12}")); 208 | } 209 | 210 | @Test 211 | public void test025ListExampleFromReadmeFullRecord() throws Exception { 212 | mockMvc.perform(get("/records/posts?filter=id,eq,1")).andExpect(status().isOk()).andExpect(content() 213 | .string("{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}]}")); 214 | } 215 | 216 | @Test 217 | public void test026ListExampleFromReadmeWithExclude() throws Exception { 218 | mockMvc.perform(get("/records/posts?exclude=id&filter=id,eq,1")).andExpect(status().isOk()).andExpect( 219 | content().string("{\"records\":[{\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}]}")); 220 | } 221 | 222 | @Test 223 | public void test027ListExampleFromReadmeUsersOnly() throws Exception { 224 | mockMvc.perform(get("/records/posts?include=users&filter=id,eq,1")).andExpect(status().isOk()).andExpect(content() 225 | .string("{\"records\":[{\"id\":1,\"user_id\":{\"id\":1,\"username\":\"user1\",\"password\":\"pass1\",\"location\":null},\"category_id\":1,\"content\":\"blog started\"}]}")); 226 | } 227 | 228 | @Test 229 | public void test028ReadExampleFromReadmeUsersOnly() throws Exception { 230 | mockMvc.perform(get("/records/posts/1?include=users")).andExpect(status().isOk()).andExpect(content().string( 231 | "{\"id\":1,\"user_id\":{\"id\":1,\"username\":\"user1\",\"password\":\"pass1\",\"location\":null},\"category_id\":1,\"content\":\"blog started\"}")); 232 | } 233 | 234 | @Test 235 | public void test029ListExampleFromReadmeCommentsOnly() throws Exception { 236 | mockMvc.perform(get("/records/posts?include=comments&filter=id,eq,1")).andExpect(status().isOk()) 237 | .andExpect(content().string( 238 | "{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\",\"comments\":[{\"id\":1,\"post_id\":1,\"message\":\"great\"},{\"id\":2,\"post_id\":1,\"message\":\"fantastic\"}]}]}")); 239 | } 240 | 241 | @Test 242 | public void test030ListExampleFromReadmeTagsOnly() throws Exception { 243 | mockMvc.perform(get("/records/posts?include=tags&filter=id,eq,1")).andExpect(status().isOk()).andExpect(content() 244 | .string("{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\",\"post_tags\":[{\"id\":1,\"name\":\"funny\",\"is_important\":false},{\"id\":2,\"name\":\"important\",\"is_important\":true}]}]}")); 245 | } 246 | 247 | @Test 248 | public void test031ListExampleFromReadmeTagsWithIncludePath() throws Exception { 249 | mockMvc.perform(get("/records/posts?include=categories&include=post_tags,tags&include=comments&filter=id,eq,1")) 250 | .andExpect(status().isOk()).andExpect(content().string( 251 | "{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":{\"id\":1,\"name\":\"announcement\",\"icon\":null},\"content\":\"blog started\",\"post_tags\":[{\"id\":1,\"post_id\":1,\"tag_id\":{\"id\":1,\"name\":\"funny\",\"is_important\":false}},{\"id\":2,\"post_id\":1,\"tag_id\":{\"id\":2,\"name\":\"important\",\"is_important\":true}}],\"comments\":[{\"id\":1,\"post_id\":1,\"message\":\"great\"},{\"id\":2,\"post_id\":1,\"message\":\"fantastic\"}]}]}")); 252 | } 253 | 254 | @Test 255 | public void test032ListExampleFromReadme() throws Exception { 256 | mockMvc.perform(get("/records/posts?include=categories&include=tags&include=comments&filter=id,eq,1")) 257 | .andExpect(status().isOk()).andExpect(content().string( 258 | "{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":{\"id\":1,\"name\":\"announcement\",\"icon\":null},\"content\":\"blog started\",\"post_tags\":[{\"id\":1,\"name\":\"funny\",\"is_important\":false},{\"id\":2,\"name\":\"important\",\"is_important\":true}],\"comments\":[{\"id\":1,\"post_id\":1,\"message\":\"great\"},{\"id\":2,\"post_id\":1,\"message\":\"fantastic\"}]}]}")); 259 | } 260 | 261 | @Test 262 | public void test033ListExampleFromReadmeTagNameOnly() throws Exception { 263 | mockMvc.perform( 264 | get("/records/posts?columns=tags.name&include=categories&include=post_tags,tags&include=comments&filter=id,eq,1")) 265 | .andExpect(status().isOk()).andExpect(content().string( 266 | "{\"records\":[{\"id\":1,\"category_id\":{\"id\":1},\"post_tags\":[{\"post_id\":1,\"tag_id\":{\"id\":1,\"name\":\"funny\"}},{\"post_id\":1,\"tag_id\":{\"id\":2,\"name\":\"important\"}}],\"comments\":[{\"post_id\":1},{\"post_id\":1}]}]}")); 267 | } 268 | 269 | @Test 270 | public void test034ListExampleFromReadmeWithTransformWithExclude() throws Exception { 271 | mockMvc.perform( 272 | get("/records/posts?include=categories&include=post_tags,tags&include=comments&exclude=comments.message&filter=id,eq,1")) 273 | .andExpect(status().isOk()).andExpect(content().string( 274 | "{\"records\":[{\"id\":1,\"user_id\":1,\"category_id\":{\"id\":1,\"name\":\"announcement\",\"icon\":null},\"content\":\"blog started\",\"post_tags\":[{\"id\":1,\"post_id\":1,\"tag_id\":{\"id\":1,\"name\":\"funny\",\"is_important\":false}},{\"id\":2,\"post_id\":1,\"tag_id\":{\"id\":2,\"name\":\"important\",\"is_important\":true}}],\"comments\":[{\"id\":1,\"post_id\":1},{\"id\":2,\"post_id\":1}]}]}")); 275 | } 276 | 277 | @Test 278 | public void test035EditCategoryWithBinaryContent() throws Exception { 279 | String string = "€ \000abc\000\n\r\\b\000"; 280 | String binary = Base64.encodeBase64String(string.getBytes("UTF-8")); 281 | String b64url = Base64.encodeBase64URLSafeString(string.getBytes("UTF-8")); 282 | mockMvc.perform( 283 | put("/records/categories/2").contentType("application/json").content("{\"icon\":\"" + b64url + "\"}")) 284 | .andExpect(status().isOk()).andExpect(content().string("1")); 285 | mockMvc.perform(get("/records/categories/2")).andExpect(status().isOk()) 286 | .andExpect(content().string("{\"id\":2,\"name\":\"article\",\"icon\":\"" + binary + "\"}")); 287 | } 288 | 289 | @Test 290 | public void test036EditCategoryWithNull() throws Exception { 291 | 292 | mockMvc.perform(put("/records/categories/2").contentType("application/json").content("{\"icon\":null}")) 293 | .andExpect(status().isOk()).andExpect(content().string("1")); 294 | mockMvc.perform(get("/records/categories/2")).andExpect(status().isOk()) 295 | .andExpect(content().string("{\"id\":2,\"name\":\"article\",\"icon\":null}")); 296 | } 297 | 298 | @Test 299 | public void test037EditCategoryWithBinaryContentWithPost() throws Exception { 300 | String string = "€ \000abc\000\n\r\\b\000"; 301 | String binary = Base64.encodeBase64String(string.getBytes("UTF-8")); 302 | String b64url = Base64.encodeBase64URLSafeString(string.getBytes("UTF-8")); 303 | mockMvc.perform( 304 | put("/records/categories/2").contentType("application/x-www-form-urlencoded").content("icon=" + b64url)) 305 | .andExpect(status().isOk()).andExpect(content().string("1")); 306 | mockMvc.perform(get("/records/categories/2")).andExpect(status().isOk()) 307 | .andExpect(content().string("{\"id\":2,\"name\":\"article\",\"icon\":\"" + binary + "\"}")); 308 | } 309 | 310 | @Test 311 | public void test038ListCategoriesWithBinaryContent() throws Exception { 312 | mockMvc.perform(get("/records/categories")).andExpect(status().isOk()).andExpect(content().string( 313 | "{\"records\":[{\"id\":1,\"name\":\"announcement\",\"icon\":null},{\"id\":2,\"name\":\"article\",\"icon\":\"4oKsIABhYmMACg1cYgA=\"}]}")); 314 | } 315 | 316 | @Test 317 | public void test039EditCategoryWithNullWithPost() throws Exception { 318 | mockMvc.perform( 319 | put("/records/categories/2").contentType("application/x-www-form-urlencoded").content("icon__is_null")) 320 | .andExpect(status().isOk()).andExpect(content().string("1")); 321 | mockMvc.perform(get("/records/categories/2")).andExpect(status().isOk()) 322 | .andExpect(content().string("{\"id\":2,\"name\":\"article\",\"icon\":null}")); 323 | } 324 | 325 | @Test 326 | public void test040AddPostFailure() throws Exception { 327 | mockMvc.perform(post("/records/posts").contentType("application/json").content("[\"truncat")).andExpect(status().isNotAcceptable()) 328 | .andExpect(content().string("{\"code\":1008,\"message\":\"Cannot read HTTP message\"}")); 329 | } 330 | 331 | @Test 332 | public void test041CorsPreFlight() throws Exception { 333 | mockMvc.perform(options("/records/posts/1?columns=id").header("Origin", "http://example.com") 334 | .header("Access-Control-Request-Method", "POST") 335 | .header("Access-Control-Request-Headers", "X-XSRF-TOKEN, X-Requested-With")).andExpect(status().isOk()) 336 | .andExpect(header().string("Access-Control-Allow-Origin", "http://example.com")) 337 | .andExpect(header().string("Access-Control-Allow-Headers", "X-XSRF-TOKEN")) 338 | .andExpect(header().string("Access-Control-Allow-Methods", "OPTIONS,GET,PUT,POST,DELETE,PATCH")) 339 | .andExpect(header().string("Access-Control-Allow-Credentials", "true")) 340 | .andExpect(header().string("Access-Control-Max-Age", "1728000")); 341 | } 342 | 343 | @Test 344 | public void test042CorsHeaders() throws Exception { 345 | mockMvc.perform(get("/records/posts/1?columns=id").header("Origin", "http://example.com")).andExpect(status().isOk()) 346 | .andExpect(header().string("Access-Control-Allow-Origin", "http://example.com")) 347 | .andExpect(header().string("Access-Control-Allow-Credentials", "true")); 348 | } 349 | 350 | @Test 351 | public void test043ErrorOnInvalidJson() throws Exception { 352 | mockMvc.perform(post("/records/posts").contentType("application/json").content("{\"}")) 353 | .andExpect(status().isNotAcceptable()) 354 | .andExpect(content().string("{\"code\":1008,\"message\":\"Cannot read HTTP message\"}")); 355 | } 356 | 357 | @Test 358 | public void test044ErrorOnDuplicatePrimaryKey() throws Exception { 359 | mockMvc.perform(post("/records/posts").contentType("application/json") 360 | .content("{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started (duplicate)\"}")) 361 | .andExpect(status().isNotAcceptable()) 362 | .andExpect(content().string("{\"code\":1009,\"message\":\"Duplicate key exception\"}")); 363 | } 364 | 365 | @Test 366 | public void test045ErrorOnFailingForeignKeyConstraint() throws Exception { 367 | mockMvc.perform(post("/records/posts").contentType("application/json") 368 | .content("{\"user_id\":3,\"category_id\":1,\"content\":\"fk constraint\"}")) 369 | .andExpect(status().isNotAcceptable()) 370 | .andExpect(content().string("{\"code\":1010,\"message\":\"Data integrity violation\"}")); 371 | } 372 | 373 | @Test 374 | public void test046ErrorOnNonExistingTable() throws Exception { 375 | mockMvc.perform(get("/records/postzzz")).andExpect(status().isNotFound()) 376 | .andExpect(content().string("{\"code\":1001,\"message\":\"Table 'postzzz' not found\"}")); 377 | } 378 | 379 | @Test 380 | public void test047ErrorOnInvalidPath() throws Exception { 381 | mockMvc.perform(get("/postzzz")).andExpect(status().isNotFound()) 382 | .andExpect(content().string("{\"code\":1000,\"message\":\"Route '/postzzz' not found\"}")); 383 | } 384 | 385 | @Test 386 | public void test048ErrorOnInvalidArgumentCount() throws Exception { 387 | mockMvc.perform(put("/records/posts/1,2").contentType("application/json") 388 | .content("{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}")) 389 | .andExpect(status().isNotAcceptable()) 390 | .andExpect(content().string("{\"code\":1002,\"message\":\"Argument count mismatch in '1,2'\"}")); 391 | } 392 | 393 | @Test 394 | public void test049ErrorOnInvalidArgumentCount() throws Exception { 395 | mockMvc.perform(put("/records/posts/1,2").contentType("application/json") 396 | .content("[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}]")) 397 | .andExpect(status().isNotAcceptable()) 398 | .andExpect(content().string("{\"code\":1002,\"message\":\"Argument count mismatch in '1,2'\"}")); 399 | } 400 | 401 | @Test 402 | public void test050NoErrorOnArgumentCountOne() throws Exception { 403 | mockMvc.perform(put("/records/posts/1").contentType("application/json") 404 | .content("[{\"id\":1,\"user_id\":1,\"category_id\":1,\"content\":\"blog started\"}]")) 405 | .andExpect(status().isOk()).andExpect(content().string("[1]")); 406 | } 407 | 408 | @Test 409 | public void test051ErrorOnInvalidArgumentCount() throws Exception { 410 | mockMvc.perform(put("/records/posts/1").contentType("application/json").content("[{\"id\":1},{\"id\":2}]")) 411 | .andExpect(status().isNotAcceptable()) 412 | .andExpect(content().string("{\"code\":1002,\"message\":\"Argument count mismatch in '1'\"}")); 413 | } 414 | 415 | @Test 416 | public void test052EditUserLocation() throws Exception { 417 | mockMvc.perform(put("/records/users/1").contentType("application/json").content("{\"location\":\"POINT(30 20)\"}")) 418 | .andExpect(status().isOk()).andExpect(content().string("1")); 419 | mockMvc.perform(get("/records/users/1?columns=id,location")).andExpect(status().isOk()) 420 | .andExpect(content().string("{\"id\":1,\"location\":\"POINT(30 20)\"}")); 421 | } 422 | 423 | @Test 424 | public void test053ListUserLocations() throws Exception { 425 | mockMvc.perform(get("/records/users?columns=id,location")).andExpect(status().isOk()).andExpect(content() 426 | .string("{\"records\":[{\"id\":1,\"location\":\"POINT(30 20)\"},{\"id\":2,\"location\":null}]}")); 427 | } 428 | 429 | @Test 430 | public void test054EditUserWithId() throws Exception { 431 | mockMvc.perform( 432 | put("/records/users/1").contentType("application/json").content("{\"id\":2,\"password\":\"testtest2\"}")) 433 | .andExpect(status().isOk()).andExpect(content().string("1")); 434 | mockMvc.perform(get("/records/users/1?columns=id,username,password")).andExpect(status().isOk()) 435 | .andExpect(content().string("{\"id\":1,\"username\":\"user1\",\"password\":\"testtest2\"}")); 436 | } 437 | 438 | @Test 439 | public void test055FilterCategoryOnNullIcon() throws Exception { 440 | mockMvc.perform(get("/records/categories?filter=icon,is,null")).andExpect(status().isOk()).andExpect(content() 441 | .string("{\"records\":[{\"id\":1,\"name\":\"announcement\",\"icon\":null},{\"id\":2,\"name\":\"article\",\"icon\":null}]}")); 442 | } 443 | 444 | @Test 445 | public void test056FilterCategoryOnNotNullIcon() throws Exception { 446 | mockMvc.perform(get("/records/categories?filter=icon,nis,null")).andExpect(status().isOk()) 447 | .andExpect(content().string("{\"records\":[]}")); 448 | } 449 | 450 | @Test 451 | public void test057FilterOnAnd() throws Exception { 452 | mockMvc.perform(get("/records/posts?columns=id&filter=id,ge,1&filter=id,le,2")).andExpect(status().isOk()) 453 | .andExpect(content().string("{\"records\":[{\"id\":1},{\"id\":2}]}")); 454 | } 455 | 456 | @Test 457 | public void test058FilterOnOr() throws Exception { 458 | mockMvc.perform(get("/records/posts?columns=id&filter1=id,eq,1&filter2=id,eq,2")).andExpect(status().isOk()) 459 | .andExpect(content().string("{\"records\":[{\"id\":1},{\"id\":2}]}")); 460 | } 461 | 462 | @Test 463 | public void test059FilterOnAndPlusOr() throws Exception { 464 | mockMvc.perform(get("/records/posts?columns=id&filter1=id,eq,1&filter2=id,gt,1&filter2=id,lt,3")) 465 | .andExpect(status().isOk()).andExpect(content().string("{\"records\":[{\"id\":1},{\"id\":2}]}")); 466 | } 467 | 468 | @Test 469 | public void test060FilterOnOrPlusAnd() throws Exception { 470 | mockMvc.perform(get("/records/posts?columns=id&filter1=id,eq,1&filter2=id,eq,2&filter=user_id,eq,1")) 471 | .andExpect(status().isOk()).andExpect(content().string("{\"records\":[{\"id\":1},{\"id\":2}]}")); 472 | } 473 | 474 | @Test 475 | public void test061GetPostContentWithIncludedTagNames() throws Exception { 476 | mockMvc.perform(get("/records/posts/1?columns=content,tags.name&include=tags")).andExpect(status().isOk()) 477 | .andExpect(content().string( 478 | "{\"id\":1,\"content\":\"blog started\",\"post_tags\":[{\"id\":1,\"name\":\"funny\"},{\"id\":2,\"name\":\"important\"}]}")); 479 | } 480 | 481 | @Test 482 | public void test062ReadKunsthåndværk() throws Exception { 483 | mockMvc.perform(get("/records/kunsthåndværk/e42c77c6-06a4-4502-816c-d112c7142e6d").contentType("text/html; charset=UTF-8")).andExpect(status().isOk()) 484 | .andExpect(content().string( 485 | "{\"id\":\"e42c77c6-06a4-4502-816c-d112c7142e6d\",\"Umlauts ä_ö_ü-COUNT\":1,\"user_id\":1}")); 486 | } 487 | 488 | @Test 489 | public void test063ListKunsthåndværk() throws Exception { 490 | mockMvc.perform(get("/records/kunsthåndværk").contentType("text/html; charset=UTF-8")).andExpect(status().isOk()) 491 | .andExpect(content().string( 492 | "{\"records\":[{\"id\":\"e42c77c6-06a4-4502-816c-d112c7142e6d\",\"Umlauts ä_ö_ü-COUNT\":1,\"user_id\":1}]}")); 493 | } 494 | 495 | @Test 496 | public void test064AddKunsthåndværk() throws Exception { 497 | mockMvc.perform(post("/records/kunsthåndværk").contentType("application/json") 498 | .content("{\"id\":\"34451583-a747-4417-bdf0-bec7a5eacffa\",\"Umlauts ä_ö_ü-COUNT\":3}")).andExpect(status().isOk()) 499 | .andExpect(content().string("34451583-a747-4417-bdf0-bec7a5eacffa")); 500 | } 501 | 502 | @Test 503 | public void test065EditKunsthåndværk() throws Exception { 504 | mockMvc.perform(put("/records/kunsthåndværk/34451583-a747-4417-bdf0-bec7a5eacffa").contentType("application/json") 505 | .content("{\"Umlauts ä_ö_ü-COUNT\":3}")).andExpect(status().isOk()) 506 | .andExpect(content().string("1")); 507 | } 508 | 509 | @Test 510 | public void test066DeleteKunsthåndværk() throws Exception { 511 | mockMvc.perform(delete("/records/kunsthåndværk/34451583-a747-4417-bdf0-bec7a5eacffa").contentType("text/html; charset=UTF-8")).andExpect(status().isOk()) 512 | .andExpect(content().string("1")); 513 | } 514 | 515 | @Test 516 | public void test067EditCommentWithValidation() throws Exception { 517 | mockMvc.perform(put("/records/comments/4").contentType("application/json") 518 | .content("{\"post_id\":\"two\"}")).andExpect(status().isUnprocessableEntity()) 519 | .andExpect(content().string("{\"code\":1013,\"message\":\"Input validation failed for 'comments'\",\"details\":{\"post_id\":\"must be numeric\"}}")); 520 | } 521 | 522 | @Test 523 | public void test068AddCommentWithSanitation() throws Exception { 524 | mockMvc.perform(post("/records/comments").contentType("application/json") 525 | .content("{\"user_id\":1,\"post_id\":2,\"message\":\"

Title

Body

\"}")).andExpect(status().isOk()) 526 | .andExpect(content().string("5")); 527 | mockMvc.perform(get("/records/comments/5")).andExpect(status().isOk()) 528 | .andExpect(content().string("{\"id\":5,\"post_id\":2,\"message\":\"Title Body\"}")); 529 | } 530 | 531 | @Test 532 | public void test069IncrementEventVisitors() throws Exception { 533 | mockMvc.perform(get("/records/events/1?include=visitors")).andExpect(status().isOk()) 534 | .andExpect(content().string("{\"visitors\":0}")); 535 | mockMvc.perform(patch("/records/events/1").contentType("application/json") 536 | .content("{\"visitors\":1}")).andExpect(status().isOk()) 537 | .andExpect(content().string("1")); 538 | mockMvc.perform(patch("/records/events/1").contentType("application/json") 539 | .content("{\"visitors\":1}")).andExpect(status().isOk()) 540 | .andExpect(content().string("1")); 541 | mockMvc.perform(patch("/records/events/1,1").contentType("application/json") 542 | .content("[{\"visitors\":1},{\"visitors\":1}]")).andExpect(status().isOk()) 543 | .andExpect(content().string("[1,1]")); 544 | mockMvc.perform(get("/records/events/1?include=visitors")).andExpect(status().isOk()) 545 | .andExpect(content().string("{\"visitors\":4}")); 546 | mockMvc.perform(patch("/records/events/1").contentType("application/json") 547 | .content("{\"visitors\":-4}")).andExpect(status().isOk()) 548 | .andExpect(content().string("1")); 549 | mockMvc.perform(get("/records/events/1?include=visitors")).andExpect(status().isOk()) 550 | .andExpect(content().string("{\"visitors\":0}")); 551 | } 552 | 553 | @Test 554 | public void test070ListInvisibles() throws Exception { 555 | mockMvc.perform(get("/records/invisibles").contentType("text/html; charset=UTF-8")).andExpect(status().isNotFound()) 556 | .andExpect(content().string( 557 | "{\"code\":1001,\"message\":\"Table 'invisibles' not found\"}")); 558 | } 559 | 560 | @Test 561 | public void test071AddCommentWithInvisibleRecord() throws Exception { 562 | mockMvc.perform(post("/records/comments").contentType("application/json") 563 | .content("{\"user_id\":1,\"post_id\":2,\"message\":\"invisible\"}")).andExpect(status().isOk()) 564 | .andExpect(content().string("6")); 565 | mockMvc.perform(get("/records/comments/6")).andExpect(status().isNotFound()) 566 | .andExpect(content().string("{\"code\":1003,\"message\":\"Record '6' not found\"}")); 567 | } 568 | 569 | @Test 570 | public void test072ListNoPk() throws Exception { 571 | mockMvc.perform(get("/records/nopk").contentType("text/html; charset=UTF-8")).andExpect(status().isNotFound()) 572 | .andExpect(content().string( 573 | "{\"records\":[{\"id\":\"e42c77c6-06a4-4502-816c-d112c7142e6d\"}]}")); 574 | } 575 | 576 | /*@Test 577 | public void test062MetaGetDatabase() throws Exception { 578 | mockMvc.perform(getTable("/meta")).andExpect(status().isOk()) 579 | .andExpect(content().string( 580 | "{\"records\":[{\"id\":1,\"Umlauts ä_ö_ü-COUNT\":1}]}")); 581 | } 582 | 583 | @Test 584 | public void test062MetaGetBarcodesTable() throws Exception { 585 | mockMvc.perform(getTable("/meta/barcodes")).andExpect(status().isOk()) 586 | .andExpect(content().string( 587 | "{\"records\":[{\"id\":1,\"Umlauts ä_ö_ü-COUNT\":1}]}")); 588 | } 589 | 590 | @Test 591 | public void test062MetaGetBarcodesIdColumn() throws Exception { 592 | mockMvc.perform(getTable("/meta/barcodes/id")).andExpect(status().isOk()) 593 | .andExpect(content().string( 594 | "{\"records\":[{\"id\":1,\"Umlauts ä_ö_ü-COUNT\":1}]}")); 595 | }*/ 596 | } -------------------------------------------------------------------------------- /full/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | contextPath: / 4 | 5 | spring: 6 | jooq: 7 | sql-dialect: H2 8 | datasource: 9 | url: jdbc:h2:mem:test 10 | -------------------------------------------------------------------------------- /full/src/test/resources/columns-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "The Simple Data Model Schema", 3 | "type": "object", 4 | "properties": { 5 | "tables": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "columns": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "properties": { 18 | "name": { 19 | "type": "string" 20 | }, 21 | "type": { 22 | "type": "string", 23 | "enum": [ 24 | "varchar", 25 | "char", 26 | "longvarchar", 27 | "bit", 28 | "numeric", 29 | "tinyint", 30 | "smallint", 31 | "integer", 32 | "bigint", 33 | "real", 34 | "float", 35 | "double", 36 | "varbinary", 37 | "binary", 38 | "date", 39 | "time", 40 | "timestamp", 41 | "clob", 42 | "blob", 43 | "array", 44 | "ref", 45 | "struct", 46 | "geometry" 47 | ] 48 | }, 49 | "length": { 50 | "type": "integer" 51 | }, 52 | "precision": { 53 | "type": "integer" 54 | }, 55 | "scale": { 56 | "type": "integer" 57 | }, 58 | "nullable": { 59 | "type": "boolean" 60 | }, 61 | "pk": { 62 | "type": "boolean" 63 | }, 64 | "fk": { 65 | "type": "string" 66 | } 67 | }, 68 | "required": ["name","type"], 69 | "additionalProperties":false 70 | } 71 | } 72 | }, 73 | "required": ["name","columns"], 74 | "additionalProperties":false 75 | } 76 | } 77 | }, 78 | "required": ["name","tables"], 79 | "additionalProperties":false 80 | } -------------------------------------------------------------------------------- /full/src/test/resources/columns.json: -------------------------------------------------------------------------------- 1 | { 2 | "tables":[ 3 | { 4 | "name":"comments", 5 | "columns":[ 6 | { 7 | "name":"id", 8 | "pk":true, 9 | "type":"integer" 10 | }, 11 | { 12 | "name":"post_id", 13 | "type":"integer", 14 | "fk":"posts" 15 | }, 16 | { 17 | "name":"message", 18 | "type":"varchar", 19 | "length":255 20 | } 21 | ] 22 | }, 23 | { 24 | "name":"tag_usage", 25 | "columns":[ 26 | { 27 | "name":"name", 28 | "type":"varchar", 29 | "length":255 30 | }, 31 | { 32 | "name":"count", 33 | "type":"bigint" 34 | } 35 | ] 36 | }, 37 | { 38 | "name":"post_tags", 39 | "columns":[ 40 | { 41 | "name":"id", 42 | "pk":true, 43 | "type":"integer" 44 | }, 45 | { 46 | "name":"post_id", 47 | "type":"integer", 48 | "fk":"posts" 49 | }, 50 | { 51 | "name":"tag_id", 52 | "type":"integer", 53 | "fk":"tags" 54 | } 55 | ] 56 | }, 57 | { 58 | "name":"categories", 59 | "columns":[ 60 | { 61 | "name":"id", 62 | "pk":true, 63 | "type":"integer" 64 | }, 65 | { 66 | "name":"name", 67 | "type":"varchar", 68 | "length":255 69 | }, 70 | { 71 | "name":"icon", 72 | "type":"blob", 73 | "nullable":true 74 | } 75 | ] 76 | }, 77 | { 78 | "name":"countries", 79 | "columns":[ 80 | { 81 | "name":"id", 82 | "pk":true, 83 | "type":"integer" 84 | }, 85 | { 86 | "name":"name", 87 | "type":"varchar", 88 | "length":255 89 | }, 90 | { 91 | "name":"shape", 92 | "type":"clob" 93 | } 94 | ] 95 | }, 96 | { 97 | "name":"barcodes", 98 | "columns":[ 99 | { 100 | "name":"id", 101 | "pk":true, 102 | "type":"integer" 103 | }, 104 | { 105 | "name":"product_id", 106 | "type":"integer", 107 | "fk":"products" 108 | }, 109 | { 110 | "name":"hex", 111 | "type":"varchar", 112 | "length":255 113 | }, 114 | { 115 | "name":"bin", 116 | "type":"varbinary", 117 | "length":255 118 | } 119 | ] 120 | }, 121 | { 122 | "name":"posts", 123 | "columns":[ 124 | { 125 | "name":"id", 126 | "pk":true, 127 | "type":"integer" 128 | }, 129 | { 130 | "name":"user_id", 131 | "type":"integer", 132 | "fk":"users" 133 | }, 134 | { 135 | "name":"category_id", 136 | "type":"integer", 137 | "fk":"categories" 138 | }, 139 | { 140 | "name":"content", 141 | "type":"varchar", 142 | "length":255 143 | } 144 | ] 145 | }, 146 | { 147 | "name":"events", 148 | "columns":[ 149 | { 150 | "name":"id", 151 | "pk":true, 152 | "type":"integer" 153 | }, 154 | { 155 | "name":"name", 156 | "type":"varchar", 157 | "length":255 158 | }, 159 | { 160 | "name":"datetime", 161 | "type":"timestamp" 162 | }, 163 | { 164 | "name":"visitors", 165 | "type":"integer" 166 | } 167 | ] 168 | }, 169 | { 170 | "name":"users", 171 | "columns":[ 172 | { 173 | "name":"id", 174 | "pk":true, 175 | "type":"integer" 176 | }, 177 | { 178 | "name":"username", 179 | "type":"varchar", 180 | "length":255 181 | }, 182 | { 183 | "name":"password", 184 | "type":"varchar", 185 | "length":255 186 | }, 187 | { 188 | "name":"location", 189 | "type":"clob", 190 | "nullable":true 191 | } 192 | ] 193 | }, 194 | { 195 | "name":"products", 196 | "columns":[ 197 | { 198 | "name":"id", 199 | "pk":true, 200 | "type":"integer" 201 | }, 202 | { 203 | "name":"name", 204 | "type":"varchar", 205 | "length":255 206 | }, 207 | { 208 | "name":"price", 209 | "type":"decimal", 210 | "precision":10, 211 | "scale":2 212 | }, 213 | { 214 | "name":"properties", 215 | "type":"clob" 216 | }, 217 | { 218 | "name":"created_at", 219 | "type":"timestamp" 220 | }, 221 | { 222 | "name":"deleted_at", 223 | "type":"timestamp", 224 | "nullable":true 225 | } 226 | ] 227 | }, 228 | { 229 | "name":"tags", 230 | "columns":[ 231 | { 232 | "name":"id", 233 | "pk":true, 234 | "type":"integer" 235 | }, 236 | { 237 | "name":"name", 238 | "type":"varchar", 239 | "length":255 240 | }, 241 | { 242 | "name":"is_important", 243 | "type":"bit" 244 | } 245 | ] 246 | }, 247 | { 248 | "name":"kunsthåndværk", 249 | "columns":[ 250 | { 251 | "name":"id", 252 | "pk":true, 253 | "type":"varchar", 254 | "length":36 255 | }, 256 | { 257 | "name":"Umlauts ä_ö_ü-COUNT", 258 | "type":"integer" 259 | }, 260 | { 261 | "name":"user_id", 262 | "type":"integer", 263 | "fk":"users" 264 | }, 265 | { 266 | "name":"invisible", 267 | "type":"varchar", 268 | "nullable": true, 269 | "length":36 270 | } 271 | ] 272 | }, 273 | { 274 | "name": "invisibles", 275 | "columns": [ 276 | { 277 | "name": "id", 278 | "type": "varchar", 279 | "length": 36 280 | } 281 | ] 282 | }, 283 | { 284 | "name": "nopk", 285 | "columns": [ 286 | { 287 | "name": "id", 288 | "type": "varchar", 289 | "length": 36 290 | } 291 | ] 292 | } 293 | ] 294 | } -------------------------------------------------------------------------------- /full/src/test/resources/records-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "The Simple Data Dump Schema", 3 | "type": "object", 4 | "properties": { 5 | "tables": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "records": { 14 | "type": "array", 15 | "items": { 16 | "type": "object" 17 | } 18 | } 19 | }, 20 | "required": ["name","records"], 21 | "additionalProperties":false 22 | } 23 | } 24 | }, 25 | "required": ["tables"], 26 | "additionalProperties":false 27 | } -------------------------------------------------------------------------------- /full/src/test/resources/records.json: -------------------------------------------------------------------------------- 1 | { 2 | "tables": [ 3 | { 4 | "name": "categories", 5 | "records": [ 6 | { 7 | "id": 1, 8 | "name": "announcement", 9 | "icon": null 10 | }, 11 | { 12 | "id": 2, 13 | "name": "article", 14 | "icon": null 15 | } 16 | ] 17 | }, 18 | { 19 | "name": "countries", 20 | "records": [ 21 | { 22 | "id": 1, 23 | "name": "Left", 24 | "shape": "POLYGON((30 10,40 40,20 40,10 20,30 10))" 25 | }, 26 | { 27 | "id": 2, 28 | "name": "Right", 29 | "shape": "POLYGON((70 10,80 40,60 40,50 20,70 10))" 30 | } 31 | ] 32 | }, 33 | { 34 | "name": "events", 35 | "records": [ 36 | { 37 | "id": 1, 38 | "name": "Launch", 39 | "datetime": "2016-01-01 12:01:01", 40 | "visitors": 0 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "users", 46 | "records": [ 47 | { 48 | "id": 1, 49 | "username": "user1", 50 | "password": "pass1", 51 | "location": null 52 | }, 53 | { 54 | "id": 2, 55 | "username": "user2", 56 | "password": "pass2", 57 | "location": null 58 | } 59 | ] 60 | }, 61 | { 62 | "name": "posts", 63 | "records": [ 64 | { 65 | "id": 1, 66 | "user_id": 1, 67 | "category_id": 1, 68 | "content": "blog started" 69 | }, 70 | { 71 | "id": 2, 72 | "user_id": 1, 73 | "category_id": 2, 74 | "content": "It works!" 75 | } 76 | ] 77 | }, 78 | { 79 | "name": "comments", 80 | "records": [ 81 | { 82 | "id": 1, 83 | "post_id": 1, 84 | "message": "great" 85 | }, 86 | { 87 | "id": 2, 88 | "post_id": 1, 89 | "message": "fantastic" 90 | }, 91 | { 92 | "id": 3, 93 | "post_id": 2, 94 | "message": "thank you" 95 | }, 96 | { 97 | "id": 4, 98 | "post_id": 2, 99 | "message": "awesome" 100 | } 101 | ] 102 | }, 103 | { 104 | "name": "products", 105 | "records": [ 106 | { 107 | "id": 1, 108 | "name": "Calculator", 109 | "price": 23.01, 110 | "properties": "{\"depth\":false,\"model\":\"TRX-120\",\"width\":100,\"height\":null}", 111 | "created_at": "1970-01-01 00:01:01", 112 | "deleted_at": null 113 | } 114 | ] 115 | }, 116 | { 117 | "name": "barcodes", 118 | "records": [ 119 | { 120 | "id": 1, 121 | "product_id": 1, 122 | "hex": "00ff01", 123 | "bin": "AP8B" 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "tags", 129 | "records": [ 130 | { 131 | "id": 1, 132 | "name": "funny", 133 | "is_important": false 134 | }, 135 | { 136 | "id": 2, 137 | "name": "important", 138 | "is_important": true 139 | } 140 | ] 141 | }, 142 | { 143 | "name": "post_tags", 144 | "records": [ 145 | { 146 | "id": 1, 147 | "post_id": 1, 148 | "tag_id": 1 149 | }, 150 | { 151 | "id": 2, 152 | "post_id": 1, 153 | "tag_id": 2 154 | }, 155 | { 156 | "id": 3, 157 | "post_id": 2, 158 | "tag_id": 1 159 | }, 160 | { 161 | "id": 4, 162 | "post_id": 2, 163 | "tag_id": 2 164 | } 165 | ] 166 | }, 167 | { 168 | "name": "kunsthåndværk", 169 | "records": [ 170 | { 171 | "id": "e42c77c6-06a4-4502-816c-d112c7142e6d", 172 | "Umlauts ä_ö_ü-COUNT": 1, 173 | "user_id": 1, 174 | "invisible": null 175 | }, 176 | { 177 | "id": "e31ecfe6-591f-4660-9fbd-1a232083037f", 178 | "Umlauts ä_ö_ü-COUNT": 2, 179 | "user_id": 2, 180 | "invisible": null 181 | } 182 | ] 183 | }, 184 | { 185 | "name": "invisibles", 186 | "records": [ 187 | { 188 | "id": "e42c77c6-06a4-4502-816c-d112c7142e6d" 189 | } 190 | ] 191 | }, 192 | { 193 | "name": "nopk", 194 | "records": [ 195 | { 196 | "id": "e42c77c6-06a4-4502-816c-d112c7142e6d" 197 | } 198 | ] 199 | } 200 | ] 201 | } --------------------------------------------------------------------------------