From 717370d9e94f1c62ef8dc7f77e6ea71a4f9cc2fb Mon Sep 17 00:00:00 2001 From: vran Date: Tue, 1 Feb 2022 22:22:57 +0800 Subject: [PATCH] feat: add document export api --- .../com/databasir/api/DocumentController.java | 37 +++++++- .../main/java/com/databasir/api/Routes.java | 2 + .../document/service/DocumentService.java | 84 ++++++++++++++++++- .../template/render/markdown/markdown.ftlh | 35 ++++---- plugin/src/test/java/App.java | 4 +- 5 files changed, 139 insertions(+), 23 deletions(-) diff --git a/api/src/main/java/com/databasir/api/DocumentController.java b/api/src/main/java/com/databasir/api/DocumentController.java index 6979b63..70885ed 100644 --- a/api/src/main/java/com/databasir/api/DocumentController.java +++ b/api/src/main/java/com/databasir/api/DocumentController.java @@ -1,6 +1,7 @@ package com.databasir.api; import com.databasir.common.JsonData; +import com.databasir.common.SystemException; import com.databasir.core.domain.document.data.DatabaseDocumentResponse; import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse; import com.databasir.core.domain.document.service.DocumentService; @@ -9,13 +10,24 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; -import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; -@RestController @RequiredArgsConstructor @Validated +@RestController public class DocumentController { private final DocumentService documentService; @@ -41,4 +53,25 @@ public class DocumentController { return JsonData.ok(documentService.getVersionsBySchemaSourceId(projectId, page)); } + @GetMapping(Routes.Document.EXPORT) + public ResponseEntity getDocumentFiles(@PathVariable Integer projectId, + @RequestParam(required = false) Long version) { + String data = documentService.toMarkdown(projectId, version).get(); + try { + Path path = Files.writeString(Paths.get(UUID.randomUUID().toString() + ".md"), data, StandardCharsets.UTF_8); + HttpHeaders headers = new HttpHeaders(); + headers.setContentDisposition(ContentDisposition.attachment() + .filename("demo.md", StandardCharsets.UTF_8) + .build()); + byte[] bytes = Files.readAllBytes(path); + Files.deleteIfExists(path); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + return ResponseEntity.ok() + .headers(headers) + .body(out -> out.write(bytes)); + } catch (IOException e) { + throw new SystemException("System error"); + } + } + } diff --git a/api/src/main/java/com/databasir/api/Routes.java b/api/src/main/java/com/databasir/api/Routes.java index b05fc94..406d09a 100644 --- a/api/src/main/java/com/databasir/api/Routes.java +++ b/api/src/main/java/com/databasir/api/Routes.java @@ -68,6 +68,8 @@ public interface Routes { String SYNC_ONE = BASE + "/projects/{projectId}/documents"; String LIST_VERSIONS = BASE + "/projects/{projectId}/document_versions"; + + String EXPORT = BASE + "/projects/{projectId}/document_files"; } interface DocumentRemark { diff --git a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java index b2123ee..1c48a26 100644 --- a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java +++ b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java @@ -11,6 +11,7 @@ import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse; import com.databasir.core.infrastructure.connection.DatabaseConnectionService; import com.databasir.core.infrastructure.converter.JsonConverter; import com.databasir.core.meta.data.DatabaseMeta; +import com.databasir.core.render.markdown.MarkdownBuilder; import com.databasir.dao.impl.*; import com.databasir.dao.tables.pojos.*; import lombok.RequiredArgsConstructor; @@ -21,10 +22,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.sql.Connection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; @Service @@ -183,4 +182,81 @@ public class DocumentService { .build())) .orElseGet(Page::empty); } + + public Optional toMarkdown(Integer projectId, Long version) { + return getOneByProjectId(projectId, version) + .map(doc -> { + MarkdownBuilder builder = MarkdownBuilder.builder(); + builder.primaryTitle(doc.getDatabaseName()); + // overview + builder.secondTitle("overview"); + List> overviewContent = new ArrayList<>(); + for (int i = 0; i < doc.getTables().size(); i++) { + DatabaseDocumentResponse.TableDocumentResponse table = doc.getTables().get(i); + overviewContent.add(List.of((i + 1) + "", table.getName(), table.getType(), table.getComment())); + } + builder.table(List.of("", "表名", "类型", "备注"), overviewContent); + + Function columnDefaultValueMapping = column -> { + if (Objects.equals(column.getNullable(), "YES")) { + return Objects.requireNonNullElse(column.getDefaultValue(), "null"); + } else { + return Objects.requireNonNullElse(column.getDefaultValue(), ""); + } + }; + // tables + doc.getTables().forEach(table -> { + builder.secondTitle(table.getName()); + + // columns + List> columnContent = new ArrayList<>(); + for (int i = 0; i < table.getColumns().size(); i++) { + var column = table.getColumns().get(i); + String type; + if (column.getDecimalDigits() == null || column.getDecimalDigits() == 0) { + type = table.getType() + "(" + column.getSize() + ")"; + } else { + type = table.getType() + "(" + column.getSize() + "," + column.getDecimalDigits() + ")"; + } + columnContent.add(List.of((i + 1) + "", + column.getName(), + type, + column.getIsPrimaryKey() ? "YES" : "NO", + column.getNullable(), + column.getAutoIncrement(), + columnDefaultValueMapping.apply(column), + column.getComment())); + } + builder.thirdTitle("columns"); + builder.table(List.of("", "名称", "类型", "是否为主键", "可为空", "自增", "默认值", "备注"), + columnContent); + + // indexes + List> indexContent = new ArrayList<>(); + for (int i = 0; i < table.getIndexes().size(); i++) { + var index = table.getIndexes().get(i); + String columnNames = String.join(", ", index.getColumnNames()); + String isUnique = index.getIsUnique() ? "YES" : "NO"; + indexContent.add(List.of((i + 1) + "", index.getName(), isUnique, columnNames)); + } + builder.thirdTitle("indexes"); + builder.table(List.of("", "名称", "是否唯一", "关联列"), indexContent); + + if (!table.getTriggers().isEmpty()) { + List> triggerContent = new ArrayList<>(); + for (int i = 0; i < table.getTriggers().size(); i++) { + var trigger = table.getTriggers().get(i); + triggerContent.add(List.of((i + 1) + "", + trigger.getName(), + trigger.getTiming(), + trigger.getManipulation(), + trigger.getStatement())); + } + builder.thirdTitle("triggers"); + builder.table(List.of("", "名称", "timing", "manipulation", "statement"), triggerContent); + } + }); + return builder.build(); + }); + } } diff --git a/plugin/src/main/resources/template/render/markdown/markdown.ftlh b/plugin/src/main/resources/template/render/markdown/markdown.ftlh index 3a1536c..4452a48 100644 --- a/plugin/src/main/resources/template/render/markdown/markdown.ftlh +++ b/plugin/src/main/resources/template/render/markdown/markdown.ftlh @@ -1,40 +1,45 @@ # ${meta.databaseName} +| | | +| --------------- | ---- | +| Database name | | +| Product name | | +| Product version | | -| seq | name | comment | -| ---- | --------------- | ------ | +## Overview +| | name | type | comment | +| ---- | --------------- | ------ | ------ | <#list meta.tables as table> - | ${table_index+1} | [${table.name}](#${table.name}) | ${table.comment!'N/A'} | +| ${table_index+1} | [${table.name}](#${table.name}) | ${table.type} | ${table.comment!'N/A'} | <#if config.renderTables> <#list meta.tables as table> - ## ${table.name} - +## ${table.name} <#if config.renderColumns> - ### Columns +### Columns - | seq | name | type | nullable | auto increment| default | comment | - | ---- | ------ | ------ | ------ | -------- | ------ | ------ | +| | name | type | primary Key | nullable | auto increment| default | comment | +| --- | ---- | ---- | ----------- | -------- | ------------- | ------- | ------- | <#list table.columns as column> - | ${column_index+1} | ${column.name} | ${column.type} | ${column.isNullable?then('YES','NO')} | ${column.isAutoIncrement?then('YES', 'NO')} | ${column.isNullable?then(column.defaultValue!'NULL', column.defaultValue!'')} | ${column.comment!''} | +| ${column_index+1} | ${column.name} | ${column.type} | ${column.isPrimaryKey?then('YES','NO')} | ${column.nullable } | ${column.autoIncrement} | ${column.defaultValue!'NULL'} | ${column.comment!''} | - <#if config.renderIndexes> +<#if config.renderIndexes> ### Indexes -| seq | name | unique | primary key | columns | -| ---- | ---- | -------- | -------- | ------ | +| | name | unique | columns | +| --- | ---- | ------ | ------- | <#list table.indexes as index> - | ${index_index+1} | ${index.name} | ${index.isUniqueKey?then('YES', 'NO')} | ${index.isPrimaryKey?then('YES','NO')} | ${index.columnNames?join(', ')} | +| ${index_index+1} | ${index.name} | ${index.isUniqueKey?then('YES', 'NO')} | ${index.columnNames?join(', ')} | <#if config.renderTriggers> ### Triggers -| seq | name | timing | statement | created | -| ---- | ---- | -------- | --------- | -------- | +| | name | timing | statement | created | +| --- | ---- | ------ | --------- | ------- | <#list table.triggers as trigger> | ${trigger_index} | ${trigger.name} | ${trigger.timing + " " + trigger.manipulation } | ${trigger.statement?replace("\n", "
")?replace("\r", " ")} | ${trigger.createAt} | diff --git a/plugin/src/test/java/App.java b/plugin/src/test/java/App.java index 219b45b..e8d45da 100644 --- a/plugin/src/test/java/App.java +++ b/plugin/src/test/java/App.java @@ -13,10 +13,10 @@ public class App { @Test public void testRenderAsMarkdown() throws SQLException, ClassNotFoundException { - try (FileOutputStream out = new FileOutputStream("user.md")) { + try (FileOutputStream out = new FileOutputStream("demo.md")) { Connection connection = getJdbcConnection(); Databasir databasir = Databasir.of(); - DatabaseMeta doc = databasir.get(connection, "user").orElseThrow(); + DatabaseMeta doc = databasir.get(connection, "demo").orElseThrow(); databasir.renderAsMarkdown(doc, out); } catch (IOException e) { throw new IllegalStateException(e);