diff --git a/.gitlab/pre-merge-test b/.gitlab/pre-merge-test new file mode 100755 index 0000000000000000000000000000000000000000..99fae848279c4f1149ffec9dfbdf7b9f01d749f2 --- /dev/null +++ b/.gitlab/pre-merge-test @@ -0,0 +1,40 @@ +#!/bin/bash + +# DESCRIPTION: script to check if the current code base passes all tests before submitting to pipeline +# WHEN: merge to dev, master + +SERVICES="container + database + discovery + gateway + query + table" + +# 1) Docker +echo -e "\e[96m1\e[39m) Docker" +echo "Building all" +docker-compose build >/dev/null 2>&1 + +if [[ $? -ne 0 ]]; then + echo -e "... \e[91mNOT OK\e[39m" +else + echo -e "... \e[92mOK\e[39m" +fi + +# 2) Maven +echo -e "\e[96m2\e[39m) Maven" + +for service in $SERVICES; do + echo "Testing ./fda-${service}-service" + RESULT=$(mvn -f "./fda-${service}-service/pom.xml" clean test verify | grep -o "FAILURE") + if [[ $RESULT ]]; then + echo -e "... \e[91mNOT OK\e[39m" + else + echo -e "... \e[92mOK\e[39m" + fi +done + +# 3) Runtime +echo -e "\e[96m3\e[39m) Runtime" +echo "Execute Docker runtime, look for errors" +docker-compose up diff --git a/docker-compose.yml b/docker-compose.yml index 4c9736d360bcefca6000b7acebb5f04a7e0d03cd..181c7945e485ed608ad734f6067595dd6474ec3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -206,6 +206,8 @@ services: - fda-public ports: - 3000:3000 + volumes: + - /tmp:/tmp depends_on: - fda-container-service - fda-database-service @@ -215,3 +217,4 @@ services: API_CONTAINER: http://fda-container-service:9091 API_DATABASE: http://fda-database-service:9092 API_TABLES: http://fda-table-service:9094 + API_ANALYSE: http://fda-analyse-service:5000 diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/CreateTableViaCsvDTO.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/CreateTableViaCsvDTO.java deleted file mode 100644 index 76b97ca03bd377304c1a082e253f386120c267f6..0000000000000000000000000000000000000000 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/CreateTableViaCsvDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package at.tuwien.api.database.table; - -import lombok.*; - -@Setter -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class CreateTableViaCsvDTO { - - private String containerId; - - private String pathToFile; - - private char delimiter; - - -} diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableCsvInformationDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableCsvInformationDto.java new file mode 100644 index 0000000000000000000000000000000000000000..bf6063fbe1c72867fe939e28a2bed9c7ed9e52ac --- /dev/null +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableCsvInformationDto.java @@ -0,0 +1,31 @@ +package at.tuwien.api.database.table; + +import at.tuwien.api.database.table.columns.ColumnTypeDto; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TableCsvInformationDto { + + @NotBlank + @ApiModelProperty(name = "name", example = "Fundamentals") + private String name; + + @NotBlank + @ApiModelProperty(name = "table description", required = true, example = "SEC 10K annual fillings (2016-2012) ") + private String description; + + @NotBlank + private List<ColumnTypeDto> columns; + + @NotBlank + private String fileLocation; + +} diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index 629e368b8c11bf52ae848ff272c9b337a2ec8ea1..82055f32cef4a06bd4ac2432eeae198397a69261 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -13,9 +13,9 @@ import at.tuwien.service.QueryService; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; +import net.sf.jsqlparser.JSQLParserException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.sql.SQLSyntaxErrorException; +import java.sql.SQLFeatureNotSupportedException; import java.util.List; import java.util.stream.Collectors; @@ -64,7 +64,8 @@ public class QueryEndpoint { @ApiResponse(code = 404, message = "The database does not exist."), @ApiResponse(code = 405, message = "The container is not running."), @ApiResponse(code = 409, message = "The container image is not supported."),}) - public ResponseEntity<?> create(@PathVariable Long id) throws ImageNotSupportedException, DatabaseConnectionException, DatabaseNotFoundException { + public ResponseEntity<?> create(@PathVariable Long id) throws ImageNotSupportedException, + DatabaseConnectionException, DatabaseNotFoundException { queryService.create(id); return ResponseEntity.status(HttpStatus.CREATED) .build(); @@ -77,22 +78,22 @@ public class QueryEndpoint { @ApiResponse(code = 404, message = "The database does not exist."), @ApiResponse(code = 405, message = "The container is not running."), @ApiResponse(code = 409, message = "The container image is not supported."),}) - public ResponseEntity<QueryResultDto> modify(@PathVariable Long id, @RequestBody ExecuteQueryDto dto) throws DatabaseNotFoundException, ImageNotSupportedException, SQLSyntaxErrorException { - final QueryResultDto qr = queryService.executeStatement(id, queryMapper.queryDTOtoQuery(dto)); - return ResponseEntity.status(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(qr); + public ResponseEntity<QueryResultDto> modify(@PathVariable Long id, @RequestBody ExecuteQueryDto dto) + throws DatabaseNotFoundException, ImageNotSupportedException, SQLFeatureNotSupportedException, + JSQLParserException { + final QueryResultDto response = queryService.executeStatement(id, queryMapper.queryDTOtoQuery(dto)); + return ResponseEntity.ok(response); } @PutMapping("/query/version/{timestamp}") @ApiOperation(value = "executes a query with a given timestamp") @ApiResponses(value = {@ApiResponse(code = 201, message = "result of Query with Timestamp")}) - public ResponseEntity<?> modify(@PathVariable Long id, @PathVariable String timestamp, @RequestBody ExecuteQueryDto dto) throws DatabaseNotFoundException, ImageNotSupportedException, SQLSyntaxErrorException { - queryService.executeStatement(id, queryMapper.queryDTOtoQuery(dto)); - return ResponseEntity.status(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .build(); + public ResponseEntity<QueryResultDto> modify(@PathVariable Long id, @PathVariable String timestamp, @RequestBody ExecuteQueryDto dto) + throws DatabaseNotFoundException, ImageNotSupportedException, SQLFeatureNotSupportedException, + JSQLParserException { + final QueryResultDto response = queryService.executeStatement(id, queryMapper.queryDTOtoQuery(dto)); + return ResponseEntity.ok(response); } } diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java index 2b815a9ced8d4ea00d3a4755d61270f4e171630a..a2d3ac3d8aa85f412d8703fe174b4cee3c1f0b16 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java @@ -10,10 +10,18 @@ import at.tuwien.exception.ImageNotSupportedException; import at.tuwien.exception.QueryMalformedException; import at.tuwien.repository.DatabaseRepository; import lombok.extern.log4j.Log4j2; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectItem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.persistence.EntityNotFoundException; +import java.io.StringReader; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLSyntaxErrorException; import java.sql.Timestamp; import java.util.List; @@ -45,11 +53,22 @@ public class QueryService { return postgresService.getQueries(findDatabase(id)); } - public QueryResultDto executeStatement(Long id, Query query) throws ImageNotSupportedException, DatabaseNotFoundException, SQLSyntaxErrorException { - if (!checkValidity(query.getQuery())) { - throw new SQLSyntaxErrorException("SQL Query contains invalid Syntax"); - } + public QueryResultDto executeStatement(Long id, Query query) throws ImageNotSupportedException, DatabaseNotFoundException, JSQLParserException, SQLFeatureNotSupportedException { + CCJSqlParserManager parserRealSql = new CCJSqlParserManager(); + + Statement stmt = parserRealSql.parse(new StringReader(query.getQuery())); Database database = findDatabase(id); + if(stmt instanceof Select) { + Select selectStatement = (Select) stmt; + PlainSelect ps = (PlainSelect)selectStatement.getSelectBody(); + + List<SelectItem> selectitems = ps.getSelectItems(); + System.out.println(ps.getFromItem().toString()); + selectitems.stream().forEach(selectItem -> System.out.println(selectItem.toString())); + } + else { + throw new SQLFeatureNotSupportedException("SQL Query is not a SELECT statement - please only use SELECT statements"); + } saveQuery(database, query, null); return null; diff --git a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index 5736265039d11360ac8447d9dc0902bdeb8b4389..61c5509e7a424e772c46df8f953d51f1525d936e 100644 --- a/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/fda-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -3,6 +3,7 @@ package at.tuwien.endpoints; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableCreateDto; +import at.tuwien.api.database.table.TableCsvInformationDto; import at.tuwien.api.database.table.TableDto; import at.tuwien.entities.database.table.Table; import at.tuwien.exception.*; @@ -19,6 +20,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -74,6 +76,39 @@ public class TableEndpoint { .body(tableMapper.tableToTableBriefDto(table)); } + @PostMapping("/table/csv") + @ApiOperation(value = "Create a table", notes = "Creates a file, which is given as a multipart file.") + @ApiResponses({ + @ApiResponse(code = 201, message = "The table was created."), + @ApiResponse(code = 400, message = "The creation form contains invalid data."), + @ApiResponse(code = 401, message = "Not authorized to create a tables."), + @ApiResponse(code = 404, message = "The database does not exist."), + @ApiResponse(code = 405, message = "The container is not running."), + @ApiResponse(code = 409, message = "The container image is not supported."), + }) + public ResponseEntity<TableDto> createViaCsv(@PathVariable("id") Long databaseId, @RequestPart("file") MultipartFile file, @RequestPart TableCsvInformationDto headers) { + final Table table = tableService.create(databaseId, file, headers); + return ResponseEntity.status(HttpStatus.CREATED) + .body(tableMapper.tableToTableDto(table)); + } + + @PostMapping("/table/csv/local") + @ApiOperation(value = "Create a table", notes = "This is done by saving a file on the shared docker filesystem and then sending the link to the file.") + @ApiResponses({ + @ApiResponse(code = 201, message = "The table was created."), + @ApiResponse(code = 400, message = "The creation form contains invalid data."), + @ApiResponse(code = 401, message = "Not authorized to create a tables."), + @ApiResponse(code = 404, message = "The database does not exist."), + @ApiResponse(code = 405, message = "The container is not running."), + @ApiResponse(code = 409, message = "The container image is not supported."), + }) + public ResponseEntity<TableDto> createViaCsv(@PathVariable("id") Long databaseId, @RequestBody TableCsvInformationDto tableCSVInformation) throws IOException { + final Table table = tableService.create(databaseId, tableCSVInformation); + return ResponseEntity.status(HttpStatus.CREATED) + .body(tableMapper.tableToTableDto(table)); + } + + @GetMapping("/table/{tableId}") @ApiOperation(value = "List all tables", notes = "Lists the tables in the metadata database for this database.") @ApiResponses({ diff --git a/fda-table-service/services/pom.xml b/fda-table-service/services/pom.xml index 057eab57483906dfd0e2dc2615432baaecb11a85..932692ebe84f7639579cb26e94f605a89c6b0faf 100644 --- a/fda-table-service/services/pom.xml +++ b/fda-table-service/services/pom.xml @@ -24,6 +24,10 @@ <artifactId>super-csv</artifactId> <version>2.4.0</version> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-test</artifactId> + </dependency> </dependencies> <build> @@ -52,4 +56,4 @@ </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/fda-table-service/services/src/main/java/at/tuwien/service/PostgresService.java b/fda-table-service/services/src/main/java/at/tuwien/service/PostgresService.java index c1a420ebaeb278825dd60fbea7688ab12b19f157..0141e0ff331b03992a81cc817d17f10e389853a7 100644 --- a/fda-table-service/services/src/main/java/at/tuwien/service/PostgresService.java +++ b/fda-table-service/services/src/main/java/at/tuwien/service/PostgresService.java @@ -96,10 +96,17 @@ public class PostgresService extends JdbcConnector { while (result.next()) { Map<String, Object> r = new HashMap<>(); for (TableColumn tc : t.getColumns()) { - r.put(tc.getName(), result.getString(tc.getInternalName())); + if (ColumnTypeDto.valueOf(tc.getColumnType()).equals(ColumnTypeDto.NUMBER)) { + r.put(tc.getName(), result.getDouble(tc.getInternalName())); + } else if (ColumnTypeDto.valueOf(tc.getColumnType()).equals(ColumnTypeDto.BOOLEAN)) { + r.put(tc.getName(), result.getBoolean(tc.getInternalName())); + } else { + r.put(tc.getName(), result.getString(tc.getInternalName())); + } } res.add(r); } + log.debug("assembled result: {}", res); qr.setResult(res); return qr; } catch (SQLException e) { diff --git a/fda-table-service/services/src/main/java/at/tuwien/service/TableService.java b/fda-table-service/services/src/main/java/at/tuwien/service/TableService.java index d1e22344f9299888abc3a2b6304be25f20d47408..252bb5b71029bc0e3590410a24cea95c44ec68c8 100644 --- a/fda-table-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/fda-table-service/services/src/main/java/at/tuwien/service/TableService.java @@ -2,6 +2,8 @@ package at.tuwien.service; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCreateDto; +import at.tuwien.api.database.table.TableCsvInformationDto; +import at.tuwien.api.database.table.columns.ColumnCreateDto; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; @@ -11,6 +13,7 @@ import at.tuwien.repository.DatabaseRepository; import at.tuwien.repository.TableRepository; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.supercsv.cellprocessor.constraint.NotNull; @@ -24,6 +27,9 @@ import javax.transaction.Transactional; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -177,14 +183,83 @@ public class TableService { } } + private String[] readHeader(MultipartFile file) throws IOException { + ICsvMapReader mapReader = null; + try { + Reader reader = new InputStreamReader(file.getInputStream()); + mapReader = new CsvMapReader(reader, CsvPreference.STANDARD_PREFERENCE); + + String[] header = mapReader.getHeader(true); + return header; + + } catch(IOException e) { + e.printStackTrace(); + } finally { + if( mapReader != null ) { + mapReader.close(); + } + } + return null; + } + + // TODO ms what is this for? It does ony print to stdout public QueryResultDto showData(Long databaseId, Long tableId) throws ImageNotSupportedException, DatabaseNotFoundException, TableNotFoundException, DatabaseConnectionException, DataProcessingException { QueryResultDto queryResult = postgresService.getAllRows(findDatabase(databaseId), findById(databaseId, tableId)); - for (Map<String, Object> m : queryResult.getResult()) { + for (Map<String, Object> m : queryResult.getResult() ) { for (Map.Entry<String, Object> entry : m.entrySet()) { - System.out.print(entry.getKey() + ": " + entry.getValue() + ", "); + log.debug("{}: {}", entry.getKey(), entry.getValue()); } } return queryResult; } + + public Table create(Long databaseId, MultipartFile file, TableCsvInformationDto tableCSVInformation) { + try { + String[] header = readHeader(file); + log.debug("table csv info: {}", tableCSVInformation); + TableCreateDto tcd = new TableCreateDto(); + tcd.setName(tableCSVInformation.getName()); + tcd.setDescription(tableCSVInformation.getDescription()); + ColumnCreateDto[] cdtos = new ColumnCreateDto[header.length]; + log.debug("header: {}", header); + for (int i = 0; i < header.length; i++) { + ColumnCreateDto c = new ColumnCreateDto(); + c.setName(header[i]); + c.setType(tableCSVInformation.getColumns().get(i)); + c.setNullAllowed(true); + //TODO FIX THAT not only id is primary key + if(header[i].equals("id")) { + c.setPrimaryKey(true); + } else { + c.setPrimaryKey(false); + } + cdtos[i] = c; + } + tcd.setColumns(cdtos); + Table table = create(databaseId, tcd); + QueryResultDto insert = insert(databaseId, table.getId(), file); + return table; + } catch (Exception e) { + e.printStackTrace(); + log.error(e.getMessage()); + } + return null; + } + + public Table create(Long databaseId, TableCsvInformationDto tableCSVInformation) throws IOException { + Path path = Paths.get("/tmp/" + tableCSVInformation.getFileLocation()); + String contentType = "multipart/form-data"; + byte[] content = null; + try { + content = Files.readAllBytes(path); + } catch (final IOException e) { + } + MultipartFile multipartFile = new MockMultipartFile(tableCSVInformation.getFileLocation(), + tableCSVInformation.getFileLocation(), contentType, content); + Files.deleteIfExists(path); + return create(databaseId, multipartFile,tableCSVInformation); + } + + } diff --git a/fda-ui/.env b/fda-ui/.env index 9e4531c8a85705c4e7b9b0e5ffcea34b37dbb34a..05057611a6ac1a8e0c5195efea37f086f99b9285 100644 --- a/fda-ui/.env +++ b/fda-ui/.env @@ -2,3 +2,4 @@ API_CONTAINER="http://localhost:9091" API_DATABASE="http://localhost:9092" API_TABLES="http://localhost:9094" +API_ANALYSE="http://localhost:5000" diff --git a/fda-ui/Dockerfile b/fda-ui/Dockerfile index f19851acb36f66ed94c3ad2ba9799009dc446eee..cd22f0bca67342f987c559e303d25ec1833fb949 100644 --- a/fda-ui/Dockerfile +++ b/fda-ui/Dockerfile @@ -14,6 +14,7 @@ COPY ./components ./components COPY ./lang ./lang COPY ./layouts ./layouts COPY ./middleware ./middleware +COPY ./server-middleware ./server-middleware COPY ./pages ./pages COPY ./plugins ./plugins COPY ./store ./store diff --git a/fda-ui/components/TableCreate.vue b/fda-ui/components/TableCreate.vue index ce0e48fdae388c6c4d01e0aae38b46a7022d7ba6..8012b2c94e78b61f31d36833eaf6a38cacad44ee 100644 --- a/fda-ui/components/TableCreate.vue +++ b/fda-ui/components/TableCreate.vue @@ -1,56 +1,58 @@ <template> - <v-card> - <v-card-title class="pb-0"> - Create Table - </v-card-title> - <!-- <v-card-subtitle> - Table is not created until the "Create Table" button is pressed. - </v-card-subtitle> --> - <v-card-text> - <v-text-field - v-model="name" - label="Table Name" - :rules="[v => !!v || $t('Required')]" - required /> - <v-text-field - v-model="description" - label="Description" /> - <v-btn @click="addColumn"> - Add Column - </v-btn> - </v-card-text> - <v-card-text v-for="(c, idx) in columns" :key="idx" class="pa-3"> - <v-row class="column pa-2 ml-1 mr-1"> - <v-col cols="4"> - <v-text-field v-model="c.name" required label="Name" /> - </v-col> - <v-col cols="3"> - <v-select - v-model="c.type" - :items="columnTypes" - item-value="value" - required - label="Data Type" /> - </v-col> - <v-col cols="2"> - <v-checkbox v-model="c.primaryKey" label="Primary Key" /> - </v-col> - <v-col cols="2"> - <v-checkbox v-model="c.nullAllowed" label="Null Allowed" /> - </v-col> + <div> + <v-card> + <v-card-title class="pb-0"> + Create Table + </v-card-title> + <!-- <v-card-subtitle> + Table is not created until the "Create Table" button is pressed. + </v-card-subtitle> --> + <v-card-text> + <v-text-field + v-model="name" + label="Table Name" + :rules="[v => !!v || $t('Required')]" + required /> + <v-text-field + v-model="description" + label="Description" /> + <v-btn @click="addColumn"> + Add Column + </v-btn> + </v-card-text> + <v-card-text v-for="(c, idx) in columns" :key="idx" class="pa-3"> + <v-row class="column pa-2 ml-1 mr-1"> + <v-col cols="4"> + <v-text-field v-model="c.name" required label="Name" /> + </v-col> + <v-col cols="3"> + <v-select + v-model="c.type" + :items="columnTypes" + item-value="value" + required + label="Data Type" /> + </v-col> + <v-col cols="2"> + <v-checkbox v-model="c.primaryKey" label="Primary Key" /> + </v-col> + <v-col cols="2"> + <v-checkbox v-model="c.nullAllowed" label="Null Allowed" /> + </v-col> + <v-spacer /> + <v-btn title="Remove column" outlined icon @click="removeColumn(idx)"> + <v-icon>mdi-minus</v-icon> + </v-btn> + </v-row> + </v-card-text> + <v-card-actions> <v-spacer /> - <v-btn title="Remove column" outlined icon @click="removeColumn(idx)"> - <v-icon>mdi-minus</v-icon> + <v-btn :disabled="!canCreateTable()" @click="createTable"> + Create Table </v-btn> - </v-row> - </v-card-text> - <v-card-actions> - <v-spacer /> - <v-btn :disabled="!canCreateTable()" @click="createTable"> - Create Table - </v-btn> - </v-card-actions> - </v-card> + </v-card-actions> + </v-card> + </div> </template> <script> @@ -98,13 +100,7 @@ export default { const data = { name: this.name, description: this.description, - columns: this.columns.map((c) => { - // c.nullAllowed = c.isNullAllowed - // c.primaryKey = c.isPrimaryKey - // delete c.isPrimaryKey - // delete c.isNullAllowed - return c - }) + columns: this.columns } try { const res = await this.$axios.post(`/api/tables/api/database/${this.$route.params.db_id}/table`, data) diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index f5065166aa6cbedfef3a38a3cfa68be62724fa2d..cbfeeb19accc05e359de03231c92a21b4d7545a3 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -1,3 +1,4 @@ +import path from 'path' import colors from 'vuetify/es5/util/colors' import isDocker from 'is-docker' @@ -76,9 +77,14 @@ export default { proxy: { '/api/container': process.env.API_CONTAINER, '/api/database': process.env.API_DATABASE, + '/api/analyse': process.env.API_ANALYSE, '/api/tables': { target: process.env.API_TABLES, pathRewrite: { '^/api/tables/': '' } } }, + serverMiddleware: [ + { path: '/server-middleware', handler: path.resolve(__dirname, 'server-middleware/index.js') } + ], + // Vuetify module configuration (https://go.nuxtjs.dev/config-vuetify) vuetify: { customVariables: ['~/assets/variables.scss'], diff --git a/fda-ui/package.json b/fda-ui/package.json index b12d61ba371c2df59cd046579b6c514c42d0bcd4..5a019acca08e9ca389f1c4ab8eb7b8cb4518bf2d 100644 --- a/fda-ui/package.json +++ b/fda-ui/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "nuxt", + "dev": "nuxt --port 3001", "docker": "nuxt > /dev/null", "build": "nuxt build", "start": "nuxt start", @@ -22,6 +22,8 @@ "core-js": "^3.6.5", "date-fns": "^2.16.1", "is-docker": "^2.2.1", + "multer": "^1.4.2", + "node-fetch": "^2.6.1", "nuxt": "^2.12.2", "nuxt-i18n": "^6.15.4", "vue-toast-notification": "^0.5.4", diff --git a/fda-ui/pages/db/_db_id/tables/index.vue b/fda-ui/pages/db/_db_id/tables/index.vue index 492567c52f72ad86e1b6b99ef22b0508243747b3..f6d687c77e972a536ec22fc8fa4c64d56621ef8d 100644 --- a/fda-ui/pages/db/_db_id/tables/index.vue +++ b/fda-ui/pages/db/_db_id/tables/index.vue @@ -5,6 +5,13 @@ </h3> <TableList /> <TableCreate /> + <v-card class="mt-1"> + <v-card-text> + <nuxt-link class="table_from_csv" :to="`/db/${$route.params.db_id}/tables/table_from_csv`"> + Create table from CSV file + </nuxt-link> + </v-card-text> + </v-card> </div> </template> <script> @@ -27,5 +34,8 @@ export default { } </script> -<style> +<style scoped> +a.table_from_csv { + font-size: 14pt; +} </style> diff --git a/fda-ui/pages/db/_db_id/tables/table_from_csv.vue b/fda-ui/pages/db/_db_id/tables/table_from_csv.vue new file mode 100644 index 0000000000000000000000000000000000000000..5140c075fde9da012aa3a410ab1ea79662e6f108 --- /dev/null +++ b/fda-ui/pages/db/_db_id/tables/table_from_csv.vue @@ -0,0 +1,166 @@ +<template> + <div> + <h3 class="mb-2 mt-1">Table from CSV</h3> + <v-stepper v-model="step" vertical> + <v-stepper-step :complete="step > 1" step="1"> + Table + </v-stepper-step> + + <v-stepper-content class="pt-0 pb-1" step="1"> + <v-text-field v-model="tableName" required label="Name" /> + <v-text-field v-model="tableDesc" label="Description" /> + <v-btn :disabled="!step1Valid" color="primary" @click="step = 2"> + Continue + </v-btn> + </v-stepper-content> + + <v-stepper-step :complete="step > 2" step="2"> + Upload CSV file + </v-stepper-step> + + <v-stepper-content step="2"> + <v-row dense> + <v-col cols="8"> + <v-file-input + v-model="file" + accept="text/csv" + show-size + label="CSV File" /> + </v-col> + <v-col cols="4" class="mt-3"> + <v-btn :disabled="!file" :loading="loading" @click="upload">Upload</v-btn> + </v-col> + </v-row> + </v-stepper-content> + + <v-stepper-step :complete="step > 3" step="3"> + Choose data type of columns + </v-stepper-step> + <v-stepper-content step="3"> + <div v-for="(c, idx) in columns" :key="idx"> + <v-row dense class="column pa-2 ml-1 mr-1 mb-2"> + <v-col cols="4"> + <v-text-field v-model="c.name" disabled required label="Name" /> + </v-col> + <v-col cols="3"> + <v-select + v-model="c.type" + :items="columnTypes" + item-value="value" + required + label="Data Type" /> + </v-col> + <v-col cols="auto" class="pl-2"> + <v-checkbox v-model="c.primaryKey" label="Primary Key" /> + </v-col> + <v-col cols="auto" class="pl-10"> + <v-checkbox v-model="c.nullAllowed" label="Null Allowed" /> + </v-col> + </v-row> + </div> + + <v-btn class="mt-2" color="primary" @click="createTable"> + Continue + </v-btn> + </v-stepper-content> + + <v-stepper-step + :complete="step > 4" + step="4"> + Done + </v-stepper-step> + + <v-stepper-content step="4"> + Proceed to table view. + <div class="mt-2"> + <v-btn :to="`/db/${$route.params.db_id}/tables/${newTableId}`" outlined> + <v-icon>mdi-table</v-icon> + View + </v-btn> + </div> + </v-stepper-content> + </v-stepper> + </div> +</template> +<script> +export default { + name: 'TableFromCSV', + components: { + }, + data () { + return { + step: 1, + tableName: '', + tableDesc: '', + loading: false, + file: null, + fileLocation: null, + columns: [], + columnTypes: [ + { value: 'ENUM', text: 'ENUM' }, + { value: 'BOOLEAN', text: 'BOOLEAN' }, + { value: 'NUMBER', text: 'NUMBER' }, + { value: 'BLOB', text: 'BLOB' }, + { value: 'DATE', text: 'DATE' }, + { value: 'STRING', text: 'STRING' }, + { value: 'TEXT', text: 'TEXT' } + ], + newTableId: 42 + } + }, + computed: { + step1Valid () { + return this.tableName.length + } + }, + mounted () { + }, + methods: { + async upload () { + this.loading = true + const url = '/server-middleware/table_from_csv' + const data = new FormData() + data.append('file', this.file) + try { + const res = await this.$axios.post(url, data, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + if (res.data.success) { + this.columns = res.data.columns + this.fileLocation = res.data.file.filename + this.step = 3 + } else { + this.$toast.error('Could not upload CSV data') + } + } catch (err) { + this.$toast.error('Could not upload data.') + } + this.loading = false + }, + async createTable () { + const url = `/api/tables/api/database/${this.$route.params.db_id}/table/csv/local` + const data = { + columns: this.columns.map(c => c.type), + description: this.tableDesc, + name: this.tableName, + fileLocation: this.fileLocation + } + let res + try { + res = await this.$axios.post(url, data) + this.newTableId = res.data.id + } catch (err) { + console.log(err) + } + if (res && res.data && res.data.id) { + this.step = 4 + } else { + this.$toast.error('Could not create table.') + } + } + } +} +</script> + +<style scoped> +</style> diff --git a/fda-ui/server-middleware/index.js b/fda-ui/server-middleware/index.js index e6bcce7d4c7740ae1ce97b500db34f31524faf52..94eabc512ac82582de6fd2b7e93fca3f41b76e0c 100644 --- a/fda-ui/server-middleware/index.js +++ b/fda-ui/server-middleware/index.js @@ -1,13 +1,48 @@ -export default function (req, res, next) { - // req is the Node.js http request object - console.log(req.url) +// const bodyParser = require('body-parser') +const app = require('express')() +const multer = require('multer') +const upload = multer({ dest: '/tmp' }) +const fetch = require('node-fetch') - // res is the Node.js http response object +// TODO extend me +const colTypeMap = { + Boolean: 'BOOLEAN', + Date: 'DATE', + Integer: 'NUMBER', + Numeric: 'NUMBER', + String: 'STRING', + Timestamp: 'DATE' +} - // next is a function to call to invoke the next middleware - // Don't forget to call next at the end if your middleware is not an endpoint! - // next() - console.log(res) +app.post('/table_from_csv', upload.single('file'), async (req, res) => { + const { file } = req + const { path } = file - return res.json({ hi: 'foo' }) -} + // send path to analyse service + let analysis + try { + analysis = await fetch(`${process.env.API_ANALYSE}/datatypesbypath?filepath=${path}`) + analysis = await analysis.json() + } catch (error) { + return res.json({ success: false, error }) + } + + // map messytables / CoMi's `determine_dt` column types to ours + // e.g. "Integer" -> "NUMBER" + let entries = Object.entries(analysis.columns) + entries = entries.map(([k, v]) => { + if (colTypeMap[v]) { + v = colTypeMap[v] + } + return { + name: k, + type: v, + nullAllowed: true, + primaryKey: false + } + }) + + res.json({ success: true, file, columns: entries }) +}) + +module.exports = app diff --git a/fda-ui/yarn.lock b/fda-ui/yarn.lock index dd67cc0b1f649aaac4ebc0aa20687ac86f2e5614..830b0ea1598ca74a15c03b90cf6ddf03b813d76b 100644 --- a/fda-ui/yarn.lock +++ b/fda-ui/yarn.lock @@ -1682,6 +1682,11 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz" @@ -2114,6 +2119,14 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" @@ -2516,7 +2529,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0: +concat-stream@^1.5.0, concat-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3007,6 +3020,14 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz" @@ -4681,6 +4702,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz" @@ -5264,6 +5290,20 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + multimap@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz" @@ -6725,6 +6765,16 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" @@ -7371,6 +7421,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz" @@ -7425,6 +7480,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz" @@ -7747,7 +7807,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==