diff --git a/NEWS.md b/NEWS.md index c128527a5..b41623216 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,26 @@ -## v27.2.0 In progress +## v28.0.0 In progress +### Breaking changes +* Migrate "publicationPeriod" data to the Dates object and remove it from the Instance schema ([MODINVSTOR-1232](https://folio-org.atlassian.net/browse/MODINVSTOR-1232)) + +### New APIs versions +* Provides `instance-storage 11.0` +* Provides `instance-storage-batch 3.0` +* Provides `instance-storage-batch-sync 3.0` +* Provides `instance-storage-batch-sync-unsafe 3.0` +* Provides `inventory-view 3.0` +* Provides `inventory-view-instance-set 3.0` +* Provides `instance-iteration 1.0` +* Requires `holdings-storage 8.0` +* Requires `bound-with-parts-storage 2.0` +* Requires `async-migration 1.0` + +### Features +* Add module descriptor validator plugin and fix the permission names ([MODINVSTOR-1247](https://folio-org.atlassian.net/browse/MODINVSTOR-1247)) + +### Tech Dept +* Upgrade localstack from 0.11.3 to s3-latest (=3.8.0) ([MODINVSTOR-1272](https://folio-org.atlassian.net/browse/MODINVSTOR-1272)) +## v27.2.0 2024-09-24 ### Breaking changes * Required sourceId field in holdings record ([MODINVSTOR-1161](https://folio-org.atlassian.net/browse/MODINVSTOR-1161)) @@ -29,7 +50,7 @@ * Add new date type fields to Instance schema ([MODINVSTOR-1188](https://folio-org.atlassian.net/browse/MODINVSTOR-1188)) * Implement endpoint for bulk instances upsert from external file ([MODINVSTOR-1225](https://folio-org.atlassian.net/browse/MODINVSTOR-1225)) * Add Subject source and Subject type to schema ([MODINVSTOR-1205](https://folio-org.atlassian.net/browse/MODINVSTOR-1205)) - +* Add codes to Subject sources ([MODINVSTOR-1264](https://folio-org.atlassian.net/browse/MODINVSTOR-1264)) ### 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)) @@ -37,6 +58,8 @@ * Do not delete Kafka topics on postTenant if collection topics is enabled ([MODINVSTOR-1192](https://folio-org.atlassian.net/browse/MODINVSTOR-1192)) * Identifier types: change Cancelled LCCN to Canceled LCCN ([MODINVSTOR-1212](https://folio-org.atlassian.net/browse/MODINVSTOR-1212)) * Add user-tenants.collection.get to all ECS APIs ([MODINVSTOR-1253](https://folio-org.atlassian.net/browse/MODINVSTOR-1253)) +* Add user-tenants.collection.get to POST /\_/tenant API ([MODINVSTOR-1260](https://folio-org.atlassian.net/browse/MODINVSTOR-1260)) +* Update "BC" name in GET instance-date-type to "B.C." ([MODINVSTOR-1255](https://folio-org.atlassian.net/browse/MODINVSTOR-1255)) ### Tech Dept * Kafka testcontainers: kafka.KafkaContainer, apache/kafka-native:3.8.0, KafkaTopicsExistsTest fix ([MODINVSTOR-1251](https://folio-org.atlassian.net/browse/MODINVSTOR-1251)) @@ -47,6 +70,7 @@ * Bump `holdings-storage` from `6.0` to `7.0` * Bump `holdings-storage-batch-sync` from `1.1` to `2.0` * Bump `holdings-storage-batch-sync-unsafe` from `1.0` to `2.0` +* Bump `folio-kafka-wrapper` from `3.1.1` to `3.2.0` * Add `folio-s3-client` `2.2.0` * Add `LIB_NAME` `2.7.4` * Remove `LIB_NAME` diff --git a/README.MD b/README.MD index f46e9e7cd..2aeee5343 100644 --- a/README.MD +++ b/README.MD @@ -114,6 +114,20 @@ These properties can be changed by setting env variable. * `KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS` Default value - `16` * `KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS` Default value - `1` +There is also possibility for customizing through properties the Kafka Topic +`message retention` (in milliseconds) and `maximum message size` (in bytes). The default values of these configurations +for any topic are 604800000 milliseconds (or 1 week) and 1048576 bytes (or 1 MB) respectively + +These are the defined topic properties for `message retention` and `maximum message size` for `reindex-records` topic + +* `KAFKA_REINDEX_RECORDS_TOPIC_MESSAGE_RETENTION` Default value - `86400000` (1 day) +* `KAFKA_REINDEX_RECORDS_TOPIC_MAX_MESSAGE_SIZE` Default value - `1048576` (1 MB) + +in case, any of these topic properties are changed for `reindex-records` the topic needs to be recreated and module needs to be reinstalled. + +Changing maximum message size for kafka producer: +* `KAFKA_REINDEX_PRODUCER_MAX_REQUEST_SIZE_BYTES` Default value - `10485760` (10 MB) + # Building run `mvn install` from the root directory. @@ -142,6 +156,8 @@ These environment variables configure Kafka topic for specific business-related * `KAFKA_SUBJECT_TYPE_TOPIC_NUM_PARTITIONS` * `KAFKA_REINDEX_RECORDS_TOPIC_NUM_PARTITIONS` * `KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS` +* `KAFKA_REINDEX_RECORDS_TOPIC_MESSAGE_RETENTION` +* `KAFKA_REINDEX_RECORDS_TOPIC_MAX_MESSAGE_SIZE` 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 0a986b04e..24a141f28 100755 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -95,7 +95,7 @@ }, { "id": "holdings-storage", - "version": "7.0", + "version": "8.0", "handlers": [ { "methods": ["GET"], @@ -104,7 +104,7 @@ },{ "methods": ["POST"], "pathPattern": "/holdings-storage/holdings/retrieve", - "permissionsRequired": ["inventory-storage.holdings.collection.get"] + "permissionsRequired": ["inventory-storage.holdings.retrieve.collection.post"] }, { "methods": ["GET"], "pathPattern": "/holdings-storage/holdings/{id}", @@ -167,7 +167,7 @@ }, { "id": "instance-storage", - "version": "10.3", + "version": "11.0", "handlers": [ { "methods": ["GET"], @@ -176,7 +176,7 @@ },{ "methods": ["POST"], "pathPattern": "/instance-storage/instances/retrieve", - "permissionsRequired": ["inventory-storage.instances.collection.get"] + "permissionsRequired": ["inventory-storage.instances.retrieve.collection.post"] }, { "methods": ["GET"], @@ -274,7 +274,7 @@ }, { "id": "instance-storage-batch", - "version": "2.0", + "version": "3.0", "handlers": [ { "methods": ["POST"], @@ -285,18 +285,18 @@ }, { "id": "instance-storage-batch-sync", - "version": "2.0", + "version": "3.0", "handlers": [ { "methods": ["POST"], "pathPattern": "/instance-storage/batch/synchronous", - "permissionsRequired": ["inventory-storage.instances.batch.post"] + "permissionsRequired": ["inventory-storage.instances.batch.synchronous.post"] } ] }, { "id": "instance-storage-batch-sync-unsafe", - "version": "2.0", + "version": "3.0", "handlers": [ { "methods": ["POST"], @@ -1240,7 +1240,7 @@ }, { "id": "bound-with-parts-storage", - "version": "1.1", + "version": "2.0", "handlers": [ { "methods": ["GET"], @@ -1266,9 +1266,7 @@ "methods": ["PUT"], "pathPattern": "/inventory-storage/bound-withs", "permissionsRequired": [ - "inventory-storage.bound-with-parts.collection.get", - "inventory-storage.bound-with-parts.item.post", - "inventory-storage.bound-with-parts.item.delete" + "inventory-storage.bound-withs.collection.put" ] } ] @@ -1280,7 +1278,10 @@ "handlers": [ { "methods": ["POST"], - "pathPattern": "/_/tenant" + "pathPattern": "/_/tenant", + "modulePermissions": [ + "user-tenants.collection.get" + ] }, { "methods": ["DELETE", "GET"], "pathPattern": "/_/tenant/{id}" @@ -1336,7 +1337,7 @@ }, { "id": "inventory-view", - "version": "2.0", + "version": "3.0", "handlers": [ { "methods": ["GET"], @@ -1347,7 +1348,7 @@ }, { "id": "inventory-view-instance-set", - "version": "2.0", + "version": "3.0", "handlers": [ { "methods": ["GET"], @@ -1384,7 +1385,7 @@ }, { "id": "instance-iteration", - "version": "0.1", + "version": "1.0", "handlers": [ { "methods": ["POST"], @@ -1405,7 +1406,7 @@ }, { "id": "async-migration", - "version": "0.1", + "version": "1.0", "handlers": [ { "methods": ["POST"], @@ -1425,7 +1426,7 @@ { "methods": ["GET"], "pathPattern": "/inventory-storage/migrations/jobs", - "permissionsRequired": ["inventory-storage.migration.job.item.get"] + "permissionsRequired": ["inventory-storage.migration.job.collection.get"] }, { "methods": ["DELETE"], @@ -1522,6 +1523,11 @@ "displayName": "inventory storage - get holdings collection", "description": "get holdings collection from storage" }, + { + "permissionName": "inventory-storage.holdings.retrieve.collection.post", + "displayName": "inventory storage - retrieve holdings collection", + "description": "retrieve holdings collection from storage" + }, { "permissionName": "inventory-storage.holdings.collection.delete", "displayName": "inventory storage - delete entire holdings collection", @@ -1562,6 +1568,11 @@ "displayName": "inventory storage - get instance collection", "description": "get instance collection from storage" }, + { + "permissionName": "inventory-storage.instances.retrieve.collection.post", + "displayName": "inventory storage - retrieve instance collection", + "description": "retrieve instance collection from storage" + }, { "permissionName": "inventory-storage.instances.collection.delete", "displayName": "inventory storage - delete entire instance collection", @@ -1597,6 +1608,11 @@ "displayName": "inventory storage - create or update a number of instances", "description": "create or update a number of instances in storage" }, + { + "permissionName": "inventory-storage.instances.batch.synchronous.post", + "displayName": "inventory storage - create or update a collection of instances in a single synchronous request", + "description": "create or update a collection of instances in a single synchronous request" + }, { "permissionName": "inventory-storage.instances.batch-unsafe.post", "displayName": "inventory storage - create or update a number of instances with optimistic locking disabled", @@ -2538,6 +2554,11 @@ "displayName": "inventory storage - add a part to a bound-with", "description": "add a holdings record to a bound-with by associating it with the bound-with item" }, + { + "permissionName": "inventory-storage.bound-withs.collection.put", + "displayName": "inventory storage - modify a bound-withs", + "description": "replace a holdings-records of a bound-withs or move it to a different bound-withs" + }, { "permissionName": "inventory-storage.bound-with-parts.item.put", "displayName": "inventory storage - modify a bound-with part", @@ -2603,6 +2624,11 @@ "displayName": "inventory storage - get migration job by id", "description": "get migration job by id" }, + { + "permissionName": "inventory-storage.migration.job.collection.get", + "displayName": "inventory storage - get migration jobs", + "description": "get migration jobs" + }, { "permissionName": "inventory-storage.migration.item.get", "displayName": "inventory storage - get list of available migrations", @@ -2644,6 +2670,7 @@ "inventory-storage.items.batch.post", "inventory-storage.items.batch-unsafe.post", "inventory-storage.holdings.collection.get", + "inventory-storage.holdings.retrieve.collection.post", "inventory-storage.holdings.item.get", "inventory-storage.holdings.item.post", "inventory-storage.holdings.item.put", @@ -2652,6 +2679,7 @@ "inventory-storage.holdings.batch.post", "inventory-storage.holdings.batch-unsafe.post", "inventory-storage.instances.collection.get", + "inventory-storage.instances.retrieve.collection.post", "inventory-storage.instances.item.get", "inventory-storage.instances.item.post", "inventory-storage.instances.item.put", @@ -2663,6 +2691,7 @@ "inventory-storage.instances.source-record.marc-json.delete", "inventory-storage.instances.collection.delete", "inventory-storage.instances.batch.post", + "inventory-storage.instances.batch.synchronous.post", "inventory-storage.instances.batch-unsafe.post", "inventory-storage.instances.bulk.post", "inventory-storage.loan-types.collection.get", @@ -2850,6 +2879,7 @@ "inventory-storage.bound-with-parts.collection.get", "inventory-storage.bound-with-parts.item.get", "inventory-storage.bound-with-parts.item.post", + "inventory-storage.bound-withs.collection.put", "inventory-storage.bound-with-parts.item.put", "inventory-storage.bound-with-parts.item.delete", "inventory-storage.inventory-hierarchy.updated-instances-ids.collection.get", @@ -2867,6 +2897,7 @@ "inventory-storage.migration.job.item.delete", "inventory-storage.migration.job.post", "inventory-storage.migration.job.item.get", + "inventory-storage.migration.job.collection.get", "inventory-storage.migration.item.get", "inventory-storage.reindex-records.publish.post", "inventory-storage.instance-date-types.collection.get", @@ -2914,6 +2945,9 @@ { "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"}, + { "name": "KAFKA_REINDEX_RECORDS_TOPIC_MESSAGE_RETENTION", "value": "86400000"}, + { "name": "KAFKA_REINDEX_RECORDS_TOPIC_MAX_MESSAGE_SIZE", "value": "1048576"}, + { "name": "KAFKA_REINDEX_PRODUCER_MAX_REQUEST_SIZE_BYTES", "value": "10485760"}, { "name": "S3_URL", "value": "http://127.0.0.1:9000/" }, { "name": "S3_REGION", "value": "" }, { "name": "S3_BUCKET", "value": "marc-migrations" }, diff --git a/pom.xml b/pom.xml index 760dd49d9..fd73aa35d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ mod-inventory-storage org.folio - 27.2.0-SNAPSHOT + 28.0.0-SNAPSHOT UTF-8 @@ -12,22 +12,22 @@ /instance-storage/instances,/instance-storage/instances/retrieve,/holdings-storage/holdings,/holdings-storage/holdings/retrieve,/item-storage/items,/record-bulk/ids,/oai-pmh-view/instances,/oai-pmh-view/updatedInstanceIds,/oai-pmh-view/enrichedInstances,/inventory-hierarchy/updated-instance-ids,/inventory-hierarchy/items-and-holdings,/inventory-view/instances - 35.2.2 + 35.3.0 4.5.10 - 2.9.5 + 2.9.6 1.9.22.1 - 3.1.1 + 3.2.0-SNAPSHOT 3.1.8 1.18.34 - 1.1.10.6 + 1.1.10.7 3.17.0 - 2.24.0 + 2.24.1 2.2.0-SNAPSHOT - 6.0.2 - 5.11.0 - 1.20.1 - 2.12.7 + 6.0.3 + 5.11.2 + 1.20.2 + 2.13.0 3.0 4.13.2 5.2.0 @@ -47,11 +47,12 @@ 3.3.1 1.0.1 3.6.0 - 3.5.0 + 3.5.1 3.5.0 - 10.18.1 - 3.5.0 + 10.18.2 + 3.5.1 2.17.1 + 1.0.0 @@ -238,7 +239,6 @@ org.testcontainers localstack - ${localstack.version} test @@ -645,6 +645,19 @@ + + org.folio + folio-module-descriptor-validator + ${folio-module-descriptor-validator.version} + false + + + + validate + + + + diff --git a/ramls/instance-iteration.raml b/ramls/instance-iteration.raml index 08912246b..955d07576 100644 --- a/ramls/instance-iteration.raml +++ b/ramls/instance-iteration.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Iterate instances -version: v0.1 +version: v1.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost diff --git a/ramls/instance-set.raml b/ramls/instance-set.raml index 6dfa2f7d1..6d2adece2 100644 --- a/ramls/instance-set.raml +++ b/ramls/instance-set.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Instance Set API -version: v2.0 +version: v3.0 protocols: [ HTTP, HTTPS ] baseUri: http://github.com/org/folio/mod-inventory-storage diff --git a/ramls/instance-storage-batch.raml b/ramls/instance-storage-batch.raml index 5224ddc97..b28f3fc2a 100644 --- a/ramls/instance-storage-batch.raml +++ b/ramls/instance-storage-batch.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Deprecated Inventory Storage Instance Batch API -version: v2.0 +version: v3.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost diff --git a/ramls/instance-storage.raml b/ramls/instance-storage.raml index 968a24439..373a5785b 100644 --- a/ramls/instance-storage.raml +++ b/ramls/instance-storage.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Instance Storage -version: v10.0 +version: v11.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost diff --git a/ramls/instance-sync-unsafe.raml b/ramls/instance-sync-unsafe.raml index ec9b8fbdd..851719f1c 100644 --- a/ramls/instance-sync-unsafe.raml +++ b/ramls/instance-sync-unsafe.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Inventory Storage Instance Batch Sync Unsafe API -version: v2.0 +version: v3.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost diff --git a/ramls/instance-sync.raml b/ramls/instance-sync.raml index e6d98290e..f0a83a416 100644 --- a/ramls/instance-sync.raml +++ b/ramls/instance-sync.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Inventory Storage Instance Batch Sync API -version: v2.0 +version: v3.0 protocols: [ HTTP, HTTPS ] baseUri: http://localhost diff --git a/ramls/inventory-view.raml b/ramls/inventory-view.raml index b0c6a7a84..1d25d5268 100644 --- a/ramls/inventory-view.raml +++ b/ramls/inventory-view.raml @@ -1,6 +1,6 @@ #%RAML 1.0 title: Inventory Storage View API -version: v2.0 +version: v3.0 protocols: [ HTTP, HTTPS ] baseUri: http://github.com/org/folio/mod-inventory-storage diff --git a/ramls/raml-util b/ramls/raml-util index c113f109d..0b1259759 160000 --- a/ramls/raml-util +++ b/ramls/raml-util @@ -1 +1 @@ -Subproject commit c113f109d1379d15230f3f8a3485e61ffccc0ad8 +Subproject commit 0b125975982df99324c88c200d50abe2b22f7688 diff --git a/ramls/subject-source.json b/ramls/subject-source.json index dd6ad63c3..9b81b4efc 100644 --- a/ramls/subject-source.json +++ b/ramls/subject-source.json @@ -7,15 +7,20 @@ "type": "string" }, "name": { - "description": "label for the subject source", + "description": "label for the subject source name", + "type": "string" + }, + "code": { + "description": "label for the subject source code", "type": "string" }, "source": { "type": "string", - "description": "label indicating where the subject source entry originates from, i.e. 'folio' or 'local'", + "description": "label indicating where the subject source entry originates from, i.e. 'folio', 'local' or 'consortium'", "enum": [ "folio", - "local" + "local", + "consortium" ] }, "metadata": { @@ -25,6 +30,7 @@ } }, "required": [ - "name" + "name", + "source" ] } diff --git a/ramls/subject-type.json b/ramls/subject-type.json index b9808b1d6..975efbde1 100644 --- a/ramls/subject-type.json +++ b/ramls/subject-type.json @@ -12,10 +12,11 @@ }, "source": { "type": "string", - "description": "label indicating where the subject type entry originates from, i.e. 'folio' or 'local'", + "description": "label indicating where the subject type entry originates from, i.e. 'folio', 'local' or 'consortium'", "enum": [ "folio", - "local" + "local", + "consortium" ] }, "metadata": { @@ -25,6 +26,7 @@ } }, "required": [ - "name" + "name", + "source" ] } diff --git a/reference-data/instance-date-types/no-dates.json b/reference-data/instance-date-types/no-dates.json index 4d4616695..3393ee3e6 100644 --- a/reference-data/instance-date-types/no-dates.json +++ b/reference-data/instance-date-types/no-dates.json @@ -1,6 +1,6 @@ { "id": "77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8", - "name": "No dates given; BC date involved", + "name": "No dates given; B.C. date involved", "code": "b", "displayFormat": { "delimiter": ",", diff --git a/reference-data/subject-sources/CanadianSubjectHeadings.json b/reference-data/subject-sources/CanadianSubjectHeadings.json new file mode 100644 index 000000000..ecb51ac69 --- /dev/null +++ b/reference-data/subject-sources/CanadianSubjectHeadings.json @@ -0,0 +1,6 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d45", + "name": "Canadian Subject Headings", + "code" : "cash", + "source": "folio" +} diff --git a/reference-data/subject-sources/LibraryofCongressChildrensandYoungAdultsSubjectHeadings.json b/reference-data/subject-sources/LibraryofCongressChildrensandYoungAdultsSubjectHeadings.json new file mode 100644 index 000000000..e98fb7cb1 --- /dev/null +++ b/reference-data/subject-sources/LibraryofCongressChildrensandYoungAdultsSubjectHeadings.json @@ -0,0 +1,6 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d41", + "name": "Library of Congress Children's and Young Adults' Subject Headings", + "code" : "cyac", + "source": "folio" +} diff --git a/reference-data/subject-sources/LibraryofCongressSubjectHeadings.json b/reference-data/subject-sources/LibraryofCongressSubjectHeadings.json new file mode 100644 index 000000000..e1ce7a480 --- /dev/null +++ b/reference-data/subject-sources/LibraryofCongressSubjectHeadings.json @@ -0,0 +1,6 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d40", + "name": "Library of Congress Subject Headings", + "code" : "lcsh", + "source": "folio" +} diff --git a/reference-data/subject-sources/MedicalSubjectHeadings.json b/reference-data/subject-sources/MedicalSubjectHeadings.json new file mode 100644 index 000000000..7acd5d1d0 --- /dev/null +++ b/reference-data/subject-sources/MedicalSubjectHeadings.json @@ -0,0 +1,6 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d42", + "name": "Medical Subject Headings", + "code" : "mesh", + "source": "folio" +} diff --git a/reference-data/subject-sources/NationalAgriculturalLibrarysubjectauthorityfile.json b/reference-data/subject-sources/NationalAgriculturalLibrarysubjectauthorityfile.json new file mode 100644 index 000000000..c45c88fc4 --- /dev/null +++ b/reference-data/subject-sources/NationalAgriculturalLibrarysubjectauthorityfile.json @@ -0,0 +1,5 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d43", + "name": "National Agricultural Library subject authority file", + "source": "folio" +} diff --git a/reference-data/subject-sources/Repertoiredevedettes-matiere.json b/reference-data/subject-sources/Repertoiredevedettes-matiere.json new file mode 100644 index 000000000..92a96d2f8 --- /dev/null +++ b/reference-data/subject-sources/Repertoiredevedettes-matiere.json @@ -0,0 +1,6 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d46", + "name": "Répertoire de vedettes-matière", + "code" : "rvm", + "source": "folio" +} diff --git a/reference-data/subject-sources/Sourcenotspecified.json b/reference-data/subject-sources/Sourcenotspecified.json new file mode 100644 index 000000000..3cb528337 --- /dev/null +++ b/reference-data/subject-sources/Sourcenotspecified.json @@ -0,0 +1,5 @@ +{ + "id": "e894d0dc-621d-4b1d-98f6-6f7120eb0d44", + "name": "Source not specified", + "source": "folio" +} diff --git a/reference-data/subject-types/Chronologicalterm.json b/reference-data/subject-types/Chronologicalterm.json new file mode 100644 index 000000000..a8caea560 --- /dev/null +++ b/reference-data/subject-types/Chronologicalterm.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b6", + "name": "Chronological term", + "source": "folio" +} diff --git a/reference-data/subject-types/Corporatename.json b/reference-data/subject-types/Corporatename.json new file mode 100644 index 000000000..734ede198 --- /dev/null +++ b/reference-data/subject-types/Corporatename.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b2", + "name": "Corporate name", + "source": "folio" +} diff --git a/reference-data/subject-types/Curriculumobjective.json b/reference-data/subject-types/Curriculumobjective.json new file mode 100644 index 000000000..2d0bcefa8 --- /dev/null +++ b/reference-data/subject-types/Curriculumobjective.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff514", + "name": "Curriculum objective", + "source": "folio" +} diff --git a/reference-data/subject-types/Facetedtopicalterms.json b/reference-data/subject-types/Facetedtopicalterms.json new file mode 100644 index 000000000..f1176238a --- /dev/null +++ b/reference-data/subject-types/Facetedtopicalterms.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff510", + "name": "Faceted topical terms", + "source": "folio" +} diff --git a/reference-data/subject-types/Function.json b/reference-data/subject-types/Function.json new file mode 100644 index 000000000..c43c2e9e0 --- /dev/null +++ b/reference-data/subject-types/Function.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff513", + "name": "Function", + "source": "folio" +} diff --git a/reference-data/subject-types/Genreform.json b/reference-data/subject-types/Genreform.json new file mode 100644 index 000000000..1d0cfcce0 --- /dev/null +++ b/reference-data/subject-types/Genreform.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff511", + "name": "Genre/form", + "source": "folio" +} diff --git a/reference-data/subject-types/Geographicname.json b/reference-data/subject-types/Geographicname.json new file mode 100644 index 000000000..0a3067049 --- /dev/null +++ b/reference-data/subject-types/Geographicname.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b8", + "name": "Geographic name", + "source": "folio" +} diff --git a/reference-data/subject-types/Hierarchicalplacename.json b/reference-data/subject-types/Hierarchicalplacename.json new file mode 100644 index 000000000..46a4b5e6b --- /dev/null +++ b/reference-data/subject-types/Hierarchicalplacename.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff515", + "name": "Hierarchical place name", + "source": "folio" +} diff --git a/reference-data/subject-types/Meetingname.json b/reference-data/subject-types/Meetingname.json new file mode 100644 index 000000000..2849a1b25 --- /dev/null +++ b/reference-data/subject-types/Meetingname.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b3", + "name": "Meeting name", + "source": "folio" +} diff --git a/reference-data/subject-types/Namedevent.json b/reference-data/subject-types/Namedevent.json new file mode 100644 index 000000000..fbba7917b --- /dev/null +++ b/reference-data/subject-types/Namedevent.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b5", + "name": "Named event", + "source": "folio" +} diff --git a/reference-data/subject-types/Occupation.json b/reference-data/subject-types/Occupation.json new file mode 100644 index 000000000..b55be58b1 --- /dev/null +++ b/reference-data/subject-types/Occupation.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff512", + "name": "Occupation", + "source": "folio" +} diff --git a/reference-data/subject-types/Personalname.json b/reference-data/subject-types/Personalname.json new file mode 100644 index 000000000..78d941222 --- /dev/null +++ b/reference-data/subject-types/Personalname.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b1", + "name": "Personal name", + "source": "folio" +} diff --git a/reference-data/subject-types/Topicalterm.json b/reference-data/subject-types/Topicalterm.json new file mode 100644 index 000000000..dbeedd146 --- /dev/null +++ b/reference-data/subject-types/Topicalterm.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b7", + "name": "Topical term", + "source": "folio" +} diff --git a/reference-data/subject-types/Typeofentityunspecified.json b/reference-data/subject-types/Typeofentityunspecified.json new file mode 100644 index 000000000..6a2634b12 --- /dev/null +++ b/reference-data/subject-types/Typeofentityunspecified.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff516", + "name": "Type of entity unspecified", + "source": "folio" +} diff --git a/reference-data/subject-types/Uncontrolled.json b/reference-data/subject-types/Uncontrolled.json new file mode 100644 index 000000000..1644db7cb --- /dev/null +++ b/reference-data/subject-types/Uncontrolled.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b9", + "name": "Uncontrolled", + "source": "folio" +} diff --git a/reference-data/subject-types/Uniformtitle.json b/reference-data/subject-types/Uniformtitle.json new file mode 100644 index 000000000..d2ebdfd87 --- /dev/null +++ b/reference-data/subject-types/Uniformtitle.json @@ -0,0 +1,5 @@ +{ + "id": "d6488f88-1e74-40ce-81b5-b19a928ff5b4", + "name": "Uniform title", + "source": "folio" +} diff --git a/src/main/java/org/folio/InventoryKafkaTopic.java b/src/main/java/org/folio/InventoryKafkaTopic.java index f0125a5f1..b5e3e7281 100644 --- a/src/main/java/org/folio/InventoryKafkaTopic.java +++ b/src/main/java/org/folio/InventoryKafkaTopic.java @@ -44,6 +44,14 @@ public enum InventoryKafkaTopic implements KafkaTopic { INSTANCE_DATE_TYPE, Pair.of("KAFKA_SUBJECT_SOURCE_TOPIC_NUM_PARTITIONS", "1") ); + private static final Map> TOPIC_MESSAGE_RETENTION_MAP = Map.of( + REINDEX_RECORDS, Pair.of("KAFKA_REINDEX_RECORDS_TOPIC_MESSAGE_RETENTION", "86400000") // 1 day + ); + + private static final Map> TOPIC_MESSAGE_MAX_SIZE_MAP = Map.of( + REINDEX_RECORDS, Pair.of("KAFKA_REINDEX_RECORDS_TOPIC_MAX_MESSAGE_SIZE", "1048576") // 1 MB + ); + private final String topic; InventoryKafkaTopic(String topic) { @@ -63,8 +71,22 @@ public String topicName() { @Override public int numPartitions() { return Optional.ofNullable(TOPIC_PARTITION_MAP.get(this)) - .map(pair -> getNumberOfPartitions(pair.getKey(), pair.getValue())) - .orElse(getNumberOfPartitions(DEFAULT_NUM_PARTITIONS_PROPERTY, DEFAULT_NUM_PARTITIONS_VALUE)); + .map(pair -> getPropertyValue(pair.getKey(), pair.getValue())) + .orElse(getPropertyValue(DEFAULT_NUM_PARTITIONS_PROPERTY, DEFAULT_NUM_PARTITIONS_VALUE)); + } + + @Override + public Integer messageRetentionTime() { + return Optional.ofNullable(TOPIC_MESSAGE_RETENTION_MAP.get(this)) + .map(pair -> getPropertyValue(pair.getKey(), pair.getValue())) + .orElse(null); + } + + @Override + public Integer messageMaxSize() { + return Optional.ofNullable(TOPIC_MESSAGE_MAX_SIZE_MAP.get(this)) + .map(pair -> getPropertyValue(pair.getKey(), pair.getValue())) + .orElse(null); } public static InventoryKafkaTopic byTopic(String topic) { @@ -76,7 +98,7 @@ public static InventoryKafkaTopic byTopic(String topic) { throw new IllegalArgumentException("Unknown topic " + topic); } - private int getNumberOfPartitions(String propertyName, String defaultNumPartitions) { + private int getPropertyValue(String propertyName, String defaultNumPartitions) { return Integer.parseInt(StringUtils.firstNonBlank( System.getenv(propertyName), System.getProperty(propertyName), diff --git a/src/main/java/org/folio/persist/HoldingsRepository.java b/src/main/java/org/folio/persist/HoldingsRepository.java index 2845cf1d1..7d8e086d6 100644 --- a/src/main/java/org/folio/persist/HoldingsRepository.java +++ b/src/main/java/org/folio/persist/HoldingsRepository.java @@ -7,6 +7,8 @@ import io.vertx.core.Future; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import org.folio.cql2pgjson.CQL2PgJSON; import org.folio.rest.jaxrs.model.HoldingsRecord; @@ -32,4 +34,17 @@ public Future> delete(String cql) { return Future.failedFuture(e); } } + + public Future>> getReindexHoldingsRecords(String fromId, String toId) { + var sql = "SELECT jsonb FROM " + postgresClientFuturized.getFullTableName(HOLDINGS_RECORD_TABLE) + + " i WHERE id >= '" + fromId + "' AND id <= '" + toId + "'" + + ";"; + return postgresClient.select(sql).map(rows -> { + var resultList = new LinkedList>(); + for (var row : rows) { + resultList.add(row.getJsonObject(0).getMap()); + } + return resultList; + }); + } } diff --git a/src/main/java/org/folio/persist/ItemRepository.java b/src/main/java/org/folio/persist/ItemRepository.java index 767785060..a8c181b88 100644 --- a/src/main/java/org/folio/persist/ItemRepository.java +++ b/src/main/java/org/folio/persist/ItemRepository.java @@ -1,5 +1,6 @@ package org.folio.persist; +import static org.folio.rest.impl.HoldingsStorageApi.HOLDINGS_RECORD_TABLE; import static org.folio.rest.impl.ItemStorageApi.ITEM_TABLE; import static org.folio.rest.persist.PgUtil.postgresClient; @@ -8,6 +9,7 @@ import io.vertx.core.Future; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import org.folio.cql2pgjson.CQL2PgJSON; @@ -45,4 +47,20 @@ public Future> delete(String cql) { } } + public Future>> getReindexItemRecords(String fromId, String toId) { + var sql = "SELECT i.jsonb || jsonb_build_object('instanceId', hr.instanceId)" + + " FROM " + postgresClientFuturized.getFullTableName(ITEM_TABLE) + " i" + + " JOIN " + postgresClientFuturized.getFullTableName(HOLDINGS_RECORD_TABLE) + + " hr ON i.holdingsrecordid = hr.id" + + " WHERE i.id >= '" + fromId + "' AND i.id <= '" + toId + "';"; + + return postgresClient.select(sql).map(rows -> { + var resultList = new LinkedList>(); + for (var row : rows) { + resultList.add(row.getJsonObject(0).getMap()); + } + return resultList; + }); + } + } diff --git a/src/main/java/org/folio/rest/support/ResponseUtil.java b/src/main/java/org/folio/rest/support/ResponseUtil.java index 94ab61402..9cce7a1c4 100644 --- a/src/main/java/org/folio/rest/support/ResponseUtil.java +++ b/src/main/java/org/folio/rest/support/ResponseUtil.java @@ -10,8 +10,12 @@ 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"; + public static final String SOURCE_FOLIO_CANNOT_BE_UPDATED = + "Illegal operation: Source folio cannot be updated"; + public static final String SOURCE_CANNOT_BE_UPDATED_AT_NON_ECS = + "Illegal operation: Source field cannot be updated at non-consortium tenant"; + public static final String SOURCE_CONSORTIUM_CANNOT_BE_APPLIED = + "Illegal operation: Source consortium cannot be applied at non-consortium tenant"; private ResponseUtil() { } diff --git a/src/main/java/org/folio/rest/support/SubjectUtil.java b/src/main/java/org/folio/rest/support/SubjectUtil.java new file mode 100644 index 000000000..5c05bf5ad --- /dev/null +++ b/src/main/java/org/folio/rest/support/SubjectUtil.java @@ -0,0 +1,73 @@ +package org.folio.rest.support; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.jaxrs.resource.SubjectSources.PostSubjectSourcesResponse.respond422WithApplicationJson; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_FOLIO; +import static org.folio.rest.support.ResponseUtil.SOURCE_CANNOT_BE_UPDATED_AT_NON_ECS; +import static org.folio.rest.support.ResponseUtil.SOURCE_CONSORTIUM_CANNOT_BE_APPLIED; +import static org.folio.rest.support.ResponseUtil.SOURCE_FOLIO_CANNOT_BE_UPDATED; +import static org.folio.rest.tools.utils.ValidationHelper.createValidationErrorMessage; + +import io.vertx.core.Future; +import java.util.Map; +import java.util.Optional; +import javax.ws.rs.core.Response; +import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.SubjectSource; +import org.folio.services.consortium.ConsortiumService; + +public final class SubjectUtil { + private static final String SOURCE = "source"; + + private SubjectUtil() { + } + + public static Future> validateSubjectSourceCreate(String subjectSource, + ConsortiumService consortiumService, + Map okapiHeaders) { + if (SubjectSource.Source.FOLIO.value().equals(subjectSource)) { + return Future.succeededFuture(getValidationErrorMessage(subjectSource, SOURCE_CANNOT_BE_FOLIO)); + } + + if (SubjectSource.Source.CONSORTIUM.value().equals(subjectSource)) { + return consortiumService.getConsortiumData(okapiHeaders) + .map(consortiumDataOptional -> { + if (consortiumDataOptional.isEmpty()) { + return getValidationErrorMessage(subjectSource, SOURCE_CONSORTIUM_CANNOT_BE_APPLIED); + } + return Optional.empty(); + }); + } + return Future.succeededFuture(Optional.empty()); + } + + public static Future> validateSubjectSourceUpdate(String incomingSubjectSource, + String existingSubjectSource, + ConsortiumService consortiumService, + Map okapiHeaders) { + if (!existingSubjectSource.equals(incomingSubjectSource)) { + if (SubjectSource.Source.FOLIO.value().equals(incomingSubjectSource)) { + return Future.succeededFuture(getValidationErrorMessage(incomingSubjectSource, SOURCE_CANNOT_BE_FOLIO)); + } + if (SubjectSource.Source.FOLIO.value().equals(existingSubjectSource)) { + return Future.succeededFuture(getValidationErrorMessage(incomingSubjectSource, SOURCE_FOLIO_CANNOT_BE_UPDATED)); + } + return consortiumService.getConsortiumData(okapiHeaders) + .map(consortiumDataOptional -> { + if (consortiumDataOptional.isEmpty()) { + return getValidationErrorMessage(incomingSubjectSource, SOURCE_CANNOT_BE_UPDATED_AT_NON_ECS); + } + return Optional.empty(); + }); + } + return Future.succeededFuture(Optional.empty()); + } + + public static Future sourceValidationError(Errors errors) { + return succeededFuture(respond422WithApplicationJson(errors)); + } + + private static Optional getValidationErrorMessage(String subjectSource, String errorMessage) { + return Optional.of(createValidationErrorMessage(SOURCE, subjectSource, errorMessage)); + } +} diff --git a/src/main/java/org/folio/services/domainevent/CommonDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/CommonDomainEventPublisher.java index 6c24c070e..74842fda8 100644 --- a/src/main/java/org/folio/services/domainevent/CommonDomainEventPublisher.java +++ b/src/main/java/org/folio/services/domainevent/CommonDomainEventPublisher.java @@ -52,15 +52,21 @@ public class CommonDomainEventPublisher { public CommonDomainEventPublisher(Context vertxContext, Map okapiHeaders, String kafkaTopic) { + this(vertxContext, okapiHeaders, kafkaTopic, 0); + } + + public CommonDomainEventPublisher(Context vertxContext, Map okapiHeaders, + String kafkaTopic, int maxRequestSize) { - this(okapiHeaders, kafkaTopic, createProducerManager(vertxContext), + this(okapiHeaders, kafkaTopic, createProducerManager(vertxContext, maxRequestSize), new LogToDbFailureHandler(vertxContext, okapiHeaders)); } - private static KafkaProducerManager createProducerManager(Context vertxContext) { + private static KafkaProducerManager createProducerManager(Context vertxContext, int maxRequestSize) { var kafkaConfig = KafkaConfig.builder() .kafkaPort(KafkaEnvironmentProperties.port()) .kafkaHost(KafkaEnvironmentProperties.host()) + .maxRequestSize(maxRequestSize) .build(); return new SimpleKafkaProducerManager(vertxContext.owner(), kafkaConfig); diff --git a/src/main/java/org/folio/services/domainevent/HoldingDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/HoldingDomainEventPublisher.java index 6d45f2f64..b2eb58c65 100644 --- a/src/main/java/org/folio/services/domainevent/HoldingDomainEventPublisher.java +++ b/src/main/java/org/folio/services/domainevent/HoldingDomainEventPublisher.java @@ -2,6 +2,7 @@ import static io.vertx.core.Future.succeededFuture; import static org.folio.InventoryKafkaTopic.HOLDINGS_RECORD; +import static org.folio.InventoryKafkaTopic.REINDEX_RECORDS; import static org.folio.rest.tools.utils.TenantTool.tenantId; import io.vertx.core.Context; @@ -18,18 +19,22 @@ public class HoldingDomainEventPublisher extends AbstractDomainEventPublisher { + private final CommonDomainEventPublisher> holdingsReindexPublisher; + public HoldingDomainEventPublisher(Context context, Map okapiHeaders) { super(new HoldingsRepository(context, okapiHeaders), new CommonDomainEventPublisher<>(context, okapiHeaders, HOLDINGS_RECORD.fullTopicName(tenantId(okapiHeaders)))); + holdingsReindexPublisher = new CommonDomainEventPublisher<>(context, okapiHeaders, + REINDEX_RECORDS.fullTopicName(tenantId(okapiHeaders))); } - public Future publishReindexHoldings(String key, List holdings) { + public Future publishReindexHoldings(String key, List> holdings) { if (StringUtils.isBlank(key)) { return succeededFuture(); } - return domainEventService.publishReindexRecords(key, PublishReindexRecords.RecordType.HOLDINGS, holdings); + return holdingsReindexPublisher.publishReindexRecords(key, PublishReindexRecords.RecordType.HOLDINGS, holdings); } @Override diff --git a/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java index 8b83d8ad5..0d9072903 100644 --- a/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java +++ b/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java @@ -19,6 +19,8 @@ import org.folio.rest.jaxrs.model.PublishReindexRecords; public class InstanceDomainEventPublisher extends AbstractDomainEventPublisher { + + private static final String MAX_REQUEST_SIZE = "KAFKA_REINDEX_PRODUCER_MAX_REQUEST_SIZE_BYTES"; private static final Logger log = getLogger(InstanceDomainEventPublisher.class); private final CommonDomainEventPublisher> instanceReindexPublisher; @@ -28,7 +30,7 @@ public InstanceDomainEventPublisher(Context context, Map okapiHe new CommonDomainEventPublisher<>(context, okapiHeaders, INSTANCE.fullTopicName(tenantId(okapiHeaders)))); instanceReindexPublisher = new CommonDomainEventPublisher<>(context, okapiHeaders, - REINDEX_RECORDS.fullTopicName(tenantId(okapiHeaders))); + REINDEX_RECORDS.fullTopicName(tenantId(okapiHeaders)), getProducerMaxRequestSize()); } public Future publishReindexInstances(String key, List> instances) { @@ -68,4 +70,11 @@ protected Instance convertDomainToEvent(String instanceId, Instance domain) { protected String getId(Instance instance) { return instance.getId(); } + + private int getProducerMaxRequestSize() { + return Integer.parseInt(StringUtils.firstNonBlank( + System.getenv(MAX_REQUEST_SIZE), + System.getProperty(MAX_REQUEST_SIZE), + "10485760")); // 10MB + } } diff --git a/src/main/java/org/folio/services/domainevent/ItemDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/ItemDomainEventPublisher.java index 11eef4a27..bef650f9b 100644 --- a/src/main/java/org/folio/services/domainevent/ItemDomainEventPublisher.java +++ b/src/main/java/org/folio/services/domainevent/ItemDomainEventPublisher.java @@ -3,6 +3,7 @@ import static io.vertx.core.Future.succeededFuture; import static org.apache.logging.log4j.LogManager.getLogger; import static org.folio.InventoryKafkaTopic.ITEM; +import static org.folio.InventoryKafkaTopic.REINDEX_RECORDS; import static org.folio.rest.tools.utils.TenantTool.tenantId; import io.vertx.core.Context; @@ -24,6 +25,7 @@ public class ItemDomainEventPublisher extends AbstractDomainEventPublisher> itemReindexPublisher; public ItemDomainEventPublisher(Context context, Map okapiHeaders) { super(new ItemRepository(context, okapiHeaders), @@ -31,6 +33,8 @@ public ItemDomainEventPublisher(Context context, Map okapiHeader ITEM.fullTopicName(tenantId(okapiHeaders)))); holdingsRepository = new HoldingsRepository(context, okapiHeaders); + itemReindexPublisher = new CommonDomainEventPublisher<>(context, okapiHeaders, + REINDEX_RECORDS.fullTopicName(tenantId(okapiHeaders))); } public Future publishUpdated(Item newItem, Item oldItem, HoldingsRecord newHoldings, @@ -54,16 +58,12 @@ public Future publishUpdated(HoldingsRecord oldHoldings, HoldingsRecord ne .compose(domainEventService::publishRecordsUpdated); } - public Future publishReindexItems(String key, List items) { + public Future publishReindexItems(String key, List> items) { if (StringUtils.isBlank(key)) { return succeededFuture(); } - var itemsWithInstance = items.stream() - .map(item -> new ItemWithInstanceId(item, null)) - .toList(); - - return domainEventService.publishReindexRecords(key, PublishReindexRecords.RecordType.ITEM, itemsWithInstance); + return itemReindexPublisher.publishReindexRecords(key, PublishReindexRecords.RecordType.ITEM, items); } @Override diff --git a/src/main/java/org/folio/services/holding/HoldingsService.java b/src/main/java/org/folio/services/holding/HoldingsService.java index 6beddc4d1..fe718c970 100644 --- a/src/main/java/org/folio/services/holding/HoldingsService.java +++ b/src/main/java/org/folio/services/holding/HoldingsService.java @@ -33,8 +33,6 @@ import org.folio.persist.InstanceRepository; import org.folio.rest.jaxrs.model.HoldingsRecord; import org.folio.rest.jaxrs.model.Item; -import org.folio.rest.persist.Criteria.Criteria; -import org.folio.rest.persist.Criteria.Criterion; import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.SQLConnection; import org.folio.rest.support.CqlQuery; @@ -177,15 +175,8 @@ public Future createHoldings(List holdings, boolean up .map(ResponseHandlerUtil::handleHridError); } - public Future publishReindexHoldingsRecords(String rangeId, String idStart, String idEnd) { - var criteriaFrom = new Criteria().setJSONB(false) - .addField("id").setOperation(">=").setVal(idStart); - var criteriaTo = new Criteria().setJSONB(false) - .addField("id").setOperation("<=").setVal(idEnd); - final Criterion criterion = new Criterion(criteriaFrom) - .addCriterion(criteriaTo); - - return holdingsRepository.get(criterion) + public Future publishReindexHoldingsRecords(String rangeId, String fromId, String toId) { + return holdingsRepository.getReindexHoldingsRecords(fromId, toId) .compose(holdings -> domainEventPublisher.publishReindexHoldings(rangeId, holdings)); } diff --git a/src/main/java/org/folio/services/item/ItemService.java b/src/main/java/org/folio/services/item/ItemService.java index 75013c415..d6ac207ee 100644 --- a/src/main/java/org/folio/services/item/ItemService.java +++ b/src/main/java/org/folio/services/item/ItemService.java @@ -49,8 +49,6 @@ import org.folio.rest.jaxrs.model.HoldingsRecord; import org.folio.rest.jaxrs.model.Item; import org.folio.rest.jaxrs.model.Metadata; -import org.folio.rest.persist.Criteria.Criteria; -import org.folio.rest.persist.Criteria.Criterion; import org.folio.rest.persist.PgExceptionUtil; import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.PostgresClientFuturized; @@ -240,15 +238,8 @@ public Future> updateItemsOnHoldingChanged(AsyncResult .map(items)); } - public Future publishReindexItemRecords(String rangeId, String idStart, String idEnd) { - var criteriaFrom = new Criteria().setJSONB(false) - .addField("id").setOperation(">=").setVal(idStart); - var criteriaTo = new Criteria().setJSONB(false) - .addField("id").setOperation("<=").setVal(idEnd); - final Criterion criterion = new Criterion(criteriaFrom) - .addCriterion(criteriaTo); - - return itemRepository.get(criterion) + public Future publishReindexItemRecords(String rangeId, String fromId, String toId) { + return itemRepository.getReindexItemRecords(fromId, toId) .compose(items -> domainEventService.publishReindexItems(rangeId, items)); } diff --git a/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java b/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java index 054967ade..a7a73d7dc 100644 --- a/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java +++ b/src/main/java/org/folio/services/subjectsource/SubjectSourceService.java @@ -1,28 +1,30 @@ 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 static org.folio.rest.support.SubjectUtil.sourceValidationError; +import static org.folio.rest.support.SubjectUtil.validateSubjectSourceCreate; +import static org.folio.rest.support.SubjectUtil.validateSubjectSourceUpdate; 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.exceptions.NotFoundException; 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.caches.ConsortiumDataCache; +import org.folio.services.consortium.ConsortiumService; +import org.folio.services.consortium.ConsortiumServiceImpl; import org.folio.services.domainevent.SubjectSourceDomainEventPublisher; public class SubjectSourceService { @@ -33,12 +35,15 @@ public class SubjectSourceService { private final Map okapiHeaders; private final SubjectSourceRepository repository; private final SubjectSourceDomainEventPublisher domainEventService; + private final ConsortiumService consortiumService; public SubjectSourceService(Context context, Map okapiHeaders) { this.context = context; this.okapiHeaders = okapiHeaders; this.repository = new SubjectSourceRepository(context, okapiHeaders); this.domainEventService = new SubjectSourceDomainEventPublisher(context, okapiHeaders); + this.consortiumService = new ConsortiumServiceImpl(context.owner().createHttpClient(), + context.get(ConsortiumDataCache.class.getName())); } public Future getByQuery(String cql, int offset, int limit) { @@ -52,22 +57,25 @@ public Future getById(String id) { } 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()); + return validateSubjectSourceCreate(subjectSource.getSource().value(), consortiumService, okapiHeaders) + .compose(errorsOptional -> errorsOptional.isPresent() ? sourceValidationError(errorsOptional.get()) : + createSubjectSource(subjectSource)); } public Future update(String id, SubjectSource subjectSource) { + if (subjectSource.getId() == null) { + subjectSource.setId(id); + } + return repository.getById(id) .compose(oldSubjectSource -> { - if (!oldSubjectSource.getSource().equals(subjectSource.getSource())) { - return sourceValidationError(subjectSource.getSource().value(), SOURCE_CANNOT_BE_UPDATED); + if (oldSubjectSource != null) { + return validateSubjectSourceUpdate(subjectSource.getSource().value(), oldSubjectSource.getSource().value(), + consortiumService, okapiHeaders) + .compose(errorsOptional -> errorsOptional.isPresent() + ? sourceValidationError(errorsOptional.get()) : updateSubjectSource(id, subjectSource)); } - return put(SUBJECT_SOURCE, subjectSource, id, okapiHeaders, context, - PutSubjectSourcesBySubjectSourceIdResponse.class) - .onSuccess(domainEventService.publishUpdated(subjectSource)); + return Future.failedFuture(new NotFoundException("SubjectSource was not found")); }); } @@ -79,10 +87,14 @@ public Future delete(String id) { ); } - private Future sourceValidationError(String field, String message) { - return succeededFuture( - respond422WithApplicationJson( - createValidationErrorMessage("source", field, - message))); + private Future createSubjectSource(SubjectSource subjectSource) { + return post(SUBJECT_SOURCE, subjectSource, okapiHeaders, context, PostSubjectSourcesResponse.class) + .onSuccess(domainEventService.publishCreated()); + } + + private Future updateSubjectSource(String id, SubjectSource subjectSource) { + return put(SUBJECT_SOURCE, subjectSource, id, okapiHeaders, context, + PutSubjectSourcesBySubjectSourceIdResponse.class) + .onSuccess(domainEventService.publishUpdated(subjectSource)); } } diff --git a/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java b/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java index 78895d341..f58ff490a 100644 --- a/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java +++ b/src/main/java/org/folio/services/subjecttype/SubjectTypeService.java @@ -1,20 +1,19 @@ package org.folio.services.subjecttype; -import static io.vertx.core.Future.succeededFuture; -import static org.folio.rest.jaxrs.resource.SubjectTypes.PostSubjectTypesResponse.respond422WithApplicationJson; 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 static org.folio.rest.support.SubjectUtil.sourceValidationError; +import static org.folio.rest.support.SubjectUtil.validateSubjectSourceCreate; +import static org.folio.rest.support.SubjectUtil.validateSubjectSourceUpdate; import io.vertx.core.Context; import io.vertx.core.Future; import java.util.Map; import javax.ws.rs.core.Response; import org.folio.persist.SubjectTypeRepository; +import org.folio.rest.exceptions.NotFoundException; import org.folio.rest.jaxrs.model.SubjectType; import org.folio.rest.jaxrs.model.SubjectTypes; import org.folio.rest.jaxrs.resource.SubjectTypes.DeleteSubjectTypesBySubjectTypeIdResponse; @@ -23,22 +22,27 @@ import org.folio.rest.jaxrs.resource.SubjectTypes.PostSubjectTypesResponse; import org.folio.rest.jaxrs.resource.SubjectTypes.PutSubjectTypesBySubjectTypeIdResponse; import org.folio.rest.persist.PgUtil; +import org.folio.services.caches.ConsortiumDataCache; +import org.folio.services.consortium.ConsortiumService; +import org.folio.services.consortium.ConsortiumServiceImpl; import org.folio.services.domainevent.SubjectTypeDomainEventPublisher; public class SubjectTypeService { - public static final String SUBJECT_TYPE = "subject_type"; private final Context context; private final Map okapiHeaders; private final SubjectTypeRepository repository; private final SubjectTypeDomainEventPublisher domainEventService; + private final ConsortiumService consortiumService; public SubjectTypeService(Context context, Map okapiHeaders) { this.context = context; this.okapiHeaders = okapiHeaders; this.repository = new SubjectTypeRepository(context, okapiHeaders); this.domainEventService = new SubjectTypeDomainEventPublisher(context, okapiHeaders); + this.consortiumService = new ConsortiumServiceImpl(context.owner().createHttpClient(), + context.get(ConsortiumDataCache.class.getName())); } public Future getByQuery(String cql, int offset, int limit) { @@ -52,21 +56,25 @@ public Future getById(String id) { } public Future create(SubjectType subjectType) { - if (subjectType.getSource().equals(SubjectType.Source.FOLIO)) { - return sourceValidationError(subjectType.getSource().value(), SOURCE_CANNOT_BE_FOLIO); - } - return post(SUBJECT_TYPE, subjectType, okapiHeaders, context, PostSubjectTypesResponse.class) - .onSuccess(domainEventService.publishCreated()); + return validateSubjectSourceCreate(subjectType.getSource().value(), consortiumService, okapiHeaders) + .compose(errorsOptional -> errorsOptional.isPresent() ? sourceValidationError(errorsOptional.get()) : + createSubjectType(subjectType)); } public Future update(String id, SubjectType subjectType) { + if (subjectType.getId() == null) { + subjectType.setId(id); + } + return repository.getById(id) .compose(oldSubjectType -> { - if (!oldSubjectType.getSource().equals(subjectType.getSource())) { - return sourceValidationError(subjectType.getSource().value(), SOURCE_CANNOT_BE_UPDATED); + if (oldSubjectType != null) { + return validateSubjectSourceUpdate(subjectType.getSource().value(), oldSubjectType.getSource().value(), + consortiumService, okapiHeaders) + .compose(errorsOptional -> errorsOptional.isPresent() + ? sourceValidationError(errorsOptional.get()) : updateSubjectType(id, subjectType)); } - return put(SUBJECT_TYPE, subjectType, id, okapiHeaders, context, PutSubjectTypesBySubjectTypeIdResponse.class) - .onSuccess(domainEventService.publishUpdated(subjectType)); + return Future.failedFuture(new NotFoundException("SubjectType was not found")); }); } @@ -78,10 +86,13 @@ public Future delete(String id) { ); } - private Future sourceValidationError(String field, String message) { - return succeededFuture( - respond422WithApplicationJson( - createValidationErrorMessage("source", field, - message))); + private Future createSubjectType(SubjectType subjectType) { + return post(SUBJECT_TYPE, subjectType, okapiHeaders, context, PostSubjectTypesResponse.class) + .onSuccess(domainEventService.publishCreated()); + } + + private Future updateSubjectType(String id, SubjectType subjectType) { + return put(SUBJECT_TYPE, subjectType, id, okapiHeaders, context, PutSubjectTypesBySubjectTypeIdResponse.class) + .onSuccess(domainEventService.publishUpdated(subjectType)); } } diff --git a/src/main/resources/templates/db_scripts/addInstanceDateTypes.sql b/src/main/resources/templates/db_scripts/addInstanceDateTypes.sql index d9b0d0055..4031ec0d7 100644 --- a/src/main/resources/templates/db_scripts/addInstanceDateTypes.sql +++ b/src/main/resources/templates/db_scripts/addInstanceDateTypes.sql @@ -9,7 +9,7 @@ VALUES ('6de732c5-c29b-4a10-9db0-0729ca960f12', '{"id":"6de732c5-c29b-4a10-9db0-0729ca960f12","name":"Inclusive dates of collection","code":"i","displayFormat":{"delimiter":"-","keepDelimiter":true},"source":"folio"}'), ('8fa6d067-41ff-4362-96a0-96b16ddce267', '{"id":"8fa6d067-41ff-4362-96a0-96b16ddce267","name":"Multiple dates","code":"m","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), ('6f8cd9a8-26ac-4df6-8709-62fe2c0d04f8', '{"id":"6f8cd9a8-26ac-4df6-8709-62fe2c0d04f8","name":"No attempt to code","code":"|","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), - ('77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8', '{"id":"77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8","name":"No dates given; BC date involved","code":"b","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), + ('77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8', '{"id":"77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8","name":"No dates given; B.C. date involved","code":"b","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), ('3a4296bf-504b-451b-9355-5806f8d88253', '{"id":"3a4296bf-504b-451b-9355-5806f8d88253","name":"Publication date and copyright date","code":"t","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), ('4afa7d3d-e6f5-4134-9ab5-32ad377d2432', '{"id":"4afa7d3d-e6f5-4134-9ab5-32ad377d2432","name":"Questionable date","code":"q","displayFormat":{"delimiter":",","keepDelimiter":false},"source":"folio"}'), ('ccc293d5-9e88-4222-ac04-d058351ddb7b', '{"id":"ccc293d5-9e88-4222-ac04-d058351ddb7b","name":"Range of years of bulk of collection","code":"k","displayFormat":{"delimiter":"-","keepDelimiter":true},"source":"folio"}'), diff --git a/src/main/resources/templates/db_scripts/addSubjectSources.sql b/src/main/resources/templates/db_scripts/addSubjectSources.sql index f571ba0e6..a81f1a461 100644 --- a/src/main/resources/templates/db_scripts/addSubjectSources.sql +++ b/src/main/resources/templates/db_scripts/addSubjectSources.sql @@ -1,38 +1,33 @@ 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')) + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d40', 'name', 'Library of Congress Subject Headings', 'code', 'lcsh', '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')) + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d41', 'name', 'Library of Congress Children''s and Young Adults'' Subject Headings', 'code', 'cyac', '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')) + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d42', 'name', 'Medical Subject Headings', 'code', 'mesh', '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')) + 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')) + 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')) + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d45', 'name', 'Canadian Subject Headings', 'code', 'cash', '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')) + json_build_object('id','e894d0dc-621d-4b1d-98f6-6f7120eb0d46', 'name', 'Répertoire de vedettes-matière', 'code', 'rvm', 'source', 'folio')) ON CONFLICT DO NOTHING; diff --git a/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql new file mode 100644 index 000000000..582795944 --- /dev/null +++ b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql @@ -0,0 +1,129 @@ +CREATE OR REPLACE FUNCTION ${myuniversity}_${mymodule}.migrate_publication_period( + jsonb_data jsonb +) RETURNS jsonb AS $$ +DECLARE + pub_period jsonb; + start_date text; + end_date text; + date_type_id text; + dates jsonb := '{}'::jsonb; +BEGIN + pub_period := jsonb_data -> 'publicationPeriod'; + start_date := NULLIF(pub_period ->> 'start', ''); + end_date := NULLIF(pub_period ->> 'end', ''); + + -- Determine Date type + IF (start_date IS NOT NULL AND end_date IS NOT NULL) THEN + date_type_id := '8fa6d067-41ff-4362-96a0-96b16ddce267'; + ELSIF (start_date IS NOT NULL OR end_date IS NOT NULL) THEN + date_type_id := '24a506e8-2a92-4ecc-bd09-ff849321fd5a'; + ELSE + -- Remove publicationPeriod from jsonb + jsonb_data := jsonb_data - 'publicationPeriod'; + RETURN jsonb_data; + END IF; + + -- Build the JSONB Dates object + IF start_date IS NOT NULL THEN + dates := jsonb_set(dates, '{date1}', to_jsonb(substring(start_date FROM 1 FOR 4)), true); + END IF; + IF end_date IS NOT NULL THEN + dates := jsonb_set(dates, '{date2}', to_jsonb(substring(end_date FROM 1 FOR 4)), true); + END IF; + dates := jsonb_set(dates, '{dateTypeId}', to_jsonb(date_type_id), true); + + -- Set the dates into jsonb + jsonb_data := jsonb_set(jsonb_data, '{dates}', dates, true); + + -- Remove publicationPeriod from jsonb + jsonb_data := jsonb_data - 'publicationPeriod'; + + RETURN jsonb_data; +END; +$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE STRICT; + + +-- Migration Script +DO +$$ +DECLARE + trigger VARCHAR; + triggers VARCHAR[] DEFAULT ARRAY[ + 'audit_instance', + 'check_subject_references_on_insert_or_update', + 'instance_check_statistical_code_references_on_insert', + 'instance_check_statistical_code_references_on_update', + 'set_id_in_jsonb', + 'set_instance_md_json_trigger', + 'set_instance_md_trigger', + 'set_instance_ol_version_trigger', + 'set_instance_sourcerecordformat', + 'set_instance_status_updated_date', + 'update_instance_references', + 'updatecompleteupdateddate_instance']; + arr UUID[] DEFAULT ARRAY[ + '10000000-0000-0000-0000-000000000000', + '20000000-0000-0000-0000-000000000000', + '30000000-0000-0000-0000-000000000000', + '40000000-0000-0000-0000-000000000000', + '50000000-0000-0000-0000-000000000000', + '60000000-0000-0000-0000-000000000000', + '70000000-0000-0000-0000-000000000000', + '80000000-0000-0000-0000-000000000000', + '90000000-0000-0000-0000-000000000000', + 'a0000000-0000-0000-0000-000000000000', + 'b0000000-0000-0000-0000-000000000000', + 'c0000000-0000-0000-0000-000000000000', + 'd0000000-0000-0000-0000-000000000000', + 'e0000000-0000-0000-0000-000000000000', + 'f0000000-0000-0000-0000-000000000000', + 'ffffffff-ffff-ffff-ffff-ffffffffffff' + ]; + lower UUID; + cur UUID; + rowcount BIGINT; + need_migration BOOLEAN; +BEGIN + -- STEP 0: Check if migration is required + SELECT EXISTS ( + SELECT 1 + FROM ${myuniversity}_${mymodule}.instance + WHERE jsonb ? 'publicationPeriod' + LIMIT 1 + ) INTO need_migration; + + IF need_migration THEN + -- STEP 1: Disable triggers + FOREACH trigger IN ARRAY triggers LOOP + EXECUTE 'ALTER TABLE ${myuniversity}_${mymodule}.instance DISABLE TRIGGER ' + || trigger; + END LOOP; + + -- STEP 2: Do updates + lower := '00000000-0000-0000-0000-000000000000'; + FOREACH cur IN ARRAY arr LOOP + RAISE INFO 'range: % - %', lower, cur; + -- Update scripts + EXECUTE format($q$ + UPDATE ${myuniversity}_${mymodule}.instance + SET jsonb = ${myuniversity}_${mymodule}.migrate_publication_period(jsonb) + WHERE (jsonb -> 'publicationPeriod' IS NOT NULL) + AND (id > %L AND id <= %L); + $q$, lower, cur); + + GET DIAGNOSTICS rowcount = ROW_COUNT; + RAISE INFO 'updated % instances', rowcount; + + lower := cur; + END LOOP; + + -- STEP 3: Enable triggers + FOREACH trigger IN ARRAY triggers LOOP + EXECUTE 'ALTER TABLE ${myuniversity}_${mymodule}.instance ENABLE TRIGGER ' + || trigger; + END LOOP; + END IF; +END; +$$ LANGUAGE 'plpgsql'; + +DROP FUNCTION IF EXISTS ${myuniversity}_${mymodule}.migrate_publication_period(jsonb); diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index ef46f769a..e3cd3a41b 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -914,6 +914,10 @@ { "fieldName": "name", "tOps": "ADD" + }, + { + "fieldName": "code", + "tOps": "ADD" } ] } diff --git a/src/test/java/org/folio/rest/InstallUpgradeIT.java b/src/test/java/org/folio/rest/InstallUpgradeIT.java index 5fe57661b..4897dfbfe 100644 --- a/src/test/java/org/folio/rest/InstallUpgradeIT.java +++ b/src/test/java/org/folio/rest/InstallUpgradeIT.java @@ -65,7 +65,7 @@ public class InstallUpgradeIT { @ClassRule(order = 1) public static final PostgreSQLContainer POSTGRES = - new PostgreSQLContainer<>("postgres:12-alpine") + new PostgreSQLContainer<>("postgres:16-alpine") .withClasspathResourceMapping("lotus-23.0.0.sql", "/lotus-23.0.0.sql", BindMode.READ_ONLY) .withNetwork(NETWORK) .withNetworkAliases("mypostgres") diff --git a/src/test/java/org/folio/rest/api/HoldingsStorageTest.java b/src/test/java/org/folio/rest/api/HoldingsStorageTest.java index 5a18cb152..1f5022d83 100644 --- a/src/test/java/org/folio/rest/api/HoldingsStorageTest.java +++ b/src/test/java/org/folio/rest/api/HoldingsStorageTest.java @@ -161,7 +161,7 @@ public void beforeEach() { WireMock.reset(); mockUserTenantsForNonConsortiumMember(); - mockUserTenantsForConsortiumMember(); + mockUserTenantsForConsortiumMember(CONSORTIUM_MEMBER_TENANT); mockUserTenantsForTenantWithoutPermissions(); } @@ -3233,8 +3233,8 @@ private static void prepareThreeHoldingSource() { .put("id", sourceId) .put("name", "holding source name for " + sourceId)); holdingsSourceClient.create(new JsonObject() - .put("id", sourceId) - .put("name", "holding source name for " + sourceId), + .put("id", sourceId) + .put("name", "holding source name for " + sourceId), CONSORTIUM_MEMBER_TENANT); } } @@ -3416,17 +3416,6 @@ private List getTags(JsonObject json) { .toList(); } - private void mockUserTenantsForConsortiumMember() { - JsonObject userTenantsCollection = new JsonObject() - .put("userTenants", new JsonArray() - .add(new JsonObject() - .put("centralTenantId", "CENTRAL_TENANT_ID") - .put("consortiumId", "mobius"))); - WireMock.stubFor(WireMock.get(USER_TENANTS_PATH) - .withHeader(X_OKAPI_TENANT, equalToIgnoreCase(CONSORTIUM_MEMBER_TENANT)) - .willReturn(WireMock.ok().withBody(userTenantsCollection.encodePrettily()))); - } - private void mockUserTenantsForTenantWithoutPermissions() { WireMock.stubFor(WireMock.get(USER_TENANTS_PATH) .withHeader(X_OKAPI_TENANT, equalToIgnoreCase(TENANT_WITHOUT_USER_TENANTS_PERMISSIONS)) diff --git a/src/test/java/org/folio/rest/api/InstanceStorageInstancesBulkApiTest.java b/src/test/java/org/folio/rest/api/InstanceStorageInstancesBulkApiTest.java index 80016be75..0a8ef90fd 100644 --- a/src/test/java/org/folio/rest/api/InstanceStorageInstancesBulkApiTest.java +++ b/src/test/java/org/folio/rest/api/InstanceStorageInstancesBulkApiTest.java @@ -79,7 +79,7 @@ public class InstanceStorageInstancesBulkApiTest extends TestBaseWithInventoryUt @BeforeClass public static void setUpClass() { - localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:0.11.3")) + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) .withServices(S3); localStackContainer.start(); diff --git a/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java new file mode 100644 index 000000000..f66d4b087 --- /dev/null +++ b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java @@ -0,0 +1,145 @@ +package org.folio.rest.api; + +import static org.folio.rest.support.http.InterfaceUrls.instancesStorageUrl; +import static org.folio.utility.ModuleUtility.getVertx; +import static org.folio.utility.RestUtility.TENANT_ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import lombok.SneakyThrows; +import org.folio.rest.persist.PostgresClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Disabled; + +@Disabled +public class PublicationPeriodMigrationTest extends MigrationTestBase { + private static final String MIGRATION_SCRIPT = loadScript("publication-period/migratePublicationPeriod.sql"); + private static final String TAG_VALUE = "test-tag"; + private static final String START_DATE = "1877"; + private static final String END_DATE = "1880"; + private static final String MULTIPLE_DATE_TYPE_ID = "8fa6d067-41ff-4362-96a0-96b16ddce267"; + private static final String SINGLE_DATE_TYPE_ID = "24a506e8-2a92-4ecc-bd09-ff849321fd5a"; + private static final String SELECT_JSONB_BY_ID = + "SELECT jsonb FROM %s_mod_inventory_storage.instance WHERE id = $1"; + private static final String UPDATE_JSONB_WITH_PUB_PERIOD = """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', $1, 'end', $2)) + WHERE id = $3 + """; + private static final String UPDATE_JSONB_WITH_PUB_PERIOD_START_DATE = """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', $1)) + WHERE id = $2 + """; + + @SneakyThrows + @Before + public void beforeEach() { + StorageTestSuite.deleteAll(instancesStorageUrl("")); + removeAllEvents(); + } + + @Test + public void canMigratePublicationPeriodToMultipleDates() throws Exception { + var instanceId = createInstance(); + + // add "publicationPeriod" object to jsonb + addPublicationPeriodToJsonb(instanceId, END_DATE); + + //migrate "publicationPeriod" to Dates object + executeMultipleSqlStatements(MIGRATION_SCRIPT); + + var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); + RowSet result = runSql(query, Tuple.of(instanceId)); + + assertEquals(1, result.rowCount()); + JsonObject entry = result.iterator().next().toJson(); + JsonObject dates = entry.getJsonObject("jsonb").getJsonObject("dates"); + assertNotNull(dates); + assertEquals(START_DATE, dates.getString("date1")); + assertEquals(END_DATE, dates.getString("date2")); + assertEquals(MULTIPLE_DATE_TYPE_ID, dates.getString("dateTypeId")); + } + + @Test + public void canMigratePublicationPeriodToSingleDates() throws Exception { + var instanceId = createInstance(); + + // add "publicationPeriod" object to jsonb + addPublicationPeriodToJsonb(instanceId, null); + + //migrate "publicationPeriod" to Dates object + executeMultipleSqlStatements(MIGRATION_SCRIPT); + + var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); + RowSet result = runSql(query, Tuple.of(instanceId)); + + assertEquals(1, result.rowCount()); + JsonObject entry = result.iterator().next().toJson(); + JsonObject dates = entry.getJsonObject("jsonb").getJsonObject("dates"); + assertNotNull(dates); + assertEquals(START_DATE, dates.getString("date1")); + assertNull(dates.getString("date2")); + assertEquals(SINGLE_DATE_TYPE_ID, dates.getString("dateTypeId")); + } + + @Test + public void canNotMigrateWhenPublicationPeriodIsNull() throws Exception { + var instanceId = createInstance(); + + //migrate "publicationPeriod" to Dates object + executeMultipleSqlStatements(MIGRATION_SCRIPT); + + var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); + RowSet result = runSql(query, Tuple.of(instanceId)); + + assertEquals(1, result.rowCount()); + JsonObject entry = result.iterator().next().toJson(); + JsonObject dates = entry.getJsonObject("jsonb").getJsonObject("dates"); + assertNull(dates); + } + + private String createInstance() { + var instanceId = UUID.randomUUID().toString(); + JsonObject instanceToCreate = new JsonObject() + .put("id", instanceId) + .put("title", "Test") + .put("source", "FOLIO") + .put("identifiers", new JsonArray().add(identifier(UUID_ISBN, "9781473619777"))) + .put("instanceTypeId", UUID_INSTANCE_TYPE.toString()) + .put("tags", new JsonObject().put("tagList", new JsonArray().add(TAG_VALUE))) + .put("_version", 1); + + instancesClient.create(instanceToCreate); + return instanceId; + } + + private void addPublicationPeriodToJsonb(String instanceId, String endDate) { + var id = UUID.fromString(instanceId); + if (endDate != null) { + var query = String.format(UPDATE_JSONB_WITH_PUB_PERIOD, TENANT_ID); + runSql(query, Tuple.of(PublicationPeriodMigrationTest.START_DATE, endDate, id)); + } else { + var query = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_START_DATE, TENANT_ID); + runSql(query, Tuple.of(PublicationPeriodMigrationTest.START_DATE, id)); + } + } + + @SneakyThrows + private RowSet runSql(String sql, Tuple tuple) { + return PostgresClient.getInstance(getVertx()) + .execute(sql, tuple) + .toCompletionStage() + .toCompletableFuture() + .get(TIMEOUT, TimeUnit.SECONDS); + } +} diff --git a/src/test/java/org/folio/rest/api/StorageTestSuite.java b/src/test/java/org/folio/rest/api/StorageTestSuite.java index 7dd4fa845..7cf0fea89 100644 --- a/src/test/java/org/folio/rest/api/StorageTestSuite.java +++ b/src/test/java/org/folio/rest/api/StorageTestSuite.java @@ -91,7 +91,10 @@ RetainLeadingZeroesMigrationScriptTest.class, StatisticalCodeTest.class, UpcIsmnMigrationScriptTest.class, - InstanceStorageInstancesBulkApiTest.class + InstanceStorageInstancesBulkApiTest.class, + PublicationPeriodMigrationTest.class, + SubjectSourceTest.class, + SubjectTypeTest.class // These fail. //ReferenceTablesTest.class, diff --git a/src/test/java/org/folio/rest/api/SubjectSourceTest.java b/src/test/java/org/folio/rest/api/SubjectSourceTest.java index 8a78a984b..045ddfbb8 100644 --- a/src/test/java/org/folio/rest/api/SubjectSourceTest.java +++ b/src/test/java/org/folio/rest/api/SubjectSourceTest.java @@ -2,55 +2,95 @@ import static org.folio.rest.support.http.InterfaceUrls.subjectSourcesUrl; import static org.folio.utility.ModuleUtility.getClient; +import static org.folio.utility.ModuleUtility.prepareTenant; +import static org.folio.utility.ModuleUtility.removeTenant; +import static org.folio.utility.RestUtility.CONSORTIUM_CENTRAL_TENANT; 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.Assert.assertTrue; 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.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import junitparams.JUnitParamsRunner; +import lombok.SneakyThrows; +import org.folio.okapi.common.XOkapiHeaders; import org.folio.rest.support.Response; import org.folio.rest.support.ResponseHandler; import org.folio.rest.support.http.ResourceClient; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; -public class SubjectSourceTest extends TestBase { - +@RunWith(JUnitParamsRunner.class) +public class SubjectSourceTest extends TestBaseWithInventoryUtil { private static ResourceClient subjectSourceClient; private static final String SUBJECT_SOURCE_ID = "e894d0dc-621d-4b1d-98f6-6f7120eb0d40"; + @SneakyThrows @BeforeClass - public static void beforeAll() { - TestBase.beforeAll(); + public static void before() { + prepareTenant(CONSORTIUM_CENTRAL_TENANT, false); subjectSourceClient = ResourceClient.forSubjectSources(getClient()); + + mockUserTenantsForNonConsortiumMember(); + mockUserTenantsForConsortiumMember(CONSORTIUM_CENTRAL_TENANT); + mockConsortiumTenants(); + } + + @SneakyThrows + @AfterClass + public static void afterClass() { + removeTenant(CONSORTIUM_CENTRAL_TENANT); } @Test - public void cannotCreateSubjectSourceWithDuplicateName() + public void cannotCreateSubjectSourceWithDuplicateName() { + + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings2") + .put("source", "local"); + + createSubjectSource(subjectSource); + + Response response = createSubjectSource(subjectSource); + assertThat(response.getStatusCode(), is(422)); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertThat(errors.size(), is(1)); + assertTrue(errors.getJsonObject(0).getString("message").contains("(jsonb ->> 'name'::text)) value already exists")); + } + + @Test + public void cannotCreateSubjectSourceWithDuplicateCode() throws InterruptedException, TimeoutException, ExecutionException { JsonObject subjectSource = new JsonObject() - .put("name", "Library of Congress Subject Headings2") + .put("name", "Test") + .put("code", "test") .put("source", "local"); - subjectSourceClient.create(subjectSource); + createSubjectSource(subjectSource); CompletableFuture postCompleted = new CompletableFuture<>(); - getClient().post(subjectSourcesUrl(""), subjectSource, TENANT_ID, ResponseHandler.json(postCompleted)); + getClient().post(subjectSourcesUrl(""), subjectSource.put("name", "Test2"), + 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)); + assertTrue(errors.getJsonObject(0).getString("message").contains("(jsonb ->> 'code'::text)) value already exists")); } @Test @@ -69,6 +109,45 @@ public void cannotCreateSubjectSourceWithSourceFolio() { errors.getJsonObject(0).getString("message")); } + @Test + public void cannotCreateSubjectSourceWithSourceConsortiumAtNonEcs() { + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings2") + .put("source", "consortium"); + + Response resource = createSubjectSource(subjectSource); + + JsonArray errors = resource.getJson().getJsonArray("errors"); + assertEquals(422, resource.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals( + "Illegal operation: Source consortium cannot be applied at non-consortium tenant", + errors.getJsonObject(0).getString("message")); + } + + @Test + public void canCreateSubjectSourceWithSourceConsortiumAtEcs() { + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings2") + .put("source", "consortium"); + + Response resource = createSubjectSource(subjectSource, CONSORTIUM_CENTRAL_TENANT); + + assertEquals(201, resource.getStatusCode()); + } + + @Test + public void cannotUpdateNonExistingSubjectSource() { + JsonObject subjectSource = new JsonObject() + .put("name", "Library of Congress Subject Headings") + .put("source", "local"); + + Response response = updateSubjectSource(UUID.randomUUID().toString(), subjectSource); + + assertEquals(404, response.getStatusCode()); + assertEquals("SubjectSource was not found", response.getBody()); + } + @Test public void cannotUpdateSubjectSourceWithSourceFolio() { JsonObject subjectSource = new JsonObject() @@ -80,27 +159,98 @@ public void cannotUpdateSubjectSourceWithSourceFolio() { JsonArray errors = response.getJson().getJsonArray("errors"); assertEquals(422, response.getStatusCode()); assertEquals(1, errors.size()); - assertEquals( - "Illegal operation: Source field cannot be updated", + assertEquals("Illegal operation: Source folio cannot be updated", errors.getJsonObject(0).getString("message")); } - private Response createSubjectSource(JsonObject object) { + @Test + public void cannotUpdateSubjectSourceToFolio() { + String subjectSourceId = UUID.randomUUID().toString(); + + JsonObject subjectSource = new JsonObject() + .put("id", subjectSourceId) + .put("name", "Library Test" + subjectSourceId) + .put("source", "local"); - CompletableFuture createSubjectSource = new CompletableFuture<>(); + Response existingSubjectSource = createSubjectSource(subjectSource); - send(subjectSourcesUrl("").toString(), HttpMethod.POST, object.toString(), - SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(createSubjectSource)); + Response response = updateSubjectSource(subjectSourceId, existingSubjectSource.getJson().put("source", "folio")); - return get(createSubjectSource); + 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")); } - private Response updateSubjectSource(String id, JsonObject object) { - CompletableFuture updateSubjectSource = new CompletableFuture<>(); + @Test + public void cannotUpdateSubjectSourceToConsortiumAtNonEcs() { + String subjectSourceId = UUID.randomUUID().toString(); + + JsonObject subjectSource = new JsonObject() + .put("id", subjectSourceId) + .put("name", "Library Test" + subjectSourceId) + .put("source", "local"); + + Response existingSubjectSource = createSubjectSource(subjectSource); + + Response response = updateSubjectSource(subjectSourceId, + existingSubjectSource.getJson().put("source", "consortium")); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertEquals(422, response.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals("Illegal operation: Source field cannot be updated at non-consortium tenant", + errors.getJsonObject(0).getString("message")); + } + + @Test + public void canUpdateSubjectSourceToConsortiumAtEcs() { + String subjectSourceId = UUID.randomUUID().toString(); - send(subjectSourcesUrl("/" + id).toString(), HttpMethod.PUT, object.toString(), - SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(updateSubjectSource)); + JsonObject subjectSource = new JsonObject() + .put("id", subjectSourceId) + .put("name", "Library Test" + subjectSourceId) + .put("source", "local"); + + Response existingSubjectSource = createSubjectSource(subjectSource, CONSORTIUM_CENTRAL_TENANT); + + Response response = updateSubjectSource(subjectSourceId, + existingSubjectSource.getJson().put("source", "consortium"), CONSORTIUM_CENTRAL_TENANT); + + assertEquals(204, response.getStatusCode()); + } + + @Test + public void canUpdateSubjectSourceToLocalAtEcs() { + String subjectSourceId = UUID.randomUUID().toString(); + + JsonObject subjectSource = new JsonObject() + .put("id", subjectSourceId) + .put("name", "Library Test" + subjectSourceId) + .put("source", "consortium"); + + Response existingSubjectSource = createSubjectSource(subjectSource, CONSORTIUM_CENTRAL_TENANT); + + Response response = updateSubjectSource(subjectSourceId, + existingSubjectSource.getJson().put("source", "local"), CONSORTIUM_CENTRAL_TENANT); + + assertEquals(204, response.getStatusCode()); + } + + private Response createSubjectSource(JsonObject object) { + return createSubjectSource(object, TENANT_ID); + } + + private Response createSubjectSource(JsonObject object, String tenantId) { + return subjectSourceClient.attemptToCreate("", object, tenantId, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); + } + + private Response updateSubjectSource(String id, JsonObject object) { + return subjectSourceClient.attemptToReplace(id, object, TENANT_ID, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); + } - return get(updateSubjectSource); + private Response updateSubjectSource(String id, JsonObject object, String tenantId) { + return subjectSourceClient.attemptToReplace(id, object, tenantId, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); } } diff --git a/src/test/java/org/folio/rest/api/SubjectTypeTest.java b/src/test/java/org/folio/rest/api/SubjectTypeTest.java index efd5becdd..0d09f0585 100644 --- a/src/test/java/org/folio/rest/api/SubjectTypeTest.java +++ b/src/test/java/org/folio/rest/api/SubjectTypeTest.java @@ -2,34 +2,53 @@ import static org.folio.rest.support.http.InterfaceUrls.subjectTypesUrl; import static org.folio.utility.ModuleUtility.getClient; +import static org.folio.utility.ModuleUtility.prepareTenant; +import static org.folio.utility.ModuleUtility.removeTenant; +import static org.folio.utility.RestUtility.CONSORTIUM_CENTRAL_TENANT; 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.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import junitparams.JUnitParamsRunner; +import lombok.SneakyThrows; +import org.folio.okapi.common.XOkapiHeaders; import org.folio.rest.support.Response; import org.folio.rest.support.ResponseHandler; import org.folio.rest.support.http.ResourceClient; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; -public class SubjectTypeTest extends TestBase { - +@RunWith(JUnitParamsRunner.class) +public class SubjectTypeTest extends TestBaseWithInventoryUtil { private static ResourceClient subjectTypeClient; private static final String SUBJECT_TYPE_ID = "d6488f88-1e74-40ce-81b5-b19a928ff5b1"; + @SneakyThrows @BeforeClass - public static void beforeAll() { - TestBase.beforeAll(); + public static void before() { subjectTypeClient = ResourceClient.forSubjectTypes(getClient()); + prepareTenant(CONSORTIUM_CENTRAL_TENANT, false); + + mockUserTenantsForNonConsortiumMember(); + mockUserTenantsForConsortiumMember(CONSORTIUM_CENTRAL_TENANT); + mockConsortiumTenants(); + } + + @SneakyThrows + @AfterClass + public static void afterClass() { + removeTenant(CONSORTIUM_CENTRAL_TENANT); } @Test @@ -69,6 +88,45 @@ public void cannotCreateSubjectTypeWithSourceFolio() { errors.getJsonObject(0).getString("message")); } + @Test + public void cannotCreateSubjectTypeWithSourceConsortiumAtNonEcs() { + JsonObject subjecType = new JsonObject() + .put("name", "Topical name2") + .put("source", "consortium"); + + Response resource = createSubjectType(subjecType); + + JsonArray errors = resource.getJson().getJsonArray("errors"); + assertEquals(422, resource.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals( + "Illegal operation: Source consortium cannot be applied at non-consortium tenant", + errors.getJsonObject(0).getString("message")); + } + + @Test + public void canCreateSubjectTypeWithSourceConsortiumAtEcs() { + JsonObject subjectType = new JsonObject() + .put("name", "Topical name2") + .put("source", "consortium"); + + Response resource = createSubjectType(subjectType, CONSORTIUM_CENTRAL_TENANT); + + assertEquals(201, resource.getStatusCode()); + } + + @Test + public void cannotUpdateNonExistingSubjectType() { + JsonObject subjectType = new JsonObject() + .put("name", "Library of Congress Subject Headings") + .put("source", "local"); + + Response response = updateSubjectType(UUID.randomUUID().toString(), subjectType); + + assertEquals(404, response.getStatusCode()); + assertEquals("SubjectType was not found", response.getBody()); + } + @Test public void cannotUpdateSubjectTypeWithSourceFolio() { JsonObject subjectType = new JsonObject() @@ -81,26 +139,97 @@ public void cannotUpdateSubjectTypeWithSourceFolio() { assertEquals(422, response.getStatusCode()); assertEquals(1, errors.size()); assertEquals( - "Illegal operation: Source field cannot be updated", + "Illegal operation: Source folio cannot be updated", errors.getJsonObject(0).getString("message")); } - private Response createSubjectType(JsonObject object) { + @Test + public void cannotUpdateSubjectTypeToFolio() { + String subjectTypeId = UUID.randomUUID().toString(); - CompletableFuture createSubjectType = new CompletableFuture<>(); + JsonObject subjectType = new JsonObject() + .put("id", subjectTypeId) + .put("name", "Library Test" + subjectTypeId) + .put("source", "local"); - send(subjectTypesUrl("").toString(), HttpMethod.POST, object.toString(), - SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(createSubjectType)); + Response existingSubjectType = createSubjectType(subjectType); - return get(createSubjectType); + Response response = updateSubjectType(subjectTypeId, existingSubjectType.getJson().put("source", "folio")); + + 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")); } - private Response updateSubjectType(String id, JsonObject object) { - CompletableFuture updateSubjectType = new CompletableFuture<>(); + @Test + public void cannotUpdateSubjectTypeToConsortiumAtNonEcs() { + String subjectTypeId = UUID.randomUUID().toString(); + + JsonObject subjectType = new JsonObject() + .put("id", subjectTypeId) + .put("name", "Library Test" + subjectTypeId) + .put("source", "local"); + + Response existingSubjectType = createSubjectType(subjectType); + + Response response = updateSubjectType(subjectTypeId, existingSubjectType.getJson().put("source", "consortium")); + + JsonArray errors = response.getJson().getJsonArray("errors"); + assertEquals(422, response.getStatusCode()); + assertEquals(1, errors.size()); + assertEquals("Illegal operation: Source field cannot be updated at non-consortium tenant", + errors.getJsonObject(0).getString("message")); + } + + @Test + public void canUpdateSubjectTypeToConsortiumAtEcs() { + String subjectTypeId = UUID.randomUUID().toString(); + + JsonObject subjectType = new JsonObject() + .put("id", subjectTypeId) + .put("name", "Library Test" + subjectTypeId) + .put("source", "local"); + + Response existingSubjectType = createSubjectType(subjectType, CONSORTIUM_CENTRAL_TENANT); - send(subjectTypesUrl("/" + id).toString(), HttpMethod.PUT, object.toString(), - SUPPORTED_CONTENT_TYPE_JSON_DEF, ResponseHandler.json(updateSubjectType)); + Response response = updateSubjectType(subjectTypeId, + existingSubjectType.getJson().put("source", "consortium"), CONSORTIUM_CENTRAL_TENANT); + + assertEquals(204, response.getStatusCode()); + } + + @Test + public void canUpdateSubjectTypeToLocalAtEcs() { + String subjectTypeId = UUID.randomUUID().toString(); + + JsonObject subjectType = new JsonObject() + .put("id", subjectTypeId) + .put("name", "Library Test" + subjectTypeId) + .put("source", "consortium"); + + Response existingSubjectType = createSubjectType(subjectType, CONSORTIUM_CENTRAL_TENANT); + + Response response = updateSubjectType(subjectTypeId, + existingSubjectType.getJson().put("source", "local"), CONSORTIUM_CENTRAL_TENANT); + + assertEquals(204, response.getStatusCode()); + } + + private Response createSubjectType(JsonObject object) { + return createSubjectType(object, TENANT_ID); + } + + private Response createSubjectType(JsonObject object, String tenantId) { + return subjectTypeClient.attemptToCreate("", object, tenantId, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); + } + + private Response updateSubjectType(String id, JsonObject object) { + return subjectTypeClient.attemptToReplace(id, object, TENANT_ID, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); + } - return get(updateSubjectType); + private Response updateSubjectType(String id, JsonObject object, String tenantId) { + return subjectTypeClient.attemptToReplace(id, object, tenantId, Map.of(XOkapiHeaders.URL, mockServer.baseUrl())); } } diff --git a/src/test/java/org/folio/rest/api/TestBaseWithInventoryUtil.java b/src/test/java/org/folio/rest/api/TestBaseWithInventoryUtil.java index fdf9bf62c..74a03eecf 100644 --- a/src/test/java/org/folio/rest/api/TestBaseWithInventoryUtil.java +++ b/src/test/java/org/folio/rest/api/TestBaseWithInventoryUtil.java @@ -6,6 +6,8 @@ import static org.folio.rest.support.http.InterfaceUrls.loanTypesStorageUrl; import static org.folio.rest.support.http.InterfaceUrls.materialTypesStorageUrl; import static org.folio.utility.ModuleUtility.getClient; +import static org.folio.utility.RestUtility.CONSORTIUM_CENTRAL_TENANT; +import static org.folio.utility.RestUtility.CONSORTIUM_MEMBER_TENANT; import static org.folio.utility.RestUtility.TENANT_ID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -113,6 +115,30 @@ public static void mockUserTenantsForNonConsortiumMember() { .willReturn(WireMock.ok().withBody(emptyUserTenantsCollection.encodePrettily()))); } + public static void mockUserTenantsForConsortiumMember(String tenantId) { + JsonObject userTenantsCollection = new JsonObject() + .put("userTenants", new JsonArray() + .add(new JsonObject() + .put("centralTenantId", CONSORTIUM_CENTRAL_TENANT) + .put("consortiumId", "mobius"))); + WireMock.stubFor(WireMock.get(USER_TENANTS_PATH) + .withHeader(XOkapiHeaders.TENANT, equalToIgnoreCase(tenantId)) + .willReturn(WireMock.ok().withBody(userTenantsCollection.encodePrettily()))); + } + + public static void mockConsortiumTenants() { + JsonObject tenantsCollection = new JsonObject() + .put("tenants", new JsonArray() + .add(new JsonObject() + .put("id", CONSORTIUM_CENTRAL_TENANT) + .put("isCentral", true)) + .add(new JsonObject() + .put("id", CONSORTIUM_MEMBER_TENANT) + .put("isCentral", false))); + WireMock.stubFor(WireMock.get("/consortia/mobius/tenants") + .willReturn(WireMock.ok().withBody(tenantsCollection.encodePrettily()))); + } + protected static void setupMaterialTypes() { setupMaterialTypes(TENANT_ID); } diff --git a/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java b/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java index af9494493..202f3ed1d 100644 --- a/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java +++ b/src/test/java/org/folio/rest/impl/SubjectSourcesIT.java @@ -36,6 +36,7 @@ protected SubjectSource sampleRecord() { return new SubjectSource() .withId(UUID.randomUUID().toString()) .withName("test_name") + .withCode("test_code") .withSource(SubjectSource.Source.LOCAL); } @@ -46,7 +47,7 @@ protected Function> collectionRecordsExtract @Override protected List> recordFieldExtractors() { - return List.of(SubjectSource::getName, SubjectSource::getSource); + return List.of(SubjectSource::getName, SubjectSource::getCode, SubjectSource::getSource); } @Override @@ -66,6 +67,6 @@ protected UnaryOperator recordModifyingFunction() { @Override protected List queries() { - return List.of("name==test_name"); + return List.of("name==test_name", "code==test_code"); } } diff --git a/src/test/java/org/folio/rest/support/HttpClient.java b/src/test/java/org/folio/rest/support/HttpClient.java index 96f51fdd4..022df9447 100644 --- a/src/test/java/org/folio/rest/support/HttpClient.java +++ b/src/test/java/org/folio/rest/support/HttpClient.java @@ -157,6 +157,21 @@ public void put( .onSuccess(responseHandler); } + public void put( + URL url, + Object body, + Map headers, + String tenantId, + Handler> responseHandler) { + + request(HttpMethod.PUT, url, body, headers, tenantId) + .recover(error -> { + LOG.error(error.getMessage(), error); + return null; + }) + .onSuccess(responseHandler); + } + public CompletableFuture put(URL url, Object body, String tenantId) { return asResponse(request(HttpMethod.PUT, url, body, tenantId)); } 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 007fab387..788bf75f4 100644 --- a/src/test/java/org/folio/rest/support/http/ResourceClient.java +++ b/src/test/java/org/folio/rest/support/http/ResourceClient.java @@ -8,6 +8,7 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -255,10 +256,13 @@ public Response attemptToReplace(UUID id, JsonObject request) { } public Response attemptToReplace(String id, JsonObject request) { + return attemptToReplace(id, request, TENANT_ID, new HashMap<>()); + } + + public Response attemptToReplace(String id, JsonObject request, String tenantId, Map headers) { CompletableFuture putCompleted = new CompletableFuture<>(); - client.put(urlMakerWithId(id), request, - TENANT_ID, ResponseHandler.any(putCompleted)); + client.put(urlMakerWithId(id), request, headers, tenantId, ResponseHandler.any(putCompleted)); return TestBase.get(putCompleted); } diff --git a/src/test/resources/instances/bulk/bulkInstanceRepresentation.json b/src/test/resources/instances/bulk/bulkInstanceRepresentation.json index 81f519466..987be5de9 100644 --- a/src/test/resources/instances/bulk/bulkInstanceRepresentation.json +++ b/src/test/resources/instances/bulk/bulkInstanceRepresentation.json @@ -96,10 +96,6 @@ "publicationRange": [ "v. 1- Apr. 1950-" ], - "publicationPeriod": { - "start": 2000, - "end": 2001 - }, "electronicAccess": [ { "uri": "http://resolver.library.cornell.edu/cgi-bin/EADresolver?id=RMA01776", diff --git a/src/test/resources/instances/bulk/expectedInstance.json b/src/test/resources/instances/bulk/expectedInstance.json index 5d0778f4e..dc01ea509 100644 --- a/src/test/resources/instances/bulk/expectedInstance.json +++ b/src/test/resources/instances/bulk/expectedInstance.json @@ -96,10 +96,6 @@ "publicationRange": [ "v. 1- Apr. 1950-" ], - "publicationPeriod": { - "start": 2000, - "end": 2001 - }, "electronicAccess": [ { "uri": "http://resolver.library.cornell.edu/cgi-bin/EADresolver?id=RMA01776",