Skip to content

Commit

Permalink
[FCFIELDS-44] - PUT /custom-fields now handles multiple entity types (#…
Browse files Browse the repository at this point in the history
…41)

* Add entityType to putCustomFieldCollection and use it in CustomFieldsService replaceAll method
* Fix test data - all custom fields in PUT /custom-fields requests should contain the same entityType
* Update tests - PUT on /custom-fields expects a PutCustomFieldCollection
* Add test cases
* Bump interface and application major version
* Add entityType validation in putCustomFields method
  • Loading branch information
alb3rtino authored Dec 11, 2023
1 parent 46118dd commit e3c95f1
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 25 deletions.
2 changes: 1 addition & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"provides": [
{
"id": "custom-fields",
"version": "2.1",
"version": "3.0",
"handlers": [
{
"methods": ["GET"],
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.folio</groupId>
<artifactId>folio-custom-fields</artifactId>
<version>1.10.1-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<licenses>
Expand Down
7 changes: 6 additions & 1 deletion ramls/putCustomFieldCollection.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
"$ref": "customField.json"
}
},
"entityType": {
"type": "string",
"description": "The entityType of custom fields"
},
"metadata": {
"description": "User metadata information",
"$ref": "raml-util/schemas/metadata.schema",
"readonly": true
}
},
"required": [
"customFields"
"customFields",
"entityType"
]
}
25 changes: 24 additions & 1 deletion src/main/java/org/folio/rest/impl/CustomFieldsImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ public void postCustomFields(String lang, String xOkapiModuleId, CustomField ent
@HandleValidationErrors
public void putCustomFields(String xOkapiModuleId, PutCustomFieldCollection request, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
validatePutCustomFieldCollection(request);
List<CustomField> customFields = request.getCustomFields();
customFields
.forEach(definitionValidator::validate);
customFields.forEach(field -> field.setMetadata(request.getMetadata()));
Future<CustomFieldCollection> updatedFields = customFieldsService.replaceAll(customFields, new OkapiParams(okapiHeaders))
Future<CustomFieldCollection> updatedFields = customFieldsService.replaceAll(customFields, request.getEntityType(), new OkapiParams(okapiHeaders))
.map(fields -> new CustomFieldCollection()
.withCustomFields(fields)
.withTotalRecords(fields.size()));
Expand Down Expand Up @@ -139,4 +140,26 @@ public void getCustomFieldsOptionsStatsByIdAndOptId(String id, String optId,
GetCustomFieldsOptionsStatsByIdAndOptIdResponse::respond200WithApplicationJson,
asyncResultHandler, excHandler);
}

private void validatePutCustomFieldCollection(PutCustomFieldCollection customFieldCollection)
throws IllegalArgumentException {
List<String> entityTypes =
customFieldCollection.getCustomFields().stream()
.map(CustomField::getEntityType)
.distinct()
.toList();
if (entityTypes.size() > 1) {
throw new IllegalArgumentException(
String.format("Multiple entityTypes found: %s", entityTypes));
}
if (!entityTypes.isEmpty()) {
String entityType = entityTypes.get(0);
if (!entityType.equals(customFieldCollection.getEntityType())) {
throw new IllegalArgumentException(
String.format(
"Collection entityType '%s' does not match custom fields entityType '%s'",
customFieldCollection.getEntityType(), entityType));
}
}
}
}
13 changes: 7 additions & 6 deletions src/main/java/org/folio/service/CustomFieldsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,17 @@ public interface CustomFieldsService {
Future<Void> delete(String id, String tenantId);

/**
* Replaces all existing custom fields with new collection of custom fields.
* If new collection has fields with ids that already exist then those fields will be updated,
* fields with ids that don't exist will be added,
* existing fields that are not present in new collection will be deleted
* Replaces all existing custom fields with new collection of custom fields. If new collection has
* fields with ids that already exist then those fields will be updated, fields with ids that
* don't exist will be added, existing fields that are not present in new collection will be
* deleted. Only custom fields with the specified entityType will be affected.
*
* @param newFields collection of new custom fields
* @param params OkapiParams
* @param entityType entityType of custom fields
* @param params OkapiParams
* @return updated collection of custom fields
*/
Future<List<CustomField>> replaceAll(List<CustomField> newFields, OkapiParams params);
Future<List<CustomField>> replaceAll(List<CustomField> newFields, String entityType, OkapiParams params);

/**
* Retrieves statistic of specific custom field definition usage.
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/org/folio/service/CustomFieldsServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ public Future<Void> delete(String id, String tenantId) {
}

@Override
public Future<List<CustomField>> replaceAll(List<CustomField> customFields, OkapiParams params) {
public Future<List<CustomField>> replaceAll(
List<CustomField> customFields, String entityType, OkapiParams params) {
log.debug("replaceAll:: Attempt to replace all customFields by [tenantId: {}]", params.getTenant());

return repository.findByQuery(null, 0, Integer.MAX_VALUE, params.getTenant())
String queryStr = String.format("query=(entityType==%s)", entityType);
return repository.findByQuery(queryStr, 0, Integer.MAX_VALUE, params.getTenant())
.compose(existingFields -> {
setOrder(customFields);
setIdIfEmpty(customFields);
Expand Down
88 changes: 78 additions & 10 deletions src/test/java/org/folio/rest/impl/CustomFieldsImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.folio.rest.jaxrs.model.CustomFieldStatistic;
import org.folio.rest.jaxrs.model.Error;
import org.folio.rest.jaxrs.model.Metadata;
import org.folio.rest.jaxrs.model.PutCustomFieldCollection;
import org.folio.rest.jaxrs.model.TextField;
import org.folio.test.util.TestBase;
import org.junit.Before;
Expand Down Expand Up @@ -223,7 +224,7 @@ public void shouldReturn404WhenUserNotFound() throws IOException, URISyntaxExcep

@Test
public void shouldReturnAllFieldsOnGetSortedByOrder() throws IOException, URISyntaxException {
createFields();
createFieldsMultipleEntityTypes();
CustomFieldCollection fields = getWithOk(CUSTOM_FIELDS_PATH).as(CustomFieldCollection.class);
assertEquals(2, fields.getCustomFields().size());
assertThat(fields.getCustomFields().get(0), is(allOf(
Expand All @@ -242,7 +243,7 @@ public void shouldReturnAllFieldsOnGetSortedByOrder() throws IOException, URISyn

@Test
public void shouldReturnFieldsByName() throws IOException, URISyntaxException {
createFields();
createFieldsMultipleEntityTypes();
String resourcePath = CUSTOM_FIELDS_PATH + "?query=name==Department";
CustomFieldCollection fields = getWithOk(resourcePath).as(CustomFieldCollection.class);
assertEquals(1, fields.getCustomFields().size());
Expand All @@ -254,7 +255,7 @@ public void shouldReturnFieldsByName() throws IOException, URISyntaxException {

@Test
public void shouldReturnFieldsByEntityType() throws IOException, URISyntaxException {
createFields();
createFieldsMultipleEntityTypes();
String resourcePath = CUSTOM_FIELDS_PATH + "?query=entityType==package";
CustomFieldCollection fields = getWithOk(resourcePath).as(CustomFieldCollection.class);
assertEquals(1, fields.getCustomFields().size());
Expand All @@ -265,7 +266,7 @@ public void shouldReturnFieldsByEntityType() throws IOException, URISyntaxExcept

@Test
public void shouldReturnFieldsWithPagination() throws IOException, URISyntaxException {
createFields();
createFieldsMultipleEntityTypes();
String resourcePath = CUSTOM_FIELDS_PATH + "?offset=0&limit=1&query=cql.allRecords=1 sortby name";
CustomFieldCollection fields = getWithOk(resourcePath).as(CustomFieldCollection.class);
assertEquals(1, fields.getCustomFields().size());
Expand Down Expand Up @@ -409,6 +410,47 @@ public void shouldReturn422WhenEntityTypeIsNullOnPut() throws IOException, URISy
assertThat(error, containsString("NotNull.message"));
}

@Test
public void putCustomFieldsShouldReturn422WhenCollectionEntityTypeIsNull()
throws IOException, URISyntaxException {
CustomField field = readJsonFile("fields/post/postCustomField.json", CustomField.class);

PutCustomFieldCollection request =
new PutCustomFieldCollection().withCustomFields(List.of(field));
String error =
putWithStatus(CUSTOM_FIELDS_PATH, Json.encode(request), SC_UNPROCESSABLE_ENTITY).asString();
assertThat(error, containsString("NotNull.message"));
}

@Test
public void putCustomFieldsShouldReturn422OnMultipleEntityTypes()
throws IOException, URISyntaxException {
PutCustomFieldCollection request =
readJsonFile("fields/put/putCustomFieldCollection.json", PutCustomFieldCollection.class);
request.getCustomFields().get(0).setEntityType("other");
String error =
putWithStatus(
CUSTOM_FIELDS_PATH, Json.encode(request), SC_UNPROCESSABLE_ENTITY, USER1_HEADER)
.asString();
assertThat(error, containsString("Multiple entityTypes found"));
}

@Test
public void putCustomFieldsShouldReturn422WhenCollectionEntityTypeDiffers()
throws IOException, URISyntaxException {
PutCustomFieldCollection request =
readJsonFile("fields/put/putCustomFieldCollection.json", PutCustomFieldCollection.class);
request.setEntityType("other");
String error =
putWithStatus(
CUSTOM_FIELDS_PATH, Json.encode(request), SC_UNPROCESSABLE_ENTITY, USER1_HEADER)
.asString();
assertThat(
error,
containsString(
"Collection entityType 'other' does not match custom fields entityType 'user'"));
}

@Test
public void shouldReturn422WhenFieldFormatIsNullOnPut() throws IOException, URISyntaxException {
CustomField field = readJsonFile("fields/post/textbox/postTextBoxShort.json", CustomField.class);
Expand All @@ -434,7 +476,7 @@ public void shouldUpdateAllCustomFields() throws IOException, URISyntaxException
assertEquals(1, (int) firstField.getOrder());
assertEquals(2, (int) secondField.getOrder());

CustomFieldCollection request = readJsonFile("fields/put/putCustomFieldCollection.json", CustomFieldCollection.class);
PutCustomFieldCollection request = readJsonFile("fields/put/putCustomFieldCollection.json", PutCustomFieldCollection.class);
request.getCustomFields().get(1).setId(field1.getId());
putWithNoContent(CUSTOM_FIELDS_PATH, Json.encode(request), USER2_HEADER);

Expand Down Expand Up @@ -470,6 +512,32 @@ public void shouldUpdateAllCustomFields() throws IOException, URISyntaxException
assertEquals("u2", secondFieldUpdatedMetadata.getUpdatedByUsername());
}

@Test
public void putCustomFieldsShouldHaveNoSideEffectsOnOtherEntityTypes()
throws IOException, URISyntaxException {
createFieldsMultipleEntityTypes();
long distinctEntityTypeCount =
getAllCustomFields(vertx).stream().map(CustomField::getEntityType).distinct().count();
assertEquals(2, distinctEntityTypeCount);

String newHelpText = "new help text";
CustomField updatedCustomField =
readJsonFile("fields/post/postCustomField.json", CustomField.class)
.withHelpText(newHelpText);
PutCustomFieldCollection request =
new PutCustomFieldCollection()
.withCustomFields(List.of(updatedCustomField))
.withEntityType("user");
putWithNoContent(CUSTOM_FIELDS_PATH, Json.encode(request), USER2_HEADER);

List<CustomField> results = getAllCustomFields(vertx);
results.sort(Comparator.comparingInt(CustomField::getOrder));
long distinctResultEntityTypeCount =
results.stream().map(CustomField::getEntityType).distinct().count();
assertEquals(2, distinctResultEntityTypeCount);
assertEquals(newHelpText, results.get(0).getHelpText());
}

@Test
public void shouldUpdateCustomFieldsWhenReOrderAndFieldRemoved() throws IOException, URISyntaxException {
createCustomField(readFile("fields/post/postCustomField.json"));
Expand All @@ -490,7 +558,7 @@ public void shouldUpdateCustomFieldsWhenReOrderAndFieldRemoved() throws IOExcept
final String cfNameUpdated = "Expiration Date updated";
field2.setName(cfNameUpdated);
field3.setRefId("some-ref-id_1");
CustomFieldCollection request = new CustomFieldCollection().withCustomFields(Arrays.asList(field3, field2));
PutCustomFieldCollection request = new PutCustomFieldCollection().withCustomFields(Arrays.asList(field3, field2)).withEntityType("user");
putWithNoContent(CUSTOM_FIELDS_PATH, Json.encode(request), USER2_HEADER);

List<CustomField> customFieldsAfterUpdate = getAllCustomFields(vertx);
Expand Down Expand Up @@ -523,7 +591,7 @@ public void shouldCreateCustomFieldWhenIdIsEmptyAndDeleteOldOnCollectionPut() th
//update cf id
field2.setId(null);

CustomFieldCollection request = new CustomFieldCollection().withCustomFields(Arrays.asList(field1, field2));
PutCustomFieldCollection request = new PutCustomFieldCollection().withCustomFields(Arrays.asList(field1, field2)).withEntityType("user");
putWithNoContent(CUSTOM_FIELDS_PATH, Json.encode(request), USER2_HEADER);

List<CustomField> customFieldsAfterUpdate = getAllCustomFields(vertx);
Expand Down Expand Up @@ -551,7 +619,7 @@ public void shouldReturn422WhenChangedTypeOnPutCollection() throws IOException,
//update cf type
field2.setType(CustomField.Type.TEXTBOX_LONG);

CustomFieldCollection request = new CustomFieldCollection().withCustomFields(Arrays.asList(field1, field2));
PutCustomFieldCollection request = new PutCustomFieldCollection().withCustomFields(Arrays.asList(field1, field2)).withEntityType("user");
String error = putWithStatus(CUSTOM_FIELDS_PATH, Json.encode(request), SC_UNPROCESSABLE_ENTITY, USER2_HEADER).asString();
assertThat(error, containsString("The type of the custom field can not be changed"));
}
Expand Down Expand Up @@ -687,9 +755,9 @@ public void shouldFailWith404WhenStatsRequestedForNonExistingField() {
assertThat(error, containsString("CustomField not found by id"));
}

private void createFields() throws IOException, URISyntaxException {
private void createFieldsMultipleEntityTypes() throws IOException, URISyntaxException {
createCustomField(readFile("fields/post/postCustomField.json"));
createCustomField(readFile("fields/post/postCustomField2.json"));
createCustomField(readFile("fields/post/postCustomField-package.json"));
}

private CustomField createCustomField(String postBody) {
Expand Down
10 changes: 10 additions & 0 deletions src/test/resources/fields/post/postCustomField-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "Expiration Date",
"type" : "TEXTBOX_SHORT",
"refId": "not-null-ref-id",
"visible": true,
"required": true,
"entityType": "package",
"helpText": "Set expiration date",
"order": 1
}
2 changes: 1 addition & 1 deletion src/test/resources/fields/post/postCustomField2.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"refId": "not-null-ref-id",
"visible": true,
"required": true,
"entityType": "package",
"entityType": "user",
"helpText": "Set expiration date",
"order": 1
}
5 changes: 3 additions & 2 deletions src/test/resources/fields/put/putCustomFieldCollection.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type" : "TEXTBOX_SHORT",
"visible": true,
"required": true,
"entityType": "package",
"entityType": "user",
"helpText": "Set new expiration date"
},
{
Expand All @@ -20,5 +20,6 @@
"default": true
}
}
]
],
"entityType": "user"
}

0 comments on commit e3c95f1

Please sign in to comment.