diff --git a/NEWS.md b/NEWS.md index 657f995ea..fb7de0dc0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,8 +3,8 @@ * 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 11.1` +* Provides `instance-storage-batch 3.1` * Provides `instance-storage-batch-sync 3.0` * Provides `instance-storage-batch-sync-unsafe 3.0` * Provides `inventory-view 3.0` @@ -16,6 +16,7 @@ ### Features * Add module descriptor validator plugin and fix the permission names ([MODINVSTOR-1247](https://folio-org.atlassian.net/browse/MODINVSTOR-1247)) +* Implement publication period migration on big dataset, create new InstanceWithoutPubPeriod schema only for input request ([MODINVSTOR-1271](https://folio-org.atlassian.net/browse/MODINVSTOR-1271)) ### Tech Dept * Upgrade localstack from 0.11.3 to s3-latest (=3.8.0) ([MODINVSTOR-1272](https://folio-org.atlassian.net/browse/MODINVSTOR-1272)) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 52f9afc72..ccf822302 100755 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -167,7 +167,7 @@ }, { "id": "instance-storage", - "version": "11.0", + "version": "11.1", "handlers": [ { "methods": ["GET"], @@ -274,7 +274,7 @@ }, { "id": "instance-storage-batch", - "version": "3.0", + "version": "3.1", "handlers": [ { "methods": ["POST"], diff --git a/pom.xml b/pom.xml index 63661b75a..1e4432ec7 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 3.17.0 2.24.1 2.2.0-SNAPSHOT + 1.9.4 6.0.3 5.11.2 @@ -138,6 +139,11 @@ folio-s3-client ${folio-s3-client.version} + + commons-beanutils + commons-beanutils + ${commons-beanutils.version} + diff --git a/ramls/examples/instance_get.json b/ramls/examples/instance_get.json index 086356896..a4c007c2c 100644 --- a/ramls/examples/instance_get.json +++ b/ramls/examples/instance_get.json @@ -19,6 +19,10 @@ "value": "1" } ], + "publicationPeriod": { + "start": 1999, + "end": 2001 + }, "instanceTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2c", "tags" : { "tagList" : [ diff --git a/ramls/examples/instances_get.json b/ramls/examples/instances_get.json index 1d948b250..27c0c9a07 100644 --- a/ramls/examples/instances_get.json +++ b/ramls/examples/instances_get.json @@ -21,6 +21,10 @@ "value": "1" } ], + "publicationPeriod": { + "start": 1999, + "end": 2001 + }, "instanceTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2c", "tags" : { "tagList" : [ diff --git a/ramls/examples/instanceswithoutpubperiod_get.json b/ramls/examples/instanceswithoutpubperiod_get.json new file mode 100644 index 000000000..1d948b250 --- /dev/null +++ b/ramls/examples/instanceswithoutpubperiod_get.json @@ -0,0 +1,56 @@ +{ + "instances": [ + { + "id": "601a8dc4-dee7-48eb-b03f-d02fdf0debd0", + "title": "ADVANCING LIBRARY EDUCATION: TECHNOLOGICAL INNOVATION AND INSTRUCTIONAL DESIGN", + "source": "Local: MARC", + "contributors": [ + { + "name": "Sigal, Ari", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a", + "primary": true + } + ], + "identifiers": [ + { + "identifierTypeId": "2e48e713-17f3-4c13-a9f8-23845bb210af", + "value": "9781466636897" + }, + { + "identifierTypeId": "6051f95c-028e-4c6a-8a9e-ee689dd51453", + "value": "1" + } + ], + "instanceTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2c", + "tags" : { + "tagList" : [ + "important" + ] + } + }, + { + "id": "f31a36de-fcf8-44f9-87ef-a55d06ad21ae", + "title": "ADVANCING RESEARCH METHODS WITH NEW TECHNOLOGIES.", + "source": "Local: MARC", + "contributors": [ + { + "name": "Sappleton, Natalie", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a", + "primary": true + } + ], + "identifiers": [ + { + "identifierTypeId": "2e48e713-17f3-4c13-a9f8-23845bb210af", + "value": "9781466639195" + }, + { + "identifierTypeId": "6051f95c-028e-4c6a-8a9e-ee689dd51453", + "value": "2" + } + ], + "instanceTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2c" + } + ], + "totalRecords": 2 +} diff --git a/ramls/examples/instancewithoutpubperiod_get.json b/ramls/examples/instancewithoutpubperiod_get.json new file mode 100644 index 000000000..086356896 --- /dev/null +++ b/ramls/examples/instancewithoutpubperiod_get.json @@ -0,0 +1,28 @@ +{ + "id": "601a8dc4-dee7-48eb-b03f-d02fdf0debd0", + "source": "Local: MARC", + "title": "ADVANCING LIBRARY EDUCATION: TECHNOLOGICAL INNOVATION AND INSTRUCTIONAL DESIGN", + "contributors": [ + { + "name": "Sigal, Ari", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a", + "primary": true + } + ], + "identifiers": [ + { + "identifierTypeId": "2e48e713-17f3-4c13-a9f8-23845bb210af", + "value": "9781466636897" + }, + { + "identifierTypeId": "6051f95c-028e-4c6a-8a9e-ee689dd51453", + "value": "1" + } + ], + "instanceTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2c", + "tags" : { + "tagList" : [ + "important" + ] + } +} diff --git a/ramls/instance-storage-batch.raml b/ramls/instance-storage-batch.raml index b28f3fc2a..6d637e8bd 100644 --- a/ramls/instance-storage-batch.raml +++ b/ramls/instance-storage-batch.raml @@ -11,6 +11,7 @@ documentation: types: errors: !include raml-util/schemas/errors.schema instances: !include instances.json + instancesWithoutPubPeriod: !include instances-without-pub-period.json instancesBatchResponse: !include instances-batch-response.json /instance-storage/batch/instances: @@ -19,7 +20,9 @@ types: description: "Create collection of instances in one request - deprecated, use /instance-storage/sync instead" body: application/json: - type: instances + type: instancesWithoutPubPeriod + example: + value: !include examples/instanceswithoutpubperiod_get.json responses: 201: description: "At least one Instance from the list was created" diff --git a/ramls/instance-storage.raml b/ramls/instance-storage.raml index 373a5785b..d7e97d287 100644 --- a/ramls/instance-storage.raml +++ b/ramls/instance-storage.raml @@ -9,6 +9,8 @@ documentation: content: Storage for instances in the inventory types: + instanceWithoutPubPeriod: !include instance-without-pub-period.json + instancesWithoutPubPeriod: !include instances-without-pub-period.json instance: !include instance.json instances: !include instances.json marcJson: !include marc.json @@ -60,16 +62,26 @@ resourceTypes: displayName: Instances type: collection: - exampleCollection: !include examples/instances_get.json - schemaCollection: instances - schemaItem: instance - exampleItem: !include examples/instance_get.json + exampleCollection: !include examples/instanceswithoutpubperiod_get.json + schemaCollection: instancesWithoutPubPeriod + schemaItem: instanceWithoutPubPeriod + exampleItem: !include examples/instancewithoutpubperiod_get.json get: is: [pageable, searchable: {description: "by title (using CQL)", example: "title=\"*uproot*\""}, ] + responses: + 200: + body: + application/json: + type: instances post: + responses: + 200: + body: + application/json: + type: instance delete: is: [searchable: { description: "CQL to select instances to delete, use cql.allRecords=1 to delete all. Deletes connected marc source records.", example: "hrid==\"in123-0*\"" } ] @@ -89,8 +101,8 @@ resourceTypes: /{instanceId}: type: collection-item: - exampleItem: !include examples/instance_get.json - schema: instance + exampleItem: !include examples/instancewithoutpubperiod_get.json + schema: instanceWithoutPubPeriod get: responses: 200: diff --git a/ramls/instance-without-pub-period.json b/ramls/instance-without-pub-period.json new file mode 100644 index 000000000..b6542c778 --- /dev/null +++ b/ramls/instance-without-pub-period.json @@ -0,0 +1,508 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An instance record without publicationPeriod", + "type": "object", + "javaType": "org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the instance record; a UUID", + "$ref": "uuid.json" + }, + "_version": { + "type": "integer", + "description": "Record version for optimistic locking" + }, + "hrid": { + "type": "string", + "description": "The human readable ID, also called eye readable ID. A system-assigned sequential ID which maps to the Instance ID" + }, + "matchKey": { + "type": "string", + "description" : "A unique instance identifier matching a client-side bibliographic record identification scheme, in particular for a scenario where multiple separate catalogs with no shared record identifiers contribute to the same Instance in Inventory. A match key is typically generated from select, normalized pieces of metadata in bibliographic records" + }, + "source": { + "type": "string", + "description": "The metadata source and its format of the underlying record to the instance record. (e.g. FOLIO if it's a record created in Inventory; MARC if it's a MARC record created in MARCcat or EPKB if it's a record coming from eHoldings; CONSORTIUM-MARC or CONSORTIUM-FOLIO for sharing Instances)." + }, + "title": { + "type": "string", + "description": "The primary title (or label) associated with the resource" + }, + "indexTitle": { + "type": "string", + "description": "Title normalized for browsing and searching; based on the title with articles removed" + }, + "alternativeTitles": { + "type": "array", + "description": "List of alternative titles for the resource (e.g. original language version title of a movie)", + "items": { + "type": "object", + "properties": { + "alternativeTitleTypeId": { + "type": "string", + "description": "UUID for an alternative title qualifier", + "$ref": "uuid.json" + }, + "alternativeTitle": { + "type": "string", + "description": "An alternative title for the resource" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls an alternative title", + "$ref": "uuid.json" + } + } + }, + "uniqueItems": true + }, + "editions": { + "type": "array", + "description": "The edition statement, imprint and other publication source information", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "series": { + "type": "array", + "description": "List of series titles associated with the resource (e.g. Harry Potter)", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Series title value" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls an series title", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "uniqueItems": true + }, + "identifiers": { + "type": "array", + "description": "An extensible set of name-value pairs of identifiers associated with the resource", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Resource identifier value" + }, + "identifierTypeId": { + "type": "string", + "description": "UUID of resource identifier type (e.g. ISBN, ISSN, LCCN, CODEN, Locally defined identifiers)", + "$ref": "uuid.json" + }, + "identifierTypeObject": { + "type": "object", + "description": "Information about identifier type, looked up from identifierTypeId", + "folio:$ref": "illpolicy.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "identifier-types", + "folio:linkFromField": "identifierTypeId", + "folio:linkToField": "id", + "folio:includedElement": "identifierTypes.0" + } + }, + "additionalProperties": false, + "required": [ + "value", + "identifierTypeId" + ] + } + }, + "contributors": { + "type": "array", + "description": "List of contributors", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Personal name, corporate name, meeting name" + }, + "contributorTypeId": { + "type": "string", + "description": "UUID for the contributor type term defined in controlled vocabulary", + "$ref": "uuid.json" + }, + "contributorTypeText": { + "type": "string", + "description": "Free text element for adding contributor type terms other that defined by the MARC code list for relators" + }, + "contributorNameTypeId": { + "type": "string", + "description": "UUID of contributor name type term defined by the MARC code list for relators", + "$ref": "uuid.json" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls the contributor", + "$ref": "uuid.json" + }, + "contributorNameType": { + "type": "object", + "description": "Dereferenced contributor-name type", + "javaType": "org.folio.rest.jaxrs.model.contributorNameTypeVirtual", + "folio:$ref": "contributornametype.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "contributor-name-types", + "folio:linkFromField": "contributorNameTypeId", + "folio:linkToField": "id", + "folio:includedElement": "contributorNameTypes.0" + }, + "primary": { + "type": "boolean", + "description": "Whether this is the primary contributor" + } + }, + "additionalProperties": false, + "required": [ + "name", + "contributorNameTypeId" + ] + } + }, + "subjects": { + "type": "array", + "description": "List of subject headings", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Subject heading value" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls a subject heading", + "$ref": "uuid.json" + }, + "sourceId": { + "type": "string", + "description": "UUID of subject source", + "$ref": "uuid.json" + }, + "typeId": { + "type": "string", + "description": "UUID of subject type", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "uniqueItems": true + }, + "classifications": { + "type": "array", + "description": "List of classifications", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "classificationNumber": { + "type": "string", + "description": "Classification (e.g. classification scheme, classification schedule)" + }, + "classificationTypeId": { + "type": "string", + "description": "UUID of classification schema (e.g. LC, Canadian Classification, NLM, National Agricultural Library, UDC, and Dewey)", + "$ref": "uuid.json" + }, + "classificationType": { + "type": "object", + "description": "Dereferenced classification schema", + "javaType": "org.folio.rest.jaxrs.model.classificationTypeVirtual", + "folio:$ref": "classificationtype.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "classification-types", + "folio:linkFromField": "classificationTypeId", + "folio:linkToField": "id", + "folio:includedElement": "classificationTypes.0" + } + }, + "additionalProperties": false, + "required": [ + "classificationNumber", + "classificationTypeId" + ] + } + }, + "publication": { + "type": "array", + "description": "List of publication items", + "items": { + "type": "object", + "properties": { + "publisher": { + "type": "string", + "description": "Name of publisher, distributor, etc." + }, + "place": { + "type": "string", + "description": "Place of publication, distribution, etc." + }, + "dateOfPublication": { + "type": "string", + "description": "Date (year YYYY) of publication, distribution, etc." + }, + "role": { + "type": "string", + "description": "The role of the publisher, distributor, etc." + } + } + } + }, + "publicationFrequency": { + "type": "array", + "description": "List of intervals at which a serial appears (e.g. daily, weekly, monthly, quarterly, etc.)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "publicationRange": { + "type": "array", + "description": "The range of sequential designation/chronology of publication, or date range", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "electronicAccess": { + "type": "array", + "description": "List of electronic access items", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "uniform resource identifier (URI) is a string of characters designed for unambiguous identification of resources" + }, + "linkText": { + "type": "string", + "description": "The value of the MARC tag field 856 2nd indicator, where the values are: no information provided, resource, version of resource, related resource, no display constant generated" + }, + "materialsSpecification": { + "type": "string", + "description": "Materials specified is used to specify to what portion or aspect of the resource the electronic location and access information applies (e.g. a portion or subset of the item is electronic, or a related electronic resource is being linked to the record)" + }, + "publicNote": { + "type": "string", + "description": "URL public note to be displayed in the discovery" + }, + "relationshipId": { + "type": "string", + "description": "UUID for the type of relationship between the electronic resource at the location identified and the item described in the record as a whole", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "uri" + ] + } + }, + "dates": { + "type": "object", + "description": "Instance Dates", + "properties": { + "dateTypeId": { + "type": "string", + "description": "Date type ID", + "$ref": "uuid.json" + }, + "date1": { + "type": "string", + "description": "Date 1", + "maxLength": 4 + }, + "date2": { + "type": "string", + "description": "Date 2", + "maxLength": 4 + } + }, + "additionalProperties": false + }, + "instanceTypeId": { + "type": "string", + "description": "UUID of the unique term for the resource type whether it's from the RDA content term list of locally defined", + "$ref": "uuid.json" + }, + "instanceFormatIds": { + "type": "array", + "description": "UUIDs for the unique terms for the format whether it's from the RDA carrier term list of locally defined", + "items": { + "type": "string", + "$ref": "uuid.json" + } + }, + "instanceFormats": { + "type": "array", + "description": "List of dereferenced instance formats", + "items": { + "type": "object", + "$ref": "instanceformat.json" + }, + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "instance-formats", + "folio:linkFromField": "instanceFormatIds", + "folio:linkToField": "id", + "folio:includedElement": "instanceFormats" + }, + "physicalDescriptions": { + "type": "array", + "description": "Physical description of the described resource, including its extent, dimensions, and such other physical details as a description of any accompanying materials and unit type and size", + "items": { + "type": "string" + } + }, + "languages": { + "type": "array", + "description": "The set of languages used by the resource", + "minItems": 0, + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "description": "Bibliographic notes (e.g. general notes, specialized notes)", + "items": { + "type": "object", + "javaType": "org.folio.rest.jaxrs.model.InstanceNote", + "additionalProperties": false, + "properties": { + "instanceNoteTypeId": { + "description": "ID of the type of note", + "$ref": "uuid.json" + }, + "note": { + "type": "string", + "description": "Text content of the note" + }, + "staffOnly": { + "type": "boolean", + "description": "If true, determines that the note should not be visible for others than staff", + "default": false + } + } + } + }, + "administrativeNotes":{ + "type": "array", + "description": "Administrative notes", + "minItems": 0, + "items": { + "type": "string" + } + }, + "modeOfIssuanceId": { + "type": "string", + "description": "UUID of the RDA mode of issuance, a categorization reflecting whether a resource is issued in one or more parts, the way it is updated, and whether its termination is predetermined or not (e.g. monograph, sequential monograph, serial; integrating Resource, other)", + "$ref": "uuid.json" + }, + "catalogedDate": { + "type": "string", + "description": "Date or timestamp on an instance for when is was considered cataloged" + }, + "previouslyHeld": { + "type": "boolean", + "description": "Records the fact that the resource was previously held by the library for things like Hathi access, etc.", + "default": false + }, + "staffSuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed for others than catalogers" + }, + "discoverySuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed in a discovery system", + "default": false + }, + "statisticalCodeIds": { + "type": "array", + "description": "List of statistical code IDs", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "sourceRecordFormat": { + "type": "string", + "description": "Format of the instance source record, if a source record exists (e.g. FOLIO if it's a record created in Inventory, MARC if it's a MARC record created in MARCcat or EPKB if it's a record coming from eHoldings)", + "enum": ["MARC-JSON"], + "readonly": true + }, + "statusId": { + "type": "string", + "description": "UUID for the Instance status term (e.g. cataloged, uncatalogued, batch loaded, temporary, other, not yet assigned)", + "$ref": "uuid.json" + }, + "statusUpdatedDate": { + "type": "string", + "description": "Date [or timestamp] for when the instance status was updated" + }, + "tags": { + "description": "arbitrary tags associated with this instance", + "id": "tags", + "type": "object", + "$ref": "raml-util/schemas/tags.schema" + }, + "metadata": { + "type": "object", + "$ref": "raml-util/schemas/metadata.schema", + "readonly": true + }, + "holdingsRecords2": { + "type": "array", + "description": "List of holdings records", + "items": { + "type": "object", + "$ref": "holdings-storage/holdingsRecord.json" + }, + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "holdings-storage/holdings", + "folio:linkFromField": "id", + "folio:linkToField": "instanceId", + "folio:includedElement": "holdingsRecords" + }, + "natureOfContentTermIds": { + "type": "array", + "description": "Array of UUID for the Instance nature of content (e.g. bibliography, biography, exhibition catalogue, festschrift, newspaper, proceedings, research report, thesis or website)", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Single UUID for the Instance nature of content", + "$ref": "uuid.json" + } + } + }, + "additionalProperties": false, + "required": [ + "source", + "title", + "instanceTypeId" + ] +} diff --git a/ramls/instances-without-pub-period.json b/ramls/instances-without-pub-period.json new file mode 100644 index 000000000..88a0ba224 --- /dev/null +++ b/ramls/instances-without-pub-period.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A collection of instance records without publicationPeriod", + "type": "object", + "properties": { + "instances": { + "description": "List of instance records without publication period", + "id": "instances", + "type": "array", + "items": { + "$ref": "instance-without-pub-period.json", + "type" : "object" + } + }, + "totalRecords": { + "description": "Estimated or exact total number of records", + "type": "integer" + }, + "resultInfo": { + "$ref": "raml-util/schemas/resultInfo.schema", + "readonly": true + } + + }, + "required": [ + "instances", + "totalRecords" + ] +} diff --git a/ramls/instances_post.json b/ramls/instances_post.json index bb857e514..1eb06e415 100644 --- a/ramls/instances_post.json +++ b/ramls/instances_post.json @@ -1,15 +1,15 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "A collection of instance records", + "description": "A collection of instance records without the publicationPeriod", "type": "object", "properties": { "instances": { - "description": "List of instance records", + "description": "List of instance records without the publicationPeriod", "id": "instances", "type": "array", "items": { - "type": "object", - "$ref": "instance.json" + "$ref": "instance-without-pub-period.json", + "type" : "object" } } }, diff --git a/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java b/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java index fc0e56ebe..20aa9b352 100644 --- a/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java @@ -11,6 +11,7 @@ import org.folio.rest.jaxrs.model.InstancesPost; import org.folio.rest.jaxrs.resource.InstanceStorageBatchSynchronous; import org.folio.services.instance.InstanceService; +import org.folio.utils.InstanceUtils; public class InstanceBatchSyncApi implements InstanceStorageBatchSynchronous { @Validate @@ -20,8 +21,10 @@ public void postInstanceStorageBatchSynchronous(boolean upsert, InstancesPost en Handler> asyncResultHandler, Context vertxContext) { + var instances = InstanceUtils.copyPropertiesToInstances(entity.getInstances()); + new InstanceService(vertxContext, okapiHeaders) - .createInstances(entity.getInstances(), upsert, true) + .createInstances(instances.getInstances(), upsert, true) .otherwise(cause -> respond500WithTextPlain(cause.getMessage())) .onComplete(asyncResultHandler); } diff --git a/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java b/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java index 565ebacd5..166465658 100644 --- a/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java @@ -11,6 +11,7 @@ import org.folio.rest.jaxrs.model.InstancesPost; import org.folio.rest.jaxrs.resource.InstanceStorageBatchSynchronousUnsafe; import org.folio.services.instance.InstanceService; +import org.folio.utils.InstanceUtils; public class InstanceBatchSyncUnsafeApi implements InstanceStorageBatchSynchronousUnsafe { @Validate @@ -19,8 +20,10 @@ public void postInstanceStorageBatchSynchronousUnsafe(InstancesPost entity, Map< Handler> asyncResultHandler, Context vertxContext) { + var instances = InstanceUtils.copyPropertiesToInstances(entity.getInstances()); + new InstanceService(vertxContext, okapiHeaders) - .createInstances(entity.getInstances(), true, false) + .createInstances(instances.getInstances(), true, false) .otherwise(cause -> respond500WithTextPlain(cause.getMessage())) .onComplete(asyncResultHandler); } diff --git a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java index ed78f7684..2b304d691 100644 --- a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java @@ -23,6 +23,7 @@ import org.folio.rest.jaxrs.model.Instance; import org.folio.rest.jaxrs.model.InstanceRelationship; import org.folio.rest.jaxrs.model.InstanceRelationships; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.MarcJson; import org.folio.rest.jaxrs.model.RetrieveDto; @@ -38,6 +39,7 @@ import org.folio.rest.tools.messages.Messages; import org.folio.rest.tools.utils.TenantTool; import org.folio.services.instance.InstanceService; +import org.folio.utils.InstanceUtils; public class InstanceStorageApi implements InstanceStorage { private static final Logger log = LogManager.getLogger(); @@ -209,13 +211,15 @@ public void getInstanceStorageInstances(String totalRecords, int offset, int lim @Override public void postInstanceStorageInstances( - Instance entity, + InstanceWithoutPubPeriod entity, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instance = InstanceUtils.copyPropertiesToInstance(entity); + new InstanceService(vertxContext, okapiHeaders) - .createInstance(entity) + .createInstance(instance) .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) .onFailure(handleFailure(asyncResultHandler)); } @@ -267,13 +271,15 @@ public void deleteInstanceStorageInstancesByInstanceId( public void putInstanceStorageInstancesByInstanceId( String instanceId, - Instance entity, + InstanceWithoutPubPeriod entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instance = InstanceUtils.copyPropertiesToInstance(entity); + new InstanceService(vertxContext, okapiHeaders) - .updateInstance(instanceId, entity) + .updateInstance(instanceId, instance) .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) .onFailure(handleFailure(asyncResultHandler)); } diff --git a/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java b/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java index 51594a4c3..dda94586c 100644 --- a/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java @@ -22,14 +22,15 @@ import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.Instance; -import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.InstancesBatchResponse; +import org.folio.rest.jaxrs.model.InstancesWithoutPubPeriod; import org.folio.rest.jaxrs.resource.InstanceStorageBatchInstances; import org.folio.rest.persist.PgUtil; import org.folio.rest.persist.PostgresClient; import org.folio.rest.support.HridManager; import org.folio.rest.tools.utils.MetadataUtil; import org.folio.services.domainevent.InstanceDomainEventPublisher; +import org.folio.utils.InstanceUtils; @SuppressWarnings("rawtypes") public class InstanceStorageBatchApi implements InstanceStorageBatchInstances { @@ -44,13 +45,15 @@ public class InstanceStorageBatchApi implements InstanceStorageBatchInstances { @Validate @Override - public void postInstanceStorageBatchInstances(Instances entity, + public void postInstanceStorageBatchInstances(InstancesWithoutPubPeriod entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instances = InstanceUtils.copyPropertiesToInstances(entity.getInstances()); + final String statusUpdatedDate = generateStatusUpdatedDate(); - for (Instance instance : entity.getInstances()) { + for (Instance instance : instances.getInstances()) { instance.setStatusUpdatedDate(statusUpdatedDate); } @@ -60,9 +63,9 @@ public void postInstanceStorageBatchInstances(Instances entity, final InstanceDomainEventPublisher instanceDomainEventPublisher = new InstanceDomainEventPublisher(vertxContext, okapiHeaders); - MetadataUtil.populateMetadata(entity.getInstances(), okapiHeaders); - executeInBatch(entity.getInstances(), - instances -> saveInstances(instances, postgresClient)) + MetadataUtil.populateMetadata(instances.getInstances(), okapiHeaders); + executeInBatch(instances.getInstances(), + instanceList -> saveInstances(instanceList, postgresClient)) .onComplete(ar -> { InstancesBatchResponse response = constructResponse(ar.result()); diff --git a/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java b/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java index 44d5c5c83..d5ed2a3a9 100644 --- a/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java +++ b/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java @@ -31,7 +31,9 @@ public final class AsyncMigrationJobService { private static final List MIGRATION_JOB_RUNNERS = List - .of(new ShelvingOrderMigrationJobRunner(), new SubjectSeriesMigrationJobRunner()); + .of(new ShelvingOrderMigrationJobRunner(), + new SubjectSeriesMigrationJobRunner(), + new PublicationPeriodMigrationJobRunner()); private static final List ACCEPTABLE_STATUSES = List .of(AsyncMigrationJob.JobStatus.IN_PROGRESS, IDS_PUBLISHED); diff --git a/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java b/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java index 4d398840d..107792b17 100644 --- a/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java +++ b/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java @@ -42,7 +42,8 @@ public static Handler> pollAsyncMigrati var availableMigrations = Set.of( new ShelvingOrderAsyncMigrationService(vertxContext, headers), - new SubjectSeriesMigrationService(vertxContext, headers)); + new SubjectSeriesMigrationService(vertxContext, headers), + new PublicationPeriodMigrationService(vertxContext, headers)); var jobService = new AsyncMigrationJobService(vertxContext, headers); var migrationEvents = buildIdsForMigrations(v.getValue()); diff --git a/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java new file mode 100644 index 000000000..2463b22e4 --- /dev/null +++ b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java @@ -0,0 +1,36 @@ +package org.folio.services.migration.async; + +import static java.lang.String.format; +import static org.folio.persist.InstanceRepository.INSTANCE_TABLE; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowStream; +import java.util.Collections; +import java.util.List; +import org.folio.rest.jaxrs.model.AffectedEntity; +import org.folio.rest.persist.PostgresClientFuturized; +import org.folio.rest.persist.SQLConnection; + +public class PublicationPeriodMigrationJobRunner extends AbstractAsyncMigrationJobRunner { + + private static final String SELECT_SQL = """ + SELECT id FROM %s + WHERE jsonb -> 'publicationPeriod' IS NOT NULL + """; + + @Override + protected Future> openStream(PostgresClientFuturized postgresClient, SQLConnection connection) { + return postgresClient.selectStream(connection, format(SELECT_SQL, postgresClient.getFullTableName(INSTANCE_TABLE))); + } + + @Override + public String getMigrationName() { + return "publicationPeriodMigration"; + } + + @Override + public List getAffectedEntities() { + return Collections.singletonList(AffectedEntity.INSTANCE); + } +} diff --git a/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java new file mode 100644 index 000000000..aec94e1df --- /dev/null +++ b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java @@ -0,0 +1,77 @@ +package org.folio.services.migration.async; + +import static org.folio.persist.InstanceRepository.INSTANCE_TABLE; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowStream; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.folio.persist.InstanceRepository; +import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.persist.PgUtil; +import org.folio.rest.persist.PostgresClientFuturized; +import org.folio.rest.persist.SQLConnection; + +public class PublicationPeriodMigrationService extends AsyncBaseMigrationService { + + private static final String SELECT_SQL = """ + SELECT migrate_publication_period(jsonb) as jsonb + FROM %s + WHERE %s FOR UPDATE + """; + private static final String WHERE_CONDITION = "id in (%s)"; + + private final PostgresClientFuturized postgresClient; + private final InstanceRepository instanceRepository; + + + public PublicationPeriodMigrationService(Context context, Map okapiHeaders) { + this(new PostgresClientFuturized(PgUtil.postgresClient(context, okapiHeaders)), + new InstanceRepository(context, okapiHeaders)); + } + + public PublicationPeriodMigrationService(PostgresClientFuturized postgresClient, + InstanceRepository instanceRepository) { + + super("28.0.0", postgresClient); + this.postgresClient = postgresClient; + this.instanceRepository = instanceRepository; + } + + @Override + protected Future> openStream(SQLConnection connection) { + return postgresClient.selectStream(connection, selectSql()); + } + + @Override + protected Future updateBatch(List batch, SQLConnection connection) { + var instances = batch.stream() + .map(row -> row.getJsonObject("jsonb")) + .map(json -> json.mapTo(Instance.class)) + .toList(); + return instanceRepository.updateBatch(instances, connection) + .map(notUsed -> instances.size()); + } + + @Override + public String getMigrationName() { + return "publicationPeriodMigration"; + } + + private String selectSql() { + var idsForMigration = getIdsForMigration(); + var whereCondition = "false"; + + if (!idsForMigration.isEmpty()) { + var ids = idsForMigration.stream() + .map(id -> "'" + id + "'") + .collect(Collectors.joining(", ")); + + whereCondition = String.format(WHERE_CONDITION, ids); + } + return String.format(SELECT_SQL, postgresClient.getFullTableName(INSTANCE_TABLE), whereCondition); + } +} diff --git a/src/main/java/org/folio/utils/InstanceUtils.java b/src/main/java/org/folio/utils/InstanceUtils.java new file mode 100644 index 000000000..21a9fa8f4 --- /dev/null +++ b/src/main/java/org/folio/utils/InstanceUtils.java @@ -0,0 +1,40 @@ +package org.folio.utils; + +import java.util.List; +import org.apache.commons.beanutils.BeanUtils; +import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; +import org.folio.rest.jaxrs.model.Instances; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class InstanceUtils { + + private static final Logger log = LoggerFactory.getLogger(InstanceUtils.class); + + private InstanceUtils() { + throw new UnsupportedOperationException("Utility class"); + } + + public static Instances copyPropertiesToInstances(List instancesWithoutPubPeriod) { + var instances = new Instances(); + instances.setInstances(instancesWithoutPubPeriod + .stream() + .map(InstanceUtils::copyPropertiesToInstance) + .toList()); + return instances; + } + + public static Instance copyPropertiesToInstance(InstanceWithoutPubPeriod instanceWithoutPubPeriod) { + var instance = new Instance(); + try { + log.debug("copyPropertiesToInstance:: Copy all fields from InstanceWithoutPubPeriod to Instance, id: '{}'", + instanceWithoutPubPeriod.getId()); + BeanUtils.copyProperties(instance, instanceWithoutPubPeriod); + return instance; + } catch (Exception e) { + log.error("Failed to copy properties from InstanceWithoutPubPeriod to Instance object: {}", e.getMessage(), e); + throw new IllegalArgumentException(e.getMessage(), e); + } + } +} diff --git a/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql index 582795944..a2f1943f0 100644 --- a/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql +++ b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql @@ -41,89 +41,3 @@ BEGIN 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 e3cd3a41b..e2c76f3e1 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -1227,6 +1227,11 @@ "run": "after", "snippetPath": "subjectIdsReferenceCheckTrigger.sql", "fromModuleVersion": "27.2.0" + }, + { + "run": "after", + "snippetPath": "publication-period/migratePublicationPeriod.sql", + "fromModuleVersion": "28.0.0" } ] } diff --git a/src/test/java/org/folio/rest/api/AsyncMigrationTest.java b/src/test/java/org/folio/rest/api/AsyncMigrationTest.java index e681a12c5..5cf304727 100644 --- a/src/test/java/org/folio/rest/api/AsyncMigrationTest.java +++ b/src/test/java/org/folio/rest/api/AsyncMigrationTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; @@ -26,14 +27,18 @@ import io.vertx.core.Context; import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import junitparams.JUnitParamsRunner; +import lombok.SneakyThrows; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.folio.persist.AsyncMigrationJobRepository; import org.folio.rest.jaxrs.model.AsyncMigrationJob; @@ -43,18 +48,36 @@ import org.folio.rest.jaxrs.model.EffectiveCallNumberComponents; import org.folio.rest.jaxrs.model.Processed; import org.folio.rest.jaxrs.model.Published; +import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.PostgresClientFuturized; import org.folio.rest.support.sql.TestRowStream; import org.folio.services.migration.async.AsyncMigrationContext; import org.folio.services.migration.async.ShelvingOrderMigrationJobRunner; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(JUnitParamsRunner.class) public class AsyncMigrationTest extends TestBaseWithInventoryUtil { + private static final String UPDATE_JSONB_WITH_PUB_PERIOD = """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = jsonb || jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', '1999', 'end', '2001')) + RETURNING id::text; + """; + private static final String SELECT_JSONB = + "SELECT jsonb FROM %s_mod_inventory_storage.instance"; + private final AsyncMigrationJobRepository repository = getRepository(); + @SneakyThrows + @Before + public void beforeEach() { + StorageTestSuite.deleteAll(itemsStorageUrl("")); + StorageTestSuite.deleteAll(holdingsStorageUrl("")); + StorageTestSuite.deleteAll(instancesStorageUrl("")); + } + private static Map okapiHeaders() { return new CaseInsensitiveMap<>(Map.of(TENANT.toLowerCase(), TENANT_ID)); } @@ -138,11 +161,66 @@ public void canMigrateInstanceSubjectsAndSeries() { assertThat(job.getSubmittedDate(), notNullValue()); } + @Test + public void canMigrateInstancePublicationPeriod() { + var numberOfRecords = 10; + + IntStream.range(0, numberOfRecords).parallel().forEach(v -> + instancesClient.create(new JsonObject() + .put("title", "test" + v) + .put("source", "MARC") + .put("instanceTypeId", "30fffe0e-e985-4144-b2e2-1e8179bdb41f"))); + + var countDownLatch = new CountDownLatch(1); + var query = String.format(UPDATE_JSONB_WITH_PUB_PERIOD, TENANT_ID); + postgresClient(getContext(), okapiHeaders()).execute(query) + .onSuccess(event -> countDownLatch.countDown()); + // check jsonb contains 'publicationPeriod' data + RowSet selectResult = runSql(String.format(SELECT_JSONB, TENANT_ID)); + + assertEquals(10, selectResult.rowCount()); + JsonObject jsonbData = selectResult.iterator().next().toJson().getJsonObject("jsonb"); + assertNull(jsonbData.getJsonObject("dates")); + assertNotNull(jsonbData.getJsonObject("publicationPeriod")); + + + await().atMost(5, SECONDS).until(() -> countDownLatch.getCount() == 0L); + + var migrationJob = asyncMigration.postMigrationJob(new AsyncMigrationJobRequest() + .withMigrations(List.of("publicationPeriodMigration"))); + + await().atMost(25, SECONDS).until(() -> asyncMigration.getMigrationJob(migrationJob.getId()) + .getJobStatus() == AsyncMigrationJob.JobStatus.COMPLETED); + + var job = asyncMigration.getMigrationJob(migrationJob.getId()); + + assertThat(job.getPublished().stream().map(Published::getCount) + .mapToInt(Integer::intValue).sum(), is(numberOfRecords)); + assertThat(job.getProcessed().stream().map(Processed::getCount) + .mapToInt(Integer::intValue).sum(), is(numberOfRecords)); + assertThat(job.getJobStatus(), is(AsyncMigrationJob.JobStatus.COMPLETED)); + assertThat(job.getSubmittedDate(), notNullValue()); + + // check that the 'publicationPeriod' data has been migrated to the 'dates' data + var selectQuery = String.format(SELECT_JSONB, TENANT_ID); + RowSet result = runSql(selectQuery); + + assertEquals(10, result.rowCount()); + JsonObject entry = result.iterator().next().toJson(); + JsonObject jsonb = entry.getJsonObject("jsonb"); + JsonObject dates = jsonb.getJsonObject("dates"); + assertNotNull(dates); + assertNull(jsonb.getString("publicationPeriod")); + assertEquals("1999", dates.getString("date1")); + assertEquals("2001", dates.getString("date2")); + assertEquals("8fa6d067-41ff-4362-96a0-96b16ddce267", dates.getString("dateTypeId")); + } + @Test public void canGetAvailableMigrations() { AsyncMigrations migrations = asyncMigration.getMigrations(); assertNotNull(migrations); - assertEquals(Integer.valueOf(2), migrations.getTotalRecords()); + assertEquals(Integer.valueOf(3), migrations.getTotalRecords()); assertEquals("itemShelvingOrderMigration", migrations.getAsyncMigrations().get(0).getMigrations().get(0)); } @@ -192,4 +270,13 @@ private ShelvingOrderMigrationJobRunner jobRunner() { private AsyncMigrationJobRepository getRepository() { return new AsyncMigrationJobRepository(getContext(), okapiHeaders()); } + + @SneakyThrows + private RowSet runSql(String sql) { + return PostgresClient.getInstance(getVertx()) + .execute(sql) + .toCompletionStage() + .toCompletableFuture() + .get(TIMEOUT, TimeUnit.SECONDS); + } } diff --git a/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java index f66d4b087..ce7889134 100644 --- a/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java +++ b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java @@ -18,11 +18,8 @@ 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"; @@ -40,6 +37,11 @@ public class PublicationPeriodMigrationTest extends MigrationTestBase { SET jsonb = jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', $1)) WHERE id = $2 """; + private static final String UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION= """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = %s_mod_inventory_storage.migrate_publication_period(jsonb) + WHERE id = $1 AND jsonb -> 'publicationPeriod' IS NOT NULL + """; @SneakyThrows @Before @@ -49,14 +51,15 @@ public void beforeEach() { } @Test - public void canMigratePublicationPeriodToMultipleDates() throws Exception { + public void canMigratePublicationPeriodToMultipleDates() { var instanceId = createInstance(); // add "publicationPeriod" object to jsonb addPublicationPeriodToJsonb(instanceId, END_DATE); //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); @@ -71,14 +74,15 @@ public void canMigratePublicationPeriodToMultipleDates() throws Exception { } @Test - public void canMigratePublicationPeriodToSingleDates() throws Exception { + public void canMigratePublicationPeriodToSingleDates() { var instanceId = createInstance(); // add "publicationPeriod" object to jsonb addPublicationPeriodToJsonb(instanceId, null); //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); @@ -93,11 +97,11 @@ public void canMigratePublicationPeriodToSingleDates() throws Exception { } @Test - public void canNotMigrateWhenPublicationPeriodIsNull() throws Exception { + public void canNotMigrateWhenPublicationPeriodIsNull() { var instanceId = createInstance(); - //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); diff --git a/src/test/java/org/folio/services/InstanceUtilsTest.java b/src/test/java/org/folio/services/InstanceUtilsTest.java new file mode 100644 index 000000000..a719b7911 --- /dev/null +++ b/src/test/java/org/folio/services/InstanceUtilsTest.java @@ -0,0 +1,51 @@ +package org.folio.services; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import lombok.SneakyThrows; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; +import org.folio.utils.InstanceUtils; +import org.junit.jupiter.api.Test; + +class InstanceUtilsTest { + + private static final String TITLE = "title"; + private static final String ID = "123456789"; + + @Test + void shouldCopyPropertiesToInstances() { + var instanceWithoutPubPeriod = new InstanceWithoutPubPeriod(); + instanceWithoutPubPeriod.setId(ID); + instanceWithoutPubPeriod.setTitle(TITLE); + + var result = InstanceUtils.copyPropertiesToInstances(List.of(instanceWithoutPubPeriod)); + var instances = result.getInstances(); + + assertNotNull(instances); + assertEquals(1, instances.size()); + assertEquals(instanceWithoutPubPeriod.getId(), instances.get(0).getId()); + assertEquals(instanceWithoutPubPeriod.getTitle(), instances.get(0).getTitle()); + } + + @Test + void shouldCopyPropertiesToInstance() { + var instanceWithoutPubPeriod = new InstanceWithoutPubPeriod(); + instanceWithoutPubPeriod.setId(ID); + instanceWithoutPubPeriod.setTitle(TITLE); + + var result = InstanceUtils.copyPropertiesToInstance(instanceWithoutPubPeriod); + + assertNotNull(result); + assertEquals(instanceWithoutPubPeriod.getId(), result.getId()); + assertEquals(instanceWithoutPubPeriod.getTitle(), result.getTitle()); + } + + @Test + @SneakyThrows + void shouldThrowIllegalArgumentExceptionWhenCannotCopyPropertiesToInstance() { + assertThrows(IllegalArgumentException.class, () -> InstanceUtils.copyPropertiesToInstance(null)); + } +}