Skip to content

Commit 8d61cd6

Browse files
committed
Add Export Example Screener button into main menu, fixed screener import for prod
1 parent 0f8bd6c commit 8d61cd6

19 files changed

Lines changed: 559 additions & 74 deletions

builder-api/src/main/java/org/acme/controller/AccountResource.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.acme.controller;
22

3+
import io.quarkus.logging.Log;
4+
import io.quarkus.runtime.LaunchMode;
35
import io.quarkus.security.identity.SecurityIdentity;
46
import jakarta.inject.Inject;
57
import jakarta.ws.rs.*;
@@ -16,13 +18,17 @@
1618
import org.acme.functions.AccountHooks;
1719
import org.acme.model.dto.Auth.AccountHookRequest;
1820
import org.acme.model.dto.Auth.AccountHookResponse;
21+
import org.acme.service.ExampleScreenerExportService;
1922

2023
@Path("/api")
2124
public class AccountResource {
2225

2326
@Inject
2427
AccountHooks accountHooks;
2528

29+
@Inject
30+
ExampleScreenerExportService exampleScreenerExportService;
31+
2632
@POST
2733
@Consumes(MediaType.APPLICATION_JSON)
2834
@Produces(MediaType.APPLICATION_JSON)
@@ -61,4 +67,36 @@ public Response accountHooks(@Context SecurityIdentity identity,
6167

6268
return Response.ok(responseBody).build();
6369
}
70+
71+
@POST
72+
@Produces(MediaType.APPLICATION_JSON)
73+
@Path("/account/export-example-screener")
74+
public Response exportExampleScreener(@Context SecurityIdentity identity) {
75+
String userId = AuthUtils.getUserId(identity);
76+
77+
if (userId == null) {
78+
return Response.status(Response.Status.UNAUTHORIZED)
79+
.entity(new ApiError(true, "Unauthorized.")).build();
80+
}
81+
82+
if (LaunchMode.current() != LaunchMode.DEVELOPMENT) {
83+
return Response.status(Response.Status.NOT_FOUND).build();
84+
}
85+
86+
try {
87+
ExampleScreenerExportService.ExportSummary summary = exampleScreenerExportService.exportForUser(userId);
88+
return Response.ok(Map.of(
89+
"success", true,
90+
"outputPath", summary.outputPath(),
91+
"screenerCount", summary.screenerCount(),
92+
"firestoreDocuments", summary.firestoreDocuments(),
93+
"storageFiles", summary.storageFiles()
94+
)).build();
95+
} catch (Exception e) {
96+
Log.error("Failed to export example screener seed data for user " + userId, e);
97+
return Response.serverError()
98+
.entity(new ApiError(true, "Failed to export example screener seed data."))
99+
.build();
100+
}
101+
}
64102
}

builder-api/src/main/java/org/acme/functions/AccountHooks.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public class AccountHooks {
1414
public Boolean addExampleScreenerToAccount(String userId) {
1515
try {
1616
Log.info("Running ADD_EXAMPLE_SCREENER hook for user: " + userId);
17-
String screenerId = exampleScreenerImportService.importForUser(userId);
18-
Log.info("Imported example screener " + screenerId + " for user " + userId);
17+
var screenerIds = exampleScreenerImportService.importForUser(userId);
18+
Log.info("Imported example screeners " + screenerIds + " for user " + userId);
1919
return true;
2020
} catch (Exception e) {
2121
Log.error(
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
package org.acme.service;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.google.cloud.Timestamp;
5+
import com.google.firebase.cloud.FirestoreClient;
6+
import io.quarkus.logging.Log;
7+
import jakarta.enterprise.context.ApplicationScoped;
8+
import jakarta.inject.Inject;
9+
import org.acme.constants.CollectionNames;
10+
import org.acme.constants.FieldNames;
11+
import org.acme.persistence.FirestoreUtils;
12+
import org.acme.persistence.StorageService;
13+
import org.eclipse.microprofile.config.inject.ConfigProperty;
14+
15+
import java.io.IOException;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.nio.file.Paths;
19+
import java.time.Instant;
20+
import java.util.ArrayList;
21+
import java.util.Comparator;
22+
import java.util.LinkedHashMap;
23+
import java.util.LinkedHashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.Set;
28+
29+
@ApplicationScoped
30+
public class ExampleScreenerExportService {
31+
private static final Path EXPORT_ROOT = Paths.get("src", "main", "resources", "seed-data", "example-screener");
32+
private static final String SYSTEM_COLLECTION = "system";
33+
private static final String SYSTEM_CONFIG_ID = "config";
34+
35+
private final StorageService storageService;
36+
private final String bucketName;
37+
private final ObjectMapper objectMapper;
38+
39+
@Inject
40+
public ExampleScreenerExportService(
41+
StorageService storageService,
42+
@ConfigProperty(name = "GCS_BUCKET_NAME", defaultValue = "demo-bdt-dev.appspot.com") String bucketName
43+
) {
44+
this.storageService = storageService;
45+
this.bucketName = bucketName;
46+
this.objectMapper = new ObjectMapper();
47+
}
48+
49+
public ExportSummary exportForUser(String userId) throws Exception {
50+
resetExportRoot();
51+
52+
List<Map<String, Object>> workingScreeners = getDocumentsByOwner(CollectionNames.WORKING_SCREENER_COLLECTION, userId);
53+
List<Map<String, Object>> workingCustomChecks = getDocumentsByOwner(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, userId);
54+
List<Map<String, Object>> publishedCustomChecks = getDocumentsByOwner(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, userId);
55+
56+
int firestoreDocuments = 0;
57+
firestoreDocuments += exportScreeners(workingScreeners);
58+
firestoreDocuments += exportChecks(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, workingCustomChecks);
59+
firestoreDocuments += exportChecks(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, publishedCustomChecks);
60+
firestoreDocuments += exportSystemConfig();
61+
62+
int storageFiles = 0;
63+
storageFiles += exportScreenerForms(workingScreeners);
64+
storageFiles += exportCheckDmns(workingCustomChecks);
65+
storageFiles += exportCheckDmns(publishedCustomChecks);
66+
67+
writeManifest(firestoreDocuments, storageFiles);
68+
69+
Log.info("Exported Firebase seed data for user " + userId + " to " + EXPORT_ROOT.toAbsolutePath().normalize());
70+
return new ExportSummary(
71+
EXPORT_ROOT.toAbsolutePath().normalize().toString(),
72+
workingScreeners.size(),
73+
firestoreDocuments,
74+
storageFiles
75+
);
76+
}
77+
78+
private List<Map<String, Object>> getDocumentsByOwner(String collectionName, String userId) {
79+
List<Map<String, Object>> documents = new ArrayList<>(
80+
FirestoreUtils.getFirestoreDocsByField(collectionName, FieldNames.OWNER_ID, userId)
81+
);
82+
documents.sort(Comparator.comparing(document -> requiredString(document, FieldNames.ID, collectionName)));
83+
return documents;
84+
}
85+
86+
private int exportScreeners(List<Map<String, Object>> workingScreeners) throws IOException {
87+
int firestoreDocuments = 0;
88+
89+
for (Map<String, Object> screener : workingScreeners) {
90+
String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION);
91+
writeJsonFile(
92+
EXPORT_ROOT.resolve("firestore").resolve("workingScreener").resolve(screenerId + ".json"),
93+
firestoreDocumentForExport(screener, screenerId)
94+
);
95+
firestoreDocuments++;
96+
97+
firestoreDocuments += exportBenefits(screenerId);
98+
}
99+
100+
return firestoreDocuments;
101+
}
102+
103+
private int exportBenefits(String screenerId) throws IOException {
104+
String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + "/" + screenerId + "/customBenefit";
105+
List<Map<String, Object>> benefits = new ArrayList<>(FirestoreUtils.getAllDocsInCollection(collectionPath));
106+
benefits.sort(Comparator.comparing(benefit -> requiredString(benefit, FieldNames.ID, collectionPath)));
107+
108+
int exportedBenefits = 0;
109+
for (Map<String, Object> benefit : benefits) {
110+
String benefitId = requiredString(benefit, FieldNames.ID, collectionPath);
111+
writeJsonFile(
112+
EXPORT_ROOT.resolve("firestore")
113+
.resolve("workingScreener")
114+
.resolve(screenerId)
115+
.resolve("customBenefit")
116+
.resolve(benefitId + ".json"),
117+
firestoreDocumentForExport(benefit, benefitId)
118+
);
119+
exportedBenefits++;
120+
}
121+
122+
return exportedBenefits;
123+
}
124+
125+
private int exportChecks(String collectionName, List<Map<String, Object>> checks) throws IOException {
126+
int exportedChecks = 0;
127+
for (Map<String, Object> check : checks) {
128+
String checkId = requiredString(check, FieldNames.ID, collectionName);
129+
writeJsonFile(
130+
EXPORT_ROOT.resolve("firestore").resolve(collectionName).resolve(checkId + ".json"),
131+
firestoreDocumentForExport(check, checkId)
132+
);
133+
exportedChecks++;
134+
}
135+
return exportedChecks;
136+
}
137+
138+
private int exportSystemConfig() throws IOException {
139+
Optional<Map<String, Object>> config = FirestoreUtils.getFirestoreDocById(SYSTEM_COLLECTION, SYSTEM_CONFIG_ID);
140+
if (config.isEmpty()) {
141+
return 0;
142+
}
143+
144+
writeJsonFile(
145+
EXPORT_ROOT.resolve("firestore").resolve(SYSTEM_COLLECTION).resolve(SYSTEM_CONFIG_ID + ".json"),
146+
firestoreDocumentForExport(config.get(), SYSTEM_CONFIG_ID)
147+
);
148+
return 1;
149+
}
150+
151+
private int exportScreenerForms(List<Map<String, Object>> workingScreeners) throws IOException {
152+
int exportedForms = 0;
153+
154+
for (Map<String, Object> screener : workingScreeners) {
155+
String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION);
156+
Optional<String> formSchema = storageService.getStringFromStorage(
157+
storageService.getScreenerWorkingFormSchemaPath(screenerId)
158+
);
159+
160+
if (formSchema.isEmpty()) {
161+
continue;
162+
}
163+
164+
writeStringFile(
165+
EXPORT_ROOT.resolve("storage").resolve("form").resolve("working").resolve(screenerId + ".json"),
166+
formSchema.get()
167+
);
168+
exportedForms++;
169+
}
170+
171+
return exportedForms;
172+
}
173+
174+
private int exportCheckDmns(List<Map<String, Object>> checks) throws IOException {
175+
int exportedDmns = 0;
176+
Set<String> exportedIds = new LinkedHashSet<>();
177+
178+
for (Map<String, Object> check : checks) {
179+
String checkId = requiredString(check, FieldNames.ID, "customCheck");
180+
if (!exportedIds.add(checkId)) {
181+
continue;
182+
}
183+
184+
Optional<String> dmnModel = storageService.getStringFromStorage(storageService.getCheckDmnModelPath(checkId));
185+
if (dmnModel.isEmpty()) {
186+
continue;
187+
}
188+
189+
writeStringFile(
190+
EXPORT_ROOT.resolve("storage").resolve("check").resolve(checkId + ".dmn"),
191+
dmnModel.get()
192+
);
193+
exportedDmns++;
194+
}
195+
196+
return exportedDmns;
197+
}
198+
199+
private void writeManifest(int firestoreDocuments, int storageFiles) throws IOException {
200+
Map<String, Object> manifest = new LinkedHashMap<>();
201+
manifest.put("exportedAt", Instant.now().toString());
202+
manifest.put("source", "builder-api");
203+
manifest.put("projectId", FirestoreClient.getFirestore().getOptions().getProjectId());
204+
manifest.put("storageBucket", bucketName);
205+
manifest.put("firestoreDocuments", firestoreDocuments);
206+
manifest.put("storageFiles", storageFiles);
207+
208+
writeJsonFile(EXPORT_ROOT.resolve("manifest.json"), manifest);
209+
}
210+
211+
private Map<String, Object> firestoreDocumentForExport(Map<String, Object> rawData, String documentId) {
212+
Map<String, Object> exportData = new LinkedHashMap<>();
213+
for (Map.Entry<String, Object> entry : rawData.entrySet()) {
214+
exportData.put(entry.getKey(), normalizeFirestoreValue(entry.getValue()));
215+
}
216+
exportData.put("_id", documentId);
217+
return exportData;
218+
}
219+
220+
private Object normalizeFirestoreValue(Object value) {
221+
if (value instanceof Timestamp timestamp) {
222+
Map<String, Object> exportedTimestamp = new LinkedHashMap<>();
223+
exportedTimestamp.put("_type", "timestamp");
224+
exportedTimestamp.put("value", timestamp.toDate().toInstant().toString());
225+
return exportedTimestamp;
226+
}
227+
228+
if (value instanceof Map<?, ?> mapValue) {
229+
Map<String, Object> normalizedMap = new LinkedHashMap<>();
230+
for (Map.Entry<?, ?> entry : mapValue.entrySet()) {
231+
normalizedMap.put(String.valueOf(entry.getKey()), normalizeFirestoreValue(entry.getValue()));
232+
}
233+
return normalizedMap;
234+
}
235+
236+
if (value instanceof List<?> listValue) {
237+
List<Object> normalizedList = new ArrayList<>();
238+
for (Object item : listValue) {
239+
normalizedList.add(normalizeFirestoreValue(item));
240+
}
241+
return normalizedList;
242+
}
243+
244+
return value;
245+
}
246+
247+
private void resetExportRoot() throws IOException {
248+
if (Files.exists(EXPORT_ROOT)) {
249+
try (var walk = Files.walk(EXPORT_ROOT)) {
250+
walk.sorted(Comparator.reverseOrder()).forEach(path -> {
251+
try {
252+
Files.delete(path);
253+
} catch (IOException e) {
254+
throw new RuntimeException("Failed to delete " + path, e);
255+
}
256+
});
257+
} catch (RuntimeException e) {
258+
if (e.getCause() instanceof IOException ioException) {
259+
throw ioException;
260+
}
261+
throw e;
262+
}
263+
}
264+
265+
Files.createDirectories(EXPORT_ROOT);
266+
}
267+
268+
private void writeJsonFile(Path path, Object data) throws IOException {
269+
Files.createDirectories(path.getParent());
270+
Files.writeString(path, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data));
271+
}
272+
273+
private void writeStringFile(Path path, String data) throws IOException {
274+
Files.createDirectories(path.getParent());
275+
Files.writeString(path, data);
276+
}
277+
278+
private String requiredString(Map<String, Object> data, String fieldName, String context) {
279+
Object value = data.get(fieldName);
280+
if (!(value instanceof String stringValue) || stringValue.isBlank()) {
281+
throw new IllegalStateException("Missing field '" + fieldName + "' for " + context);
282+
}
283+
return stringValue;
284+
}
285+
286+
public record ExportSummary(
287+
String outputPath,
288+
int screenerCount,
289+
int firestoreDocuments,
290+
int storageFiles
291+
) {}
292+
}

0 commit comments

Comments
 (0)