Spike: MODKBEKBJ-390 - describe design for "Single tenant multiple EBSCO KBs" scenario

MODKBEKBJ-390 - Getting issue details... STATUS

Participants:

RoleNameApproval
Solution Architect(tick)
Java Lead(tick)
Product Owner

Spike points:

Storing EBSCO KB Credentials

Existing Solution: 

Currently, mod-configuration stores EBSCO KB Credentials as 3 separate config files, such as follows

KB Credentials API Key
{
  "id": "b5dbb466-f941-4ef0-9a8f-ff6003dbb5eb",
  "code": "kb.ebsco.apiKey",
  "value": "XXXX",
  "module": "EKB",
  "enabled": true,
  "metadata": {
    "createdDate": "2020-03-17T15:22:04.098",
    "updatedDate": "2020-03-17T15:22:04.098+0000",
    "createdByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b",
    "updatedByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b"
  },
  "configName": "api_access",
  "description": "EBSCO RM-API API Key"
}
KB Credentials Url
{
  "id": "9ef1c0c9-b774-4a1d-9f3f-1399e9a0292e",
  "code": "kb.ebsco.url",
  "value": "https://test.url",
  "module": "EKB",
  "enabled": true,
  "metadata": {
    "createdDate": "2020-03-17T15:22:04.052",
    "updatedDate": "2020-03-17T15:22:04.052+0000",
    "createdByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b",
    "updatedByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b"
  },
  "configName": "api_access",
  "description": "EBSCO RM-API URL"
}
KB Credentials Customer Id
{
  "id": "ba80d8c1-a6c5-4df0-9b32-b35960c3a68b",
  "code": "kb.ebsco.customerId",
  "value": "test",
  "module": "EKB",
  "enabled": true,
  "metadata": {
    "createdDate": "2020-03-17T15:22:04.154",
    "updatedDate": "2020-03-17T15:22:04.154+0000",
    "createdByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b",
    "updatedByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b"
  },
  "configName": "api_access",
  "description": "EBSCO RM-API Customer ID"
}

Proposed Solution:

  • move configuration from mod-configuration to mod-kb-ebsco-java
  • store configuration information(apiKey, url, customerId) as one entity in db. DB schema,  json schema and sample payload file can be found below.

current implementation assumed that there is no permission distinction between users and particular credentials. This means the following:

  • in case system has 3 credentials (Hampshire, Amherst, Smith) 
  • if John has permission to edit KB credentials - he can edit credentials for all (Hampshire, Amherst, Smith) knowledge bases. This applies to all CRUD operations.


MethodPathPermission(s)ResponseNote(s)
GET/eholdings/kb-credentialskb-ebsco.credentials.collection.get200 OK(with collection)
POST/eholdings/kb-credentialskb-ebsco.credentials.collection.post201 Created(with record)
GET/eholdings/kb-credentials/{credId}kb-ebsco.credentials.item.get200 OK(with record){credId} - UUID of the credential
PUT/eholdings/kb-credentials/{credId}kb-ebsco.credentials.item.put204 No Content{credId} - UUID of the credential
DELETE/eholdings/kb-credentials/{credId}kb-ebsco.credentials.item.delete204 No Content

{credId} - UUID of the credential

in case of credentials deletion users, already assigned to it, will be deleted. 

The appropriate files attached below:

DB schema

Json Example
KB Credentials JSON example
{
  "id": "2ffa1940-2cf6-48b1-8cc9-5e539c61d93f",
  "type": "kbCredentials",
  "attributes": {
    "name": "University of Massachusetts - Amherst",
    "apiKey": "XXXX",
    "url": "YYYY",
    "customerId": "ZZZZ"
  },
  "metadata": {
    "createdDate": "2020-03-17T15:22:04.098",
    "updatedDate": "2020-03-17T15:23:04.098+0000",
    "createdByUserId": "1f8f660e-7dc9-4f6f-828f-96284c68a250",
    "updatedByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b",
    "createdByUsername": "john_doe",
    "updatedByUsername": "jane_doe"
  }
}
  • property "name" proposed to have unique value
  • property "customerId" assumed to have a unique value (in case of shared credentials will be duplicated). Back-end will not perform any check on that.
Json schema
 JSON schema 
KB Credentials JSON schema 
{
  "$schema": "http://json-schema.org/draft-04/schema",
  "type": "object",
  "title": "The Root Schema",
  "description": "The root schema comprises the entire JSON document.",
  "required": [
    "id",
    "type",
    "attributes"
  ],
  "properties": {
    "id": {
      "type": "string",
      "title": "The UUID of entry",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "examples": "2ffa1940-2cf6-48b1-8cc9-5e539c61d93f"
    },
    "type": {
      "type": "string",
      "title": "The Type Schema",
      "examples": ["kbCredentials"]
    },
    "attributes": {
      "type": "object",
      "title": "The Attributes Schema",
      "examples": [
        {
          "url": "YYYY",
          "customerId": "ZZZZ",
          "name": "University of Massachusetts - Amherst",
          "apiKey": "XXXX"
        }
      ],
      "required": [
        "name",
        "apiKey",
        "url",
        "customerId"
      ],
      "properties": {
        "name": {
          "type": "string",
          "title": "The Unique Name of the credentials",
          "examples": ["University of Massachusetts - Amherst"]
        },
        "apiKey": {
          "type": "string",
          "title": "The Apikey Schema",
          "examples": ["XXXX"]
        },
        "url": {
          "type": "string",
          "title": "The Url Schema",
          "examples": ["YYYY"]
        },
        "customerId": {
          "type": "string",
          "title": "The Unique Customer id",
          "examples": ["ZZZZ"]
        }
      }
    },
    "metadata": {
      "type": "object",
      "$ref" : "../metadata.schema",
      "readonly": true,
      "examples": [
        {
          "updatedByUserId": "6893f51f-b40c-479d-bd78-1704ab5b802b",
          "createdByUserId": "1f8f660e-7dc9-4f6f-828f-96284c68a250",
          "updatedDate": "2020-03-17T15:23:04.098+0000",
          "createdDate": "2020-03-17T15:22:04.098",
          "createdByUsername": "john_doe",
          "updatedByUsername": "jane_doe"
        }
      ]
    }
  }
}

Configuration cache

kb-ebsco-java sequence diagram

codex-ekb sequence diagram

UI mockups


Storing association between user and settings(EBSCO KB Credentials)


Proposed Solution:

MethodPathPermission(s)ResponseNote(s)
GET/eholdings/kb-credentials/{credId}/userskb-ebsco.credentials.users.collection.get200 OK(with collection){credId} - UUID of the credential
POST/eholdings/kb-credentials/{credId}/userskb-ebsco.credentials.users.collection.post201 Created(with record){credId} - UUID of the credential
PUT/eholdings/kb-credentials/{credId}/users/{userId}kb-ebsco.credentials.users.item.put204 No Content

{credId} - UUID of the credential

{userId) - UUID of a user

DELETE/eholdings/kb-credentials/{credId}/users/{userId}kb-ebsco.credentials.users.item.delete204 No Content

{credId} - UUID of the credential

{userId) - UUID of a user

The appropriate files attached below:

DB schema

It means that we have one-to-many relationship between users and credentials. Below we can see a simplified example of storing such relationship

kb_credentials
idname
2ffa1940-2cf6-48b1-8cc9-5e539c61d93fUniversity of Massachusetts - Amherst
09db24ba-a562-42c3-acf6-325efaf34958Hampshire
assigned_user 
2ffa1940-2cf6-48b1-8cc9-5e539c61d93fjohn_doe
2ffa1940-2cf6-48b1-8cc9-5e539c61d93fjane_doe
09db24ba-a562-42c3-acf6-325efaf34958aleen_braun

It means that:

  • john_doe and jane_doe belong to a University of Massachusetts - Amherst
  • aleen_braun to Hampshire

Restrictions

  • user can be assigned to one KB credential only

Json Example
 GET collection
/eholdings/kb-credentials/{credId}/users
{
  "data": [
    {
      "id": "1f8f660e-7dc9-4f6f-828f-96284c68a25",
      "type": "assignedUsers",
      "attributes": {
        "credentialsId": "2ffa1940-2cf6-48b1-8cc9-5e539c61d93f",
        "firstName": "John",
        "middleName": "William",
        "lastName": "Doe",
        "patronGroup": "Staff",
        "userName": "john_doe"
      }
    },
    {
   	  "id": "6893f51f-b40c-479d-bd78-1704ab5b802b",
      "type": "assignedUsers",
      "attributes": {
        "credentialsId": "2ffa1940-2cf6-48b1-8cc9-5e539c61d93f",
        "firstName": "Jane",
        "middleName": "Rosemary",
        "lastName": "Doe",
        "patronGroup": "Staff",
        "userName": "jane_doe"
      }
    }
  ],
  "meta": {
    "totalResults": 2
  },
  "jsonapi": {
    "version": "1.0"
  }
}
  • array of users should not have duplicates
 POST collection item
/eholdings/kb-credentials/{credId}/users
{
 "user":
	{
	  "id": "1f8f660e-7dc9-4f6f-828f-96284c68a25",
	  "credentialsId": "2ffa1940-2cf6-48b1-8cc9-5e539c61d93f",	
	  "firstName": "John",
	  "middleName": "William",
	  "lastName": "Doe",
	  "patronGroup": "Staff",
	  "userName": "john_doe"
	}
}
Json schema
 GET collection schema
{
    "$schema": "http://json-schema.org/draft-04/schema",
    "type": "object",
    "title": "The Root Schema",
    "properties": {
        "credentialsId": {
            "type": "string",
            "title": "The Credentials Id Schema",
			"$ref": "../../raml-util/schemas/uuid.schema",
            "examples": ["2ffa1940-2cf6-48b1-8cc9-5e539c61d93f"]
        },
        "users": {
            "type": "array",
			"uniqueItems": true,
            "title": "The Assignment users Schema",
            "items": {
                "type": "object",
                "title": "The Items Schema",
                "examples": [
                    {
						"id": "1f8f660e-7dc9-4f6f-828f-96284c68a25",
                        "userName": "john_doe",
						"firstName": "John",
						"middleName": "William",
                        "lastName": "Doe",
                        "patronGroup": "Staff"
                    },
   					{
      					"id": "6893f51f-b40c-479d-bd78-1704ab5b802b",
						"userName": "jane_doe",
	  					"firstName": "Jane",
	  					"middleName": "Rosemary",
	  					"lastName": "Doe",
	  					"patronGroup": "Staff"
  					}
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "title": "The Id Schema",
						"$ref": "../../raml-util/schemas/uuid.schema",
                        "examples": ["1f8f660e-7dc9-4f6f-828f-96284c68a25"]
                    },
                    "firstName": {
                        "type": "string",
                        "title": "The Firstname Schema",
                        "examples": ["John"]
                    },
                    "middleName": {
                        "type": "string",
                        "title": "The Middlename Schema",
                        "examples": ["William"]
                    },
                    "lastName": {
                        "type": "string",
                        "title": "The Lastname Schema",
                        "examples": ["Doe"]
                    },
                    "patronGroup": {
                        "type": "string",
                        "title": "The Patrongroup Schema",
                        "examples": ["Staff"]
                    },
                    "userName": {
                        "type": "string",
                        "title": "The Username Schema",
                        "examples": ["john_doe"]
                    }
                },
                "required": [
                    "id",
                    "firstName",
                    "middleName",
                    "lastName",
                    "patronGroup",
                    "userName"
                ]
            }
        }
    },
    "required": [
        "credentialsId",
        "users"
    ]
}

UI mockups

Update mapping between credentials and user

(error) Option #1 - add PUT collection endpoint

  • UI sends full collection of users assigned to a credential(see json example above) to PUT /eholdings/kb-credentials/{credId}/users 
  • back-end selects records tied to credential and removes them
  • back-end inserts new records based on data received

(tick) Option #2 -  use separate endpoints 

  • on each update (assignment user to a credential) UI will send request to PUT /eholdings/kb-credentials/{credId}/users/{userId}
    • back-end adds record based on credential and user id
  • on each removal (unassignment user from a credential) UI will send request to DELETE /eholdings/kb-credentials/{credId}/users/{userId}
    • back-end removes record based on credential and user id


Summary

Option #2 has been chosen since it's easy to implement and it follows more traditional pattern

Association between KB Credentials and access status types

To implement the association between KB Credentials and access status types several changes should be made:

  • DB schema


It means that we have 

  • one-to-many relationship between kb_credentials and access_types
  • one-to-many relationship between access_types and access_types_mapping

This approach will provide the ability to separate assignment-data between each KB credentials

  • Endpoints

MethodOld endpointNew endpointRequest bodyResponse status Response body
GET/eholdings/access-types/eholdings/kb-credentials/{credId}/access-types
200 OK
 Response
Example
{
  "data": [
    {
      "id": "77e8b4b1-81d6-4030-892e-29e4bceaf862",
	  "credentialsId": "{credId}",
      "type": "accessTypes",
      "attributes": {
        "name": "Trial",
        "description": "some description"
      },
      "creator": {
        "lastName": "John",
        "firstName": "Dough"
      },
      "usageNumber": 1,
      "metadata": {
        "createdDate": "2020-03-13T14:39:04.663+0000",
        "createdByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065",
        "createdByUsername": "diku_admin",
        "updatedDate": "2020-03-13T14:39:04.663+0000",
        "updatedByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065"
      }
    },
    {
      "id": "63f35ca5-317f-49e5-9404-9ec8e25b28a4",
      "type": "accessTypes",
      "attributes": {
        "name": "Subscribed",
        "description": "some description"
      },
      "creator": {
        "lastName": "John",
        "firstName": "Dough"
      },
      "usageNumber": 0,
      "metadata": {
        "createdDate": "2020-03-13T14:44:36.072+0000",
        "createdByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065",
        "createdByUsername": "dough",
        "updatedDate": "2020-03-13T14:44:36.072+0000",
        "updatedByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065"
      }
    }
  ],
  "meta": {
    "totalResults": 2
  },
  "jsonapi": {
    "version": "1.0"
  }
}
POST/eholdings/access-types/eholdings/kb-credentials/{credId}/access-types
 Request
Example
{
  "type": "accessTypes",
  "attributes": {
    "name": "Subscribed",
    "description": "Indicates subscription of the entity"
  }
}
201 Created(with record)
 Response
Example
{
  "id": "{randomUUID}",
  "credentialsId": "{credId}",
  "type": "accessTypes",
  "attributes": {
    "name": "Subscribed",
    "description": "Indicates subscription of the entity"
  },
  "creator": {
    "lastName": "John",
    "firstName": "Dough"
  },
  "usageNumber": 0,
  "metadata": {
    "createdDate": "2020-03-13T14:39:04.663+0000",
    "createdByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065",
    "createdByUsername": "dough",
    "updatedDate": "2020-03-13T14:39:04.663+0000",
    "updatedByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065"
  }
}
GET/eholdings/access-types/{atId}/eholdings/kb-credentials/{credId}/access-types/{atId}
200 OK
 Response
Example
{
  "id": "{atId}",
  "credentialsId": "{credId}",
  "type": "accessTypes",
  "attributes": {
    "name": "Trial",
    "description": "some description"
  },
  "creator": {
    "lastName": "John",
    "firstName": "Dough"
  },
  "usageNumber": 0,
  "metadata": {
    "createdDate": "2020-03-13T14:39:04.663+0000",
    "createdByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065",
    "createdByUsername": "dough",
    "updatedDate": "2020-03-13T14:39:04.663+0000",
    "updatedByUserId": "97da18d8-dda7-5cac-84d6-b393bea2b065"
  }
}
PUT/eholdings/access-types/{atId}/eholdings/kb-credentials/{credId}/access-types/{atId}
 Request
Example
{
  "type": "accessTypes",
  "attributes": {
    "name": "Subscribed updated",
    "description": "Indicates subscription of the entity"
  }
}
204 No Content
DELETE/eholdings/access-types/{atId}/eholdings/kb-credentials/{credId}/access-types/{atId}
204 No Content

JSON schemas that will be used (changes will be affected only for Access Types Collection Item Schema):

 Access Types Collection Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Access Types Collection Schema",
  "description": "Access Types Collection Schema",
  "javaType": "org.folio.rest.jaxrs.model.AccessTypeCollection",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "type": "array",
      "description": "List of access types",
      "items": {
        "type": "object",
        "$ref": "accessTypeCollectionItem.json"
      }
    },
    "meta": {
      "type": "object",
      "description": "metadata containing total results in collection",
      "$ref": "../metaTotalResults.json",
      "readonly": true
    },
    "jsonapi": {
      "type": "object",
      "description": "version of json api",
      "$ref": "../jsonapi.json"
    }
  },
  "required": ["data", "meta", "jsonapi"]
}
 Access Types Collection Item Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Access Types Collection Item Schema",
  "description": "Access Types Collection Item Schema",
  "javaType": "org.folio.rest.jaxrs.model.AccessTypeCollectionItem",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "UUID of access type",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "example": "f973c3b6-85fc-4d35-bda8-f31b568957bf"
    },
    "type": {
      "type": "string",
      "description": "Type of resource",
      "enum": ["accessTypes"],
      "example": "accessTypes"
    },
    "attributes": {
      "type": "object",
      "description": "Custom label object data attributes",
      "$ref": "accessTypeDataAttributes.json"
    },
    "credentialsId": {
      "type": "string",
      "description": "UUID of KB credentials",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "example": "a173c3b6-134d-4d35-bda8-f31bfwd957bf"
    },
    "usageNumber": {
      "type": "integer",
      "description": "Number of records that use the access type",
      "readonly": true
    },
    "creator": {
      "type": "object",
      "description": "User display info for creator of the note",
      "$ref": "userDisplayInfo.json",
      "readonly": true
    },
    "updater": {
      "type": "object",
      "description": "User display info for updater of the note",
      "$ref": "userDisplayInfo.json",
      "readonly": true
    },
    "metadata": {
      "type": "object",
      "description": "Metadata for the entity",
      "$ref": "../../raml-util/schemas/metadata.schema",
      "readonly": true
    }
  },
  "required": [
    "type",
    "attributes"
  ]
}
 Access Types Collection Item Data Attributes Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Access Types Collection Item Data Attributes Schema",
  "description": "Access Types Collection Item Data Attributes Schema",
  "javaType": "org.folio.rest.jaxrs.model.AccessTypeDataAttributes",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "name": {
      "type": "string",
      "description": "Access Type Name",
      "example": "Subscribed"
    },
    "description": {
      "type": "string",
      "description": "Access Type Description",
      "example": "Indicates subscription of the entity"
    }
  },
  "required": ["name"]
}

 User Display Information
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "description": "User Display Information",
  "javaType": "org.folio.rest.jaxrs.model.UserDisplayInfo",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "lastName": {
      "type": "string",
      "description": "Last name of the user",
      "example": "Doe",
      "readonly": true
    },
    "firstName": {
      "type": "string",
      "description": "First name of the user",
      "example": "John",
      "readonly": true
    },
    "middleName": {
      "type": "string",
      "description": "Middle name or initial of the user",
      "example": "X.",
      "readonly": true
    }
  }
}
  • Access status type to record (package, resource) mapping and filtering implementation

Endpoints that will be affected:

MethodEndpointActionNotes
GET/eholdings/packagesfiltering

Most interactions with DB must be updated.

In most cases, we need to update only 'SELECT ... FROM access_types' SQL-queries to get ids of access status types that will be used in queries to access_types_mapping 



SELECT ...
  FROM access_types
 WHERE credentialsId = ? AND ...;



In other cases SQL-queries need to be updated by joining access_types_mapping and access_types tables: 

SELECT ...
  FROM access_types_mapping atp
  JOIN access_types at
    ON at.id = atp.accessTypeId
 WHERE ... AND at.credentialsId = ?;
POST/eholdings/packagesmapping
PUT/eholdings/packages/{packageId}mapping
GET/eholdings/packages/{packageId}/resourcesfiltering
PUT/eholdings/resources/{resourceId}mapping
GET/eholdings/providers/{providerId}/packagesfiltering
GET/eholdings/titlesfiltering

Association between KB Credentials and custom labels


Custom labels are stored on the RM API side, so in this case, we don't have to change DB schema for custom labels.

Endpoints are needed to be changed

MethodOld endpointNew endpointRequest bodyResponse status Response body
GET/eholdings/custom-labels/eholdings/kb-credentials/{credId}/custom-labels
200 OK
 Response
Example
{
  "data": [
    {
      "type": "customLabel",
      "credentialsId": "{credId}",
      "attributes": {
        "id": 1,
        "displayLabel": "Department",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    },
    {
      "type": "customLabel",
      "credentialsId": "{credId}",
      "attributes": {
        "id": 2,
        "displayLabel": "Faculty",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    }
  ],
  "meta": {
    "totalResults": 2
  },
  "jsonapi": {
    "version": "1.0"
  }
}
PUT/eholdings/custom-labels/eholdings/kb-credentials/{credId}/custom-labels
 Request
Example
{
  "data": [
    {
      "type": "customLabel",
      "attributes": {
        "id": 1,
        "displayLabel": "Department",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    },
    {
      "type": "customLabel",
      "attributes": {
        "id": 2,
        "displayLabel": "Faculty",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    }
  ]
}
200 OK
 Response
Example
{
  "data": [
    {
      "type": "customLabel",
      "credentialsId": "{credId}",
      "attributes": {
        "id": 1,
        "displayLabel": "Department",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    },
    {
      "type": "customLabel",
      "credentialsId": "{credId}",
      "attributes": {
        "id": 2,
        "displayLabel": "Faculty",
        "displayOnFullTextFinder": false,
        "displayOnPublicationFinder": false
      }
    }
  ],
  "meta": {
    "totalResults": 2
  },
  "jsonapi": {
    "version": "1.0"
  }
}

JSON schemas that will be used (changes will be affected only for Custom Labels Collection Item Schema):

 Custom Labels Collection Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Custom Labels Collection Schema",
  "description": "Custom Labels Collection Schema",
  "javaType": "org.folio.rest.jaxrs.model.CustomLabelsCollection",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "type": "array",
      "description": "List of custom labels",
      "items": {
        "type": "object",
        "$ref": "customLabelCollectionItem.json"
      }
    },
    "meta": {
      "type": "object",
      "description": "metadata containing total results in packages collection",
      "$ref": "../metaTotalResults.json"
    },
    "jsonapi": {
      "type": "object",
      "description": "version of json api",
      "$ref": "../jsonapi.json"
    }
  },
  "required": ["data", "meta", "jsonapi"]
}
 Custom Labels Collection Item Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Custom Labels Collection Item Schema",
  "description": "Custom Labels Collection Item Schema",
  "javaType": "org.folio.rest.jaxrs.model.CustomLabelCollectionItem",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "type": {
      "type": "string",
      "description": "Type of resource",
      "example": "customLabel"
    },
    "credentialsId": {
      "type": "string",
      "description": "UUID of KB credentials",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "example": "a173c3b6-134d-4d35-bda8-f31bfwd957bf"
    },
    "attributes": {
      "type": "object",
      "description": "Custom label object data attributes",
      "$ref": "customLabelDataAttributes.json"
    }
  },
  "required": ["type", "attributes"]
}
 Custom Labels Collection Item Data Attributes Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Custom Labels Collection Item Data Attributes Schema",
  "description": "Custom Labels Collection Item Data Attributes Schema",
  "javaType": "org.folio.rest.jaxrs.model.CustomLabelDataAttributes",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "integer",
      "description": "Label id",
      "example": 1
    },
    "displayLabel": {
      "type": "string",
      "description": "Display Label Name",
      "example": "User defined field 1"
    },
    "displayOnFullTextFinder": {
      "type": "boolean",
      "description": "Indicates if displayed on Full Text Finder",
      "example": false
    },
    "displayOnPublicationFinder": {
      "type": "boolean",
      "description": "Indicates if displayed on Publication Finder",
      "example": false
    }
  },
  "required": ["id", "displayLabel", "displayOnFullTextFinder", "displayOnPublicationFinder"]
}
 Custom Labels Collection Update Request Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Custom Label Collection Update Request Schema",
  "description": "Custom Label Collection Update Request Schema",
  "javaType": "org.folio.rest.jaxrs.model.CustomLabelPutRequest",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "description": "List of custom labels",
      "type": "array",
      "items": {
        "type": "object",
        "$ref": "customLabelCollectionItem.json"
      }
    }
  },
  "required": ["data"]
}


Association between KB Credentials and Proxy


Root proxy and proxy types are stored on the RM API side, so in this case, we don't have to change DB schema.

Endpoints are needed to be changed

MethodOld endpointNew endpointRequest bodyResponse status Response body
GET/eholdings/proxy-types/eholdings/kb-credentials/{credId}/proxy-types
200 OK
 Response
Example
{
  "data": [
    {
      "id": "Proxy-123",
      "type": "proxyTypes",
	  "credentialsId": "{credId}",
      "attributes": {
        "id": "Proxy-123",
        "name": "Proxy-123",
        "urlMask": "http://ezproxy.myinstitute.edu/"
      }
    },
    {
      "id": "Proxy-321",
      "type": "proxyTypes",
	  "credentialsId": "{credId}",
      "attributes": {
        "id": "Proxy-321",
        "name": "Proxy-321",
        "urlMask": "https://ezproxy.myinstitute.edu/"
      }
    }
  ],
  "meta": {
    "totalResults": 2
  },
  "jsonapi": {
    "version": "1.0"
  }
}
GET/eholdings/root-proxy/eholdings/kb-credentials/{credId}/root-proxy
200 OK
 Response
{
  "data": {
    "id": "root-proxy",
    "type": "rootProxies",
	"credentialsId": "{credId}",
    "attributes": {
      "id": "root-proxy",
      "proxyTypeId": "Proxy-123"
    }
  },
  "jsonapi": {
    "version": "1.0"
  }
}
PUT/eholdings/root-proxy/eholdings/kb-credentials/{credId}/root-proxy
 Request

Root Proxy Data Attributes Schema

Example
{
  "data": {
    "id": "root-proxy",
    "type": "rootProxies",
    "attributes": {
      "id": "root-proxy",
      "proxyTypeId": "Proxy-321"
    }
  }
}
200 OK
 Response
Example
{
  "data": {
    "id": "root-proxy",
    "type": "rootProxies",
	"credentialsId": "{credId}",
    "attributes": {
      "id": "root-proxy",
      "proxyTypeId": "Proxy-321"
    }
  },
  "jsonapi": {
    "version": "1.0"
  }
}

JSON schemas that will be used:

 Proxy Types Collection Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Proxy Types Collection Schema",
  "description": "Proxy Types Collection Schema",
  "javaType": "org.folio.rest.jaxrs.model.ProxyTypesCollection",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "type": "array",
      "description": "List of proxy types for a given customer",
      "items": {
        "type": "object",
        "$ref": "proxyTypesCollectionItem.json"
      }
    },
    "meta": {
      "type": "object",
      "description": "metadata containing total results in packages collection",
      "$ref": "../metaTotalResults.json"
    },
    "jsonapi": {
      "type": "object",
      "description": "version of json api",
      "$ref": "../jsonapi.json"
    }
  },
  "required": [
    "data",
    "meta",
    "jsonapi"
  ]
}
 Proxy Types Collection Item Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Proxy Types Collection Item Schema",
  "description": "Proxy Types Collection Item Schema",
  "javaType": "org.folio.rest.jaxrs.model.ProxyTypesCollectionItem",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "Unique identifier of proxy",
      "example": "EZProxy"
    },
    "type": {
      "type": "string",
      "description": "Type of resource",
      "enum": [
        "proxyTypes"
      ],
      "example": "proxyTypes"
    },
	"credentialsId": {
      "type": "string",
      "description": "UUID of KB credentials",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "example": "a173c3b6-134d-4d35-bda8-f31bfwd957bf"
    },
    "attributes": {
      "type": "object",
      "description": "Proxy Type object data attributes",
      "$ref": "proxyTypeDataAttributes.json"
    }
  },
  "required": [
    "type",
    "attributes"
  ]
}


 Proxy Types Collection Item Data Attributes Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Proxy Types Collection Item Data Attributes Schema",
  "description": "Proxy Types Collection Item Data Attributes Schema",
  "javaType": "org.folio.rest.jaxrs.model.ProxyTypeDataAttributes",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "Proxy Type ID",
      "example": "EZProxy"
    },
    "name": {
      "type": "string",
      "description": "Proxy Type Name",
      "example": "EZProxy"
    },
    "urlMask": {
      "type": "string",
      "description": "Proxy Type URL Mask",
      "example": "http://ezproxy.myinstitute.edu/login?url={targetURL}"
    }
  },
  "required": [
    "id",
    "name",
    "urlMask"
  ]
}
 Root Proxy Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Root Proxy Schema",
  "description": "Root Proxy Schema",
  "javaType": "org.folio.rest.jaxrs.model.RootProxy",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "type": "object",
      "description": "Details of root proxy set for a given customer",
      "$ref": "rootProxyData.json"
    },
    "jsonapi": {
      "type": "object",
      "description": "version of json api",
      "$ref": "../jsonapi.json"
    }
  },
  "required": [
    "data",
    "jsonapi"
  ]
}
 Root Proxy Data Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Root Proxy Data Schema",
  "description": "Root Proxy Data Schema",
  "javaType": "org.folio.rest.jaxrs.model.RootProxyData",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "Unique identifier of root proxy",
      "example": "root-proxy"
    },
    "type": {
      "type": "string",
      "enum": [
        "rootProxies"
      ],
      "example": "rootProxies"
    },
    "credentialsId": {
      "type": "string",
      "description": "UUID of KB credentials",
      "$ref": "../../raml-util/schemas/uuid.schema",
      "example": "a173c3b6-134d-4d35-bda8-f31bfwd957bf"
    },
    "attributes": {
      "type": "object",
      "description": "Root Proxy object data attributes",
      "$ref": "rootProxyDataAttributes.json"
    }
  },
  "required": [
    "type",
    "attributes"
  ]
}
 Root Proxy Data Attributes Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Root Proxy Data Attributes Schema",
  "description": "Root Proxy Data Attributes Schema",
  "javaType": "org.folio.rest.jaxrs.model.RootProxyDataAttributes",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "Root Proxy ID",
      "example": "root-proxy"
    },
    "proxyTypeId": {
      "type": "string",
      "description": "Proxy Type Id",
      "example": "EZProxy"
    }
  },
  "required": [
    "id",
    "proxyTypeId"
  ]
}
 Root Proxy Update Request Schema
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Root Proxy Update Request Schema",
  "description": "Root Proxy Update Request Schema",
  "javaType": "org.folio.rest.jaxrs.model.RootProxyPutRequest",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "data": {
      "type": "object",
      "description": "Data object of root proxy update request",
      "$ref": "rootProxyData.json"
    }
  },
  "required": [
    "data"
  ]
}


Updating existing endpoints for providers/packages/resources/titles

Problem

Eholdings module provides several endpoints to operate with HoldingsIQ resources (providers/packages/resources/titles). Endpoint interfaces are listed below:

  • EholdingsProviders
  • EholdingsPackages

  • EholdingsResources
  • EholdingsTitles

Detailed definition with methods of each enpoint can be found in Eholdings Record Interfaces appendix

Endpoints call HoldingsIQ services (EBSCO KB) to get or update records. Each call has to be preconfigured with valid EBSCO knowledgebase credentials. There is only one credential set available at the moment. But it has to be changed to support multiple sets and a set can be associated with a user. This requires changes in the configuration routine of endpoints so that the appropriate KB credentials could be selected from several options depending on the current user.

Another point to consider is the data that stored internally in Eholdings module and has to be also associated with different KB credentials. Some samples of the data are:

  • package details (name, content type)
  • provider name
  • access types assigned to resource.

Each time an endpoint needs to work with such data it has to understand what the correct set of credentials is in the current context.

The code snippets provided below represent both cases:

  1. working with locally stored data
  2. calling HoldingsIQ services

Steps to address the problem

  • modify configuration routine to support user related KB credentials
  • apply the routine to each enpoint method which talks to HoldingsIQ service(s)
  • make the code that works with locally stored data aware of applicable KB credentials

Let's describe the steps one by one in more details

Modify configuration routine to support user related KB credentials

Following points to consider:

  1. Configuration service modification

  2. Retrieving KB credentials for the user

  3. Changes in RM API template context

Configuration service modification

The core service that works with KB credentials at the moment is ConfigurationService. It provides 3 methods as shown below:

Modifications required:

  • retrieveConfiguration() method will return KB credentials associated with the current user.
    • It differes from how it works right now: single available credentials obtained from mod-configuration regardless of the current user
    • new method implementation will benefit from a new method in KBCredentialsService (see later on) which is going to be responsible for finding KB credentials applicable to the current user. So retrieveConfiguration() will delegate almost all of its work to this new method in KBCredentialsService
  • updateConfiguration() method should be removed
    • all KB credentals management operations (adding, changing, removing) will be done by KBCredentialsService
  • verifyCredentials() – NO changes needed
Retrieving KB credentials for the user

All credential management will be accumulated inside a new service: KBCredentialsService. It will be responsible for:

  • CRUD operations with KB credentials
  • resolving the approproate KB credentials for user

Below is a class diagram of the service.

First four methods should provide standart CRUD operations.

The last one is responsible for getting the right KB credentials for the user. It will do the work by 

  • quering users assigned to KB credentials with the current user
    • if no assignment present then
  • testing if there is single KB credential present

Changes in RM API template context

The context serves as a holder for preconfigured HoldingsIQ services and a couple of parameters important during the calls to those services and module methods. It already contains Configuration object to provide apiKey/customerId/url to access KB knowledge base. What's missing is KB credentials details such as id (credentialsId) and name.  Credentials id will be important to filter local data, name might come in handy in troubleshooting/logging. So both these attributes will be added to the context and it'll look like the following:

Respectively RMAPITemplateContextBuilder that constructs the context will be adjusted to support new fields.

Alternative

Another option would be to include credentials id/name into Configuration class. Then the context doesn't need to be updated

Pros:

  • all configuration attributes incupsulated in a single class – all the details will be available whereever Configuration class is currently available at
  • no changes in RM API template context

Cons:

  • Configuration contains only the data that is really necessary to communicate with HoldingsIQ. By adding credentials id/name it gets poluted with irrelevant info.
  • KBCredentials becomes equal to Configuration: duplication of code and resposibility. Something has to be gone then

Apply new configuration routine to each enpoint method which talks to HoldingsIQ service(s)

This should be fairly straightforward since there is a single place where credentail details (configuration) are obtained and applied to a service. This takes place inside RMAPITemplate class:

  1. configuration retrieved by ConfigurationService
  2. context builder populated with configuration found
  3. context with configuration is created by builder and passed to request action which calls HoldingIQ service

What has to be changed:

  • replace ConfigurationService with KBCredentialsService
  • instead of calling retrieveConfiguration() method call findByUser() method of KBCredentialsService to find KBCredentials applicable in the current context
  • populate context with Configuration, credentialsId, credentialsName obtained from KBCredentials found

Make the code that works with locally stored data aware of applicable KB credentials

The code that works with local data which has become dependent on KB credentials should be refactored to accept credentailsId and apply it to DB statements. There are 2 cases:

  • the code is called from inside RMAPITemplate request action →

In this case context already contains valid credentialsId thus the method could get it from there. So for instance findById() method could be changed to:

accessTypesService.findById(accessTypeId, context)
  • the code is executed on its own (there is no RMAPITemplate which wraps it around) →

There is no place to take credentialsId from so it has to be retreived first by executing KBCredentialsService.findByUser()method. The result should be transformed into RMAPIContext similar to how it would be done in RMAPITemplate.executeInternal() method (see previous section). Then context can be passed to updateTags().

Having the above RMAPIContext becomes more general than just a place to store everything relevant to RM API calls. It's worth thinking about renaming it to something more universal


Updating the process of holdings loading

To support the background loading of holdings by an invocation of OKAPI timer interface we have to add an additional endpoint to add the possibility of loading holdings for particular credentials 

The path or pathPattern can be any fixed string (no pattern). 

which means the URL /loadHoldings will remain the same.

No user is involved in this.

which means we can not use 'X-Okapi-Header' to define a user and then find tied credentials to load holdings.

MethodOld Endpoint New Endpoint Notes
POST/loadHoldings/eholdings/loadingan endpoint to load holdings for all credentials available in the system
POST/loadHoldings/eholdings/loading/kb-credentials/{credentialsId}

load holdings for particular KB Credentials

Interactions with the database should be updated by adding additional credentialsId column to SQL queries.

For instance, the query to get holdings will look like

SELECT ...
  FROM holdings
 WHERE credentialsId = ? 
   AND id IN (...);

all other SQL queries should be updated accordingly.

GET/loadHoldings/status/eholdings/loading/kb-credentials/{credentialsId}/status

Back-end will support storing a single record for holdings_status table  as it is already implemented

with one difference -  adding the additional column credentialsId to have a one-to-one relationship

between KB credential and status 

Select queries should be updated by adding WHERE clause with specifying a KB credentialsId

SELECT ...
  FROM holdings_status
 WHERE credentialsId = ?;

  • the initial status "NOT_STARTED" should be inserted when KB credentials created via the endpoint POST /eholdings/kb-credentials 
    • along with the insertion of initial status the retry_status table should be updated (retry_status table should also have credentialsId column)

The current version of OKAPI /_/timer interface does allow us to have only fixed string as a path as it is mentioned above, but the  mod-kb-ebsco-java module should still support holdings background loading/update. For this purpose, the existing 

POST /loadHoldings endpoint will be updated to initiate the loading process for KB Credentials. 

The activity diagram posted below shows the process of loading holdings when it is triggered by the OKAPI /_/timer interface. 

The mod-kb-ebsco-java module is configured to load holdings every 5 days. It means that for every KB Credentials the  mod-kb-ebsco-java module should load holdings respectively. To reduce the mess of possible errors, the process of loading and storing holdings records will be organized sequentially, i.e. one by one for each KB credentials. It is also will be available to initiate a loading holdings process for particular KB credentials using endpoint POST /eholdings/loading/kb-credentials/{credentialsId}. Once the request is received (i.e POST /loadHoldings), back-end gets the list of KB Credentials stored in mod-kb-ebsco-java, iterates over it and start loading for certain KB Credentials. The next KB Credentials loading starts only when the previous iteration has been finished(by finished means status is Completed or Failed).  


General overview of mod-kb-ebsco-java module tables with KB Credentials applying


Data migration

initial script to migrate mod-kb-ebsco-java existing data to  "Single tenant multiple EBSCO KBs" scenario  - kb_ebsco_java_new_DB_schema.sql 


Appendix A. Eholdings Record Interfaces