Skip to content

Commit

Permalink
!feat: enable user defined 'id' for reports so that the get URL can b…
Browse files Browse the repository at this point in the history
…e stable. Breaking change as 'id' will be required to function. (#4520)
  • Loading branch information
mswertz authored Dec 3, 2024
1 parent 139c78e commit 77fa01e
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 130 deletions.
8 changes: 4 additions & 4 deletions apps/reports/src/components/ListReports.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
>
<TableSimple
@rowClick="open"
:columns="['id', 'name']"
:columns="['id', 'description']"
:rows="reportsWithId"
class="bg-white"
selectColumn="id"
Expand Down Expand Up @@ -101,7 +101,7 @@ export default {
if (this.reports) {
let index = 0;
return this.reports.map((report) => {
report.id = index++;
report.index = index++;
return report;
});
}
Expand All @@ -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));
Expand All @@ -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(
Expand Down
46 changes: 35 additions & 11 deletions apps/reports/src/components/ViewEditReport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@
<h2>
Edit report: {{ id }}<IconAction icon="eye" @click="edit = false" />
</h2>
<InputString id="reportName" v-model="name" label="name" />
<InputString
id="reportId"
v-model="id"
label="id"
:required="true"
description="unique index"
/>
<InputString
id="reportDescription"
v-model="description"
label="description"
description="human-readable description"
/>
<InputText
id="reportSql"
v-model="sql"
Expand All @@ -30,9 +42,10 @@
</div>
</div>
<h2 v-else>
Report: {{ name
}}<IconAction v-if="canEdit" icon="pencil-alt" @click="edit = true" />
View report id={{ id }}
<IconAction v-if="canEdit" icon="pencil-alt" @click="edit = true" />
</h2>
<p v-if="description">Description: {{ description }}</p>
<div v-if="parameterInputs">
Please provide parameters:
<FormInput
Expand Down Expand Up @@ -96,15 +109,16 @@ export default {
},
props: {
session: Object,
id: String,
index: String,
limit: { type: Number, default: 5 },
},
data() {
return {
rows: undefined,
count: null,
id: null,
sql: 'select * from "Pet"',
name: null,
description: null,
parameters: {},
error: null,
success: null,
Expand Down Expand Up @@ -175,7 +189,7 @@ export default {
const offset = this.limit * (this.page - 1);
const result = await request(
"graphql",
`query report($parameters:[MolgenisSettingsInput]) {_reports(id:${this.id},parameters:$parameters,limit:${this.limit},offset:${offset}){data,count}}`,
`query report($parameters:[MolgenisSettingsInput]) {_reports(id:"${this.id}",parameters:$parameters,limit:${this.limit},offset:${offset}){data,count}}`,
{
parameters: this.parameterKeyValueMap,
}
Expand All @@ -186,11 +200,20 @@ export default {
this.count = result._reports.count;
},
async save() {
if (this.id == null) {
this.error = "id is required";
return;
}
if (this.sql == null) {
this.error = "sql is required";
return;
}
this.succes = null;
this.error = null;
const reports = await this.client.fetchSettingValue("reports");
reports[this.id].sql = this.sql;
reports[this.id].name = this.name;
reports[this.index].id = this.id;
reports[this.index].sql = this.sql;
reports[this.index].description = this.description;
this.client
.saveSetting("reports", reports)
.then((res) => {
Expand All @@ -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";
}
Expand Down
2 changes: 1 addition & 1 deletion apps/reports/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const router = createRouter({
props: true,
},
{
path: "/:id",
path: "/:index",
name: "edit",
component: ViewReport,
props: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\\\"\"}]");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -872,52 +870,16 @@ public GraphQLFieldDefinition schemaReportsField(Schema schema) {
.argument(GraphQLArgument.newArgument().name(OFFSET).type(Scalars.GraphQLInt))
.dataFetcher(
dataFetchingEnvironment -> {
Integer id = null;
Map<String, Object> 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<String, String> parameters =
convertKeyValueListToMap(dataFetchingEnvironment.getArgument(PARAMETERS));
List<Map<String, Object>> reportList =
new ObjectMapper().readValue(reportsJson, List.class);
Map<String, Object> 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<String, String> 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<Row> rows) {
ObjectMapper objectMapper = new ObjectMapper();
try {
List<Map<String, Object>> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> getReportById(String reportId, Schema schema) {
try {
String reportsJson = schema.getMetadata().getSetting("reports");
List<Map<String, String>> reportList = new ObjectMapper().readValue(reportsJson, List.class);
if (reportsJson != null) {
Optional<Map<String, String>> 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<String, ?> parameters) {
Map<String, String> report = getReportById(id, schema);
return convertToJson(schema.retrieveSql(report.get("sql"), parameters));
}

public static String getReportAsJson(
String id, Schema schema, Map<String, ?> parameters, int limit, int offset) {
Map<String, String> report = getReportById(id, schema);
String sql = report.get("sql") + " LIMIT " + limit + " OFFSET " + offset;
return convertToJson(schema.retrieveSql(sql, parameters));
}

public static List<Row> getReportAsRows(String id, Schema schema, Map<String, ?> parameters) {
Map<String, String> report = getReportById(id, schema);
return schema.retrieveSql(report.get("sql"), parameters);
}

public static Integer getReportCount(String id, Schema schema, Map<String, ?> parameters) {
Map<String, String> 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<Row> rows) {
ObjectMapper objectMapper = new ObjectMapper();
try {
List<Map<String, Object>> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Loading

0 comments on commit 77fa01e

Please sign in to comment.