Skip to content

Commit f11da3a

Browse files
committed
Add custom entity type support
1 parent 5ee7f70 commit f11da3a

13 files changed

Lines changed: 392 additions & 52 deletions

docs/Migration.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
# Query Migration
1+
# Migration
22

33
Entity types and their fields change over time, be it adding fields, moving them between entity types, or completely rethinking the way some fields are handled. As such, we have a robust migration system to ensure that consuming apps will not break, and their queries will continue to work despite any internal FQM changes.
44

5-
- [Query versions](#query-versions)
5+
- [Versions](#versions)
66
- [Updating a query](#updating-a-query)
7+
- [Updating an entity type](#updating-an-entity-type)
78
- [Writing migrations](#writing-migrations)
89
- [Changes](#changes)
910
- [Entity type changes](#entity-type-changes)
@@ -26,8 +27,9 @@ Entity types and their fields change over time, be it adding fields, moving them
2627
- [State](#state)
2728
- [Extra magic](#extra-magic)
2829
- [Advanced migration tips](#advanced-migration-tips)
30+
- [Custom entity types support](#custom-entity-types-support)
2931

30-
## Query versions
32+
## Versions
3133

3234
The version of a query is stored inside the FQL string:
3335

@@ -38,14 +40,28 @@ The version of a query is stored inside the FQL string:
3840
}
3941
```
4042

41-
These are arbitrary strings, and consuming applications should make no assumptions about them (they are currently integers, but may be changed in the future to commit hashes, module versions, or anything else).
43+
And versions of custom entity types are stored inside the entity definition:
44+
45+
```json
46+
{
47+
"id": "d41130e9-0302-5ef3-a6b2-70f6ae1678ce",
48+
"name": "my_custom_entity",
49+
"_version": "3"
50+
}
51+
```
52+
53+
These are arbitrary strings, and consuming applications should make no assumptions about them (they are currently semver-adjacent, but may be changed in the future to commit hashes, module versions, or anything else).
4254

4355
Queries from Quesnelia or earlier will have no version associated with them and will be considered version `"0"`.
4456

4557
## Updating a query
4658

4759
To update a query, send it, the entity type ID, and a list of fields (if desired) to `/fqm/migrate`. See our [API documentation](https://dev.folio.org/reference/api/#mod-fqm-manager) for more information about this endpoint. Our module will return the updated query, entity type ID, and list of fields, all of which should be saved. Additionally, the response may contain [warnings](#warnings), meaning that some parts of the query or field list was unable to be migrated.
4860

61+
## Updating an entity type
62+
63+
Custom entity types will be migrated when the module is installed. No additional action is required; for more information see [custom entity types support](#custom-entity-types-support).
64+
4965
## Writing migrations
5066

5167
Any change to an entity type that results in a field being removed or renamed should result in a migration script. The easiest way to do this is to do the following:
@@ -108,7 +124,16 @@ public Map<UUID, UUID> getEntityTypeChanges() {
108124

109125
### Defining source maps
110126

111-
<!-- TODO: describe this (in next PR) -->
127+
Source maps are used in migrations to define relations between composite and simple entity types. For example, if your migration alters `simple_instance_status`, it's necessary for the migration system to know that `composite_instances`'s `inst_stat` source points to `simple_instance_status`. To define these relationships, override `getEntityTypeSourceMaps` (note that the inner keys are the source aliases used by the composite):
128+
129+
```java
130+
public Map<UUID, Map<String, UUID>> getEntityTypeSourceMaps() {
131+
return Map.of(
132+
COMPOSITE_INSTANCES_ID, Map.of("inst_stat", SIMPLE_INSTANCE_STATUS_ID),
133+
COMPOSITE_ITEM_DETAILS_ID, Map.of("instance_status", SIMPLE_INSTANCE_STATUS_ID)
134+
);
135+
}
136+
```
112137

113138
### Warnings
114139

src/main/java/org/folio/fqm/migration/MigratableQueryInformation.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ public record MigratableQueryInformation(
1616
@CheckForNull String fqlQuery,
1717
List<String> fields,
1818
@Singular List<Warning> warnings,
19-
String version,
2019
boolean hadBreakingChanges
2120
)
2221
implements Serializable {
2322
public MigratableQueryInformation(UUID entityTypeId, String fqlQuery, List<String> fields) {
24-
this(entityTypeId, fqlQuery, fields, List.of(), null, false);
23+
this(entityTypeId, fqlQuery, fields, List.of(), false);
2524
}
2625
}

src/main/java/org/folio/fqm/migration/warnings/RemovedFieldWarning.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import lombok.EqualsAndHashCode;
77
import lombok.RequiredArgsConstructor;
88
import lombok.ToString;
9+
import org.folio.fqm.service.LocalizationService;
910
import org.folio.spring.i18n.service.TranslationService;
1011

1112
@Builder
@@ -31,7 +32,20 @@ public WarningType getType() {
3132

3233
@Override
3334
public String getDescription(TranslationService translationService) {
34-
return Warning.getDescriptionByAlternativeAndFql(translationService, this.getType(), field, fql, alternative);
35+
String translationKey = LocalizationService.MIGRATION_WARNING_TRANSLATION_TEMPLATE.formatted(TYPE.toString());
36+
37+
if (fql == null) {
38+
translationKey += ".field";
39+
} else {
40+
translationKey += ".query";
41+
}
42+
if (alternative == null) {
43+
translationKey += ".withoutAlternative";
44+
} else {
45+
translationKey += ".withAlternative";
46+
}
47+
48+
return translationService.format(translationKey, "name", field, "alternative", alternative, "fql", fql);
3549
}
3650

3751
public static FieldWarningFactory withoutAlternative() {

src/main/java/org/folio/fqm/service/EntityTypeInitializationService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class EntityTypeInitializationService {
4040
private final CrossTenantQueryService crossTenantQueryService;
4141
private final EntityTypeRepository entityTypeRepository;
4242
private final EntityTypeValidationService entityTypeValidationService;
43+
private final MigrationService migrationService;
4344
private final SourceViewService sourceViewService;
4445

4546
private final FolioExecutionContext folioExecutionContext;
@@ -52,13 +53,15 @@ public EntityTypeInitializationService(
5253
CrossTenantQueryService crossTenantQueryService,
5354
EntityTypeRepository entityTypeRepository,
5455
EntityTypeValidationService entityTypeValidationService,
56+
MigrationService migrationService,
5557
SourceViewService sourceViewService,
5658
FolioExecutionContext folioExecutionContext,
5759
ResourcePatternResolver resourceResolver
5860
) {
5961
this.crossTenantQueryService = crossTenantQueryService;
6062
this.entityTypeRepository = entityTypeRepository;
6163
this.entityTypeValidationService = entityTypeValidationService;
64+
this.migrationService = migrationService;
6265
this.sourceViewService = sourceViewService;
6366
this.folioExecutionContext = folioExecutionContext;
6467
this.resourceResolver = resourceResolver;
@@ -85,6 +88,7 @@ public void initializeEntityTypes(String providedCentralTenantId) throws IOExcep
8588
);
8689

8790
entityTypeRepository.replaceEntityTypeDefinitions(availableEntityTypes);
91+
migrationService.migrateCustomEntityTypes();
8892
}
8993

9094
protected Pair<String, String> getCentralTenantIdSafely(String centralTenantId) {

src/main/java/org/folio/fqm/service/EntityTypeService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.folio.fqm.client.CrossTenantHttpClient;
1919
import org.folio.fqm.client.LanguageClient;
2020
import org.folio.fqm.client.SimpleHttpClient;
21+
import org.folio.fqm.config.MigrationConfiguration;
2122
import org.folio.fqm.domain.dto.EntityTypeSummary;
2223
import org.folio.fqm.exception.EntityTypeInUseException;
2324
import org.folio.fqm.exception.EntityTypeNotFoundException;
@@ -91,6 +92,7 @@ public class EntityTypeService {
9192
private final EntityTypeFlatteningService entityTypeFlatteningService;
9293
private final EntityTypeValidationService entityTypeValidationService;
9394
private final LocalizationService localizationService;
95+
private final MigrationConfiguration migrationConfiguration;
9496
private final MigrationService migrationService;
9597
private final QueryProcessorService queryService;
9698
private final CrossTenantHttpClient crossTenantHttpClient;
@@ -487,8 +489,9 @@ public CustomEntityType createCustomEntityType(CustomEntityType customEntityType
487489
);
488490
}
489491

490-
var updatedCustomEntityType = customEntityType.toBuilder()
492+
CustomEntityType updatedCustomEntityType = customEntityType.toBuilder()
491493
.id(customEntityTypeId.toString())
494+
.version(migrationConfiguration.getCurrentVersion())
492495
.createdAt(now)
493496
.updatedAt(now)
494497
.owner(folioExecutionContext.getUserId())
@@ -505,6 +508,7 @@ public CustomEntityType updateCustomEntityType(UUID entityTypeId, CustomEntityTy
505508
permissionsService.verifyUserCanAccessCustomEntityType(oldET);
506509

507510
CustomEntityType updatedCustomEntityType = customEntityType.toBuilder()
511+
.version(migrationConfiguration.getCurrentVersion())
508512
.createdAt(oldET.getCreatedAt())
509513
.updatedAt(clockService.now())
510514
.owner(Objects.requireNonNullElse(customEntityType.getOwner(), oldET.getOwner()))

src/main/java/org/folio/fqm/service/EntityTypeValidationService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.UUID;
55
import lombok.RequiredArgsConstructor;
66
import lombok.extern.log4j.Log4j2;
7+
import org.folio.fqm.config.MigrationConfiguration;
78
import org.folio.fqm.exception.EntityTypeNotFoundException;
89
import org.folio.fqm.exception.InvalidEntityTypeDefinitionException;
910
import org.folio.fqm.repository.EntityTypeRepository;
@@ -26,9 +27,16 @@ public class EntityTypeValidationService {
2627

2728
private final EntityTypeRepository entityTypeRepository;
2829
private final FolioExecutionContext folioExecutionContext;
30+
private final MigrationConfiguration migrationConfiguration;
2931

3032
public void validateCustomEntityType(UUID entityTypeId, CustomEntityType customEntityType) {
3133
validateEntityType(entityTypeId, customEntityType, null);
34+
if (!migrationConfiguration.getCurrentVersion().equals(customEntityType.getVersion())) {
35+
throw new InvalidEntityTypeDefinitionException(
36+
"Custom entity type must have _version=%s".formatted(migrationConfiguration.getCurrentVersion()),
37+
customEntityType
38+
);
39+
}
3240
if (customEntityType.getOwner() == null) {
3341
throw new InvalidEntityTypeDefinitionException("Custom entity type must have an owner", customEntityType);
3442
}

0 commit comments

Comments
 (0)