From f3fe2568c191fff113448e9aae4decffba4b6ae9 Mon Sep 17 00:00:00 2001 From: Javokhir Abdullaev <101543142+JavokhirAbdullayev@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:18:55 +0500 Subject: [PATCH] MODINVSTOR-1222 Implement Subject sources management (#1056) * MODINVSTOR-1222 Implement Subject sources management * fix checkstyle * small fixes * fix test --- NEWS.md | 3 + README.MD | 4 + descriptors/ModuleDescriptor-template.json | 58 ++++++++++ ramls/examples/subject-source.json | 5 + ramls/examples/subject-sources.json | 20 ++++ ramls/subject-source.json | 30 +++++ ramls/subject-source.raml | 47 ++++++++ ramls/subject-sources.json | 24 ++++ ramls/subject-type.json | 4 +- .../java/org/folio/InventoryKafkaTopic.java | 6 +- .../persist/SubjectSourceRepository.java | 15 +++ .../org/folio/rest/impl/SubjectSourceApi.java | 73 ++++++++++++ .../org/folio/rest/support/ResponseUtil.java | 6 + .../SubjectSourceDomainEventPublisher.java | 41 +++++++ .../subjectsource/SubjectSourceService.java | 88 +++++++++++++++ .../subjecttype/SubjectTypeService.java | 6 +- .../db_scripts/addSubjectSources.sql | 38 +++++++ .../templates/db_scripts/schema.json | 16 +++ .../org/folio/rest/api/SubjectSourceTest.java | 106 ++++++++++++++++++ .../org/folio/rest/impl/SubjectSourcesIT.java | 71 ++++++++++++ .../rest/support/http/InterfaceUrls.java | 4 + .../rest/support/http/ResourceClient.java | 5 + .../topic/KafkaAdminClientServiceTest.java | 3 +- 23 files changed, 664 insertions(+), 9 deletions(-) create mode 100644 ramls/examples/subject-source.json create mode 100644 ramls/examples/subject-sources.json create mode 100644 ramls/subject-source.json create mode 100644 ramls/subject-source.raml create mode 100644 ramls/subject-sources.json create mode 100644 src/main/java/org/folio/persist/SubjectSourceRepository.java create mode 100644 src/main/java/org/folio/rest/impl/SubjectSourceApi.java create mode 100644 src/main/java/org/folio/services/domainevent/SubjectSourceDomainEventPublisher.java create mode 100644 src/main/java/org/folio/services/subjectsource/SubjectSourceService.java create mode 100644 src/main/resources/templates/db_scripts/addSubjectSources.sql create mode 100644 src/test/java/org/folio/rest/api/SubjectSourceTest.java create mode 100644 src/test/java/org/folio/rest/impl/SubjectSourcesIT.java diff --git a/NEWS.md b/NEWS.md index 51ce8d64a..88e0e1568 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ * Required sourceId field in holdings record ([MODINVSTOR-1161](https://folio-org.atlassian.net/browse/MODINVSTOR-1161)) ### New APIs versions +* Provides `subject-source 1.0` * Provides `subject-types 1.0` * Provides `instance-date-types 1.0` * Provides `instance-storage 10.1` @@ -19,6 +20,8 @@ * Implement Subject types management ([MODINVSTOR-1221](https://folio-org.atlassian.net/browse/MODINVSTOR-1221)) * Implement endpoint to publish reindex event for the range of instance/item/holding records ([MODINVSTOR-1230](https://folio-org.atlassian.net/browse/MODINVSTOR-1230)) * Info, not warn, about expected 403 from /user-tenants ([MODINVSTOR-1237](https://folio-org.atlassian.net/browse/MODINVSTOR-1237)) +* Implement Subject sources management ([MODINVSTOR-1222](https://folio-org.atlassian.net/browse/MODINVSTOR-1222)) + ### Bug fixes * Unintended update of instance records \_version (optimistic locking) whenever any of its holdings or items are created, updated or deleted. ([MODINVSTOR-1186](https://folio-org.atlassian.net/browse/MODINVSTOR-1186)) diff --git a/README.MD b/README.MD index 9ff3b5c10..6f31c26ce 100644 --- a/README.MD +++ b/README.MD @@ -112,6 +112,7 @@ These properties can be changed by setting env variable. * `KAFKA_INSTITUTION_TOPIC_NUM_PARTITIONS` Default value - `1` * `KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS` Default value - `1` * `KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS` Default value - `16` +* `KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS` Default value - `1` # Building @@ -129,6 +130,8 @@ These environment variables configure Kafka, for details see [Kafka](#kafka): * `KAFKA_CLASSIFICATION_TYPE_TOPIC_NUM_PARTITIONS` * `KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS` * `KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS` +* `KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS` + These environment variables configure Kafka topic for specific business-related topics * `KAFKA_CLASSIFICATION_TYPE_TOPIC_NUM_PARTITIONS` @@ -138,6 +141,7 @@ These environment variables configure Kafka topic for specific business-related * `KAFKA_INSTITUTION_TOPIC_NUM_PARTITIONS` * `KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS` * `KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS` +* `KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS` mod-inventory-storage also supports all Raml Module Builder (RMB) environment variables, for details see [RMB](https://github.com/folio-org/raml-module-builder#environment-variables): diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index dc9c15598..33d4b6ac2 100755 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -557,6 +557,33 @@ } ] }, + { + "id": "subject-sources", + "version": "1.0", + "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/subject-sources", + "permissionsRequired": ["inventory-storage.subject-sources.collection.get"] + }, { + "methods": ["GET"], + "pathPattern": "/subject-sources/{id}", + "permissionsRequired": ["inventory-storage.subject-sources.item.get"] + }, { + "methods": ["POST"], + "pathPattern": "/subject-sources", + "permissionsRequired": ["inventory-storage.subject-sources.item.post"] + }, { + "methods": ["PUT"], + "pathPattern": "/subject-sources/{id}", + "permissionsRequired": ["inventory-storage.subject-sources.item.put"] + }, { + "methods": ["DELETE"], + "pathPattern": "/subject-sources/{id}", + "permissionsRequired": ["inventory-storage.subject-sources.item.delete"] + } + ] + }, { "id": "contributor-types", "version": "2.0", @@ -1988,6 +2015,31 @@ "displayName": "inventory storage - delete individual subject type", "description": "delete subject type in storage" }, + { + "permissionName": "inventory-storage.subject-sources.collection.get", + "displayName": "inventory storage - get subject sources collection", + "description": "get subject-sources collection from storage" + }, + { + "permissionName": "inventory-storage.subject-sources.item.get", + "displayName": "inventory storage - get individual subject source", + "description": "get individual subject source from storage" + }, + { + "permissionName": "inventory-storage.subject-sources.item.post", + "displayName": "inventory storage - create individual subject source", + "description": "create individual subject source in storage" + }, + { + "permissionName": "inventory-storage.subject-sources.item.put", + "displayName": "inventory storage - modify subject source", + "description": "modify subject source in storage" + }, + { + "permissionName": "inventory-storage.subject-sources.item.delete", + "displayName": "inventory storage - delete individual subject source", + "description": "delete subject source in storage" + }, { "permissionName": "inventory-storage.instance-types.collection.get", "displayName": "inventory storage - get instance types collection", @@ -2661,6 +2713,11 @@ "inventory-storage.subject-types.item.post", "inventory-storage.subject-types.item.put", "inventory-storage.subject-types.item.delete", + "inventory-storage.subject-sources.collection.get", + "inventory-storage.subject-sources.item.get", + "inventory-storage.subject-sources.item.post", + "inventory-storage.subject-sources.item.put", + "inventory-storage.subject-sources.item.delete", "inventory-storage.instance-types.collection.get", "inventory-storage.instance-types.item.get", "inventory-storage.instance-types.item.post", @@ -2832,6 +2889,7 @@ { "name": "KAFKA_CAMPUS_TOPIC_NUM_PARTITIONS", "value": "1"}, { "name": "KAFKA_INSTITUTION_TOPIC_NUM_PARTITIONS", "value": "1"}, { "name": "KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS", "value": "1"}, + { "name": "KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS", "value": "1"}, { "name": "KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS", "value": "16"} ] } diff --git a/ramls/examples/subject-source.json b/ramls/examples/subject-source.json new file mode 100644 index 000000000..a8e932af7 --- /dev/null +++ b/ramls/examples/subject-source.json @@ -0,0 +1,5 @@ +{ + "id": "535e3160-763a-42f9-b0c0-d8ed7df6e2a3", + "name": "Library of Congress Subject Headings", + "source": "folio" +} diff --git a/ramls/examples/subject-sources.json b/ramls/examples/subject-sources.json new file mode 100644 index 000000000..f220f9c20 --- /dev/null +++ b/ramls/examples/subject-sources.json @@ -0,0 +1,20 @@ +{ + "subjectSources": [ + { + "id": "06b2cbd8-66bf-4956-9d90-97c9776365b8", + "name": "Library of Congress Subject Headings", + "source": "folio" + }, + { + "id": "f9e5b41b-8d5b-47d3-91d0-ca9004796400", + "name": "Library of Congress Children's and Young Adults' Subject Headings", + "source": "folio" + }, + { + "id": "6e09d47d-95e2-4d8a-831b-f777b8ef6d99", + "name": "Non-medical Subject Headings", + "source": "local" + } + ], + "totalRecords": 3 +} diff --git a/ramls/subject-source.json b/ramls/subject-source.json new file mode 100644 index 000000000..dd6ad63c3 --- /dev/null +++ b/ramls/subject-source.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A subject source", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "description": "label for the subject source", + "type": "string" + }, + "source": { + "type": "string", + "description": "label indicating where the subject source entry originates from, i.e. 'folio' or 'local'", + "enum": [ + "folio", + "local" + ] + }, + "metadata": { + "type": "object", + "$ref": "raml-util/schemas/metadata.schema", + "readonly": true + } + }, + "required": [ + "name" + ] +} diff --git a/ramls/subject-source.raml b/ramls/subject-source.raml new file mode 100644 index 000000000..b9b465e2f --- /dev/null +++ b/ramls/subject-source.raml @@ -0,0 +1,47 @@ +#%RAML 1.0 +title: Subject Sources API +version: v1.0 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost + +documentation: + - title: Subject Sources API + content: This documents the API calls that can be made to query and manage subject types + +types: + subjectSource: !include subject-source.json + subjectSources: !include subject-sources.json + errors: !include raml-util/schemas/errors.schema + +traits: + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml + +resourceTypes: + collection: !include raml-util/rtypes/collection.raml + collection-item: !include raml-util/rtypes/item-collection.raml + get-delete-only: !include raml-util/rtypes/get-delete.raml + +/subject-sources: + type: + collection: + exampleCollection: !include examples/subject-sources.json + exampleItem: !include examples/subject-source.json + schemaCollection: subjectSources + schemaItem: subjectSource + get: + is: [ + searchable: {description: "with valid searchable fields", example: "name=aaa"}, + pageable + ] + description: Return a list of subject sources + post: + description: Create a new subject source + is: [validate] + /{subjectSourceId}: + description: Pass in the subject source id + type: + collection-item: + exampleItem: !include examples/subject-source.json + schema: subjectSource diff --git a/ramls/subject-sources.json b/ramls/subject-sources.json new file mode 100644 index 000000000..c8bb12cbc --- /dev/null +++ b/ramls/subject-sources.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A collection of subject sources", + "type": "object", + "properties": { + "subjectSources": { + "description": "List of subject sources", + "id": "subjectSource", + "type": "array", + "items": { + "type": "object", + "$ref": "subject-source.json" + } + }, + "totalRecords": { + "description": "Estimated or exact total number of records", + "type": "integer" + } + }, + "required": [ + "subjectSources", + "totalRecords" + ] +} diff --git a/ramls/subject-type.json b/ramls/subject-type.json index 968a7daf5..b9808b1d6 100644 --- a/ramls/subject-type.json +++ b/ramls/subject-type.json @@ -7,12 +7,12 @@ "type": "string" }, "name": { - "description": "label for the identifier type", + "description": "label for the subject type", "type": "string" }, "source": { "type": "string", - "description": "label indicating where the identifier type entry originates from, i.e. 'folio' or 'local'", + "description": "label indicating where the subject type entry originates from, i.e. 'folio' or 'local'", "enum": [ "folio", "local" diff --git a/src/main/java/org/folio/InventoryKafkaTopic.java b/src/main/java/org/folio/InventoryKafkaTopic.java index 00435a798..9b6210511 100644 --- a/src/main/java/org/folio/InventoryKafkaTopic.java +++ b/src/main/java/org/folio/InventoryKafkaTopic.java @@ -21,7 +21,8 @@ public enum InventoryKafkaTopic implements KafkaTopic { CAMPUS("campus"), SUBJECT_TYPE("subject-types"), INSTITUTION("institution"), - REINDEX_RECORDS("reindex-records"); + REINDEX_RECORDS("reindex-records"), + SUBJECT_SOURCE("subject-sources"); private static final String DEFAULT_NUM_PARTITIONS_PROPERTY = "KAFKA_DOMAIN_TOPIC_NUM_PARTITIONS"; private static final String DEFAULT_NUM_PARTITIONS_VALUE = "50"; @@ -37,7 +38,8 @@ public enum InventoryKafkaTopic implements KafkaTopic { CAMPUS, Pair.of("KAFKA_CAMPUS_TOPIC_NUM_PARTITIONS", "1"), INSTITUTION, Pair.of("KAFKA_INSTITUTION_TOPIC_NUM_PARTITIONS", "1"), SUBJECT_TYPE, Pair.of("KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS", "1"), - REINDEX_RECORDS, Pair.of("KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS", "16") + REINDEX_RECORDS, Pair.of("KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS", "16"), + SUBJECT_SOURCE, Pair.of("KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS", "1") ); private final String topic; diff --git a/src/main/java/org/folio/persist/SubjectSourceRepository.java b/src/main/java/org/folio/persist/SubjectSourceRepository.java new file mode 100644 index 000000000..36c47e8ea --- /dev/null +++ b/src/main/java/org/folio/persist/SubjectSourceRepository.java @@ -0,0 +1,15 @@ +package org.folio.persist; + +import static org.folio.rest.persist.PgUtil.postgresClient; +import static org.folio.services.subjectsource.SubjectSourceService.SUBJECT_SOURCE; + +import io.vertx.core.Context; +import java.util.Map; +import org.folio.rest.jaxrs.model.SubjectSource; + +public class SubjectSourceRepository extends AbstractRepository { + + public SubjectSourceRepository(Context context, Map okapiHeaders) { + super(postgresClient(context, okapiHeaders), SUBJECT_SOURCE, SubjectSource.class); + } +} diff --git a/src/main/java/org/folio/rest/impl/SubjectSourceApi.java b/src/main/java/org/folio/rest/impl/SubjectSourceApi.java new file mode 100644 index 000000000..b608856de --- /dev/null +++ b/src/main/java/org/folio/rest/impl/SubjectSourceApi.java @@ -0,0 +1,73 @@ +package org.folio.rest.impl; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.support.EndpointFailureHandler.handleFailure; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import java.util.Map; +import javax.ws.rs.core.Response; +import org.folio.rest.jaxrs.model.SubjectSource; +import org.folio.rest.jaxrs.resource.SubjectSources; +import org.folio.services.subjectsource.SubjectSourceService; + +public class SubjectSourceApi implements SubjectSources { + + @Override + public void getSubjectSources(String query, String totalRecords, int offset, + int limit, + Map okapiHeaders, + Handler> asyncResultHandler, + Context vertxContext) { + new SubjectSourceService(vertxContext, okapiHeaders) + .getByQuery(query, offset, limit) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(handleFailure(asyncResultHandler)); + } + + @Override + public void postSubjectSources(SubjectSource entity, + Map okapiHeaders, + Handler> asyncResultHandler, + Context vertxContext) { + new SubjectSourceService(vertxContext, okapiHeaders) + .create(entity) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(handleFailure(asyncResultHandler)); + } + + @Override + public void getSubjectSourcesBySubjectSourceId(String subjectSourceId, + Map okapiHeaders, + Handler> asyncResultHandler, + Context vertxContext) { + new SubjectSourceService(vertxContext, okapiHeaders) + .getById(subjectSourceId) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(handleFailure(asyncResultHandler)); + } + + @Override + public void deleteSubjectSourcesBySubjectSourceId(String subjectSourceId, + Map okapiHeaders, + Handler> asyncResultHandler, + Context vertxContext) { + new SubjectSourceService(vertxContext, okapiHeaders) + .delete(subjectSourceId) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(handleFailure(asyncResultHandler)); + } + + @Override + public void putSubjectSourcesBySubjectSourceId(String subjectSourceId, + SubjectSource entity, + Map okapiHeaders, + Handler> asyncResultHandler, + Context vertxContext) { + new SubjectSourceService(vertxContext, okapiHeaders) + .update(subjectSourceId, entity) + .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) + .onFailure(handleFailure(asyncResultHandler)); + } +} diff --git a/src/main/java/org/folio/rest/support/ResponseUtil.java b/src/main/java/org/folio/rest/support/ResponseUtil.java index c490f230e..94ab61402 100644 --- a/src/main/java/org/folio/rest/support/ResponseUtil.java +++ b/src/main/java/org/folio/rest/support/ResponseUtil.java @@ -7,6 +7,12 @@ import org.folio.HttpStatus; public final class ResponseUtil { + + public static final String SOURCE_CANNOT_BE_FOLIO = + "Illegal operation: Source field cannot be set to folio"; + public static final String SOURCE_CANNOT_BE_UPDATED = + "Illegal operation: Source field cannot be updated"; + private ResponseUtil() { } public static boolean isUpdateSuccessResponse(Response response) { diff --git a/src/main/java/org/folio/services/domainevent/SubjectSourceDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/SubjectSourceDomainEventPublisher.java new file mode 100644 index 000000000..47906d6f0 --- /dev/null +++ b/src/main/java/org/folio/services/domainevent/SubjectSourceDomainEventPublisher.java @@ -0,0 +1,41 @@ +package org.folio.services.domainevent; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.InventoryKafkaTopic.SUBJECT_SOURCE; +import static org.folio.rest.tools.utils.TenantTool.tenantId; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.tuple.Pair; +import org.folio.persist.SubjectSourceRepository; +import org.folio.rest.jaxrs.model.SubjectSource; + +public class SubjectSourceDomainEventPublisher extends AbstractDomainEventPublisher { + + public SubjectSourceDomainEventPublisher(Context context, Map okapiHeaders) { + super(new SubjectSourceRepository(context, okapiHeaders), + new CommonDomainEventPublisher<>(context, okapiHeaders, SUBJECT_SOURCE.fullTopicName(tenantId(okapiHeaders)))); + } + + @Override + protected Future>> getRecordIds( + Collection subjectSources) { + return succeededFuture(subjectSources.stream() + .map(subjectSource -> pair(subjectSource.getId(), subjectSource)) + .toList() + ); + } + + @Override + protected SubjectSource convertDomainToEvent(String instanceId, SubjectSource subjectSource) { + return subjectSource; + } + + @Override + protected String getId(SubjectSource subjectSource) { + return subjectSource.getId(); + } +} diff --git a/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java b/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java new file mode 100644 index 000000000..054967ade --- /dev/null +++ b/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java @@ -0,0 +1,88 @@ +package org.folio.services.subjectsource; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.jaxrs.resource.SubjectSources.DeleteSubjectSourcesBySubjectSourceIdResponse; +import static org.folio.rest.jaxrs.resource.SubjectSources.PostSubjectSourcesResponse; +import static org.folio.rest.jaxrs.resource.SubjectSources.PostSubjectSourcesResponse.respond422WithApplicationJson; +import static org.folio.rest.jaxrs.resource.SubjectSources.PutSubjectSourcesBySubjectSourceIdResponse; +import static org.folio.rest.persist.PgUtil.deleteById; +import static org.folio.rest.persist.PgUtil.get; +import static org.folio.rest.persist.PgUtil.post; +import static org.folio.rest.persist.PgUtil.put; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_FOLIO; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_UPDATED; +import static org.folio.rest.tools.utils.ValidationHelper.createValidationErrorMessage; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import java.util.Map; +import javax.ws.rs.core.Response; +import org.folio.persist.SubjectSourceRepository; +import org.folio.rest.jaxrs.model.SubjectSource; +import org.folio.rest.jaxrs.model.SubjectSources; +import org.folio.rest.jaxrs.resource.SubjectSources.GetSubjectSourcesBySubjectSourceIdResponse; +import org.folio.rest.jaxrs.resource.SubjectSources.GetSubjectSourcesResponse; +import org.folio.rest.persist.PgUtil; +import org.folio.services.domainevent.SubjectSourceDomainEventPublisher; + +public class SubjectSourceService { + + public static final String SUBJECT_SOURCE = "subject_source"; + + private final Context context; + private final Map okapiHeaders; + private final SubjectSourceRepository repository; + private final SubjectSourceDomainEventPublisher domainEventService; + + public SubjectSourceService(Context context, Map okapiHeaders) { + this.context = context; + this.okapiHeaders = okapiHeaders; + this.repository = new SubjectSourceRepository(context, okapiHeaders); + this.domainEventService = new SubjectSourceDomainEventPublisher(context, okapiHeaders); + } + + public Future getByQuery(String cql, int offset, int limit) { + return get(SUBJECT_SOURCE, SubjectSource.class, SubjectSources.class, + cql, offset, limit, okapiHeaders, context, GetSubjectSourcesResponse.class); + } + + public Future getById(String id) { + return PgUtil.getById(SUBJECT_SOURCE, SubjectSource.class, id, okapiHeaders, context, + GetSubjectSourcesBySubjectSourceIdResponse.class); + } + + public Future create(SubjectSource subjectSource) { + if (subjectSource.getSource().equals(SubjectSource.Source.FOLIO)) { + return sourceValidationError(subjectSource.getSource().value(), SOURCE_CANNOT_BE_FOLIO); + } + return post(SUBJECT_SOURCE, subjectSource, okapiHeaders, context, PostSubjectSourcesResponse.class) + .onSuccess(domainEventService.publishCreated()); + } + + public Future update(String id, SubjectSource subjectSource) { + return repository.getById(id) + .compose(oldSubjectSource -> { + if (!oldSubjectSource.getSource().equals(subjectSource.getSource())) { + return sourceValidationError(subjectSource.getSource().value(), SOURCE_CANNOT_BE_UPDATED); + } + return put(SUBJECT_SOURCE, subjectSource, id, okapiHeaders, context, + PutSubjectSourcesBySubjectSourceIdResponse.class) + .onSuccess(domainEventService.publishUpdated(subjectSource)); + }); + } + + public Future delete(String id) { + return repository.getById(id) + .compose(oldSubjectSource -> deleteById(SUBJECT_SOURCE, id, okapiHeaders, context, + DeleteSubjectSourcesBySubjectSourceIdResponse.class) + .onSuccess(domainEventService.publishRemoved(oldSubjectSource)) + ); + } + + private Future sourceValidationError(String field, String message) { + return succeededFuture( + respond422WithApplicationJson( + createValidationErrorMessage("source", field, + message))); + } +} diff --git a/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java b/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java index 82af7f874..78895d341 100644 --- a/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java +++ b/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java @@ -6,6 +6,8 @@ import static org.folio.rest.persist.PgUtil.get; import static org.folio.rest.persist.PgUtil.post; import static org.folio.rest.persist.PgUtil.put; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_FOLIO; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_UPDATED; import static org.folio.rest.tools.utils.ValidationHelper.createValidationErrorMessage; import io.vertx.core.Context; @@ -26,10 +28,6 @@ public class SubjectTypeService { public static final String SUBJECT_TYPE = "subject_type"; - private static final String SOURCE_CANNOT_BE_FOLIO = - "Illegal operation: Source field cannot be set to folio"; - private static final String SOURCE_CANNOT_BE_UPDATED = - "Illegal operation: Source field cannot be updated"; private final Context context; private final Map okapiHeaders; diff --git a/src/main/resources/templates/db_scripts/addSubjectSources.sql b/src/main/resources/templates/db_scripts/addSubjectSources.sql new file mode 100644 index 000000000..f571ba0e6 --- /dev/null +++ b/src/main/resources/templates/db_scripts/addSubjectSources.sql @@ -0,0 +1,38 @@ +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d40', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d40', 'name', 'Library of Congress Subject Headings', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d41', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d41', 'name', 'Library of Congress Children''s and Young Adults'' Subject Headings', 'source', 'folio')) +ON CONFLICT DO NOTHING; +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d42', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d42', 'name', 'Medical Subject Headings', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d43', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d43', 'name', 'National Agricultural Library subject authority file', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d44', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d44', 'name', 'Source not specified', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d45', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d45', 'name', 'Canadian Subject Headings', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d46', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d46', 'name', 'Répertoire de vedettes-matière', 'source', 'folio')) +ON CONFLICT DO NOTHING; + +INSERT INTO ${myuniversity}_${mymodule}.subject_source (id, jsonb) +VALUES ('e894d0dc-621d-4b1d-98f6-6f7120eb0d47', + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d47', 'name', 'Source specified in subfield $2', 'source', 'folio')) +ON CONFLICT DO NOTHING; diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 6d79845da..e5ad7d242 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -900,6 +900,17 @@ "tOps": "ADD" } ] + }, + { + "tableName": "subject_source", + "withMetadata": true, + "withAuditing": false, + "uniqueIndex": [ + { + "fieldName": "name", + "tOps": "ADD" + } + ] } ], "scripts": [ @@ -1197,6 +1208,11 @@ "run": "after", "snippetPath": "addSubjectTypes.sql", "fromModuleVersion": "27.2.0" + }, + { + "run": "after", + "snippetPath": "addSubjectSources.sql", + "fromModuleVersion": "27.2.0" } ] } diff --git a/src/test/java/org/folio/rest/api/SubjectSourceTest.java b/src/test/java/org/folio/rest/api/SubjectSourceTest.java new file mode 100644 index 000000000..8a78a984b --- /dev/null +++ b/src/test/java/org/folio/rest/api/SubjectSourceTest.java @@ -0,0 +1,106 @@ +package org.folio.rest.api; + +import static org.folio.rest.support.http.InterfaceUrls.subjectSourcesUrl; +import static org.folio.utility.ModuleUtility.getClient; +import static org.folio.utility.RestUtility.TENANT_ID; +import static org.folio.utility.RestUtility.send; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.folio.rest.support.Response; +import org.folio.rest.support.ResponseHandler; +import org.folio.rest.support.http.ResourceClient; +import org.junit.BeforeClass; +import org.junit.Test; + +public class SubjectSourceTest extends TestBase { + + private static ResourceClient subjectSourceClient; + private static final String SUBJECT_SOURCE_ID = "e894d0dc-621d-4b1d-98f6-6f7120eb0d40"; + + @BeforeClass + public static void beforeAll() { + TestBase.beforeAll(); + subjectSourceClient = ResourceClient.forSubjectSources(getClient()); + } + + @Test + public void cannotCreateSubjectSourceWithDuplicateName() + throws InterruptedException, TimeoutException, + ExecutionException { + + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings2") + .put("source", "local"); + + subjectSourceClient.create(subjectSource); + + CompletableFuture postCompleted = new CompletableFuture<>(); + getClient().post(subjectSourcesUrl(""), subjectSource, TENANT_ID, ResponseHandler.json(postCompleted)); + + Response response = postCompleted.get(TIMEOUT, TimeUnit.SECONDS); + assertThat(response.getStatusCode(), is(422)); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertThat(errors.size(), is(1)); + } + + @Test + public void cannotCreateSubjectSourceWithSourceFolio() { + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings2") + .put("source", "folio"); + + Response response = createSubjectSource(subjectSource); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertEquals(422, response.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals( + "Illegal operation: Source field cannot be set to folio", + errors.getJsonObject(0).getString("message")); + } + + @Test + public void cannotUpdateSubjectSourceWithSourceFolio() { + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings") + .put("source", "local"); + + Response response = updateSubjectSource(SUBJECT_SOURCE_ID, subjectSource); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertEquals(422, response.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals( + "Illegal operation: Source field cannot be updated", + errors.getJsonObject(0).getString("message")); + } + + private Response createSubjectSource(JsonObject object) { + + CompletableFuture createSubjectSource = new CompletableFuture<>(); + + send(subjectSourcesUrl("").toString(), HttpMethod.POST, object.toString(), + SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(createSubjectSource)); + + return get(createSubjectSource); + } + + private Response updateSubjectSource(String id, JsonObject object) { + CompletableFuture updateSubjectSource = new CompletableFuture<>(); + + send(subjectSourcesUrl("/" + id).toString(), HttpMethod.PUT, object.toString(), + SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(updateSubjectSource)); + + return get(updateSubjectSource); + } +} diff --git a/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java b/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java new file mode 100644 index 000000000..af9494493 --- /dev/null +++ b/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java @@ -0,0 +1,71 @@ +package org.folio.rest.impl; + +import java.util.List; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.jaxrs.model.SubjectSource; +import org.folio.rest.jaxrs.model.SubjectSources; +import org.folio.services.subjectsource.SubjectSourceService; + +public class SubjectSourcesIT extends BaseReferenceDataIntegrationTest { + + @Override + protected String referenceTable() { + return SubjectSourceService.SUBJECT_SOURCE; + } + + @Override + protected String resourceUrl() { + return "/subject-sources"; + } + + @Override + protected Class targetClass() { + return SubjectSource.class; + } + + @Override + protected Class collectionClass() { + return SubjectSources.class; + } + + @Override + protected SubjectSource sampleRecord() { + return new SubjectSource() + .withId(UUID.randomUUID().toString()) + .withName("test_name") + .withSource(SubjectSource.Source.LOCAL); + } + + @Override + protected Function> collectionRecordsExtractor() { + return SubjectSources::getSubjectSources; + } + + @Override + protected List> recordFieldExtractors() { + return List.of(SubjectSource::getName, SubjectSource::getSource); + } + + @Override + protected Function idExtractor() { + return SubjectSource::getId; + } + + @Override + protected Function metadataExtractor() { + return SubjectSource::getMetadata; + } + + @Override + protected UnaryOperator recordModifyingFunction() { + return subjectSource -> subjectSource.withName("updated"); + } + + @Override + protected List queries() { + return List.of("name==test_name"); + } +} diff --git a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java index 35159f990..10dabb88d 100644 --- a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java +++ b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java @@ -26,6 +26,10 @@ public static URL subjectTypesUrl(String subPath) { return vertxUrl("/subject-types" + subPath); } + public static URL subjectSourcesUrl(String subPath) { + return vertxUrl("/subject-sources" + subPath); + } + public static URL itemsStorageUrl(String subPath) { return vertxUrl("/item-storage/items" + subPath); } diff --git a/src/test/java/org/folio/rest/support/http/ResourceClient.java b/src/test/java/org/folio/rest/support/http/ResourceClient.java index b259f1e5b..007fab387 100644 --- a/src/test/java/org/folio/rest/support/http/ResourceClient.java +++ b/src/test/java/org/folio/rest/support/http/ResourceClient.java @@ -126,6 +126,11 @@ public static ResourceClient forSubjectTypes(HttpClient client) { "subject types", "subjectTypes"); } + public static ResourceClient forSubjectSources(HttpClient client) { + return new ResourceClient(client, InterfaceUrls::subjectSourcesUrl, + "subject sources", "subjectSources"); + } + public static ResourceClient forCallNumberTypes(HttpClient client) { return new ResourceClient(client, InterfaceUrls::callNumberTypesUrl, "call number types", "callNumberTypes"); diff --git a/src/test/java/org/folio/services/kafka/topic/KafkaAdminClientServiceTest.java b/src/test/java/org/folio/services/kafka/topic/KafkaAdminClientServiceTest.java index 2be7fdd9e..4535cd584 100644 --- a/src/test/java/org/folio/services/kafka/topic/KafkaAdminClientServiceTest.java +++ b/src/test/java/org/folio/services/kafka/topic/KafkaAdminClientServiceTest.java @@ -42,7 +42,8 @@ public class KafkaAdminClientServiceTest { "folio.foo-tenant.inventory.service-point", "folio.foo-tenant.inventory.classification-type", "folio.foo-tenant.inventory.location", "folio.foo-tenant.inventory.library", "folio.foo-tenant.inventory.campus", "folio.foo-tenant.inventory.subject-types", - "folio.foo-tenant.inventory.institution", "folio.foo-tenant.inventory.reindex-records"); + "folio.foo-tenant.inventory.institution", "folio.foo-tenant.inventory.reindex-records", + "folio.foo-tenant.inventory.subject-sources"); private KafkaAdminClient mockClient; private Vertx vertx;