Skip to content

Commit

Permalink
avniproject#762 | A working API has been successfully built to automa…
Browse files Browse the repository at this point in the history
…te the creation of questions for the tables subject types and Address
  • Loading branch information
ombhardwajj committed Jul 24, 2024
1 parent 813146b commit 25dbade
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.avni.server.dao.metabase;

import com.fasterxml.jackson.databind.JsonNode;
import org.avni.server.domain.metabase.Database;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Repository;
Expand All @@ -16,4 +17,24 @@ public Database save(Database database) {
database.setId(response.getId());
return database;
}

public JsonNode getDatabaseDetails(int databaseId) {
String url = metabaseApiUrl + "/database/" + databaseId + "?include=tables";
return getForObject(url, JsonNode.class);
}

public JsonNode getFields(int databaseId) {
String url = metabaseApiUrl + "/database/" + databaseId + "/fields";
return getForObject(url, JsonNode.class);
}

public JsonNode getInitialSyncStatus(int databaseId) {
String url = metabaseApiUrl + "/database/" + databaseId;
return getForObject(url, JsonNode.class);
}

public JsonNode getDataset(String requestBody) {
String url = metabaseApiUrl + "/dataset";
return postForObject(url, requestBody, JsonNode.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class MetabaseConnector {
protected String metabaseApiUrl;

@Value("${metabase.api.key}")
private String apiKey;
protected String apiKey;

public MetabaseConnector(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
Expand All @@ -42,7 +42,7 @@ protected void sendPutRequest(String url, Map<String, ?> requestBody) {
restTemplate.exchange(url, HttpMethod.PUT, entity, Map.class);
}

protected <T> T postForObject(String url, Object request, Class<T> responseType) {
public <T> T postForObject(String url, Object request, Class<T> responseType) {
HttpEntity<Object> entity = createHttpEntity(request);
return restTemplate.postForObject(url, entity, responseType);
}
Expand All @@ -57,4 +57,5 @@ protected HttpEntity<Map<String, Object>> createJsonEntity(GroupPermissionsBody
HttpHeaders headers = getHeaders();
return new HttpEntity<>(body.getBody(), headers);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.avni.server.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.avni.server.dao.metabase.DatabaseRepository;
import org.avni.server.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class DatabaseService {

This comment has been minimized.

Copy link
@vinayvenu

vinayvenu Jul 26, 2024

This feels a bit Generic. Maybe call it MetabaseDatabaseService, or put all these services under a metabase package?

This comment has been minimized.

Copy link
@ombhardwajj

ombhardwajj Jul 27, 2024

Author Owner

Done!


private final DatabaseRepository databaseRepository;
private final ObjectMapper objectMapper;
private final MetabaseService metabaseService;

@Value("${metabase.api.url}")
private String metabaseApiUrl;

@Value("${metabase.api.key}")
private String apiKey;

@Autowired
public DatabaseService(DatabaseRepository databaseRepository, ObjectMapper objectMapper, MetabaseService metabaseService) {
this.databaseRepository = databaseRepository;
this.objectMapper = objectMapper;
this.metabaseService = metabaseService;
}

public int getTableIdByName(int databaseId, String tableName) {
JsonNode rootNode = databaseRepository.getDatabaseDetails(databaseId);
JsonNode tablesArray = rootNode.path("tables");
for (JsonNode tableNode : tablesArray) {
if (tableName.equals(tableNode.path("display_name").asText())) {
return tableNode.path("id").asInt();
}
}
return -1;
}

public int getFieldIdByTableNameAndFieldName(int databaseId, String tableName, String fieldName) {
JsonNode fieldsArray = databaseRepository.getFields(databaseId);
String snakeCaseTableName = StringUtils.toSnakeCase(tableName);
for (JsonNode fieldNode : fieldsArray) {
if (snakeCaseTableName.equals(fieldNode.path("table_name").asText()) && fieldName.equals(fieldNode.path("name").asText())) {
return fieldNode.path("id").asInt();
}
}
return -1;
}

public void waitForSyncCompletion(int databaseId) {
while (true) {
String syncStatus = getInitialSyncStatus(databaseId);
if ("complete".equals(syncStatus)) {
return;
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread was interrupted while waiting for sync completion", e);
}
}
}

public String getInitialSyncStatus(int databaseId) {
JsonNode responseBody = databaseRepository.getInitialSyncStatus(databaseId);
return responseBody.path("initial_sync_status").asText();
}

public List<String> getSubjectTypeNames(int databaseId) {
int tableId = getTableIdByName(databaseId, "Subject Type");
String requestBody = "{\"database\":" + databaseId + ",\"query\":{\"source-table\":" + tableId + "},\"type\":\"query\",\"parameters\":[]}";

JsonNode response = databaseRepository.getDataset(requestBody);

JsonNode dataNode = response.path("data");
JsonNode rows = dataNode.path("rows");

List<String> subjectTypeNames = new ArrayList<>();
for (JsonNode row : rows) {
String name = row.get(4).asText();
boolean isVoided = row.get(6).asBoolean();
if (!isVoided) {
subjectTypeNames.add(name);
}
}
return subjectTypeNames;
}

public void createQuestionsForSubjectTypes() {
int databaseId = metabaseService.getGlobalDatabaseId();
int collectionId = metabaseService.getGlobalCollectionId();

waitForSyncCompletion(databaseId);

This comment has been minimized.

Copy link
@vinayvenu

vinayvenu Jul 26, 2024

This is a really long operation. I am not sure waiting is a good idea.

This comment has been minimized.

Copy link
@vinayvenu

vinayvenu Jul 26, 2024

One way to do this is for the operation to fail if sync is not complete. We can do the creation later. Also have another api that verifies if sync is complete.

This comment has been minimized.

Copy link
@ombhardwajj

ombhardwajj Jul 27, 2024

Author Owner

Done!


List<String> subjectTypeNames = getSubjectTypeNames(databaseId);

int addressTableId = getTableIdByName(databaseId, "Address");
int joinFieldId1 = getFieldIdByTableNameAndFieldName(databaseId, "Address", "id");

for (String subjectTypeName : subjectTypeNames) {
int subjectTableId = getTableIdByName(databaseId, subjectTypeName);
int joinFieldId2 = getFieldIdByTableNameAndFieldName(databaseId, subjectTypeName, "address_id");

ObjectNode datasetQuery = objectMapper.createObjectNode();
datasetQuery.put("database", databaseId);
datasetQuery.put("type", "query");

ObjectNode query = objectMapper.createObjectNode();
query.put("source-table", addressTableId);

ArrayNode joins = objectMapper.createArrayNode();
ObjectNode join = objectMapper.createObjectNode();
join.put("fields", "all");
join.put("alias", subjectTypeName);

ArrayNode condition = objectMapper.createArrayNode();
condition.add("=");
condition.add(objectMapper.createArrayNode().add("field").add(joinFieldId1).add(objectMapper.createObjectNode().put("base-type", "type/Integer")));
condition.add(objectMapper.createArrayNode().add("field").add(joinFieldId2).add(objectMapper.createObjectNode().put("base-type", "type/Integer").put("join-alias", subjectTypeName)));

join.set("condition", condition);
join.put("source-table", subjectTableId);
joins.add(join);

query.set("joins", joins);
datasetQuery.set("query", query);

ObjectNode body = objectMapper.createObjectNode();
body.put("name", "Address + " + subjectTypeName);
body.set("dataset_query", datasetQuery);
body.put("display", "table");
body.putNull("description");
body.set("visualization_settings", objectMapper.createObjectNode());
body.put("collection_id", collectionId);
body.putNull("collection_position");
body.putNull("result_metadata");

databaseRepository.postForObject(metabaseApiUrl + "/card", body, JsonNode.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,37 @@
import org.avni.server.dao.metabase.DatabaseRepository;
import org.avni.server.dao.metabase.GroupPermissionsRepository;
import org.avni.server.domain.Organisation;
import org.avni.server.domain.metabase.AvniDatabase;
import org.avni.server.domain.metabase.Collection;
import org.avni.server.domain.metabase.CollectionPermissionsService;
import org.avni.server.domain.metabase.CollectionResponse;
import org.avni.server.domain.metabase.Database;
import org.avni.server.domain.metabase.DatabaseDetails;
import org.avni.server.domain.metabase.Group;
import org.avni.server.domain.metabase.GroupPermissionsService;
import org.avni.server.domain.metabase.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;


@Service
public class MetabaseService {

private final OrganisationService organisationService;
private final AvniDatabase avniDatabase;
private final DatabaseRepository databaseRepository;
private final DatabaseService databaseService;
private final GroupPermissionsRepository groupPermissionsRepository;
private final CollectionPermissionsRepository collectionPermissionsRepository;
private final CollectionRepository collectionRepository;
private Database globalDatabase;
private CollectionResponse globalCollection;

@Autowired
public MetabaseService(OrganisationService organisationService,
AvniDatabase avniDatabase,
DatabaseRepository databaseRepository,
@Lazy DatabaseService databaseService,
GroupPermissionsRepository groupPermissionsRepository,
GroupPermissionsService permissions,
CollectionPermissionsRepository collectionPermissionsRepository,
CollectionRepository collectionRepository) {
this.organisationService = organisationService;
this.avniDatabase = avniDatabase;
this.databaseRepository = databaseRepository;
this.databaseService = databaseService;
this.groupPermissionsRepository = groupPermissionsRepository;
this.collectionPermissionsRepository = collectionPermissionsRepository;
this.collectionRepository = collectionRepository;
Expand All @@ -48,8 +47,10 @@ public void setupMetabase() {
String dbUser = currentOrganisation.getDbUser();

Database database = databaseRepository.save(new Database(name, "postgres", new DatabaseDetails(avniDatabase, dbUser)));

this.globalDatabase = database;

CollectionResponse metabaseCollection = collectionRepository.save(new Collection(name, name + " collection"));
this.globalCollection = metabaseCollection;

Group metabaseGroup = groupPermissionsRepository.save(new Group(name));

Expand All @@ -61,4 +62,16 @@ public void setupMetabase() {
collectionPermissions.updatePermissions(metabaseGroup.getId(), metabaseCollection.getId());
collectionPermissionsRepository.updateCollectionPermissions(collectionPermissions, metabaseGroup.getId(), metabaseCollection.getId());
}

public void createQuestionsForSubjectTypes() {
databaseService.createQuestionsForSubjectTypes();
}

public int getGlobalDatabaseId() {
return globalDatabase.getId();
}

public int getGlobalCollectionId() {
return globalCollection.getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.avni.server.util;

public class StringUtils {
public static String toSnakeCase(String input) {
if (input == null) {
return null;
}
return input.trim().replaceAll(" +", "_").toLowerCase();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.avni.server.web;

import org.avni.server.service.DatabaseService;
import org.avni.server.domain.accessControl.PrivilegeType;
import org.avni.server.dao.metabase.MetabaseConnector;
import org.avni.server.dao.metabase.DatabaseRepository;
import org.avni.server.service.MetabaseService;
import org.avni.server.service.UserService;
import org.avni.server.service.accessControl.AccessControlService;
Expand All @@ -9,10 +11,12 @@
@RestController
@RequestMapping("/api/metabase")
public class MetabaseController {
private final DatabaseService databaseService;
private final MetabaseService metabaseService;
private final AccessControlService accessControlService;

public MetabaseController(MetabaseService metabaseService, UserService userService,AccessControlService accessControlService) {
public MetabaseController(DatabaseService databaseService,MetabaseService metabaseService, MetabaseConnector metabaseConnector,DatabaseRepository databaseRepository, UserService userService,AccessControlService accessControlService) {
this.databaseService = databaseService;
this.metabaseService = metabaseService;
this.accessControlService= accessControlService;
}
Expand All @@ -21,5 +25,10 @@ public MetabaseController(MetabaseService metabaseService, UserService userServi
public void setupMetabase() {
accessControlService.checkPrivilege(PrivilegeType.EditOrganisationConfiguration);
metabaseService.setupMetabase();
}
}

@PostMapping("/create-questions")
public void createQuestions() {
databaseService.createQuestionsForSubjectTypes();
}
}

0 comments on commit 25dbade

Please sign in to comment.