From 8d61cd6009719302682a096f888321ff94f115b6 Mon Sep 17 00:00:00 2001 From: joshwanf <17016446+joshwanf@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:59:13 -0400 Subject: [PATCH 1/4] Add Export Example Screener button into main menu, fixed screener import for prod --- .../org/acme/controller/AccountResource.java | 38 +++ .../java/org/acme/functions/AccountHooks.java | 4 +- .../service/ExampleScreenerExportService.java | 292 ++++++++++++++++++ .../service/ExampleScreenerImportService.java | 209 +++++++++---- ...saJ48BFa77NmDeL-testchecks-test-2.0.0.json | 0 .../firestore/system/config.json | 0 ...yzho27saJ48BFa77NmDeL-testchecks-test.json | 0 .../workingScreener/hEStvPeFmEte58GQTC7Y.json | 0 .../1c09392c-913c-4b2b-9870-a1951534c3fb.json | 0 .../fdd4405a-1a00-4650-8005-9595f16e3788.json | 0 .../seed-data}/example-screener/manifest.json | 0 ...7saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn | 0 ...Gyzho27saJ48BFa77NmDeL-testchecks-test.dmn | 0 .../form/working/hEStvPeFmEte58GQTC7Y.json | 0 builder-frontend/src/api/account.ts | 17 + .../src/components/Header/Header.tsx | 51 ++- .../shared/HamburgerMenu/HamburgerMenu.css | 2 +- .../HamburgerMenu/HamburgerMenuWrapper.tsx | 14 +- .../src/components/shared/Modal.tsx | 6 +- 19 files changed, 559 insertions(+), 74 deletions(-) create mode 100644 builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/system/config.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/manifest.json (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn (100%) rename {seed-data => builder-api/src/main/resources/seed-data}/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json (100%) diff --git a/builder-api/src/main/java/org/acme/controller/AccountResource.java b/builder-api/src/main/java/org/acme/controller/AccountResource.java index 5b522a6d..e66c3843 100644 --- a/builder-api/src/main/java/org/acme/controller/AccountResource.java +++ b/builder-api/src/main/java/org/acme/controller/AccountResource.java @@ -1,5 +1,7 @@ package org.acme.controller; +import io.quarkus.logging.Log; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; import jakarta.ws.rs.*; @@ -16,6 +18,7 @@ import org.acme.functions.AccountHooks; import org.acme.model.dto.Auth.AccountHookRequest; import org.acme.model.dto.Auth.AccountHookResponse; +import org.acme.service.ExampleScreenerExportService; @Path("/api") public class AccountResource { @@ -23,6 +26,9 @@ public class AccountResource { @Inject AccountHooks accountHooks; + @Inject + ExampleScreenerExportService exampleScreenerExportService; + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -61,4 +67,36 @@ public Response accountHooks(@Context SecurityIdentity identity, return Response.ok(responseBody).build(); } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Path("/account/export-example-screener") + public Response exportExampleScreener(@Context SecurityIdentity identity) { + String userId = AuthUtils.getUserId(identity); + + if (userId == null) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(new ApiError(true, "Unauthorized.")).build(); + } + + if (LaunchMode.current() != LaunchMode.DEVELOPMENT) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + try { + ExampleScreenerExportService.ExportSummary summary = exampleScreenerExportService.exportForUser(userId); + return Response.ok(Map.of( + "success", true, + "outputPath", summary.outputPath(), + "screenerCount", summary.screenerCount(), + "firestoreDocuments", summary.firestoreDocuments(), + "storageFiles", summary.storageFiles() + )).build(); + } catch (Exception e) { + Log.error("Failed to export example screener seed data for user " + userId, e); + return Response.serverError() + .entity(new ApiError(true, "Failed to export example screener seed data.")) + .build(); + } + } } diff --git a/builder-api/src/main/java/org/acme/functions/AccountHooks.java b/builder-api/src/main/java/org/acme/functions/AccountHooks.java index b91b166e..d53501e4 100644 --- a/builder-api/src/main/java/org/acme/functions/AccountHooks.java +++ b/builder-api/src/main/java/org/acme/functions/AccountHooks.java @@ -14,8 +14,8 @@ public class AccountHooks { public Boolean addExampleScreenerToAccount(String userId) { try { Log.info("Running ADD_EXAMPLE_SCREENER hook for user: " + userId); - String screenerId = exampleScreenerImportService.importForUser(userId); - Log.info("Imported example screener " + screenerId + " for user " + userId); + var screenerIds = exampleScreenerImportService.importForUser(userId); + Log.info("Imported example screeners " + screenerIds + " for user " + userId); return true; } catch (Exception e) { Log.error( diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java new file mode 100644 index 00000000..f40428ad --- /dev/null +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java @@ -0,0 +1,292 @@ +package org.acme.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.Timestamp; +import com.google.firebase.cloud.FirestoreClient; +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.acme.constants.CollectionNames; +import org.acme.constants.FieldNames; +import org.acme.persistence.FirestoreUtils; +import org.acme.persistence.StorageService; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@ApplicationScoped +public class ExampleScreenerExportService { + private static final Path EXPORT_ROOT = Paths.get("src", "main", "resources", "seed-data", "example-screener"); + private static final String SYSTEM_COLLECTION = "system"; + private static final String SYSTEM_CONFIG_ID = "config"; + + private final StorageService storageService; + private final String bucketName; + private final ObjectMapper objectMapper; + + @Inject + public ExampleScreenerExportService( + StorageService storageService, + @ConfigProperty(name = "GCS_BUCKET_NAME", defaultValue = "demo-bdt-dev.appspot.com") String bucketName + ) { + this.storageService = storageService; + this.bucketName = bucketName; + this.objectMapper = new ObjectMapper(); + } + + public ExportSummary exportForUser(String userId) throws Exception { + resetExportRoot(); + + List> workingScreeners = getDocumentsByOwner(CollectionNames.WORKING_SCREENER_COLLECTION, userId); + List> workingCustomChecks = getDocumentsByOwner(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, userId); + List> publishedCustomChecks = getDocumentsByOwner(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, userId); + + int firestoreDocuments = 0; + firestoreDocuments += exportScreeners(workingScreeners); + firestoreDocuments += exportChecks(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, workingCustomChecks); + firestoreDocuments += exportChecks(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, publishedCustomChecks); + firestoreDocuments += exportSystemConfig(); + + int storageFiles = 0; + storageFiles += exportScreenerForms(workingScreeners); + storageFiles += exportCheckDmns(workingCustomChecks); + storageFiles += exportCheckDmns(publishedCustomChecks); + + writeManifest(firestoreDocuments, storageFiles); + + Log.info("Exported Firebase seed data for user " + userId + " to " + EXPORT_ROOT.toAbsolutePath().normalize()); + return new ExportSummary( + EXPORT_ROOT.toAbsolutePath().normalize().toString(), + workingScreeners.size(), + firestoreDocuments, + storageFiles + ); + } + + private List> getDocumentsByOwner(String collectionName, String userId) { + List> documents = new ArrayList<>( + FirestoreUtils.getFirestoreDocsByField(collectionName, FieldNames.OWNER_ID, userId) + ); + documents.sort(Comparator.comparing(document -> requiredString(document, FieldNames.ID, collectionName))); + return documents; + } + + private int exportScreeners(List> workingScreeners) throws IOException { + int firestoreDocuments = 0; + + for (Map screener : workingScreeners) { + String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION); + writeJsonFile( + EXPORT_ROOT.resolve("firestore").resolve("workingScreener").resolve(screenerId + ".json"), + firestoreDocumentForExport(screener, screenerId) + ); + firestoreDocuments++; + + firestoreDocuments += exportBenefits(screenerId); + } + + return firestoreDocuments; + } + + private int exportBenefits(String screenerId) throws IOException { + String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + "/" + screenerId + "/customBenefit"; + List> benefits = new ArrayList<>(FirestoreUtils.getAllDocsInCollection(collectionPath)); + benefits.sort(Comparator.comparing(benefit -> requiredString(benefit, FieldNames.ID, collectionPath))); + + int exportedBenefits = 0; + for (Map benefit : benefits) { + String benefitId = requiredString(benefit, FieldNames.ID, collectionPath); + writeJsonFile( + EXPORT_ROOT.resolve("firestore") + .resolve("workingScreener") + .resolve(screenerId) + .resolve("customBenefit") + .resolve(benefitId + ".json"), + firestoreDocumentForExport(benefit, benefitId) + ); + exportedBenefits++; + } + + return exportedBenefits; + } + + private int exportChecks(String collectionName, List> checks) throws IOException { + int exportedChecks = 0; + for (Map check : checks) { + String checkId = requiredString(check, FieldNames.ID, collectionName); + writeJsonFile( + EXPORT_ROOT.resolve("firestore").resolve(collectionName).resolve(checkId + ".json"), + firestoreDocumentForExport(check, checkId) + ); + exportedChecks++; + } + return exportedChecks; + } + + private int exportSystemConfig() throws IOException { + Optional> config = FirestoreUtils.getFirestoreDocById(SYSTEM_COLLECTION, SYSTEM_CONFIG_ID); + if (config.isEmpty()) { + return 0; + } + + writeJsonFile( + EXPORT_ROOT.resolve("firestore").resolve(SYSTEM_COLLECTION).resolve(SYSTEM_CONFIG_ID + ".json"), + firestoreDocumentForExport(config.get(), SYSTEM_CONFIG_ID) + ); + return 1; + } + + private int exportScreenerForms(List> workingScreeners) throws IOException { + int exportedForms = 0; + + for (Map screener : workingScreeners) { + String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION); + Optional formSchema = storageService.getStringFromStorage( + storageService.getScreenerWorkingFormSchemaPath(screenerId) + ); + + if (formSchema.isEmpty()) { + continue; + } + + writeStringFile( + EXPORT_ROOT.resolve("storage").resolve("form").resolve("working").resolve(screenerId + ".json"), + formSchema.get() + ); + exportedForms++; + } + + return exportedForms; + } + + private int exportCheckDmns(List> checks) throws IOException { + int exportedDmns = 0; + Set exportedIds = new LinkedHashSet<>(); + + for (Map check : checks) { + String checkId = requiredString(check, FieldNames.ID, "customCheck"); + if (!exportedIds.add(checkId)) { + continue; + } + + Optional dmnModel = storageService.getStringFromStorage(storageService.getCheckDmnModelPath(checkId)); + if (dmnModel.isEmpty()) { + continue; + } + + writeStringFile( + EXPORT_ROOT.resolve("storage").resolve("check").resolve(checkId + ".dmn"), + dmnModel.get() + ); + exportedDmns++; + } + + return exportedDmns; + } + + private void writeManifest(int firestoreDocuments, int storageFiles) throws IOException { + Map manifest = new LinkedHashMap<>(); + manifest.put("exportedAt", Instant.now().toString()); + manifest.put("source", "builder-api"); + manifest.put("projectId", FirestoreClient.getFirestore().getOptions().getProjectId()); + manifest.put("storageBucket", bucketName); + manifest.put("firestoreDocuments", firestoreDocuments); + manifest.put("storageFiles", storageFiles); + + writeJsonFile(EXPORT_ROOT.resolve("manifest.json"), manifest); + } + + private Map firestoreDocumentForExport(Map rawData, String documentId) { + Map exportData = new LinkedHashMap<>(); + for (Map.Entry entry : rawData.entrySet()) { + exportData.put(entry.getKey(), normalizeFirestoreValue(entry.getValue())); + } + exportData.put("_id", documentId); + return exportData; + } + + private Object normalizeFirestoreValue(Object value) { + if (value instanceof Timestamp timestamp) { + Map exportedTimestamp = new LinkedHashMap<>(); + exportedTimestamp.put("_type", "timestamp"); + exportedTimestamp.put("value", timestamp.toDate().toInstant().toString()); + return exportedTimestamp; + } + + if (value instanceof Map mapValue) { + Map normalizedMap = new LinkedHashMap<>(); + for (Map.Entry entry : mapValue.entrySet()) { + normalizedMap.put(String.valueOf(entry.getKey()), normalizeFirestoreValue(entry.getValue())); + } + return normalizedMap; + } + + if (value instanceof List listValue) { + List normalizedList = new ArrayList<>(); + for (Object item : listValue) { + normalizedList.add(normalizeFirestoreValue(item)); + } + return normalizedList; + } + + return value; + } + + private void resetExportRoot() throws IOException { + if (Files.exists(EXPORT_ROOT)) { + try (var walk = Files.walk(EXPORT_ROOT)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete " + path, e); + } + }); + } catch (RuntimeException e) { + if (e.getCause() instanceof IOException ioException) { + throw ioException; + } + throw e; + } + } + + Files.createDirectories(EXPORT_ROOT); + } + + private void writeJsonFile(Path path, Object data) throws IOException { + Files.createDirectories(path.getParent()); + Files.writeString(path, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data)); + } + + private void writeStringFile(Path path, String data) throws IOException { + Files.createDirectories(path.getParent()); + Files.writeString(path, data); + } + + private String requiredString(Map data, String fieldName, String context) { + Object value = data.get(fieldName); + if (!(value instanceof String stringValue) || stringValue.isBlank()) { + throw new IllegalStateException("Missing field '" + fieldName + "' for " + context); + } + return stringValue; + } + + public record ExportSummary( + String outputPath, + int screenerCount, + int firestoreDocuments, + int storageFiles + ) {} +} diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java index 54cd3989..4f6c6b8c 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java @@ -16,7 +16,12 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Files; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -33,6 +38,8 @@ @ApplicationScoped public class ExampleScreenerImportService { + private static final String BUNDLED_SEED_ROOT = "seed-data/example-screener"; + private static final String BUNDLED_SEED_MANIFEST = BUNDLED_SEED_ROOT + "/manifest.json"; private final ScreenerRepository screenerRepository; private final EligibilityCheckRepository eligibilityCheckRepository; @@ -54,53 +61,64 @@ public ExampleScreenerImportService( this.objectMapper = new ObjectMapper(); } - public String importForUser(String userId) throws Exception { - Path seedRoot = resolveSeedRoot(); - SeedData seedData = loadSeedData(seedRoot); - - Map importedCustomCheckIds = importReferencedCustomChecks(seedData, userId); - - List importedBenefits = new ArrayList<>(); - List importedBenefitDetails = new ArrayList<>(); - for (Benefit seedBenefit : seedData.benefits()) { - Benefit importedBenefit = cloneBenefit(seedBenefit, userId, importedCustomCheckIds); - importedBenefits.add(importedBenefit); - importedBenefitDetails.add(new BenefitDetail( - importedBenefit.getId(), - importedBenefit.getName(), - importedBenefit.getDescription() - )); - } + public List importForUser(String userId) throws Exception { + SeedRoot seedRoot = resolveSeedRoot(); + try (seedRoot) { + SeedData seedData = loadSeedData(seedRoot.path()); + + Map importedCustomCheckIds = importReferencedCustomChecks(seedData, userId); + List importedScreenerIds = new ArrayList<>(); + + for (SeedScreenerData seedScreener : seedData.screeners()) { + List importedBenefits = new ArrayList<>(); + List importedBenefitDetails = new ArrayList<>(); + for (Benefit seedBenefit : seedScreener.benefits()) { + Benefit importedBenefit = cloneBenefit(seedBenefit, userId, importedCustomCheckIds); + importedBenefits.add(importedBenefit); + importedBenefitDetails.add(new BenefitDetail( + importedBenefit.getId(), + importedBenefit.getName(), + importedBenefit.getDescription() + )); + } - Screener importedScreener = new Screener(); - importedScreener.setOwnerId(userId); - importedScreener.setScreenerName(seedData.screener().getScreenerName()); - importedScreener.setBenefits(importedBenefitDetails); + Screener importedScreener = new Screener(); + importedScreener.setOwnerId(userId); + importedScreener.setScreenerName(seedScreener.screener().getScreenerName()); + importedScreener.setBenefits(importedBenefitDetails); - String newScreenerId = screenerRepository.saveNewWorkingScreener(importedScreener); - importedScreener.setId(newScreenerId); + String newScreenerId = screenerRepository.saveNewWorkingScreener(importedScreener); + importedScreener.setId(newScreenerId); - for (Benefit importedBenefit : importedBenefits) { - screenerRepository.saveNewCustomBenefit(newScreenerId, importedBenefit); - } + for (Benefit importedBenefit : importedBenefits) { + screenerRepository.saveNewCustomBenefit(newScreenerId, importedBenefit); + } - String formPath = storageService.getScreenerWorkingFormSchemaPath(newScreenerId); - storageService.writeJsonToStorage(formPath, seedData.formSchema()); + if (seedScreener.formSchema() != null) { + String formPath = storageService.getScreenerWorkingFormSchemaPath(newScreenerId); + storageService.writeJsonToStorage(formPath, seedScreener.formSchema()); + } - Log.info("Imported example screener " + newScreenerId + " for user " + userId); - return newScreenerId; + importedScreenerIds.add(newScreenerId); + Log.info("Imported example screener " + newScreenerId + " for user " + userId); + } + + return importedScreenerIds; + } } private Map importReferencedCustomChecks(SeedData seedData, String userId) throws Exception { Set referencedCustomCheckIds = new LinkedHashSet<>(); - for (Benefit benefit : seedData.benefits()) { - if (benefit.getChecks() == null) { - continue; - } - for (CheckConfig checkConfig : benefit.getChecks()) { - String sourceCheckId = resolveSourceCheckId(checkConfig); - if (sourceCheckId != null && !isLibraryCheckId(sourceCheckId)) { - referencedCustomCheckIds.add(sourceCheckId); + for (SeedScreenerData seedScreener : seedData.screeners()) { + for (Benefit benefit : seedScreener.benefits()) { + if (benefit.getChecks() == null) { + continue; + } + for (CheckConfig checkConfig : benefit.getChecks()) { + String sourceCheckId = resolveSourceCheckId(checkConfig); + if (sourceCheckId != null && !isLibraryCheckId(sourceCheckId)) { + referencedCustomCheckIds.add(sourceCheckId); + } } } } @@ -287,34 +305,44 @@ private boolean isLibraryCheckId(String checkId) { private SeedData loadSeedData(Path seedRoot) throws IOException { Path workingScreenersDir = seedRoot.resolve("firestore").resolve("workingScreener"); List screenerFiles = listJsonFiles(workingScreenersDir); - if (screenerFiles.size() != 1) { - throw new IllegalStateException("Expected exactly one working screener seed document, found " + screenerFiles.size()); + if (screenerFiles.isEmpty()) { + throw new IllegalStateException("Expected at least one working screener seed document"); } - Path screenerFile = screenerFiles.get(0); - Screener screener = readJsonFile(screenerFile, Screener.class); - String screenerDocId = stripExtension(screenerFile.getFileName().toString()); + List screeners = new ArrayList<>(); + for (Path screenerFile : screenerFiles) { + Screener screener = readJsonFile(screenerFile, Screener.class); + String screenerDocId = stripExtension(screenerFile.getFileName().toString()); - Path benefitsDir = workingScreenersDir.resolve(screenerDocId).resolve("customBenefit"); - List benefits = new ArrayList<>(); - for (Path benefitFile : listJsonFiles(benefitsDir)) { - benefits.add(readJsonFile(benefitFile, Benefit.class)); - } + Path benefitsDir = workingScreenersDir.resolve(screenerDocId).resolve("customBenefit"); + List benefits = new ArrayList<>(); + for (Path benefitFile : listJsonFiles(benefitsDir)) { + benefits.add(readJsonFile(benefitFile, Benefit.class)); + } - JsonNode formSchema = objectMapper.readTree( - Files.readString(seedRoot.resolve("storage").resolve("form").resolve("working").resolve(screenerDocId + ".json")) - ); + screeners.add(new SeedScreenerData( + screener, + benefits, + loadFormSchema(seedRoot, screenerDocId) + )); + } return new SeedData( - screener, - benefits, - formSchema, + screeners, loadChecks(seedRoot.resolve("firestore").resolve("workingCustomCheck")), loadChecks(seedRoot.resolve("firestore").resolve("publishedCustomCheck")), loadDmnFiles(seedRoot.resolve("storage").resolve("check")) ); } + private JsonNode loadFormSchema(Path seedRoot, String screenerDocId) throws IOException { + Path formSchemaPath = seedRoot.resolve("storage").resolve("form").resolve("working").resolve(screenerDocId + ".json"); + if (!Files.isRegularFile(formSchemaPath)) { + return null; + } + return objectMapper.readTree(Files.readString(formSchemaPath)); + } + private Map loadChecks(Path checksDir) throws IOException { Map checksById = new LinkedHashMap<>(); if (!Files.isDirectory(checksDir)) { @@ -374,24 +402,56 @@ private List listJsonFiles(Path directory) throws IOException { } } - private Path resolveSeedRoot() { - List candidates = new ArrayList<>(); - configuredSeedPath + private SeedRoot resolveSeedRoot() { + Optional configuredPath = configuredSeedPath .map(String::trim) .filter(path -> !path.isBlank()) - .map(Paths::get) - .ifPresent(candidates::add); - candidates.add(Paths.get("seed-data", "example-screener")); - candidates.add(Paths.get("..", "seed-data", "example-screener")); + .map(Paths::get); - for (Path candidate : candidates) { - Path absoluteCandidate = candidate.toAbsolutePath().normalize(); + if (configuredPath.isPresent()) { + Path absoluteCandidate = configuredPath.get().toAbsolutePath().normalize(); if (Files.isDirectory(absoluteCandidate)) { - return absoluteCandidate; + return new SeedRoot(absoluteCandidate, null); + } + } + + try { + return resolveBundledSeedRoot(); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Could not load bundled example screener seed data", e); + } + } + + private SeedRoot resolveBundledSeedRoot() throws IOException, URISyntaxException { + var manifestUrl = ExampleScreenerImportService.class.getClassLoader().getResource(BUNDLED_SEED_MANIFEST); + if (manifestUrl == null) { + throw new IllegalStateException( + "Could not find bundled example screener seed data at classpath:" + BUNDLED_SEED_ROOT + ); + } + + URI manifestUri = manifestUrl.toURI(); + if ("jar".equalsIgnoreCase(manifestUri.getScheme())) { + String manifestUriString = manifestUri.toString(); + int archiveSeparatorIndex = manifestUriString.indexOf("!/"); + if (archiveSeparatorIndex < 0) { + throw new IllegalStateException("Unexpected jar resource URI for bundled seed data: " + manifestUri); } + + URI archiveUri = URI.create(manifestUriString.substring(0, archiveSeparatorIndex)); + FileSystem fileSystem = getOrCreateFileSystem(archiveUri); + return new SeedRoot(fileSystem.getPath("/" + BUNDLED_SEED_ROOT), fileSystem); } - throw new IllegalStateException("Could not find example screener seed data in any expected location"); + return new SeedRoot(Paths.get(manifestUri).getParent(), null); + } + + private FileSystem getOrCreateFileSystem(URI archiveUri) throws IOException { + try { + return FileSystems.getFileSystem(archiveUri); + } catch (FileSystemNotFoundException ignored) { + return FileSystems.newFileSystem(archiveUri, Collections.emptyMap()); + } } private String buildWorkingCheckId(String ownerId, String module, String name) { @@ -411,16 +471,29 @@ private String stripExtension(String filename) { } private record SeedData( - Screener screener, - List benefits, - JsonNode formSchema, + List screeners, Map workingCustomChecks, Map publishedCustomChecks, Map dmnByCheckId ) {} + private record SeedScreenerData( + Screener screener, + List benefits, + JsonNode formSchema + ) {} + private record SeedCustomCheckVersions( EligibilityCheck workingCheck, EligibilityCheck publishedCheck ) {} + + private record SeedRoot(Path path, FileSystem fileSystem) implements AutoCloseable { + @Override + public void close() throws IOException { + if (fileSystem != null && fileSystem.isOpen()) { + fileSystem.close(); + } + } + } } diff --git a/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json similarity index 100% rename from seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json diff --git a/seed-data/example-screener/firestore/system/config.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json similarity index 100% rename from seed-data/example-screener/firestore/system/config.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json diff --git a/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json similarity index 100% rename from seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json similarity index 100% rename from seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json similarity index 100% rename from seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json diff --git a/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json similarity index 100% rename from seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json rename to builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json diff --git a/seed-data/example-screener/manifest.json b/builder-api/src/main/resources/seed-data/example-screener/manifest.json similarity index 100% rename from seed-data/example-screener/manifest.json rename to builder-api/src/main/resources/seed-data/example-screener/manifest.json diff --git a/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn similarity index 100% rename from seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn diff --git a/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn similarity index 100% rename from seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn diff --git a/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json b/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json similarity index 100% rename from seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json rename to builder-api/src/main/resources/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json diff --git a/builder-frontend/src/api/account.ts b/builder-frontend/src/api/account.ts index 28dad71a..fb04490d 100644 --- a/builder-frontend/src/api/account.ts +++ b/builder-frontend/src/api/account.ts @@ -24,3 +24,20 @@ export const runAccountHooks = async () => { throw error; // rethrow so you can handle it in your component if needed } }; + +export const exportExampleScreener = async () => { + const url = new URL(`${apiUrl}/account/export-example-screener`); + + try { + const response = await authPost(url.toString()); + + if (!response.ok) { + return { success: false }; + } + const data = (await response.json()) as { success: boolean }; + return data; + } catch (err) { + console.error("Error calling account hooks:", err); + return { success: false }; + } +}; diff --git a/builder-frontend/src/components/Header/Header.tsx b/builder-frontend/src/components/Header/Header.tsx index 290e80b0..3996c8e2 100644 --- a/builder-frontend/src/components/Header/Header.tsx +++ b/builder-frontend/src/components/Header/Header.tsx @@ -1,11 +1,24 @@ import { useAuth } from "../../context/AuthContext"; import { useLocation, useNavigate } from "@solidjs/router"; -import { Component, createMemo, For, Show } from "solid-js"; +import { + Component, + createMemo, + createSignal, + DEV, + For, + JSX, + Match, + Show, + Switch, +} from "solid-js"; import { HamburgerMenu } from "@/components/shared/HamburgerMenu"; import "./Header.css"; import { Menu } from "lucide-solid"; +import { Button } from "@/components/shared/Button"; +import { Modal } from "@/components/shared/Modal"; +import { exportExampleScreener } from "@/api/account"; const HeaderButton = ({ buttonText, @@ -35,6 +48,10 @@ interface MenuProps { const HeaderMenu: Component = (props) => { const navigate = useNavigate(); + const [showExportMenu, setShowExportMenu] = createSignal(false); + const [exportingMessage, setExportingMessage] = createSignal(""); + const [isExportingExample, setIsExportingExample] = createSignal(false); + const menuItems: { label: string; onClick: () => void }[] = [ { label: "Custom Checks", @@ -47,6 +64,20 @@ const HeaderMenu: Component = (props) => { { label: "Logout", onClick: props.logout }, ]; + const handleExportExampleScreener: JSX.EventHandler< + HTMLButtonElement, + MouseEvent + > = async (e) => { + setIsExportingExample(true); + setExportingMessage(""); + const result = await exportExampleScreener(); + if (!result.success) { + setExportingMessage("An error occurred exporting."); + } else { + setExportingMessage("Successfully exported screeners."); + } + setIsExportingExample(false); + }; return (
@@ -62,6 +93,24 @@ const HeaderMenu: Component = (props) => { )} + + + setShowExportMenu(false)}> +
Ready to save changes to the example screener?
+ + + + + + Waiting for export + +
{exportingMessage()}
+
+
); }; diff --git a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css index 6a5f0b8c..91ebfd02 100644 --- a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css +++ b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenu.css @@ -19,7 +19,7 @@ width: 25%; height: 100%; - z-index: 1000; + z-index: 5; background-color: white; padding: 0.5rem; diff --git a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx index 336b4038..a7647aba 100644 --- a/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx +++ b/builder-frontend/src/components/shared/HamburgerMenu/HamburgerMenuWrapper.tsx @@ -30,8 +30,20 @@ export const HamburgerMenuWrapper: ParentComponent = (props) => { const [showMenu, setShowMenu] = createSignal(false); const handleClickOutside = (ev: MouseEvent) => { + if (!showMenu()) return; + const el = root(); - if (showMenu() && el && !el.contains(ev.target as Node)) { + if (!el) return; + + const path = ev.composedPath(); + + const clickedInsideMenu = path.includes(el); + const clickedInsideModal = path.some( + (node) => + node instanceof HTMLElement && node.hasAttribute("data-modal-root"), + ); + + if (!clickedInsideMenu && !clickedInsideModal) { setShowMenu(false); } }; diff --git a/builder-frontend/src/components/shared/Modal.tsx b/builder-frontend/src/components/shared/Modal.tsx index 7d0db47f..85561add 100644 --- a/builder-frontend/src/components/shared/Modal.tsx +++ b/builder-frontend/src/components/shared/Modal.tsx @@ -12,7 +12,11 @@ export const Modal: Component> = (props) => { return ( -
props.onClose()}> +
props.onClose()} + data-modal-root + >
e.stopPropagation()} From bfb8f4f4b05add70e5f3ade846d692998326b48a Mon Sep 17 00:00:00 2001 From: joshwanf <17016446+joshwanf@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:03:57 -0400 Subject: [PATCH 2/4] Updated account hook endpoint to match export-exmple-screener endpoint --- .../org/acme/controller/AccountResource.java | 39 ++++++++++++------- builder-frontend/src/api/account.ts | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/builder-api/src/main/java/org/acme/controller/AccountResource.java b/builder-api/src/main/java/org/acme/controller/AccountResource.java index e66c3843..40d6cda6 100644 --- a/builder-api/src/main/java/org/acme/controller/AccountResource.java +++ b/builder-api/src/main/java/org/acme/controller/AccountResource.java @@ -32,7 +32,7 @@ public class AccountResource { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Path("/account-hooks") + @Path("/account/hooks") public Response accountHooks(@Context SecurityIdentity identity, AccountHookRequest request) { @@ -76,7 +76,7 @@ public Response exportExampleScreener(@Context SecurityIdentity identity) { if (userId == null) { return Response.status(Response.Status.UNAUTHORIZED) - .entity(new ApiError(true, "Unauthorized.")).build(); + .entity(new ApiError(true, "Unauthorized.")).build(); } if (LaunchMode.current() != LaunchMode.DEVELOPMENT) { @@ -84,19 +84,30 @@ public Response exportExampleScreener(@Context SecurityIdentity identity) { } try { - ExampleScreenerExportService.ExportSummary summary = exampleScreenerExportService.exportForUser(userId); - return Response.ok(Map.of( - "success", true, - "outputPath", summary.outputPath(), - "screenerCount", summary.screenerCount(), - "firestoreDocuments", summary.firestoreDocuments(), - "storageFiles", summary.storageFiles() - )).build(); + ExampleScreenerExportService.ExportSummary summary = exampleScreenerExportService + .exportForUser(userId); + return Response.ok( + Map.of( + "success", + true, + "outputPath", + summary.outputPath(), + "screenerCount", + summary.screenerCount(), + "firestoreDocuments", + summary.firestoreDocuments(), + "storageFiles", + summary.storageFiles())) + .build(); } catch (Exception e) { - Log.error("Failed to export example screener seed data for user " + userId, e); - return Response.serverError() - .entity(new ApiError(true, "Failed to export example screener seed data.")) - .build(); + Log.error( + "Failed to export example screener seed data for user " + + userId, + e); + return Response.serverError().entity( + new ApiError(true, + "Failed to export example screener seed data.")) + .build(); } } } diff --git a/builder-frontend/src/api/account.ts b/builder-frontend/src/api/account.ts index fb04490d..025df1e3 100644 --- a/builder-frontend/src/api/account.ts +++ b/builder-frontend/src/api/account.ts @@ -5,7 +5,7 @@ import { authPost } from "@/api/auth"; const apiUrl = env.apiUrl; export const runAccountHooks = async () => { - const accountHookUrl = new URL(`${apiUrl}/account-hooks`); + const accountHookUrl = new URL(`${apiUrl}/account/hooks`); const hooksToCall = ["add example screener"]; From 70c846e975fb9c1c0489896d6f350aea9804e58e Mon Sep 17 00:00:00 2001 From: joshwanf <17016446+joshwanf@users.noreply.github.com> Date: Thu, 21 May 2026 13:28:57 -0400 Subject: [PATCH 3/4] white space changes --- .../service/ExampleScreenerExportService.java | 203 ++++++---- .../service/ExampleScreenerImportService.java | 373 ++++++++++-------- 2 files changed, 345 insertions(+), 231 deletions(-) diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java index f40428ad..2424492f 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java @@ -28,7 +28,8 @@ @ApplicationScoped public class ExampleScreenerExportService { - private static final Path EXPORT_ROOT = Paths.get("src", "main", "resources", "seed-data", "example-screener"); + private static final Path EXPORT_ROOT = Paths + .get("src", "main", "resources", "seed-data", "example-screener"); private static final String SYSTEM_COLLECTION = "system"; private static final String SYSTEM_CONFIG_ID = "config"; @@ -37,10 +38,10 @@ public class ExampleScreenerExportService { private final ObjectMapper objectMapper; @Inject - public ExampleScreenerExportService( - StorageService storageService, - @ConfigProperty(name = "GCS_BUCKET_NAME", defaultValue = "demo-bdt-dev.appspot.com") String bucketName - ) { + public ExampleScreenerExportService(StorageService storageService, + @ConfigProperty( + name = "GCS_BUCKET_NAME", + defaultValue = "demo-bdt-dev.appspot.com") String bucketName) { this.storageService = storageService; this.bucketName = bucketName; this.objectMapper = new ObjectMapper(); @@ -49,14 +50,24 @@ public ExampleScreenerExportService( public ExportSummary exportForUser(String userId) throws Exception { resetExportRoot(); - List> workingScreeners = getDocumentsByOwner(CollectionNames.WORKING_SCREENER_COLLECTION, userId); - List> workingCustomChecks = getDocumentsByOwner(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, userId); - List> publishedCustomChecks = getDocumentsByOwner(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, userId); + List> workingScreeners = getDocumentsByOwner( + CollectionNames.WORKING_SCREENER_COLLECTION, + userId); + List> workingCustomChecks = getDocumentsByOwner( + CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, + userId); + List> publishedCustomChecks = getDocumentsByOwner( + CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, + userId); int firestoreDocuments = 0; firestoreDocuments += exportScreeners(workingScreeners); - firestoreDocuments += exportChecks(CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, workingCustomChecks); - firestoreDocuments += exportChecks(CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, publishedCustomChecks); + firestoreDocuments += exportChecks( + CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, + workingCustomChecks); + firestoreDocuments += exportChecks( + CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, + publishedCustomChecks); firestoreDocuments += exportSystemConfig(); int storageFiles = 0; @@ -66,32 +77,43 @@ public ExportSummary exportForUser(String userId) throws Exception { writeManifest(firestoreDocuments, storageFiles); - Log.info("Exported Firebase seed data for user " + userId + " to " + EXPORT_ROOT.toAbsolutePath().normalize()); + Log.info( + "Exported Firebase seed data for user " + userId + " to " + + EXPORT_ROOT.toAbsolutePath().normalize()); return new ExportSummary( - EXPORT_ROOT.toAbsolutePath().normalize().toString(), - workingScreeners.size(), - firestoreDocuments, - storageFiles - ); + EXPORT_ROOT.toAbsolutePath().normalize().toString(), + workingScreeners.size(), firestoreDocuments, storageFiles); } - private List> getDocumentsByOwner(String collectionName, String userId) { + private List> getDocumentsByOwner(String collectionName, + String userId) { List> documents = new ArrayList<>( - FirestoreUtils.getFirestoreDocsByField(collectionName, FieldNames.OWNER_ID, userId) - ); - documents.sort(Comparator.comparing(document -> requiredString(document, FieldNames.ID, collectionName))); + FirestoreUtils.getFirestoreDocsByField( + collectionName, + FieldNames.OWNER_ID, + userId)); + documents.sort( + Comparator.comparing( + document -> requiredString( + document, + FieldNames.ID, + collectionName))); return documents; } - private int exportScreeners(List> workingScreeners) throws IOException { + private int exportScreeners(List> workingScreeners) + throws IOException { int firestoreDocuments = 0; for (Map screener : workingScreeners) { - String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION); + String screenerId = requiredString( + screener, + FieldNames.ID, + CollectionNames.WORKING_SCREENER_COLLECTION); writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve("workingScreener").resolve(screenerId + ".json"), - firestoreDocumentForExport(screener, screenerId) - ); + EXPORT_ROOT.resolve("firestore").resolve("workingScreener") + .resolve(screenerId + ".json"), + firestoreDocumentForExport(screener, screenerId)); firestoreDocuments++; firestoreDocuments += exportBenefits(screenerId); @@ -101,106 +123,130 @@ private int exportScreeners(List> workingScreeners) throws I } private int exportBenefits(String screenerId) throws IOException { - String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + "/" + screenerId + "/customBenefit"; - List> benefits = new ArrayList<>(FirestoreUtils.getAllDocsInCollection(collectionPath)); - benefits.sort(Comparator.comparing(benefit -> requiredString(benefit, FieldNames.ID, collectionPath))); + String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + + "/" + screenerId + "/customBenefit"; + List> benefits = new ArrayList<>( + FirestoreUtils.getAllDocsInCollection(collectionPath)); + benefits.sort( + Comparator.comparing( + benefit -> requiredString( + benefit, + FieldNames.ID, + collectionPath))); int exportedBenefits = 0; for (Map benefit : benefits) { - String benefitId = requiredString(benefit, FieldNames.ID, collectionPath); + String benefitId = requiredString( + benefit, + FieldNames.ID, + collectionPath); writeJsonFile( - EXPORT_ROOT.resolve("firestore") - .resolve("workingScreener") - .resolve(screenerId) - .resolve("customBenefit") - .resolve(benefitId + ".json"), - firestoreDocumentForExport(benefit, benefitId) - ); + EXPORT_ROOT.resolve("firestore").resolve("workingScreener") + .resolve(screenerId).resolve("customBenefit") + .resolve(benefitId + ".json"), + firestoreDocumentForExport(benefit, benefitId)); exportedBenefits++; } return exportedBenefits; } - private int exportChecks(String collectionName, List> checks) throws IOException { + private int exportChecks(String collectionName, + List> checks) throws IOException { int exportedChecks = 0; for (Map check : checks) { - String checkId = requiredString(check, FieldNames.ID, collectionName); + String checkId = requiredString( + check, + FieldNames.ID, + collectionName); writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve(collectionName).resolve(checkId + ".json"), - firestoreDocumentForExport(check, checkId) - ); + EXPORT_ROOT.resolve("firestore").resolve(collectionName) + .resolve(checkId + ".json"), + firestoreDocumentForExport(check, checkId)); exportedChecks++; } return exportedChecks; } private int exportSystemConfig() throws IOException { - Optional> config = FirestoreUtils.getFirestoreDocById(SYSTEM_COLLECTION, SYSTEM_CONFIG_ID); + Optional> config = FirestoreUtils + .getFirestoreDocById(SYSTEM_COLLECTION, SYSTEM_CONFIG_ID); if (config.isEmpty()) { return 0; } writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve(SYSTEM_COLLECTION).resolve(SYSTEM_CONFIG_ID + ".json"), - firestoreDocumentForExport(config.get(), SYSTEM_CONFIG_ID) - ); + EXPORT_ROOT.resolve("firestore").resolve(SYSTEM_COLLECTION) + .resolve(SYSTEM_CONFIG_ID + ".json"), + firestoreDocumentForExport(config.get(), SYSTEM_CONFIG_ID)); return 1; } - private int exportScreenerForms(List> workingScreeners) throws IOException { + private int exportScreenerForms(List> workingScreeners) + throws IOException { int exportedForms = 0; for (Map screener : workingScreeners) { - String screenerId = requiredString(screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION); + String screenerId = requiredString( + screener, + FieldNames.ID, + CollectionNames.WORKING_SCREENER_COLLECTION); Optional formSchema = storageService.getStringFromStorage( - storageService.getScreenerWorkingFormSchemaPath(screenerId) - ); + storageService + .getScreenerWorkingFormSchemaPath(screenerId)); if (formSchema.isEmpty()) { continue; } writeStringFile( - EXPORT_ROOT.resolve("storage").resolve("form").resolve("working").resolve(screenerId + ".json"), - formSchema.get() - ); + EXPORT_ROOT.resolve("storage").resolve("form") + .resolve("working").resolve(screenerId + ".json"), + formSchema.get()); exportedForms++; } return exportedForms; } - private int exportCheckDmns(List> checks) throws IOException { + private int exportCheckDmns(List> checks) + throws IOException { int exportedDmns = 0; Set exportedIds = new LinkedHashSet<>(); for (Map check : checks) { - String checkId = requiredString(check, FieldNames.ID, "customCheck"); + String checkId = requiredString( + check, + FieldNames.ID, + "customCheck"); if (!exportedIds.add(checkId)) { continue; } - Optional dmnModel = storageService.getStringFromStorage(storageService.getCheckDmnModelPath(checkId)); + Optional dmnModel = storageService.getStringFromStorage( + storageService.getCheckDmnModelPath(checkId)); if (dmnModel.isEmpty()) { continue; } writeStringFile( - EXPORT_ROOT.resolve("storage").resolve("check").resolve(checkId + ".dmn"), - dmnModel.get() - ); + EXPORT_ROOT.resolve("storage").resolve("check") + .resolve(checkId + ".dmn"), + dmnModel.get()); exportedDmns++; } return exportedDmns; } - private void writeManifest(int firestoreDocuments, int storageFiles) throws IOException { + private void writeManifest(int firestoreDocuments, int storageFiles) + throws IOException { Map manifest = new LinkedHashMap<>(); manifest.put("exportedAt", Instant.now().toString()); manifest.put("source", "builder-api"); - manifest.put("projectId", FirestoreClient.getFirestore().getOptions().getProjectId()); + manifest.put( + "projectId", + FirestoreClient.getFirestore().getOptions().getProjectId()); manifest.put("storageBucket", bucketName); manifest.put("firestoreDocuments", firestoreDocuments); manifest.put("storageFiles", storageFiles); @@ -208,10 +254,13 @@ private void writeManifest(int firestoreDocuments, int storageFiles) throws IOEx writeJsonFile(EXPORT_ROOT.resolve("manifest.json"), manifest); } - private Map firestoreDocumentForExport(Map rawData, String documentId) { + private Map firestoreDocumentForExport( + Map rawData, String documentId) { Map exportData = new LinkedHashMap<>(); for (Map.Entry entry : rawData.entrySet()) { - exportData.put(entry.getKey(), normalizeFirestoreValue(entry.getValue())); + exportData.put( + entry.getKey(), + normalizeFirestoreValue(entry.getValue())); } exportData.put("_id", documentId); return exportData; @@ -221,14 +270,17 @@ private Object normalizeFirestoreValue(Object value) { if (value instanceof Timestamp timestamp) { Map exportedTimestamp = new LinkedHashMap<>(); exportedTimestamp.put("_type", "timestamp"); - exportedTimestamp.put("value", timestamp.toDate().toInstant().toString()); + exportedTimestamp + .put("value", timestamp.toDate().toInstant().toString()); return exportedTimestamp; } if (value instanceof Map mapValue) { Map normalizedMap = new LinkedHashMap<>(); for (Map.Entry entry : mapValue.entrySet()) { - normalizedMap.put(String.valueOf(entry.getKey()), normalizeFirestoreValue(entry.getValue())); + normalizedMap.put( + String.valueOf(entry.getKey()), + normalizeFirestoreValue(entry.getValue())); } return normalizedMap; } @@ -251,7 +303,8 @@ private void resetExportRoot() throws IOException { try { Files.delete(path); } catch (IOException e) { - throw new RuntimeException("Failed to delete " + path, e); + throw new RuntimeException("Failed to delete " + path, + e); } }); } catch (RuntimeException e) { @@ -267,7 +320,10 @@ private void resetExportRoot() throws IOException { private void writeJsonFile(Path path, Object data) throws IOException { Files.createDirectories(path.getParent()); - Files.writeString(path, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data)); + Files.writeString( + path, + objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(data)); } private void writeStringFile(Path path, String data) throws IOException { @@ -275,18 +331,17 @@ private void writeStringFile(Path path, String data) throws IOException { Files.writeString(path, data); } - private String requiredString(Map data, String fieldName, String context) { + private String requiredString(Map data, String fieldName, + String context) { Object value = data.get(fieldName); if (!(value instanceof String stringValue) || stringValue.isBlank()) { - throw new IllegalStateException("Missing field '" + fieldName + "' for " + context); + throw new IllegalStateException( + "Missing field '" + fieldName + "' for " + context); } return stringValue; } - public record ExportSummary( - String outputPath, - int screenerCount, - int firestoreDocuments, - int storageFiles - ) {} + public record ExportSummary(String outputPath, int screenerCount, + int firestoreDocuments, int storageFiles) { + } } diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java index 4f6c6b8c..87860c58 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java @@ -39,7 +39,8 @@ @ApplicationScoped public class ExampleScreenerImportService { private static final String BUNDLED_SEED_ROOT = "seed-data/example-screener"; - private static final String BUNDLED_SEED_MANIFEST = BUNDLED_SEED_ROOT + "/manifest.json"; + private static final String BUNDLED_SEED_MANIFEST = BUNDLED_SEED_ROOT + + "/manifest.json"; private final ScreenerRepository screenerRepository; private final EligibilityCheckRepository eligibilityCheckRepository; @@ -48,12 +49,10 @@ public class ExampleScreenerImportService { private final ObjectMapper objectMapper; @Inject - public ExampleScreenerImportService( - ScreenerRepository screenerRepository, - EligibilityCheckRepository eligibilityCheckRepository, - StorageService storageService, - @ConfigProperty(name = "example-screener.seed-path") Optional configuredSeedPath - ) { + public ExampleScreenerImportService(ScreenerRepository screenerRepository, + EligibilityCheckRepository eligibilityCheckRepository, + StorageService storageService, @ConfigProperty( + name = "example-screener.seed-path") Optional configuredSeedPath) { this.screenerRepository = screenerRepository; this.eligibilityCheckRepository = eligibilityCheckRepository; this.storageService = storageService; @@ -66,48 +65,62 @@ public List importForUser(String userId) throws Exception { try (seedRoot) { SeedData seedData = loadSeedData(seedRoot.path()); - Map importedCustomCheckIds = importReferencedCustomChecks(seedData, userId); + Map importedCustomCheckIds = importReferencedCustomChecks( + seedData, + userId); List importedScreenerIds = new ArrayList<>(); for (SeedScreenerData seedScreener : seedData.screeners()) { List importedBenefits = new ArrayList<>(); List importedBenefitDetails = new ArrayList<>(); for (Benefit seedBenefit : seedScreener.benefits()) { - Benefit importedBenefit = cloneBenefit(seedBenefit, userId, importedCustomCheckIds); + Benefit importedBenefit = cloneBenefit( + seedBenefit, + userId, + importedCustomCheckIds); importedBenefits.add(importedBenefit); - importedBenefitDetails.add(new BenefitDetail( - importedBenefit.getId(), - importedBenefit.getName(), - importedBenefit.getDescription() - )); + importedBenefitDetails.add( + new BenefitDetail(importedBenefit.getId(), + importedBenefit.getName(), + importedBenefit.getDescription())); } Screener importedScreener = new Screener(); importedScreener.setOwnerId(userId); - importedScreener.setScreenerName(seedScreener.screener().getScreenerName()); + importedScreener.setScreenerName( + seedScreener.screener().getScreenerName()); importedScreener.setBenefits(importedBenefitDetails); - String newScreenerId = screenerRepository.saveNewWorkingScreener(importedScreener); + String newScreenerId = screenerRepository + .saveNewWorkingScreener(importedScreener); importedScreener.setId(newScreenerId); for (Benefit importedBenefit : importedBenefits) { - screenerRepository.saveNewCustomBenefit(newScreenerId, importedBenefit); + screenerRepository.saveNewCustomBenefit( + newScreenerId, + importedBenefit); } if (seedScreener.formSchema() != null) { - String formPath = storageService.getScreenerWorkingFormSchemaPath(newScreenerId); - storageService.writeJsonToStorage(formPath, seedScreener.formSchema()); + String formPath = storageService + .getScreenerWorkingFormSchemaPath(newScreenerId); + storageService.writeJsonToStorage( + formPath, + seedScreener.formSchema()); } importedScreenerIds.add(newScreenerId); - Log.info("Imported example screener " + newScreenerId + " for user " + userId); + Log.info( + "Imported example screener " + newScreenerId + + " for user " + userId); } return importedScreenerIds; } } - private Map importReferencedCustomChecks(SeedData seedData, String userId) throws Exception { + private Map importReferencedCustomChecks(SeedData seedData, + String userId) throws Exception { Set referencedCustomCheckIds = new LinkedHashSet<>(); for (SeedScreenerData seedScreener : seedData.screeners()) { for (Benefit benefit : seedScreener.benefits()) { @@ -116,7 +129,8 @@ private Map importReferencedCustomChecks(SeedData seedData, Stri } for (CheckConfig checkConfig : benefit.getChecks()) { String sourceCheckId = resolveSourceCheckId(checkConfig); - if (sourceCheckId != null && !isLibraryCheckId(sourceCheckId)) { + if (sourceCheckId != null + && !isLibraryCheckId(sourceCheckId)) { referencedCustomCheckIds.add(sourceCheckId); } } @@ -125,74 +139,85 @@ private Map importReferencedCustomChecks(SeedData seedData, Stri Map remappedCheckIds = new HashMap<>(); for (String seedSourceCheckId : referencedCustomCheckIds) { - SeedCustomCheckVersions seedCustomCheckVersions = findSeedCustomCheckVersions(seedData, seedSourceCheckId); + SeedCustomCheckVersions seedCustomCheckVersions = findSeedCustomCheckVersions( + seedData, + seedSourceCheckId); if (seedCustomCheckVersions.workingCheck() != null) { String newWorkingId = upsertWorkingCustomCheck( - userId, - seedCustomCheckVersions.workingCheck(), - seedData.dmnByCheckId() - ); - remappedCheckIds.put(seedCustomCheckVersions.workingCheck().getId(), newWorkingId); + userId, + seedCustomCheckVersions.workingCheck(), + seedData.dmnByCheckId()); + remappedCheckIds.put( + seedCustomCheckVersions.workingCheck().getId(), + newWorkingId); } if (seedCustomCheckVersions.publishedCheck() != null) { String newPublishedId = upsertPublishedCustomCheck( - userId, - seedCustomCheckVersions.publishedCheck(), - seedData.dmnByCheckId() - ); - remappedCheckIds.put(seedCustomCheckVersions.publishedCheck().getId(), newPublishedId); + userId, + seedCustomCheckVersions.publishedCheck(), + seedData.dmnByCheckId()); + remappedCheckIds.put( + seedCustomCheckVersions.publishedCheck().getId(), + newPublishedId); } if (!remappedCheckIds.containsKey(seedSourceCheckId)) { - throw new IllegalStateException("No imported check mapping found for seed check " + seedSourceCheckId); + throw new IllegalStateException( + "No imported check mapping found for seed check " + + seedSourceCheckId); } } return remappedCheckIds; } - private SeedCustomCheckVersions findSeedCustomCheckVersions(SeedData seedData, String seedSourceCheckId) { - EligibilityCheck referencedCheck = seedData.workingCustomChecks().get(seedSourceCheckId); + private SeedCustomCheckVersions findSeedCustomCheckVersions( + SeedData seedData, String seedSourceCheckId) { + EligibilityCheck referencedCheck = seedData.workingCustomChecks() + .get(seedSourceCheckId); if (referencedCheck == null) { - referencedCheck = seedData.publishedCustomChecks().get(seedSourceCheckId); + referencedCheck = seedData.publishedCustomChecks() + .get(seedSourceCheckId); } if (referencedCheck == null) { - throw new IllegalStateException("Missing seed custom check for referenced id " + seedSourceCheckId); + throw new IllegalStateException( + "Missing seed custom check for referenced id " + + seedSourceCheckId); } String seedWorkingId = buildWorkingCheckId( - referencedCheck.getOwnerId(), - referencedCheck.getModule(), - referencedCheck.getName() - ); + referencedCheck.getOwnerId(), + referencedCheck.getModule(), + referencedCheck.getName()); String seedPublishedId = buildPublishedCheckId( - referencedCheck.getOwnerId(), - referencedCheck.getModule(), - referencedCheck.getName(), - referencedCheck.getVersion() - ); + referencedCheck.getOwnerId(), + referencedCheck.getModule(), + referencedCheck.getName(), + referencedCheck.getVersion()); return new SeedCustomCheckVersions( - seedData.workingCustomChecks().get(seedWorkingId), - seedData.publishedCustomChecks().get(seedPublishedId) - ); + seedData.workingCustomChecks().get(seedWorkingId), + seedData.publishedCustomChecks().get(seedPublishedId)); } - private String upsertWorkingCustomCheck( - String userId, - EligibilityCheck seedCheck, - Map dmnByCheckId - ) throws Exception { + private String upsertWorkingCustomCheck(String userId, + EligibilityCheck seedCheck, Map dmnByCheckId) + throws Exception { EligibilityCheck importedCheck = cloneEligibilityCheck(seedCheck); importedCheck.setOwnerId(userId); importedCheck.setIsArchived(false); - String newWorkingId = buildWorkingCheckId(userId, importedCheck.getModule(), importedCheck.getName()); + String newWorkingId = buildWorkingCheckId( + userId, + importedCheck.getModule(), + importedCheck.getName()); importedCheck.setId(newWorkingId); - if (eligibilityCheckRepository.getWorkingCustomCheck(userId, newWorkingId, true).isPresent()) { + if (eligibilityCheckRepository + .getWorkingCustomCheck(userId, newWorkingId, true) + .isPresent()) { eligibilityCheckRepository.updateWorkingCustomCheck(importedCheck); } else { eligibilityCheckRepository.saveNewWorkingCustomCheck(importedCheck); @@ -202,31 +227,32 @@ private String upsertWorkingCustomCheck( return newWorkingId; } - private String upsertPublishedCustomCheck( - String userId, - EligibilityCheck seedCheck, - Map dmnByCheckId - ) throws Exception { + private String upsertPublishedCustomCheck(String userId, + EligibilityCheck seedCheck, Map dmnByCheckId) + throws Exception { EligibilityCheck importedCheck = cloneEligibilityCheck(seedCheck); importedCheck.setOwnerId(userId); importedCheck.setIsArchived(false); String newPublishedId = buildPublishedCheckId( - userId, - importedCheck.getModule(), - importedCheck.getName(), - importedCheck.getVersion() - ); + userId, + importedCheck.getModule(), + importedCheck.getName(), + importedCheck.getVersion()); importedCheck.setId(newPublishedId); - if (eligibilityCheckRepository.getPublishedCustomCheck(userId, newPublishedId).isPresent()) { - eligibilityCheckRepository.updatePublishedCustomCheck(importedCheck); + if (eligibilityCheckRepository + .getPublishedCustomCheck(userId, newPublishedId).isPresent()) { + eligibilityCheckRepository + .updatePublishedCustomCheck(importedCheck); } else { try { - eligibilityCheckRepository.saveNewPublishedCustomCheck(importedCheck); + eligibilityCheckRepository + .saveNewPublishedCustomCheck(importedCheck); } catch (Exception e) { Log.info(e); - eligibilityCheckRepository.updatePublishedCustomCheck(importedCheck); + eligibilityCheckRepository + .updatePublishedCustomCheck(importedCheck); } } @@ -234,38 +260,47 @@ private String upsertPublishedCustomCheck( return newPublishedId; } - private void writeCheckDmn(String newCheckId, EligibilityCheck seedCheck, Map dmnByCheckId) throws Exception { + private void writeCheckDmn(String newCheckId, EligibilityCheck seedCheck, + Map dmnByCheckId) throws Exception { String dmnModel = dmnByCheckId.get(seedCheck.getId()); - if ((dmnModel == null || dmnModel.isBlank()) && seedCheck.getDmnModel() != null) { + if ((dmnModel == null || dmnModel.isBlank()) + && seedCheck.getDmnModel() != null) { dmnModel = seedCheck.getDmnModel(); } if (dmnModel == null || dmnModel.isBlank()) { - throw new IllegalStateException("Missing DMN model for seed check " + seedCheck.getId()); + throw new IllegalStateException( + "Missing DMN model for seed check " + seedCheck.getId()); } storageService.writeStringToStorage( - storageService.getCheckDmnModelPath(newCheckId), - dmnModel, - "application/xml" - ); + storageService.getCheckDmnModelPath(newCheckId), + dmnModel, + "application/xml"); } - private Benefit cloneBenefit(Benefit seedBenefit, String userId, Map importedCustomCheckIds) { - Benefit importedBenefit = objectMapper.convertValue(seedBenefit, Benefit.class); + private Benefit cloneBenefit(Benefit seedBenefit, String userId, + Map importedCustomCheckIds) { + Benefit importedBenefit = objectMapper + .convertValue(seedBenefit, Benefit.class); importedBenefit.setId(UUID.randomUUID().toString()); importedBenefit.setOwnerId(userId); - importedBenefit.setChecks(remapCheckConfigs(seedBenefit.getChecks(), importedCustomCheckIds)); + importedBenefit.setChecks( + remapCheckConfigs( + seedBenefit.getChecks(), + importedCustomCheckIds)); return importedBenefit; } - private List remapCheckConfigs(List seedChecks, Map importedCustomCheckIds) { + private List remapCheckConfigs(List seedChecks, + Map importedCustomCheckIds) { if (seedChecks == null || seedChecks.isEmpty()) { return Collections.emptyList(); } List importedChecks = new ArrayList<>(); for (CheckConfig seedCheck : seedChecks) { - CheckConfig importedCheck = objectMapper.convertValue(seedCheck, CheckConfig.class); + CheckConfig importedCheck = objectMapper + .convertValue(seedCheck, CheckConfig.class); importedCheck.setCheckId(UUID.randomUUID().toString()); String sourceCheckId = resolveSourceCheckId(seedCheck); @@ -273,9 +308,12 @@ private List remapCheckConfigs(List seedChecks, Map screenerFiles = listJsonFiles(workingScreenersDir); if (screenerFiles.isEmpty()) { - throw new IllegalStateException("Expected at least one working screener seed document"); + throw new IllegalStateException( + "Expected at least one working screener seed document"); } List screeners = new ArrayList<>(); for (Path screenerFile : screenerFiles) { Screener screener = readJsonFile(screenerFile, Screener.class); - String screenerDocId = stripExtension(screenerFile.getFileName().toString()); + String screenerDocId = stripExtension( + screenerFile.getFileName().toString()); - Path benefitsDir = workingScreenersDir.resolve(screenerDocId).resolve("customBenefit"); + Path benefitsDir = workingScreenersDir.resolve(screenerDocId) + .resolve("customBenefit"); List benefits = new ArrayList<>(); for (Path benefitFile : listJsonFiles(benefitsDir)) { benefits.add(readJsonFile(benefitFile, Benefit.class)); } - screeners.add(new SeedScreenerData( - screener, - benefits, - loadFormSchema(seedRoot, screenerDocId) - )); + screeners.add( + new SeedScreenerData(screener, benefits, + loadFormSchema(seedRoot, screenerDocId))); } - return new SeedData( - screeners, - loadChecks(seedRoot.resolve("firestore").resolve("workingCustomCheck")), - loadChecks(seedRoot.resolve("firestore").resolve("publishedCustomCheck")), - loadDmnFiles(seedRoot.resolve("storage").resolve("check")) - ); + return new SeedData(screeners, loadChecks( + seedRoot.resolve("firestore").resolve("workingCustomCheck")), + loadChecks( + seedRoot.resolve("firestore") + .resolve("publishedCustomCheck")), + loadDmnFiles(seedRoot.resolve("storage").resolve("check"))); } - private JsonNode loadFormSchema(Path seedRoot, String screenerDocId) throws IOException { - Path formSchemaPath = seedRoot.resolve("storage").resolve("form").resolve("working").resolve(screenerDocId + ".json"); + private JsonNode loadFormSchema(Path seedRoot, String screenerDocId) + throws IOException { + Path formSchemaPath = seedRoot.resolve("storage").resolve("form") + .resolve("working").resolve(screenerDocId + ".json"); if (!Files.isRegularFile(formSchemaPath)) { return null; } return objectMapper.readTree(Files.readString(formSchemaPath)); } - private Map loadChecks(Path checksDir) throws IOException { + private Map loadChecks(Path checksDir) + throws IOException { Map checksById = new LinkedHashMap<>(); if (!Files.isDirectory(checksDir)) { return checksById; } for (Path checkFile : listJsonFiles(checksDir)) { - EligibilityCheck check = readJsonFile(checkFile, EligibilityCheck.class); + EligibilityCheck check = readJsonFile( + checkFile, + EligibilityCheck.class); checksById.put(check.getId(), check); } return checksById; @@ -363,17 +409,22 @@ private Map loadDmnFiles(Path dmnDir) throws IOException { } try (var stream = Files.list(dmnDir)) { - stream - .filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".dmn")) - .sorted(Comparator.comparing(path -> path.getFileName().toString())) - .forEach(path -> { - try { - dmnByCheckId.put(stripExtension(path.getFileName().toString()), Files.readString(path)); - } catch (IOException e) { - throw new RuntimeException("Failed to read DMN file " + path, e); - } - }); + stream.filter(Files::isRegularFile).filter( + path -> path.getFileName().toString().endsWith(".dmn")) + .sorted( + Comparator.comparing( + path -> path.getFileName().toString())) + .forEach(path -> { + try { + dmnByCheckId.put( + stripExtension( + path.getFileName().toString()), + Files.readString(path)); + } catch (IOException e) { + throw new RuntimeException( + "Failed to read DMN file " + path, e); + } + }); } catch (RuntimeException e) { if (e.getCause() instanceof IOException ioException) { throw ioException; @@ -394,22 +445,22 @@ private List listJsonFiles(Path directory) throws IOException { } try (var stream = Files.list(directory)) { - return stream - .filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".json")) - .sorted(Comparator.comparing(path -> path.getFileName().toString())) - .toList(); + return stream.filter(Files::isRegularFile).filter( + path -> path.getFileName().toString().endsWith(".json")) + .sorted( + Comparator.comparing( + path -> path.getFileName().toString())) + .toList(); } } private SeedRoot resolveSeedRoot() { - Optional configuredPath = configuredSeedPath - .map(String::trim) - .filter(path -> !path.isBlank()) - .map(Paths::get); + Optional configuredPath = configuredSeedPath.map(String::trim) + .filter(path -> !path.isBlank()).map(Paths::get); if (configuredPath.isPresent()) { - Path absoluteCandidate = configuredPath.get().toAbsolutePath().normalize(); + Path absoluteCandidate = configuredPath.get().toAbsolutePath() + .normalize(); if (Files.isDirectory(absoluteCandidate)) { return new SeedRoot(absoluteCandidate, null); } @@ -418,16 +469,19 @@ private SeedRoot resolveSeedRoot() { try { return resolveBundledSeedRoot(); } catch (IOException | URISyntaxException e) { - throw new IllegalStateException("Could not load bundled example screener seed data", e); + throw new IllegalStateException( + "Could not load bundled example screener seed data", e); } } - private SeedRoot resolveBundledSeedRoot() throws IOException, URISyntaxException { - var manifestUrl = ExampleScreenerImportService.class.getClassLoader().getResource(BUNDLED_SEED_MANIFEST); + private SeedRoot resolveBundledSeedRoot() + throws IOException, URISyntaxException { + var manifestUrl = ExampleScreenerImportService.class.getClassLoader() + .getResource(BUNDLED_SEED_MANIFEST); if (manifestUrl == null) { throw new IllegalStateException( - "Could not find bundled example screener seed data at classpath:" + BUNDLED_SEED_ROOT - ); + "Could not find bundled example screener seed data at classpath:" + + BUNDLED_SEED_ROOT); } URI manifestUri = manifestUrl.toURI(); @@ -435,30 +489,38 @@ private SeedRoot resolveBundledSeedRoot() throws IOException, URISyntaxException String manifestUriString = manifestUri.toString(); int archiveSeparatorIndex = manifestUriString.indexOf("!/"); if (archiveSeparatorIndex < 0) { - throw new IllegalStateException("Unexpected jar resource URI for bundled seed data: " + manifestUri); + throw new IllegalStateException( + "Unexpected jar resource URI for bundled seed data: " + + manifestUri); } - URI archiveUri = URI.create(manifestUriString.substring(0, archiveSeparatorIndex)); + URI archiveUri = URI.create( + manifestUriString.substring(0, archiveSeparatorIndex)); FileSystem fileSystem = getOrCreateFileSystem(archiveUri); - return new SeedRoot(fileSystem.getPath("/" + BUNDLED_SEED_ROOT), fileSystem); + return new SeedRoot(fileSystem.getPath("/" + BUNDLED_SEED_ROOT), + fileSystem); } return new SeedRoot(Paths.get(manifestUri).getParent(), null); } - private FileSystem getOrCreateFileSystem(URI archiveUri) throws IOException { + private FileSystem getOrCreateFileSystem(URI archiveUri) + throws IOException { try { return FileSystems.getFileSystem(archiveUri); } catch (FileSystemNotFoundException ignored) { - return FileSystems.newFileSystem(archiveUri, Collections.emptyMap()); + return FileSystems + .newFileSystem(archiveUri, Collections.emptyMap()); } } - private String buildWorkingCheckId(String ownerId, String module, String name) { + private String buildWorkingCheckId(String ownerId, String module, + String name) { return "W-" + ownerId + "-" + module + "-" + name; } - private String buildPublishedCheckId(String ownerId, String module, String name, String version) { + private String buildPublishedCheckId(String ownerId, String module, + String name, String version) { return "P-" + ownerId + "-" + module + "-" + name + "-" + version; } @@ -470,25 +532,22 @@ private String stripExtension(String filename) { return filename.substring(0, extensionIndex); } - private record SeedData( - List screeners, - Map workingCustomChecks, - Map publishedCustomChecks, - Map dmnByCheckId - ) {} - - private record SeedScreenerData( - Screener screener, - List benefits, - JsonNode formSchema - ) {} - - private record SeedCustomCheckVersions( - EligibilityCheck workingCheck, - EligibilityCheck publishedCheck - ) {} - - private record SeedRoot(Path path, FileSystem fileSystem) implements AutoCloseable { + private record SeedData(List screeners, + Map workingCustomChecks, + Map publishedCustomChecks, + Map dmnByCheckId) { + } + + private record SeedScreenerData(Screener screener, List benefits, + JsonNode formSchema) { + } + + private record SeedCustomCheckVersions(EligibilityCheck workingCheck, + EligibilityCheck publishedCheck) { + } + + private record SeedRoot(Path path, FileSystem fileSystem) + implements AutoCloseable { @Override public void close() throws IOException { if (fileSystem != null && fileSystem.isOpen()) { From 2ab7e7750894d29fca02481842b95f8c7cd0d8f1 Mon Sep 17 00:00:00 2001 From: joshwanf <17016446+joshwanf@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:58:29 -0400 Subject: [PATCH 4/4] ExampleScreenerImportServices reads file system on dev and loads packaged resource on prod using manifest.json. --- .../java/org/acme/functions/AccountHooks.java | 9 +- .../model/dto/ExampleScreener/Manifest.java | 8 + .../dto/ExampleScreener/ScreenerManifest.java | 7 + .../service/ExampleScreenerExportService.java | 181 ++++++---- .../service/ExampleScreenerImportService.java | 311 ++++++------------ ...saJ48BFa77NmDeL-testchecks-test-2.0.0.json | 37 --- ...9RaBsf5WIyRErjr-testchecks-test-2.0.0.json | 31 ++ .../firestore/system/config.json | 11 +- ...yzho27saJ48BFa77NmDeL-testchecks-test.json | 33 -- ...aM6GUk9RaBsf5WIyRErjr-testchecks-test.json | 27 ++ .../workingScreener/Am3FMS6ID2keNa82MbPh.json | 15 + .../b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json | 99 ++++++ .../ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json | 112 +++++++ .../workingScreener/hEStvPeFmEte58GQTC7Y.json | 17 - .../1c09392c-913c-4b2b-9870-a1951534c3fb.json | 110 ------- .../fdd4405a-1a00-4650-8005-9595f16e3788.json | 120 ------- .../seed-data/example-screener/manifest.json | 14 +- ...9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn} | 0 ...aM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn} | 0 ...8GQTC7Y.json => Am3FMS6ID2keNa82MbPh.json} | 0 .../Header/ExportExampleScreener.tsx | 45 +++ .../src/components/Header/Header.tsx | 42 +-- package-lock.json | 31 -- package.json | 6 - 24 files changed, 585 insertions(+), 681 deletions(-) create mode 100644 builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java create mode 100644 builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java delete mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json create mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json delete mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json create mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json create mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json create mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json create mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json delete mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json delete mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json delete mode 100644 builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json rename builder-api/src/main/resources/seed-data/example-screener/storage/check/{P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn => P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn} (100%) rename builder-api/src/main/resources/seed-data/example-screener/storage/check/{W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn => W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn} (100%) rename builder-api/src/main/resources/seed-data/example-screener/storage/form/working/{hEStvPeFmEte58GQTC7Y.json => Am3FMS6ID2keNa82MbPh.json} (100%) create mode 100644 builder-frontend/src/components/Header/ExportExampleScreener.tsx delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/builder-api/src/main/java/org/acme/functions/AccountHooks.java b/builder-api/src/main/java/org/acme/functions/AccountHooks.java index d53501e4..c8b734be 100644 --- a/builder-api/src/main/java/org/acme/functions/AccountHooks.java +++ b/builder-api/src/main/java/org/acme/functions/AccountHooks.java @@ -4,6 +4,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import java.util.List; + import org.acme.service.ExampleScreenerImportService; @ApplicationScoped @@ -14,8 +16,11 @@ public class AccountHooks { public Boolean addExampleScreenerToAccount(String userId) { try { Log.info("Running ADD_EXAMPLE_SCREENER hook for user: " + userId); - var screenerIds = exampleScreenerImportService.importForUser(userId); - Log.info("Imported example screeners " + screenerIds + " for user " + userId); + List screenerIds = exampleScreenerImportService + .importForUser(userId); + Log.info( + "Imported example screeners " + screenerIds + " for user " + + userId); return true; } catch (Exception e) { Log.error( diff --git a/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java new file mode 100644 index 00000000..ac8b7a13 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/Manifest.java @@ -0,0 +1,8 @@ +package org.acme.model.dto.ExampleScreener; + +import java.util.List; + +public record Manifest(List screeners, + List workingCustomChecks, List publishedCustomChecks, + List dmnPaths) { +} diff --git a/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java new file mode 100644 index 00000000..449cd277 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/ExampleScreener/ScreenerManifest.java @@ -0,0 +1,7 @@ +package org.acme.model.dto.ExampleScreener; + +import java.util.List; + +public record ScreenerManifest(String screenerPath, List benefits, + String formSchema) { +} diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java index 2424492f..5df421a9 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerExportService.java @@ -2,12 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.cloud.Timestamp; -import com.google.firebase.cloud.FirestoreClient; import io.quarkus.logging.Log; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.acme.constants.CollectionNames; import org.acme.constants.FieldNames; +import org.acme.model.dto.ExampleScreener.ScreenerManifest; import org.acme.persistence.FirestoreUtils; import org.acme.persistence.StorageService; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -16,7 +16,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -61,21 +60,35 @@ public ExportSummary exportForUser(String userId) throws Exception { userId); int firestoreDocuments = 0; - firestoreDocuments += exportScreeners(workingScreeners); - firestoreDocuments += exportChecks( + ExportScreenerResult exportedScreeners = exportScreeners( + workingScreeners); + ExportDocumentResult exportedWorkingChecks = exportChecks( CollectionNames.WORKING_CUSTOM_CHECK_COLLECTION, workingCustomChecks); - firestoreDocuments += exportChecks( + ExportDocumentResult exportedPublishedChecks = exportChecks( CollectionNames.PUBLISHED_CUSTOM_CHECK_COLLECTION, publishedCustomChecks); + + firestoreDocuments += exportedScreeners.numExported() + + exportedWorkingChecks.numExported() + + exportedPublishedChecks.numExported(); firestoreDocuments += exportSystemConfig(); int storageFiles = 0; - storageFiles += exportScreenerForms(workingScreeners); - storageFiles += exportCheckDmns(workingCustomChecks); - storageFiles += exportCheckDmns(publishedCustomChecks); + ExportDocumentResult exportedWorkingCheckDmns = exportCheckDmns( + workingCustomChecks); + ExportDocumentResult exportedPublishedCheckDmns = exportCheckDmns( + publishedCustomChecks); + + storageFiles += exportedWorkingCheckDmns.numExported() + + exportedPublishedCheckDmns.numExported(); - writeManifest(firestoreDocuments, storageFiles); + writeManifest( + exportedScreeners, + exportedWorkingChecks, + exportedPublishedChecks, + exportedWorkingCheckDmns, + exportedPublishedCheckDmns); Log.info( "Exported Firebase seed data for user " + userId + " to " @@ -101,28 +114,38 @@ private List> getDocumentsByOwner(String collectionName, return documents; } - private int exportScreeners(List> workingScreeners) - throws IOException { - int firestoreDocuments = 0; + private ExportScreenerResult exportScreeners( + List> workingScreeners) throws IOException { + int numExported = 0; + List screeners = new ArrayList<>(); for (Map screener : workingScreeners) { String screenerId = requiredString( screener, FieldNames.ID, CollectionNames.WORKING_SCREENER_COLLECTION); + Path screenerPath = EXPORT_ROOT.resolve("firestore") + .resolve("workingScreener").resolve(screenerId + ".json"); writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve("workingScreener") - .resolve(screenerId + ".json"), + screenerPath, firestoreDocumentForExport(screener, screenerId)); - firestoreDocuments++; + numExported++; + + ExportDocumentResult exportedBenefits = exportBenefits(screenerId); + String exportedFormPath = exportScreenerForm(screenerId); + + numExported += exportedBenefits.numExported(); - firestoreDocuments += exportBenefits(screenerId); + screeners.add( + new ScreenerManifest(skipFirstPath(screenerPath).toString(), + exportedBenefits.outputPaths(), exportedFormPath)); } - return firestoreDocuments; + return new ExportScreenerResult(numExported, screeners); } - private int exportBenefits(String screenerId) throws IOException { + private ExportDocumentResult exportBenefits(String screenerId) + throws IOException { String collectionPath = CollectionNames.WORKING_SCREENER_COLLECTION + "/" + screenerId + "/customBenefit"; List> benefits = new ArrayList<>( @@ -134,38 +157,45 @@ private int exportBenefits(String screenerId) throws IOException { FieldNames.ID, collectionPath))); - int exportedBenefits = 0; + int numExported = 0; + List outputPaths = new ArrayList<>(); for (Map benefit : benefits) { String benefitId = requiredString( benefit, FieldNames.ID, collectionPath); + Path outputPath = EXPORT_ROOT.resolve("firestore") + .resolve("workingScreener").resolve(screenerId) + .resolve("customBenefit").resolve(benefitId + ".json"); writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve("workingScreener") - .resolve(screenerId).resolve("customBenefit") - .resolve(benefitId + ".json"), + outputPath, firestoreDocumentForExport(benefit, benefitId)); - exportedBenefits++; + numExported++; + outputPaths.add(skipFirstPath(outputPath).toString()); } - return exportedBenefits; + return new ExportDocumentResult(numExported, outputPaths); } - private int exportChecks(String collectionName, + private ExportDocumentResult exportChecks(String collectionName, List> checks) throws IOException { - int exportedChecks = 0; + int numExported = 0; + List checkPaths = new ArrayList<>(); + for (Map check : checks) { String checkId = requiredString( check, FieldNames.ID, collectionName); + Path checkPath = EXPORT_ROOT.resolve("firestore") + .resolve(collectionName).resolve(checkId + ".json"); writeJsonFile( - EXPORT_ROOT.resolve("firestore").resolve(collectionName) - .resolve(checkId + ".json"), + checkPath, firestoreDocumentForExport(check, checkId)); - exportedChecks++; + numExported++; + checkPaths.add(skipFirstPath(checkPath).toString()); } - return exportedChecks; + return new ExportDocumentResult(numExported, checkPaths); } private int exportSystemConfig() throws IOException { @@ -182,36 +212,26 @@ private int exportSystemConfig() throws IOException { return 1; } - private int exportScreenerForms(List> workingScreeners) - throws IOException { - int exportedForms = 0; + private String exportScreenerForm(String screenerId) throws IOException { - for (Map screener : workingScreeners) { - String screenerId = requiredString( - screener, - FieldNames.ID, - CollectionNames.WORKING_SCREENER_COLLECTION); - Optional formSchema = storageService.getStringFromStorage( - storageService - .getScreenerWorkingFormSchemaPath(screenerId)); + Optional formSchema = storageService.getStringFromStorage( + storageService.getScreenerWorkingFormSchemaPath(screenerId)); - if (formSchema.isEmpty()) { - continue; - } - - writeStringFile( - EXPORT_ROOT.resolve("storage").resolve("form") - .resolve("working").resolve(screenerId + ".json"), - formSchema.get()); - exportedForms++; + Path formPath = !formSchema.isEmpty() + ? EXPORT_ROOT.resolve("storage").resolve("form") + .resolve("working").resolve(screenerId + ".json") + : Path.of(""); + if (!formSchema.isEmpty()) { + writeStringFile(formPath, formSchema.get()); } - return exportedForms; + return skipFirstPath(formPath).toString(); } - private int exportCheckDmns(List> checks) - throws IOException { - int exportedDmns = 0; + private ExportDocumentResult exportCheckDmns( + List> checks) throws IOException { + int numExported = 0; + List exportedDmns = new ArrayList<>(); Set exportedIds = new LinkedHashSet<>(); for (Map check : checks) { @@ -228,28 +248,35 @@ private int exportCheckDmns(List> checks) if (dmnModel.isEmpty()) { continue; } - - writeStringFile( - EXPORT_ROOT.resolve("storage").resolve("check") - .resolve(checkId + ".dmn"), - dmnModel.get()); - exportedDmns++; + Path exportPath = EXPORT_ROOT.resolve("storage").resolve("check") + .resolve(checkId + ".dmn"); + writeStringFile(exportPath, dmnModel.get()); + numExported++; + exportedDmns.add(skipFirstPath(exportPath).toString()); } - return exportedDmns; + return new ExportDocumentResult(numExported, exportedDmns); } - private void writeManifest(int firestoreDocuments, int storageFiles) + private void writeManifest(ExportScreenerResult exportedScreeners, + ExportDocumentResult exportedWorkingChecks, + ExportDocumentResult exportedPublishedChecks, + ExportDocumentResult exportedWorkingCheckDmns, + ExportDocumentResult exportedPublishedCheckDmns) throws IOException { + List combinedDmnPaths = new ArrayList<>( + exportedWorkingCheckDmns.outputPaths()); + combinedDmnPaths.addAll(exportedPublishedCheckDmns.outputPaths()); + Map manifest = new LinkedHashMap<>(); - manifest.put("exportedAt", Instant.now().toString()); - manifest.put("source", "builder-api"); + manifest.put("screeners", exportedScreeners.screeners()); + manifest.put( + "workingCustomChecks", + exportedWorkingChecks.outputPaths()); manifest.put( - "projectId", - FirestoreClient.getFirestore().getOptions().getProjectId()); - manifest.put("storageBucket", bucketName); - manifest.put("firestoreDocuments", firestoreDocuments); - manifest.put("storageFiles", storageFiles); + "publishedCustomChecks", + exportedPublishedChecks.outputPaths()); + manifest.put("dmnPaths", combinedDmnPaths); writeJsonFile(EXPORT_ROOT.resolve("manifest.json"), manifest); } @@ -341,6 +368,22 @@ private String requiredString(Map data, String fieldName, return stringValue; } + private Path skipFirstPath(Path path) { + int pathCount = path.getNameCount(); + if (pathCount <= 3) { + return path; + } + return path.subpath(3, path.getNameCount()); + } + + private record ExportDocumentResult(int numExported, + List outputPaths) { + } + + private record ExportScreenerResult(int numExported, + List screeners) { + } + public record ExportSummary(String outputPath, int screenerCount, int firestoreDocuments, int storageFiles) { } diff --git a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java index 87860c58..b100976f 100644 --- a/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java +++ b/builder-api/src/main/java/org/acme/service/ExampleScreenerImportService.java @@ -2,7 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + import io.quarkus.logging.Log; +import io.quarkus.runtime.LaunchMode; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.acme.model.domain.Benefit; @@ -10,23 +13,21 @@ import org.acme.model.domain.CheckConfig; import org.acme.model.domain.EligibilityCheck; import org.acme.model.domain.Screener; +import org.acme.model.dto.ExampleScreener.Manifest; +import org.acme.model.dto.ExampleScreener.ScreenerManifest; import org.acme.persistence.EligibilityCheckRepository; import org.acme.persistence.ScreenerRepository; import org.acme.persistence.StorageService; import org.eclipse.microprofile.config.inject.ConfigProperty; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; +import java.io.InputStream; import java.nio.file.Files; -import java.nio.file.FileSystem; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.FileSystems; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -38,14 +39,11 @@ @ApplicationScoped public class ExampleScreenerImportService { - private static final String BUNDLED_SEED_ROOT = "seed-data/example-screener"; - private static final String BUNDLED_SEED_MANIFEST = BUNDLED_SEED_ROOT - + "/manifest.json"; + private static final String BUNDLED_SEED_MANIFEST = "seed-data/example-screener/manifest.json"; private final ScreenerRepository screenerRepository; private final EligibilityCheckRepository eligibilityCheckRepository; private final StorageService storageService; - private final Optional configuredSeedPath; private final ObjectMapper objectMapper; @Inject @@ -56,67 +54,63 @@ public ExampleScreenerImportService(ScreenerRepository screenerRepository, this.screenerRepository = screenerRepository; this.eligibilityCheckRepository = eligibilityCheckRepository; this.storageService = storageService; - this.configuredSeedPath = configuredSeedPath; this.objectMapper = new ObjectMapper(); } public List importForUser(String userId) throws Exception { - SeedRoot seedRoot = resolveSeedRoot(); - try (seedRoot) { - SeedData seedData = loadSeedData(seedRoot.path()); + Manifest manifest = readJsonRes(BUNDLED_SEED_MANIFEST, Manifest.class); + SeedData seedData = loadSeedData(manifest); - Map importedCustomCheckIds = importReferencedCustomChecks( - seedData, - userId); - List importedScreenerIds = new ArrayList<>(); - - for (SeedScreenerData seedScreener : seedData.screeners()) { - List importedBenefits = new ArrayList<>(); - List importedBenefitDetails = new ArrayList<>(); - for (Benefit seedBenefit : seedScreener.benefits()) { - Benefit importedBenefit = cloneBenefit( - seedBenefit, - userId, - importedCustomCheckIds); - importedBenefits.add(importedBenefit); - importedBenefitDetails.add( - new BenefitDetail(importedBenefit.getId(), - importedBenefit.getName(), - importedBenefit.getDescription())); - } + Map importedCustomCheckIds = importReferencedCustomChecks( + seedData, + userId); + List importedScreenerIds = new ArrayList<>(); - Screener importedScreener = new Screener(); - importedScreener.setOwnerId(userId); - importedScreener.setScreenerName( - seedScreener.screener().getScreenerName()); - importedScreener.setBenefits(importedBenefitDetails); + for (SeedScreenerData seedScreener : seedData.screeners()) { + List importedBenefits = new ArrayList<>(); + List importedBenefitDetails = new ArrayList<>(); + for (Benefit seedBenefit : seedScreener.benefits()) { + Benefit importedBenefit = cloneBenefit( + seedBenefit, + userId, + importedCustomCheckIds); + importedBenefits.add(importedBenefit); + importedBenefitDetails.add( + new BenefitDetail(importedBenefit.getId(), + importedBenefit.getName(), + importedBenefit.getDescription())); + } - String newScreenerId = screenerRepository - .saveNewWorkingScreener(importedScreener); - importedScreener.setId(newScreenerId); + Screener importedScreener = new Screener(); + importedScreener.setOwnerId(userId); + importedScreener + .setScreenerName(seedScreener.screener().getScreenerName()); + importedScreener.setBenefits(importedBenefitDetails); - for (Benefit importedBenefit : importedBenefits) { - screenerRepository.saveNewCustomBenefit( - newScreenerId, - importedBenefit); - } + String newScreenerId = screenerRepository + .saveNewWorkingScreener(importedScreener); + importedScreener.setId(newScreenerId); - if (seedScreener.formSchema() != null) { - String formPath = storageService - .getScreenerWorkingFormSchemaPath(newScreenerId); - storageService.writeJsonToStorage( - formPath, - seedScreener.formSchema()); - } + for (Benefit importedBenefit : importedBenefits) { + screenerRepository + .saveNewCustomBenefit(newScreenerId, importedBenefit); + } - importedScreenerIds.add(newScreenerId); - Log.info( - "Imported example screener " + newScreenerId - + " for user " + userId); + if (seedScreener.formSchema() != null) { + String formPath = storageService + .getScreenerWorkingFormSchemaPath(newScreenerId); + storageService.writeJsonToStorage( + formPath, + seedScreener.formSchema()); } - return importedScreenerIds; + importedScreenerIds.add(newScreenerId); + Log.info( + "Imported example screener " + newScreenerId + " for user " + + userId); } + + return importedScreenerIds; } private Map importReferencedCustomChecks(SeedData seedData, @@ -341,176 +335,87 @@ private boolean isLibraryCheckId(String checkId) { return checkId != null && checkId.startsWith("L"); } - private SeedData loadSeedData(Path seedRoot) throws IOException { - Path workingScreenersDir = seedRoot.resolve("firestore") - .resolve("workingScreener"); - List screenerFiles = listJsonFiles(workingScreenersDir); - if (screenerFiles.isEmpty()) { - throw new IllegalStateException( - "Expected at least one working screener seed document"); - } + private SeedData loadSeedData(Manifest manifest) throws IOException { + List screenerFiles = manifest.screeners(); List screeners = new ArrayList<>(); - for (Path screenerFile : screenerFiles) { - Screener screener = readJsonFile(screenerFile, Screener.class); - String screenerDocId = stripExtension( - screenerFile.getFileName().toString()); + for (ScreenerManifest screenerManifest : screenerFiles) { + String screenerPath = screenerManifest.screenerPath(); + Screener screener = readJsonRes(screenerPath, Screener.class); - Path benefitsDir = workingScreenersDir.resolve(screenerDocId) - .resolve("customBenefit"); + List benefitsFiles = screenerManifest.benefits(); List benefits = new ArrayList<>(); - for (Path benefitFile : listJsonFiles(benefitsDir)) { - benefits.add(readJsonFile(benefitFile, Benefit.class)); + for (String benefitPath : benefitsFiles) { + benefits.add(readJsonRes(benefitPath, Benefit.class)); } - screeners.add( - new SeedScreenerData(screener, benefits, - loadFormSchema(seedRoot, screenerDocId))); + JsonNode formSchema = screenerManifest.formSchema().length() > 0 + ? loadFormSchema(screenerManifest.formSchema()) + : JsonNodeFactory.instance.objectNode(); + + screeners.add(new SeedScreenerData(screener, benefits, formSchema)); } - return new SeedData(screeners, loadChecks( - seedRoot.resolve("firestore").resolve("workingCustomCheck")), - loadChecks( - seedRoot.resolve("firestore") - .resolve("publishedCustomCheck")), - loadDmnFiles(seedRoot.resolve("storage").resolve("check"))); + return new SeedData(screeners, + loadChecks(manifest.workingCustomChecks()), + loadChecks(manifest.publishedCustomChecks()), + loadDmnFiles(manifest.dmnPaths())); } - private JsonNode loadFormSchema(Path seedRoot, String screenerDocId) - throws IOException { - Path formSchemaPath = seedRoot.resolve("storage").resolve("form") - .resolve("working").resolve(screenerDocId + ".json"); - if (!Files.isRegularFile(formSchemaPath)) { - return null; - } - return objectMapper.readTree(Files.readString(formSchemaPath)); + private JsonNode loadFormSchema(String formPath) { + return readJsonRes(formPath, JsonNode.class); } - private Map loadChecks(Path checksDir) - throws IOException { + private Map loadChecks(List checksPaths) { Map checksById = new LinkedHashMap<>(); - if (!Files.isDirectory(checksDir)) { - return checksById; - } - for (Path checkFile : listJsonFiles(checksDir)) { - EligibilityCheck check = readJsonFile( - checkFile, + for (String checkPath : checksPaths) { + EligibilityCheck check = readJsonRes( + checkPath, EligibilityCheck.class); checksById.put(check.getId(), check); } return checksById; } - private Map loadDmnFiles(Path dmnDir) throws IOException { + private Map loadDmnFiles(List dmnPaths) { Map dmnByCheckId = new HashMap<>(); - if (!Files.isDirectory(dmnDir)) { - return dmnByCheckId; - } - try (var stream = Files.list(dmnDir)) { - stream.filter(Files::isRegularFile).filter( - path -> path.getFileName().toString().endsWith(".dmn")) - .sorted( - Comparator.comparing( - path -> path.getFileName().toString())) - .forEach(path -> { - try { - dmnByCheckId.put( - stripExtension( - path.getFileName().toString()), - Files.readString(path)); - } catch (IOException e) { - throw new RuntimeException( - "Failed to read DMN file " + path, e); - } - }); - } catch (RuntimeException e) { - if (e.getCause() instanceof IOException ioException) { - throw ioException; + dmnPaths.stream().forEach(path -> { + try { + InputStream stream = getPathStream(path); + String contents = new String(stream.readAllBytes(), + StandardCharsets.UTF_8); + dmnByCheckId.put(stripExtension(getIdFromPath(path)), contents); + } catch (IOException exception) { + Log.info("Error reading DMN file: " + path); } - throw e; - } + }); return dmnByCheckId; } - private T readJsonFile(Path path, Class clazz) throws IOException { - return objectMapper.readValue(Files.readString(path), clazz); - } - - private List listJsonFiles(Path directory) throws IOException { - if (!Files.isDirectory(directory)) { - return Collections.emptyList(); - } - - try (var stream = Files.list(directory)) { - return stream.filter(Files::isRegularFile).filter( - path -> path.getFileName().toString().endsWith(".json")) - .sorted( - Comparator.comparing( - path -> path.getFileName().toString())) - .toList(); - } - } - - private SeedRoot resolveSeedRoot() { - Optional configuredPath = configuredSeedPath.map(String::trim) - .filter(path -> !path.isBlank()).map(Paths::get); - - if (configuredPath.isPresent()) { - Path absoluteCandidate = configuredPath.get().toAbsolutePath() - .normalize(); - if (Files.isDirectory(absoluteCandidate)) { - return new SeedRoot(absoluteCandidate, null); - } - } - + private T readJsonRes(String path, Class clazz) { try { - return resolveBundledSeedRoot(); - } catch (IOException | URISyntaxException e) { - throw new IllegalStateException( - "Could not load bundled example screener seed data", e); - } - } - - private SeedRoot resolveBundledSeedRoot() - throws IOException, URISyntaxException { - var manifestUrl = ExampleScreenerImportService.class.getClassLoader() - .getResource(BUNDLED_SEED_MANIFEST); - if (manifestUrl == null) { - throw new IllegalStateException( - "Could not find bundled example screener seed data at classpath:" - + BUNDLED_SEED_ROOT); - } - - URI manifestUri = manifestUrl.toURI(); - if ("jar".equalsIgnoreCase(manifestUri.getScheme())) { - String manifestUriString = manifestUri.toString(); - int archiveSeparatorIndex = manifestUriString.indexOf("!/"); - if (archiveSeparatorIndex < 0) { - throw new IllegalStateException( - "Unexpected jar resource URI for bundled seed data: " - + manifestUri); - } - - URI archiveUri = URI.create( - manifestUriString.substring(0, archiveSeparatorIndex)); - FileSystem fileSystem = getOrCreateFileSystem(archiveUri); - return new SeedRoot(fileSystem.getPath("/" + BUNDLED_SEED_ROOT), - fileSystem); + InputStream stream = getPathStream(path); + return objectMapper.readValue(stream, clazz); + } catch (IOException exception) { + Log.info("Failed to read resource: " + path); + throw new IllegalStateException(exception); } - - return new SeedRoot(Paths.get(manifestUri).getParent(), null); } - private FileSystem getOrCreateFileSystem(URI archiveUri) - throws IOException { + private InputStream getPathStream(String path) throws IOException { try { - return FileSystems.getFileSystem(archiveUri); - } catch (FileSystemNotFoundException ignored) { - return FileSystems - .newFileSystem(archiveUri, Collections.emptyMap()); + if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { + return Files.newInputStream( + Path.of("src", "main", "resources").resolve(path)); + } else { + return Thread.currentThread().getContextClassLoader() + .getResourceAsStream(path); + } + } catch (IOException exception) { + throw new IOException("Could not find: " + path); } } @@ -524,6 +429,10 @@ private String buildPublishedCheckId(String ownerId, String module, return "P-" + ownerId + "-" + module + "-" + name + "-" + version; } + private String getIdFromPath(String path) { + return stripExtension(Paths.get(path).getFileName().toString()); + } + private String stripExtension(String filename) { int extensionIndex = filename.lastIndexOf('.'); if (extensionIndex == -1) { @@ -545,14 +454,4 @@ private record SeedScreenerData(Screener screener, List benefits, private record SeedCustomCheckVersions(EligibilityCheck workingCheck, EligibilityCheck publishedCheck) { } - - private record SeedRoot(Path path, FileSystem fileSystem) - implements AutoCloseable { - @Override - public void close() throws IOException { - if (fileSystem != null && fileSystem.isOpen()) { - fileSystem.close(); - } - } - } } diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json deleted file mode 100644 index 67d59804..00000000 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "version": "2.0.0", - "description": "desc", - "inputDefinition": { - "properties": { - "custom": { - "properties": { - "testinput": {} - }, - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "dmnModel": "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", - "isArchived": false, - "datePublished": 1775619072497, - "module": "testchecks", - "name": "test", - "id": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0", - "_id": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0" -} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json new file mode 100644 index 00000000..ac172c51 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json @@ -0,0 +1,31 @@ +{ + "datePublished" : 1780418141112, + "dmnModel" : "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "isArchived" : false, + "module" : "testchecks", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "properties" : { + "testinput" : { } + }, + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "name" : "test", + "description" : "desc", + "id" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "version" : "2.0.0", + "_id" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json index bed52047..8a181974 100644 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/system/config.json @@ -1,8 +1,9 @@ { - "latestJsonStoragePath": "LibraryApiSchemaExports/export_2026-04-08_03-28-10.json", - "updatedAt": { - "_type": "timestamp", - "value": "2026-04-08T03:28:11.430000+00:00" + "latestJsonStoragePath" : "LibraryApiSchemaExports/export_2026-06-02_15-50-34.json", + "id" : "config", + "updatedAt" : { + "_type" : "timestamp", + "value" : "2026-06-02T15:50:34.539Z" }, - "_id": "config" + "_id" : "config" } \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json deleted file mode 100644 index 8d0cca35..00000000 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "version": "2.0.0", - "description": "desc", - "inputDefinition": { - "properties": { - "custom": { - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "dmnModel": "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", - "isArchived": false, - "module": "testchecks", - "name": "test", - "id": "W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test", - "_id": "W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test" -} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json new file mode 100644 index 00000000..ac31796c --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json @@ -0,0 +1,27 @@ +{ + "dmnModel" : "\n\n \n \n \n \n \n \n \n \n \n \n \n \n true\n \n \n \n \n \n \n \n 300\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "isArchived" : false, + "module" : "testchecks", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "name" : "test", + "description" : "desc", + "id" : "W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "version" : "2.0.0", + "_id" : "W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json new file mode 100644 index 00000000..e677f296 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json @@ -0,0 +1,15 @@ +{ + "benefits" : [ { + "name" : "OOPA", + "description" : "desc", + "id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9" + }, { + "name" : "Homestead Exemption", + "description" : "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", + "id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba" + } ], + "id" : "Am3FMS6ID2keNa82MbPh", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "screenerName" : "Example - Philly Property Tax Relief", + "_id" : "Am3FMS6ID2keNa82MbPh" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json new file mode 100644 index 00000000..9363c374 --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json @@ -0,0 +1,99 @@ +{ + "checks" : [ { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "taxDelinquent" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-tax-delinquent-0.7.1", + "checkName" : "tax-delinquent", + "evaluationUrl" : "/api/v1/checks/residence/tax-delinquent", + "checkId" : "b33675f3-5ec6-4f53-80ce-494a257905ed", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "livesInPhiladelphiaPa" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-lives-in-philadelphia-pa-0.7.1", + "checkName" : "lives-in-philadelphia-pa", + "evaluationUrl" : "/api/v1/checks/residence/lives-in-philadelphia-pa", + "checkId" : "9ae0d755-22b8-4d91-91b7-8e3f20e2baf7", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "ownerOccupant" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-owner-occupant-0.7.1", + "checkName" : "owner-occupant", + "evaluationUrl" : "/api/v1/checks/residence/owner-occupant", + "checkId" : "a6f46b66-9867-4353-a8d7-a18e13933d68", + "parameters" : { } + }, { + "checkModule" : "testchecks", + "checkVersion" : "2.0.0", + "inputDefinition" : { + "type" : "object", + "properties" : { + "custom" : { + "type" : "object", + "properties" : { + "testinput" : { } + }, + "required" : [ "testinput" ] + } + }, + "required" : [ "custom" ] + }, + "parameterDefinitions" : [ { + "label" : "P 1", + "type" : "string", + "key" : "p1", + "required" : true + } ], + "sourceCheckId" : "P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0", + "checkName" : "test", + "checkId" : "9490bea9-be68-465c-a11e-263cb98c4ef3", + "parameters" : { + "p1" : "blah" + } + } ], + "name" : "OOPA", + "description" : "desc", + "id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "_id" : "b481f008-5ed6-4d5f-a0d1-a08733ab91f9" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json new file mode 100644 index 00000000..fa4d1a3c --- /dev/null +++ b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json @@ -0,0 +1,112 @@ +{ + "checks" : [ { + "checkModule" : "enrollment", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "enrollments" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "personId" : { + "type" : "string" + }, + "benefit" : { + "type" : "string" + } + } + } + } + } + }, + "parameterDefinitions" : [ { + "label" : "personId", + "type" : "string", + "key" : "personId", + "required" : false + }, { + "label" : "benefit", + "type" : "string", + "key" : "benefit", + "required" : false + } ], + "sourceCheckId" : "L-enrollment-person-not-enrolled-in-benefit-0.7.1", + "checkName" : "person-not-enrolled-in-benefit", + "evaluationUrl" : "/api/v1/checks/enrollment/person-not-enrolled-in-benefit", + "checkId" : "b3b28b1e-64da-41d2-b9c2-e086a7dc0ec3", + "parameters" : { + "personId" : "client", + "benefit" : "PhlHomesteadExemption" + } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "ownerOccupant" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-owner-occupant-0.7.1", + "checkName" : "owner-occupant", + "evaluationUrl" : "/api/v1/checks/residence/owner-occupant", + "checkId" : "c2ba3e25-9fa4-42fa-a8c9-9624a4b0348d", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "livesInPhiladelphiaPa" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-lives-in-philadelphia-pa-0.7.1", + "checkName" : "lives-in-philadelphia-pa", + "evaluationUrl" : "/api/v1/checks/residence/lives-in-philadelphia-pa", + "checkId" : "6af91242-a6d7-40f9-8693-ec75ad70903a", + "parameters" : { } + }, { + "checkModule" : "residence", + "checkVersion" : "0.7.1", + "inputDefinition" : { + "type" : "object", + "properties" : { + "simpleChecks" : { + "type" : "object", + "properties" : { + "tenYearTaxAbatement" : { + "type" : "boolean" + } + } + } + } + }, + "sourceCheckId" : "L-residence-no-ten-year-tax-abatement-0.7.1", + "checkName" : "no-ten-year-tax-abatement", + "evaluationUrl" : "/api/v1/checks/residence/no-ten-year-tax-abatement", + "checkId" : "f8495889-388c-4b06-935c-cac3f056b68d", + "parameters" : { } + } ], + "name" : "Homestead Exemption", + "description" : "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", + "id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba", + "ownerId" : "uwx3W7vaM6GUk9RaBsf5WIyRErjr", + "_id" : "ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba" +} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json deleted file mode 100644 index fead8af3..00000000 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "screenerName": "Example - Philly Property Tax Relief", - "benefits": [ - { - "description": "desc", - "name": "OOPA", - "id": "1c09392c-913c-4b2b-9870-a1951534c3fb" - }, - { - "description": "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", - "name": "Homestead Exemption", - "id": "fdd4405a-1a00-4650-8005-9595f16e3788" - } - ], - "_id": "hEStvPeFmEte58GQTC7Y" -} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json deleted file mode 100644 index 2052c809..00000000 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/1c09392c-913c-4b2b-9870-a1951534c3fb.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "description": "desc", - "name": "OOPA", - "checks": [ - { - "evaluationUrl": "/api/v1/checks/residence/tax-delinquent", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "taxDelinquent": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "c4a9f028-9617-4c3a-8617-1ffdae6b3ebe", - "parameters": {}, - "checkName": "tax-delinquent", - "checkModule": "residence", - "sourceCheckId": "L-residence-tax-delinquent-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/lives-in-philadelphia-pa", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "livesInPhiladelphiaPa": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "2b330906-4c65-4209-acf8-8b404b1614e7", - "parameters": {}, - "checkName": "lives-in-philadelphia-pa", - "checkModule": "residence", - "sourceCheckId": "L-residence-lives-in-philadelphia-pa-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/owner-occupant", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "ownerOccupant": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "daff8d3a-02e1-42d1-a8cc-173acc24c4fb", - "parameters": {}, - "checkName": "owner-occupant", - "checkModule": "residence", - "sourceCheckId": "L-residence-owner-occupant-0.7.1" - }, - { - "checkVersion": "2.0.0", - "parameterDefinitions": [ - { - "label": "P 1", - "key": "p1", - "type": "string", - "required": true - } - ], - "checkId": "751cc016-6e24-4d0c-b4f3-83b6ca9afe3f", - "inputDefinition": { - "properties": { - "custom": { - "properties": { - "testinput": {} - }, - "type": "object", - "required": [ - "testinput" - ] - } - }, - "type": "object", - "required": [ - "custom" - ] - }, - "checkName": "test", - "checkModule": "testchecks", - "parameters": { - "p1": "blah" - }, - "sourceCheckId": "P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0" - } - ], - "id": "1c09392c-913c-4b2b-9870-a1951534c3fb", - "_id": "1c09392c-913c-4b2b-9870-a1951534c3fb" -} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json b/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json deleted file mode 100644 index 0a50fdcd..00000000 --- a/builder-api/src/main/resources/seed-data/example-screener/firestore/workingScreener/hEStvPeFmEte58GQTC7Y/customBenefit/fdd4405a-1a00-4650-8005-9595f16e3788.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "ownerId": "i7d0OHGyzho27saJ48BFa77NmDeL", - "description": "If you own your primary residence, you are eligible for the Homestead Exemption on your Real Estate Tax. The Homestead Exemption reduces the taxable portion of your property’s assessed value.\n\nWith this exemption, the property’s assessed value is reduced by $100,000. Most homeowners will save about $1,399 a year on their Real Estate Tax bill starting in 2025.\n\nmore info:\nhttps://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/get-the-homestead-exemption/", - "name": "Homestead Exemption", - "checks": [ - { - "evaluationUrl": "/api/v1/checks/enrollment/person-not-enrolled-in-benefit", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "enrollments": { - "items": { - "properties": { - "personId": { - "type": "string" - }, - "benefit": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - }, - "checkId": "7621168e-e456-4690-a8bc-f081753c9535", - "parameterDefinitions": [ - { - "label": "personId", - "key": "personId", - "type": "string", - "required": false - }, - { - "label": "benefit", - "key": "benefit", - "type": "string", - "required": false - } - ], - "checkName": "person-not-enrolled-in-benefit", - "checkModule": "enrollment", - "parameters": { - "personId": "client", - "benefit": "PhlHomesteadExemption" - }, - "sourceCheckId": "L-enrollment-person-not-enrolled-in-benefit-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/owner-occupant", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "ownerOccupant": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "d6376311-49d2-44d8-bed3-4cb619df68f0", - "parameters": {}, - "checkName": "owner-occupant", - "checkModule": "residence", - "sourceCheckId": "L-residence-owner-occupant-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/lives-in-philadelphia-pa", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "livesInPhiladelphiaPa": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "352e984a-5dfc-46fe-9518-0bf61a0defeb", - "parameters": {}, - "checkName": "lives-in-philadelphia-pa", - "checkModule": "residence", - "sourceCheckId": "L-residence-lives-in-philadelphia-pa-0.7.1" - }, - { - "evaluationUrl": "/api/v1/checks/residence/no-ten-year-tax-abatement", - "checkVersion": "0.7.1", - "inputDefinition": { - "properties": { - "simpleChecks": { - "properties": { - "tenYearTaxAbatement": { - "type": "boolean" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "checkId": "658fc9f2-4789-45f6-a2d2-e851e0d85c58", - "parameters": {}, - "checkName": "no-ten-year-tax-abatement", - "checkModule": "residence", - "sourceCheckId": "L-residence-no-ten-year-tax-abatement-0.7.1" - } - ], - "id": "fdd4405a-1a00-4650-8005-9595f16e3788", - "_id": "fdd4405a-1a00-4650-8005-9595f16e3788" -} \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/manifest.json b/builder-api/src/main/resources/seed-data/example-screener/manifest.json index 35ad8ed1..65557621 100644 --- a/builder-api/src/main/resources/seed-data/example-screener/manifest.json +++ b/builder-api/src/main/resources/seed-data/example-screener/manifest.json @@ -1,8 +1,10 @@ { - "exportedAt": "2026-04-08T03:42:19.210460+00:00Z", - "source": "firebase-emulators", - "projectId": "demo-bdt-dev", - "storageBucket": "demo-bdt-dev.appspot.com", - "firestoreDocuments": 4, - "storageFiles": 3 + "screeners" : [ { + "screenerPath" : "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh.json", + "benefits" : [ "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/b481f008-5ed6-4d5f-a0d1-a08733ab91f9.json", "seed-data/example-screener/firestore/workingScreener/Am3FMS6ID2keNa82MbPh/customBenefit/ea6688ae-0a00-4e3e-a0d6-73d8e8be7cba.json" ], + "formSchema" : "seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json" + } ], + "workingCustomChecks" : [ "seed-data/example-screener/firestore/workingCustomCheck/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.json" ], + "publishedCustomChecks" : [ "seed-data/example-screener/firestore/publishedCustomCheck/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.json" ], + "dmnPaths" : [ "seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn", "seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn" ] } \ No newline at end of file diff --git a/builder-api/src/main/resources/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn similarity index 100% rename from builder-api/src/main/resources/seed-data/example-screener/storage/check/P-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test-2.0.0.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/P-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test-2.0.0.dmn diff --git a/builder-api/src/main/resources/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn b/builder-api/src/main/resources/seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn similarity index 100% rename from builder-api/src/main/resources/seed-data/example-screener/storage/check/W-i7d0OHGyzho27saJ48BFa77NmDeL-testchecks-test.dmn rename to builder-api/src/main/resources/seed-data/example-screener/storage/check/W-uwx3W7vaM6GUk9RaBsf5WIyRErjr-testchecks-test.dmn diff --git a/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json b/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json similarity index 100% rename from builder-api/src/main/resources/seed-data/example-screener/storage/form/working/hEStvPeFmEte58GQTC7Y.json rename to builder-api/src/main/resources/seed-data/example-screener/storage/form/working/Am3FMS6ID2keNa82MbPh.json diff --git a/builder-frontend/src/components/Header/ExportExampleScreener.tsx b/builder-frontend/src/components/Header/ExportExampleScreener.tsx new file mode 100644 index 00000000..2faa1bda --- /dev/null +++ b/builder-frontend/src/components/Header/ExportExampleScreener.tsx @@ -0,0 +1,45 @@ +import { exportExampleScreener } from "@/api/account"; +import { Button } from "@/components/shared/Button"; +import { createSignal, JSX, Match, Setter, Switch } from "solid-js"; + +interface Props { + setShowExportMenu: Setter; +} +export const ExportExampleScreener = (props: Props) => { + const [exportingMessage, setExportingMessage] = createSignal(""); + const [isExportingExample, setIsExportingExample] = createSignal(false); + + const handleExportExampleScreener: JSX.EventHandler< + HTMLButtonElement, + MouseEvent + > = async (e) => { + setIsExportingExample(true); + setExportingMessage(""); + const result = await exportExampleScreener(); + if (!result.success) { + setExportingMessage("An error occurred exporting."); + } else { + setExportingMessage("Successfully exported screeners."); + } + setIsExportingExample(false); + }; + + return ( + <> +
Ready to save changes to the example screener?
+ + + + + + Waiting for export + +
{exportingMessage()}
+ + ); +}; diff --git a/builder-frontend/src/components/Header/Header.tsx b/builder-frontend/src/components/Header/Header.tsx index 3996c8e2..50766ee6 100644 --- a/builder-frontend/src/components/Header/Header.tsx +++ b/builder-frontend/src/components/Header/Header.tsx @@ -1,16 +1,6 @@ import { useAuth } from "../../context/AuthContext"; import { useLocation, useNavigate } from "@solidjs/router"; -import { - Component, - createMemo, - createSignal, - DEV, - For, - JSX, - Match, - Show, - Switch, -} from "solid-js"; +import { Component, createMemo, createSignal, DEV, For, Show } from "solid-js"; import { HamburgerMenu } from "@/components/shared/HamburgerMenu"; @@ -18,7 +8,7 @@ import "./Header.css"; import { Menu } from "lucide-solid"; import { Button } from "@/components/shared/Button"; import { Modal } from "@/components/shared/Modal"; -import { exportExampleScreener } from "@/api/account"; +import { ExportExampleScreener } from "@/components/Header/ExportExampleScreener"; const HeaderButton = ({ buttonText, @@ -49,8 +39,6 @@ interface MenuProps { const HeaderMenu: Component = (props) => { const navigate = useNavigate(); const [showExportMenu, setShowExportMenu] = createSignal(false); - const [exportingMessage, setExportingMessage] = createSignal(""); - const [isExportingExample, setIsExportingExample] = createSignal(false); const menuItems: { label: string; onClick: () => void }[] = [ { @@ -64,20 +52,6 @@ const HeaderMenu: Component = (props) => { { label: "Logout", onClick: props.logout }, ]; - const handleExportExampleScreener: JSX.EventHandler< - HTMLButtonElement, - MouseEvent - > = async (e) => { - setIsExportingExample(true); - setExportingMessage(""); - const result = await exportExampleScreener(); - if (!result.success) { - setExportingMessage("An error occurred exporting."); - } else { - setExportingMessage("Successfully exported screeners."); - } - setIsExportingExample(false); - }; return (
@@ -98,17 +72,7 @@ const HeaderMenu: Component = (props) => { Export Example Screener setShowExportMenu(false)}> -
Ready to save changes to the example screener?
- - - - - - Waiting for export - -
{exportingMessage()}
+
diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1ca62a68..00000000 --- a/package-lock.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "benefit-decision-toolkit", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@fontsource/open-sans": "^5.2.7", - "@fontsource/ramaraja": "^5.2.8" - } - }, - "node_modules/@fontsource/open-sans": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.7.tgz", - "integrity": "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/ramaraja": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/ramaraja/-/ramaraja-5.2.8.tgz", - "integrity": "sha512-MIrLpCbfo+L750CnkLmvkV/UKbvOXLfliPCDDHgolliTbryL7UqRRSq32UdxfAXGisSRrzq82zVNTdwljA0Mxw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8fed6b9d..00000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "@fontsource/open-sans": "^5.2.7", - "@fontsource/ramaraja": "^5.2.8" - } -}