Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
627fb84
remove unnecessary annotations from FixtureHistoryRepositoryTests
dennisvang Dec 2, 2025
b4c43e5
add FixtureHistoryRepository.deleteByFilenameIn() with test
dennisvang Dec 2, 2025
e0b3202
rename repositoryPopulator to repositoryPopulatorFactoryBean
dennisvang Dec 2, 2025
c7bb0f4
move resource logic into separate BootstrapConfig.getNewResources method
dennisvang Dec 2, 2025
8b133f7
implement restoreDefaultUsers and restoreDefaultMemberships using pop…
dennisvang Dec 2, 2025
ae816d1
fix type definitions
dennisvang Dec 2, 2025
6cca79a
implement generic restoreDefaultFixtures method
dennisvang Dec 2, 2025
f00c6d7
fix indentation
dennisvang Dec 2, 2025
6f7fad1
rename fixture files according to <order>_<package>_<description>.jso…
dennisvang Dec 11, 2025
b935b16
add BootstrapConfig.checkFixtureFilename with tests
dennisvang Dec 11, 2025
223c130
rename to BootstrapConfig.validateFixtureFilename and throw if invalid
dennisvang Dec 11, 2025
c6f5201
validate fixture names before populating
dennisvang Dec 11, 2025
ca112b1
replace deleteByFilenameIn by deleteByFilenameContains (wip)
dennisvang Dec 11, 2025
da4ed88
specify allowed package names in fixture filename pattern
dennisvang Dec 12, 2025
7503290
use generic arg name for deleteByFilenameContains and fix test
dennisvang Dec 12, 2025
e27cc72
add FixtureHistoryRepository.deleteByFilename with test
dennisvang Dec 12, 2025
c253a56
move fixture related methods into new BootstrapService
dennisvang Dec 12, 2025
3169903
todo move to BootstrapService
dennisvang Dec 12, 2025
75b843f
add a rudimentary test for the ResetService
dennisvang Dec 12, 2025
3ebee9c
add license headers
dennisvang Dec 12, 2025
ae89ffb
reorder ResetService class attributes
dennisvang Dec 12, 2025
1a95957
Merge branch 'proposal/634-remove-sql-test-data-migrations' into prop…
dennisvang Dec 15, 2025
36300bd
split up and rename test-fixtures to match required filename pattern
dennisvang Dec 15, 2025
e52a675
run testResetToFactoryDefaultsAll as admin
dennisvang Dec 15, 2025
921fde3
check entity creation time in testResetToFactoryDefaultsAll
dennisvang Dec 18, 2025
f90a2de
Merge branch 'feature/634-boostrapping-fdp' into proposal/634-reset
dennisvang Dec 19, 2025
88f0f55
make testResetToFactoryDefaultsAll transactional
dennisvang Dec 19, 2025
c0d2c41
fix RescourceDefinition cascade delete problem
dennisvang Dec 19, 2025
ed8a5a1
move logic from BootstrapConfig and ResetService into BootstrapService
dennisvang Dec 17, 2025
ad25788
replace bootstrap.enabled property by a bootstrap-disabled profile
dennisvang Dec 17, 2025
5a981fc
implement BootstrapService.repopulate()
dennisvang Dec 17, 2025
106a035
Revert "replace bootstrap.enabled property by a bootstrap-disabled pr…
dennisvang Dec 19, 2025
d4e5f3a
consistent log messages in resetservice
dennisvang Dec 19, 2025
5ec480b
update fixture history based on populator resources
dennisvang Dec 19, 2025
f8ab3fa
consistent usage of cascade for ResourceDefinition.metadataSchemaUsages
dennisvang Jan 16, 2026
9dd5cda
Rely on cascading deletes in ResourceDefinitionService.deleteByUuid
dennisvang Jan 16, 2026
b267109
move repopulate() back from BootstrapService to ResetService
dennisvang Jan 16, 2026
4617593
remove redundant flush call from ResourceDefinitionService.deleteDepe…
dennisvang Jan 16, 2026
cc8cb53
deserialize to ResourceDefinitionDTO in List_POST test
dennisvang Jan 19, 2026
02264f5
check ResourceDefinition relations in List_POST test
dennisvang Jan 19, 2026
d47ae5e
initialize ResourceDefinition relations to empty list
dennisvang Jan 19, 2026
b5aed47
refresh the in-memory definition after calling createDependents()
dennisvang Jan 19, 2026
efe925e
Revert "remove redundant flush call from ResourceDefinitionService.de…
dennisvang Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 41 additions & 31 deletions src/main/java/org/fairdatapoint/config/BootstrapConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

/**
* The {@code BootstrapConfig} class configures a repository populator that loads initial data into the relational
Expand All @@ -61,49 +62,58 @@ public class BootstrapConfig {
private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
private final BootstrapProperties bootstrap;
private final FixtureHistoryRepository fixtureHistoryRepository;
private final List<Resource> resources = new ArrayList<>();

public BootstrapConfig(BootstrapProperties bootstrapProperties, FixtureHistoryRepository fixtureHistoryRepository) {
this.bootstrap = bootstrapProperties;
this.fixtureHistoryRepository = fixtureHistoryRepository;
}

@Bean
public Jackson2RepositoryPopulatorFactoryBean repositoryPopulator() {
final Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
if (this.bootstrap.isEnabled()) {
log.info("Bootstrap repository populator enabled");
/**
* Creates a sorted array of unique fixture resources, representing files in the specified fixtures directories.
* Checks the fixture history repository for files that have already been applied, and removes them from the array.
* @return sorted array of unique Resource objects
*/
public Resource[] getNewResources() {
// use TreeSet with comparator for lexicographic order and uniqueness
final SortedSet<Resource> resources = new TreeSet<>(
Comparator.comparing(Resource::getFilename, Comparator.nullsLast(String::compareTo)));
// collect fixture resources from specified directories
log.info("Looking for db fixtures in the following directories: {}",
String.join(", ", this.bootstrap.getDbFixturesDirs()));
for (String fixturesDir : this.bootstrap.getDbFixturesDirs()) {
// Path.of() removes trailing slashes, so it is safe to concatenate "/*.json".
// Note that Path.of(fixturesDir).resolve("*.json") could work on unix but fails on windows.
final String locationPattern = "file:" + Path.of(fixturesDir) + "/*.json";
try {
// collect fixture resources
log.info("Looking for db fixtures in the following directories: {}",
String.join(", ", this.bootstrap.getDbFixturesDirs()));
for (String fixturesDir : this.bootstrap.getDbFixturesDirs()) {
// Path.of() removes trailing slashes, so it is safe to concatenate "/*.json".
// Note that Path.of(fixturesDir).resolve("*.json") could work on unix but fails on windows.
final String locationPattern = "file:" + Path.of(fixturesDir) + "/*.json";
resources.addAll(List.of(resourceResolver.getResources(locationPattern)));
}
// remove resources that have been applied already
final List<String> appliedFixtures = fixtureHistoryRepository.findAll().stream()
.map(FixtureHistory::getFilename).toList();
final List<Resource> resourcesToSkip = resources.stream()
.filter(resource -> appliedFixtures.contains(resource.getFilename())).toList();
resources.removeAll(resourcesToSkip);
// sort resources to guarantee lexicographic order
resources.sort(Comparator.comparing(Resource::getFilename, Comparator.nullsLast(String::compareTo)));
// add resources to factory
log.info("Applying {} db fixtures ({} have been applied already)",
resources.size(), resourcesToSkip.size());
factory.setResources(resources.toArray(new Resource[0]));
resources.addAll(List.of(resourceResolver.getResources(locationPattern)));
}
catch (IOException exception) {
log.error("Failed to load relational database fixtures", exception);
log.error("Failed to resolve fixture resources", exception);
}
}
// remove resources that have been applied already
final List<String> appliedFixtures = fixtureHistoryRepository.findAll().stream()
.map(FixtureHistory::getFilename).toList();
final List<Resource> resourcesToSkip = resources.stream()
.filter(resource -> appliedFixtures.contains(resource.getFilename())).toList();
resourcesToSkip.forEach(resources::remove);
// return the result
log.info("Found {} new db fixture files ({} have been applied already)",
resources.size(), resourcesToSkip.size());
return resources.toArray(new Resource[0]);
}

@Bean
public Jackson2RepositoryPopulatorFactoryBean repositoryPopulatorFactoryBean() {
final Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
if (this.bootstrap.isEnabled()) {
log.info("Bootstrap repository populator enabled");
// add resources to factory
factory.setResources(getNewResources());
}
else {
log.info("Bootstrap repository populator disabled");
}

return factory;
}

Expand All @@ -116,7 +126,7 @@ public void onApplicationEvent(@NotNull RepositoriesPopulatedEvent event) {
// Note: This assumes that all items in the resources list have been *successfully* applied. However, I'm
// not sure if this can be guaranteed. If it does turn out to be a problem, we could try e.g. extending the
// ResourceReaderRepositoryPopulator.persist() method, so the history record is added there.
for (final Resource resource : resources) {
for (final Resource resource : getNewResources()) {
final String filename = resource.getFilename();
final FixtureHistory fixtureHistory = fixtureHistoryRepository.save(new FixtureHistory(filename));
log.debug("Fixture history updated: {} ({})", fixtureHistory.getFilename(), fixtureHistory.getUuid());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@
import org.fairdatapoint.database.db.repository.base.BaseRepository;
import org.fairdatapoint.entity.bootstrap.FixtureHistory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Repository
public interface FixtureHistoryRepository extends BaseRepository<FixtureHistory> {
Optional<FixtureHistory> findByFilename(String filename);

@Transactional
void deleteByFilenameIn(String[] filenames);
}
93 changes: 65 additions & 28 deletions src/main/java/org/fairdatapoint/service/reset/ResetService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import lombok.extern.slf4j.Slf4j;
import org.fairdatapoint.api.dto.reset.ResetDTO;
import org.fairdatapoint.config.BootstrapConfig;
import org.fairdatapoint.database.db.repository.*;
import org.fairdatapoint.entity.resource.ResourceDefinition;
import org.fairdatapoint.service.metadata.exception.MetadataServiceException;
Expand All @@ -39,6 +40,9 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.data.repository.init.ResourceReaderRepositoryPopulator;
import org.springframework.data.repository.support.Repositories;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.model.AclCache;
import org.springframework.stereotype.Service;
Expand All @@ -52,6 +56,21 @@
@Service
public class ResetService {

@Autowired
private ApplicationContext applicationContext;

@Autowired
private BootstrapConfig bootstrapConfig;

@Autowired
private FixtureHistoryRepository fixtureHistoryRepository;

@Autowired
private MembershipPermissionRepository membershipPermissionRepository;

@Autowired
private ResourceReaderRepositoryPopulator populator;

@Autowired
@Qualifier("persistentUrl")
private String persistentUrl;
Expand Down Expand Up @@ -86,9 +105,21 @@ public class ResetService {
@Autowired
private MetadataSchemaRepository metadataSchemaRepository;

@Autowired
private MetadataSchemaUsageRepository metadataSchemaUsageRepository;

@Autowired
private ResourceDefinitionRepository resourceDefinitionRepository;

@Autowired
private ResourceDefinitionChildRepository resourceDefinitionChildRepository;

@Autowired
private ResourceDefinitionChildMetadataRepository resourceDefinitionChildMetadataRepository;

@Autowired
private ResourceDefinitionLinkRepository resourceDefinitionLinkRepository;

@Autowired
private ResourceDefinitionCache resourceDefinitionCache;

Expand All @@ -109,52 +140,66 @@ public void resetToFactoryDefaults(ResetDTO reqDto) throws Exception {
}
if (reqDto.isUsers() || reqDto.isMetadata()) {
clearMemberships();
restoreDefaultMemberships();
}
if (reqDto.isUsers()) {
clearApiKeys();
clearUsers();
restoreDefaultUsers();
}
if (reqDto.isMetadata()) {
clearMetadata();
restoreDefaultMetadata();
}
if (reqDto.isResourceDefinitions()) {
clearResourceDefinitions();
clearMetadataSchemas();
restoreDefaultMetadataSchemas();
restoreDefaultResourceDefinitions();
clearMetadataSchemasAndResourceDefinitions();
}
// restoreDefaultFixtures is safe to call even if there are no changes
restoreDefaultFixtures();
resourceDefinitionCache.computeCache();
resourceDefinitionTargetClassesCache.computeCache();
}

private void clearApiKeys() {
log.debug("Clearing API keys");
apiKeyRepository.deleteAll();
removeFromFixtureHistory(new String[]{"0110_api-keys.json"});
}

private void clearMemberships() {
log.debug("Clearing membership permissions");
membershipPermissionRepository.deleteAll();
log.debug("Clearing memberships");
membershipRepository.deleteAll();
log.debug("Clearing ACL cache");
aclCache.clearCache();
removeFromFixtureHistory(new String[]{"0400_memberships_owner.json", "0410_memberships_data-provider.json"});
}

private void clearUsers() {
log.debug("Clearing users");
userRepository.deleteAll();
removeFromFixtureHistory(new String[]{"0100_user-accounts.json", "0120_saved-queries.json"});
}

private void clearMetadataSchemas() {
log.debug("Clearing metadata schemas");
private void clearMetadataSchemasAndResourceDefinitions() {
log.debug("Clearing metadata schemas and resource definitions");
metadataSchemaUsageRepository.deleteAll();
metadataSchemaRepository.deleteAll();
}

private void clearResourceDefinitions() {
log.debug("Clearing resource definitions");
resourceDefinitionChildMetadataRepository.deleteAll();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these added to make sure nothing is left out? Otherwise, cascading delete should work I believe...

Copy link
Contributor Author

@dennisvang dennisvang Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @MarekSuchanek thanks for the review! :)

You're right, the deleteAll() calls for metadataSchemaUsageRepository, resourceDefinitionChildMetadataRepository, andresourceDefinitionLinkRepository are redundant.
However, line 188 (resourceDefinitionChildRepository.deleteAll()) does seem to be required, for some reason.

Still, I like to be explicit here, and not rely on cascades.

Note

In my experience cascade deletes, in general, can lead to very nasty surprises.
One example of cascade deletes causing confusion is in #802, where it turned out that overriding the default Einstein user account in a test fixture caused the related objects to be deleted as well.

resourceDefinitionChildRepository.deleteAll();
resourceDefinitionLinkRepository.deleteAll();
resourceDefinitionRepository.deleteAll();
removeFromFixtureHistory(new String[]{
"0200_metadata-schemas_resource.json",
"0210_metadata-schemas_data-service.json",
"0220_metadata-schemas_metadata-service.json",
"0230_metadata-schemas_fdp.json",
"0240_metadata-schemas_catalog.json",
"0250_metadata-schemas_dataset.json",
"0260_metadata-schemas_distribution.json",
"0300_resource-definitions_distribution.json",
"0310_resource-definitions_dataset.json",
"0320_resource-definitions_catalog.json",
"0330_resource-definitions_repository.json"});
}

private void clearMetadata() throws MetadataServiceException {
Expand All @@ -166,14 +211,16 @@ private void clearMetadata() throws MetadataServiceException {
}
}

private void restoreDefaultUsers() {
log.debug("Creating default users");
// TODO: data seed from specs
protected void removeFromFixtureHistory(String[] filenames) {
log.debug("Removing filenames from fixture history: {}", String.join(", ", filenames));
fixtureHistoryRepository.deleteByFilenameIn(filenames);
}

private void restoreDefaultMemberships() {
log.debug("Creating default memberships");
// TODO: data seed from specs
protected void restoreDefaultFixtures() {
log.debug("Restoring default fixtures");
// getNewResources() checks the updated fixture history
populator.setResources(bootstrapConfig.getNewResources());
populator.populate(new Repositories(applicationContext));
}

private void restoreDefaultMetadata() {
Expand All @@ -191,14 +238,4 @@ private void restoreDefaultMetadata() {
log.error(exception.getMessage(), exception);
}
}

private void restoreDefaultMetadataSchemas() throws Exception {
log.debug("Creating default metadata schemas");
// TODO: data seed from specs
}

private void restoreDefaultResourceDefinitions() {
log.debug("Creating default resource definitions");
// TODO: data seed from specs
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,28 @@
import org.fairdatapoint.BaseIntegrationTest;
import org.fairdatapoint.database.db.repository.FixtureHistoryRepository;
import org.fairdatapoint.entity.bootstrap.FixtureHistory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.TestPropertySource;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;


@AutoConfigureTestEntityManager
@Transactional
@TestPropertySource(properties = "bootstrap.enabled=false")
public class FixtureHistoryRepositoryTests extends BaseIntegrationTest {
@Autowired
FixtureHistoryRepository repository;

final String filename = "0001-whatever.json";

@BeforeEach
public void clearFixtureHistory() {
repository.deleteAll();
}

@Test
public void testSave() {
FixtureHistory fixtureHistory = repository.saveAndFlush(new FixtureHistory(filename));
Expand Down Expand Up @@ -79,4 +81,14 @@ public void testFindByFilenameWithExistingFilename() {
Optional<FixtureHistory> appliedFixture = repository.findByFilename(filename);
assertTrue(appliedFixture.isPresent());
}

@Test
public void testDeleteByFilenameIn() {
// prepare
repository.saveAndFlush(new FixtureHistory(filename));
assertEquals(1, repository.count());
// test
repository.deleteByFilenameIn(new String[] {filename});
assertEquals(0, repository.count());
}
}