diff --git a/apps/reports/src/components/ListReports.vue b/apps/reports/src/components/ListReports.vue index e6b74f4fe3..9e574ed32c 100644 --- a/apps/reports/src/components/ListReports.vue +++ b/apps/reports/src/components/ListReports.vue @@ -32,7 +32,7 @@ > { - report.id = index++; + report.index = index++; return report; }); } @@ -116,7 +116,7 @@ export default { }, async add() { this.error = null; - this.reports.push({ name: "new report", sql: "" }); + this.reports.push({ id: "uniqueid", description: "new report", sql: "" }); await this.client .saveSetting("reports", this.reports) .catch((error) => (this.error = error)); @@ -131,7 +131,7 @@ export default { this.reload(); }, open(row) { - this.$router.push({ name: "edit", params: { id: row.id } }); + this.$router.push({ name: "edit", params: { index: row.index } }); }, downloadSelected() { window.open( diff --git a/apps/reports/src/components/ViewEditReport.vue b/apps/reports/src/components/ViewEditReport.vue index e03a784744..f91fef8dbf 100644 --- a/apps/reports/src/components/ViewEditReport.vue +++ b/apps/reports/src/components/ViewEditReport.vue @@ -16,7 +16,19 @@

Edit report: {{ id }}

- + +

- Report: {{ name - }} + View report id={{ id }} +

+

Description: {{ description }}

Please provide parameters: { @@ -201,9 +224,10 @@ export default { }, async reload() { const reports = await this.client.fetchSettingValue("reports"); - if (reports[this.id]) { - this.sql = reports[this.id].sql; - this.name = reports[this.id].name; + if (reports[this.index]) { + this.id = reports[this.index].id; + this.sql = reports[this.index].sql; + this.description = reports[this.index].description; } else { this.error = "report not found"; } diff --git a/apps/reports/src/main.js b/apps/reports/src/main.js index 53aae0a698..8b335cb9cb 100644 --- a/apps/reports/src/main.js +++ b/apps/reports/src/main.js @@ -15,7 +15,7 @@ const router = createRouter({ props: true, }, { - path: "/:id", + path: "/:index", name: "edit", component: ViewReport, props: true, diff --git a/backend/molgenis-emx2-datamodels/src/main/java/org/molgenis/emx2/datamodels/PetStoreLoader.java b/backend/molgenis-emx2-datamodels/src/main/java/org/molgenis/emx2/datamodels/PetStoreLoader.java index 3db119374d..177496a940 100644 --- a/backend/molgenis-emx2-datamodels/src/main/java/org/molgenis/emx2/datamodels/PetStoreLoader.java +++ b/backend/molgenis-emx2-datamodels/src/main/java/org/molgenis/emx2/datamodels/PetStoreLoader.java @@ -225,9 +225,9 @@ private void loadExampleData(Schema schema) { .getMetadata() .setSetting( "reports", - "[{\"id\":0,\"name\":\"pet report\",\"sql\":\"select * from \\\"Pet\\\"\"}," - + "{\"id\":1,\"name\":\"pet report with parameters\",\"sql\":\"select * from \\\"Pet\\\" p where p.name=ANY(${name:string_array})\"}," - + "{\"id\":2,\"name\":\"jsonb\",\"sql\":\"SELECT jsonb_agg(to_jsonb(\\\"Pet\\\")) AS result FROM \\\"Pet\\\"\"}," - + "{\"id\":3,\"name\":\"jsonb rows\",\"sql\":\"SELECT to_jsonb(\\\"Pet\\\") AS result FROM \\\"Pet\\\"\"}]"); + "[{\"id\":\"report1\",\"description\":\"pet report\",\"sql\":\"select * from \\\"Pet\\\"\"}," + + "{\"id\":\"report2\",\"description\":\"pet report with parameters\",\"sql\":\"select * from \\\"Pet\\\" p where p.name=ANY(${name:string_array})\"}," + + "{\"id\":\"report3\",\"description\":\"jsonb\",\"sql\":\"SELECT jsonb_agg(to_jsonb(\\\"Pet\\\")) AS result FROM \\\"Pet\\\"\"}," + + "{\"id\":\"report4\",\"description\":\"jsonb rows\",\"sql\":\"SELECT to_jsonb(\\\"Pet\\\") AS result FROM \\\"Pet\\\"\"}]"); } } diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java index 1b703a6120..749df2d313 100644 --- a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java +++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java @@ -8,17 +8,15 @@ import static org.molgenis.emx2.graphql.GraphqlConstants.INHERITED; import static org.molgenis.emx2.graphql.GraphqlConstants.KEY; import static org.molgenis.emx2.json.JsonUtil.jsonToSchema; +import static org.molgenis.emx2.settings.ReportUtils.getReportAsJson; +import static org.molgenis.emx2.settings.ReportUtils.getReportCount; import com.fasterxml.jackson.databind.ObjectMapper; import graphql.Scalars; import graphql.schema.*; import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Stream; -import org.jooq.JSONB; import org.molgenis.emx2.*; import org.molgenis.emx2.json.JsonUtil; import org.molgenis.emx2.sql.SqlDatabase; @@ -863,7 +861,7 @@ public GraphQLFieldDefinition schemaReportsField(Schema schema) { GraphQLFieldDefinition.newFieldDefinition() .name(COUNT) .type(Scalars.GraphQLInt))) - .argument(GraphQLArgument.newArgument().name(ID).type(Scalars.GraphQLInt)) + .argument(GraphQLArgument.newArgument().name(ID).type(Scalars.GraphQLString)) .argument( GraphQLArgument.newArgument() .name(PARAMETERS) @@ -872,52 +870,16 @@ public GraphQLFieldDefinition schemaReportsField(Schema schema) { .argument(GraphQLArgument.newArgument().name(OFFSET).type(Scalars.GraphQLInt)) .dataFetcher( dataFetchingEnvironment -> { - Integer id = null; Map result = new LinkedHashMap<>(); - try { - String reportsJson = schema.getMetadata().getSetting("reports"); - logger.info("REPORT value: " + reportsJson); - if (reportsJson != null) { - id = dataFetchingEnvironment.getArgument(ID); - Integer offset = dataFetchingEnvironment.getArgumentOrDefault(OFFSET, 0); - Integer limit = dataFetchingEnvironment.getArgumentOrDefault(LIMIT, 10); - Map parameters = - convertKeyValueListToMap(dataFetchingEnvironment.getArgument(PARAMETERS)); - List> reportList = - new ObjectMapper().readValue(reportsJson, List.class); - Map report = reportList.get(id); - String sql = report.get("sql") + " LIMIT " + limit + " OFFSET " + offset; - String countSql = - String.format("select count(*) from (%s) as count", report.get("sql")); - result.put(DATA, convertToJson(schema.retrieveSql(sql, parameters))); - result.put( - COUNT, - schema.retrieveSql(countSql, parameters).get(0).get("count", Integer.class)); - } - return result; - } catch (Exception e) { - throw new MolgenisException("Retrieve of report '" + id + "' failed ", e); - } + final String id = dataFetchingEnvironment.getArgument(ID); + Integer offset = dataFetchingEnvironment.getArgumentOrDefault(OFFSET, 0); + Integer limit = dataFetchingEnvironment.getArgumentOrDefault(LIMIT, 10); + Map parameters = + convertKeyValueListToMap(dataFetchingEnvironment.getArgument(PARAMETERS)); + result.put(DATA, getReportAsJson(id, schema, parameters, limit, offset)); + result.put(COUNT, getReportCount(id, schema, parameters)); + return result; }) .build(); } - - private String convertToJson(List rows) { - ObjectMapper objectMapper = new ObjectMapper(); - try { - List> result = new ArrayList<>(); - for (Row row : rows) { - if (row.getValueMap().size() == 1) { - Object value = row.getValueMap().values().iterator().next(); - if (value instanceof JSONB) { - return value.toString(); - } - } - result.add(row.getValueMap()); - } - return objectMapper.writeValueAsString(result); - } catch (Exception e) { - throw new MolgenisException("Cannot convert sql result set to json", e); - } - } } diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/settings/ReportUtils.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/settings/ReportUtils.java new file mode 100644 index 0000000000..5d5044db30 --- /dev/null +++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/settings/ReportUtils.java @@ -0,0 +1,72 @@ +package org.molgenis.emx2.settings; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.jooq.JSONB; +import org.molgenis.emx2.MolgenisException; +import org.molgenis.emx2.Row; +import org.molgenis.emx2.Schema; + +public class ReportUtils { + public static Map getReportById(String reportId, Schema schema) { + try { + String reportsJson = schema.getMetadata().getSetting("reports"); + List> reportList = new ObjectMapper().readValue(reportsJson, List.class); + if (reportsJson != null) { + Optional> reportOptional = + reportList.stream().filter(r -> reportId.equals(r.get("id"))).findFirst(); + if (reportOptional.isPresent()) { + return reportOptional.get(); + } + } + } catch (Exception e) { + // nothing to do, error will be handled below + } + throw new MolgenisException("Report not found id=" + reportId); + } + + public static String getReportAsJson(String id, Schema schema, Map parameters) { + Map report = getReportById(id, schema); + return convertToJson(schema.retrieveSql(report.get("sql"), parameters)); + } + + public static String getReportAsJson( + String id, Schema schema, Map parameters, int limit, int offset) { + Map report = getReportById(id, schema); + String sql = report.get("sql") + " LIMIT " + limit + " OFFSET " + offset; + return convertToJson(schema.retrieveSql(sql, parameters)); + } + + public static List getReportAsRows(String id, Schema schema, Map parameters) { + Map report = getReportById(id, schema); + return schema.retrieveSql(report.get("sql"), parameters); + } + + public static Integer getReportCount(String id, Schema schema, Map parameters) { + Map report = getReportById(id, schema); + String countSql = String.format("select count(*) from (%s) as count", report.get("sql")); + return schema.retrieveSql(countSql, parameters).get(0).get("count", Integer.class); + } + + private static String convertToJson(List rows) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + List> result = new ArrayList<>(); + for (Row row : rows) { + if (row.getValueMap().size() == 1) { + Object value = row.getValueMap().values().iterator().next(); + if (value instanceof JSONB) { + return value.toString(); + } + } + result.add(row.getValueMap()); + } + return objectMapper.writeValueAsString(result); + } catch (Exception e) { + throw new MolgenisException("Cannot convert sql result set to json", e); + } + } +} diff --git a/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java b/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java index ab93425dd2..2468d76e61 100644 --- a/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java +++ b/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java @@ -875,12 +875,21 @@ public void testReport() throws IOException { schema = database.dropCreateSchema(schemaName); PET_STORE.getImportTask(schema, true).run(); grapql = new GraphqlApiFactory().createGraphqlForSchema(schema, taskService); - JsonNode result = execute("{_reports(id:0){data,count}}"); + JsonNode result = execute("{_reports(id:\"report1\"){data,count}}"); assertTrue(result.at("/_reports/data").textValue().contains("pooky")); assertEquals(8, result.at("/_reports/count").intValue()); - // report 1 has parameters - result = execute("{_reports(id:1,parameters:{key:\"name\", value:\"spike\"}){data,count}}"); + // report 2 has parameters + result = + execute( + "{_reports(id:\"report2\",parameters:{key:\"name\", value:\"spike\"}){data,count}}"); + assertTrue(result.at("/_reports/data").textValue().contains("spike")); + assertEquals(1, result.at("/_reports/count").intValue()); + + // report by id=report1 + result = + execute( + "{_reports(id:\"report2\",parameters:{key:\"name\", value:\"spike\"}){data,count}}"); assertTrue(result.at("/_reports/data").textValue().contains("spike")); assertEquals(1, result.at("/_reports/count").intValue()); } diff --git a/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/JsonApi.java b/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/JsonApi.java index 662c578768..cbaad1d955 100644 --- a/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/JsonApi.java +++ b/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/JsonApi.java @@ -1,5 +1,6 @@ package org.molgenis.emx2.web; +import static org.molgenis.emx2.settings.ReportUtils.getReportAsJson; import static org.molgenis.emx2.web.MolgenisWebservice.getSchema; import static org.molgenis.emx2.web.ZipApi.getReportParameters; @@ -7,12 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import io.javalin.http.Context; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.jooq.JSONB; -import org.molgenis.emx2.Row; +import java.util.*; import org.molgenis.emx2.Schema; public class JsonApi { @@ -29,30 +25,11 @@ public static void create(Javalin app) { public static void getJsonReport(Context ctx) throws JsonProcessingException { Schema schema = getSchema(ctx); String reports = ctx.queryParam("id"); - Map parameters = getReportParameters(ctx); - String reportsJson = schema.getMetadata().getSetting("reports"); - + Map parameters = getReportParameters(ctx); ObjectMapper mapper = new ObjectMapper(); - List> reportList = mapper.readValue(reportsJson, List.class); Map jsonResponse = new HashMap<>(); - for (String reportId : reports.split(",")) { - Map reportObject = reportList.get(Integer.parseInt(reportId.trim())); - String sql = (String) reportObject.get("sql"); - String name = (String) reportObject.get("name"); - List rows = schema.retrieveSql(sql, parameters); - List result = new ArrayList<>(); - for (Row row : rows) { - // single json object will not be nested in key/value - if (rows.get(0).getValueMap().size() == 1 - && rows.get(0).getValueMap().values().iterator().next() instanceof JSONB) { - result.add( - mapper.readTree(rows.get(0).getValueMap().values().iterator().next().toString())); - } else { - result.add(row.getValueMap()); - } - jsonResponse.put(name, result); - } + jsonResponse.put(reportId, getReportAsJson(reportId, schema, parameters)); } if (jsonResponse.size() == 1) { ctx.json(jsonResponse.values().iterator().next()); diff --git a/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/ZipApi.java b/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/ZipApi.java index ff6d96e127..97b6393fa6 100644 --- a/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/ZipApi.java +++ b/backend/molgenis-emx2-webapi/src/main/java/org/molgenis/emx2/web/ZipApi.java @@ -1,11 +1,10 @@ package org.molgenis.emx2.web; +import static org.molgenis.emx2.settings.ReportUtils.getReportAsRows; import static org.molgenis.emx2.web.Constants.TABLE; import static org.molgenis.emx2.web.DownloadApiUtils.includeSystemColumns; import static org.molgenis.emx2.web.MolgenisWebservice.getSchema; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import io.javalin.http.Context; import jakarta.servlet.MultipartConfigElement; @@ -174,21 +173,16 @@ static void getZippedReports(Context ctx) throws IOException { } } - static void generateReportsToStore(Context ctx, TableStore store) throws JsonProcessingException { + static void generateReportsToStore(Context ctx, TableStore store) { String reports = ctx.queryParam("id"); Schema schema = getSchema(ctx); Map parameters = getReportParameters(ctx); - String reportsJson = schema.getMetadata().getSetting("reports"); - List> reportList = new ObjectMapper().readValue(reportsJson, List.class); for (String reportId : reports.split(",")) { - Map reportObject = reportList.get(Integer.parseInt(reportId)); - String sql = (String) reportObject.get("sql"); - String name = (String) reportObject.get("name"); - List rows = schema.retrieveSql(sql, parameters); + List rows = getReportAsRows(reportId, schema, parameters); if (rows.size() > 0) { - store.writeTable(name, new ArrayList<>(rows.get(0).getColumnNames()), rows); + store.writeTable(reportId, new ArrayList<>(rows.get(0).getColumnNames()), rows); } else { - store.writeTable(name, new ArrayList<>(), rows); + store.writeTable(reportId, new ArrayList<>(), rows); } } } diff --git a/backend/molgenis-emx2-webapi/src/test/java/org.molgenis.emx2.web/WebApiSmokeTests.java b/backend/molgenis-emx2-webapi/src/test/java/org.molgenis.emx2.web/WebApiSmokeTests.java index b39131d7d2..d6682e4f90 100644 --- a/backend/molgenis-emx2-webapi/src/test/java/org.molgenis.emx2.web/WebApiSmokeTests.java +++ b/backend/molgenis-emx2-webapi/src/test/java/org.molgenis.emx2.web/WebApiSmokeTests.java @@ -169,7 +169,7 @@ public void testReports() throws IOException { // check if reports work byte[] zipContents = - getContentAsByteArray(ACCEPT_ZIP, "/pet store reports/api/reports/zip?id=0"); + getContentAsByteArray(ACCEPT_ZIP, "/pet store reports/api/reports/zip?id=report1"); File zipFile = createTempFile(zipContents, ".zip"); TableStore store = new TableStoreForCsvInZipFile(zipFile.toPath()); store.containsTable("pet report"); @@ -177,58 +177,78 @@ public void testReports() throws IOException { // check if reports work with parameters zipContents = getContentAsByteArray( - ACCEPT_ZIP, "/pet store reports/api/reports/zip?id=1&name=spike,pooky"); + ACCEPT_ZIP, "/pet store reports/api/reports/zip?id=report2&name=spike,pooky"); zipFile = createTempFile(zipContents, ".zip"); store = new TableStoreForCsvInZipFile(zipFile.toPath()); store.containsTable("pet report with parameters"); // check if reports work byte[] excelContents = - getContentAsByteArray(ACCEPT_ZIP, "/pet store reports/api/reports/excel?id=0"); + getContentAsByteArray(ACCEPT_ZIP, "/pet store reports/api/reports/excel?id=report1"); File excelFile = createTempFile(excelContents, ".xlsx"); store = new TableStoreForXlsxFile(excelFile.toPath()); - assertTrue(store.containsTable("pet report")); + assertTrue(store.containsTable("report1")); // check if reports work with parameters excelContents = getContentAsByteArray( - ACCEPT_ZIP, "/pet store reports/api/reports/excel?id=1&name=spike,pooky"); + ACCEPT_ZIP, "/pet store reports/api/reports/excel?id=report2&name=spike,pooky"); excelFile = createTempFile(excelContents, ".xlsx"); store = new TableStoreForXlsxFile(excelFile.toPath()); - assertTrue(store.containsTable("pet report with parameters")); + assertTrue(store.containsTable("report2")); assertTrue(excelContents.length > 0); // test json report api String jsonResults = - given().sessionId(SESSION_ID).get("/pet store reports/api/reports/json?id=0").asString(); - assertFalse(jsonResults.contains("pet report"), "single result should not include report name"); + given() + .sessionId(SESSION_ID) + .get("/pet store reports/api/reports/json?id=report1") + .asString(); + assertFalse( + jsonResults.contains("report1"), + "single result should not include report name"); // are we sure about this? jsonResults = given() .sessionId(SESSION_ID) - .get("/pet store reports/api/reports/json?id=0,1&name=pooky") + .get("/pet store reports/api/reports/json?id=report1,report2&name=pooky") .asString(); assertTrue( - jsonResults.contains("pet report"), + jsonResults.contains("report1"), "multiple results should use the report name to nest results"); + // check that id is for keys + jsonResults = + given() + .sessionId(SESSION_ID) + .get("/pet store reports/api/reports/json?id=report1,report2&name=pooky") + .asString(); + assertTrue(jsonResults.contains("report1"), "should use report id as key"); + assertTrue(jsonResults.contains("report2"), "should use report id as key"); + jsonResults = given() .sessionId(SESSION_ID) - .get("/pet store reports/api/reports/json?id=1&name=spike,pooky") + .get("/pet store reports/api/reports/json?id=report2&name=spike,pooky") .asString(); assertTrue(jsonResults.contains("pooky")); // test report using jsonb_agg jsonResults = - given().sessionId(SESSION_ID).get("/pet store reports/api/reports/json?id=2").asString(); + given() + .sessionId(SESSION_ID) + .get("/pet store reports/api/reports/json?id=report3") + .asString(); ObjectMapper objectMapper = new ObjectMapper(); List jsonbResult = objectMapper.readValue(jsonResults, List.class); assertTrue(jsonbResult.get(0).toString().contains("pooky")); // test report using jsonb rows jsonResults = - given().sessionId(SESSION_ID).get("/pet store reports/api/reports/json?id=3").asString(); - jsonbResult = objectMapper.readValue(jsonResults, List.class); - assertTrue(jsonbResult.get(0).toString().contains("pooky")); + given() + .sessionId(SESSION_ID) + .get("/pet store reports/api/reports/json?id=report4") + .asString(); + Object result = objectMapper.readValue(jsonResults, Object.class); + assertTrue(result.toString().contains("pooky")); } @Test diff --git a/docs/molgenis/use_reports.md b/docs/molgenis/use_reports.md index dbcfe9cc76..3c426cf1b0 100644 --- a/docs/molgenis/use_reports.md +++ b/docs/molgenis/use_reports.md @@ -19,6 +19,26 @@ Example of a report: ```select * from "Pet"``` +Each report requires an unique id to retrieve, and an optional human readable description. + +n.b. since version 12 the 'id' is required and used, please update reports when you created them before 12. Technically reports are stored as json into +settings under key 'reports', for example: + +``` +[ + { + "id":"report1", + "description":"pet report", + "sql":"select * from \"Pet\"" + }, + { + "id":"report2", + "description":"pet report with parameters", + "sql":"select * from \"Pet\" p where p.name=ANY(${name:string_array})" + }, +] +``` + # Using parameters You can parameterize your queries using ```${name}``` or when you need strong typing ```${name:string_array}``` @@ -36,9 +56,10 @@ it into SQL. You can also use these reports in scripts, to directly download the results, for example: -* http://myserver.com/schema/api/report/1 -* http://myserver.com/schema/api/report/1,2 (then you get result from two reports) -* http://myserver.com/schema/api/report/1,2?name=pooky,spike (then you get result from two reports using 'name' as parameter) +* http:///api/reports/report1 +* http:////api/reports/report1,report2 (then you get result from two reports) +* http:////api/reports/report2,report3?name=pooky,spike (then you get result from two reports using 'name' as parameter) +* https:////api/reports/json?id=report3 # Returning JSON so you can use reports as REST like 'get' API @@ -50,4 +71,4 @@ Results in something like: ```[{name= ...},{name=...}]``` N.B. when you ask for result from multiple reports you will get a nested result using the report names as keys, i.e. -```{report1:[{...}], report2:[{...}]}``` \ No newline at end of file +```{report1:[{...}], report2:[{...}]}```