Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion broker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Configuration is provided via environment variables:
| `DB_DATABASE` | Database name | `crosslink` |
| `DB_PORT` | Database port | `25432` |
| `DB_SCHEMA` | Database schema to use | `crosslink_broker` |
| `DB_PROVISION` | Should app create DB role/schema (`true`/`false`) | `false` |
| `DB_PROVISION` | Should app create DB role/schema (`true`/`false`) | `false` |
| `DB_MIGRATE` | Should app run DB migrations (`true`/`false`) | `true` |
| `LOG_LEVEL` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG` | `INFO` |
| `ENABLE_JSON_LOG` | Should JSON log format be enabled | `false` |
Expand All @@ -108,6 +108,7 @@ Configuration is provided via environment variables:
| `TENANT_TO_SYMBOL` | Pattern to map tenant to `requesterSymbol` when accessing the API via Okapi, | (empty value) |
| | the `{tenant}` token is replaced by the `X-Okapi-Tenant` header value | |
| `SUPPLIER_PATRON_PATTERN` | Pattern used to create patron ID when receiving Request on supplier side | `%v_user` |
| `LANGUAGE` | Language parameter used for ts_vector search in DB | `english` |

# Build

Expand Down
40 changes: 40 additions & 0 deletions broker/migrations/022_add_items_to_pr.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
DROP TRIGGER IF EXISTS trigger_update_patron_request_search_tsvector ON patron_request;

DROP FUNCTION IF EXISTS update_patron_request_search_tsvector;

DROP INDEX IF EXISTS idx_patron_request_search;

DROP VIEW IF EXISTS patron_request_search_view;

ALTER TABLE patron_request DROP COLUMN IF EXISTS search;
ALTER TABLE patron_request DROP COLUMN IF EXISTS language;

DROP TRIGGER IF EXISTS trigger_update_patron_request_items ON item;

DROP FUNCTION IF EXISTS update_patron_request_items CASCADE;

ALTER TABLE patron_request DROP COLUMN IF EXISTS items;

CREATE OR REPLACE VIEW patron_request_search_view AS
SELECT
pr.*,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id
) AS has_notification,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id and cost is not null
) AS has_cost,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id and acknowledged_at is null
) AS has_unread_notification,
pr.ill_request -> 'serviceInfo' ->> 'serviceType' AS service_type,
pr.ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text' AS service_level,
immutable_to_timestamp(pr.ill_request -> 'serviceInfo' ->> 'needBeforeDate') AS needed_at
FROM patron_request pr;

134 changes: 134 additions & 0 deletions broker/migrations/022_add_items_to_pr.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
ALTER TABLE patron_request ADD COLUMN items JSONB NOT NULL DEFAULT '[]'::jsonb;

CREATE OR REPLACE FUNCTION update_patron_request_items()
RETURNS TRIGGER AS $$
BEGIN
UPDATE patron_request
SET items = (
SELECT jsonb_agg(
(to_jsonb(i) - 'pr_id') ||
jsonb_build_object(
'created_at',
to_char(i.created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US') || to_char(i.created_at, 'TZH:TZM')
)
)
FROM item i
WHERE i.pr_id = NEW.pr_id
)
WHERE id = NEW.pr_id;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create the trigger
CREATE TRIGGER trigger_update_patron_request_items
AFTER INSERT OR UPDATE ON item
FOR EACH ROW
EXECUTE FUNCTION update_patron_request_items();


-- Add the search field as a tsvector column
ALTER TABLE patron_request
ADD COLUMN search tsvector,
ADD COLUMN language regconfig NOT NULL DEFAULT 'english';

-- Create a trigger function to update the search tsvector
CREATE OR REPLACE FUNCTION update_patron_request_search_tsvector()
RETURNS TRIGGER AS $$
BEGIN
-- Update the search tsvector column
NEW.search := to_tsvector(NEW.language,
COALESCE(NEW.requester_req_id, '') || ' ' ||
COALESCE(NEW.patron, '') || ' ' ||
COALESCE(NEW.ill_request->'patronInfo'->>'givenName', '') || ' ' ||
COALESCE(NEW.ill_request->'patronInfo'->>'surname', '') || ' ' ||
COALESCE(NEW.ill_request->'patronInfo'->>'patronId', '') || ' ' ||
COALESCE(NEW.ill_request->'bibliographicInfo'->>'title', '') || ' ' ||
COALESCE(NEW.ill_request->'bibliographicInfo'->>'author', '') || ' ' ||
COALESCE(
(SELECT string_agg(
COALESCE(item->>'item_id', '') || ' ' ||
COALESCE(item->>'barcode', '') || ' ' ||
COALESCE(item->>'call_number', ''), ' '
)
FROM jsonb_array_elements(NEW.items) AS item), ''
)
);

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create a trigger to update the search tsvector on insert or update
CREATE TRIGGER trigger_update_patron_request_search_tsvector
BEFORE INSERT OR UPDATE ON patron_request
FOR EACH ROW
EXECUTE FUNCTION update_patron_request_search_tsvector();

CREATE INDEX idx_patron_request_search ON patron_request USING gin(search);

DROP VIEW IF EXISTS patron_request_search_view;
CREATE OR REPLACE VIEW patron_request_search_view AS
SELECT
pr.*,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id
) AS has_notification,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id and cost is not null
) AS has_cost,
EXISTS (
SELECT 1
FROM notification n
WHERE n.pr_id = pr.id and acknowledged_at is null
) AS has_unread_notification,
pr.ill_request -> 'serviceInfo' ->> 'serviceType' AS service_type,
pr.ill_request -> 'serviceInfo' -> 'serviceLevel' ->> '#text' AS service_level,
immutable_to_timestamp(pr.ill_request -> 'serviceInfo' ->> 'needBeforeDate') AS needed_at
FROM patron_request pr;

-- One-time backfill of items for existing patron_request rows
UPDATE patron_request pr
SET items = COALESCE(
(
SELECT jsonb_agg(
(to_jsonb(i) - 'pr_id') ||
jsonb_build_object(
'created_at',
to_char(i.created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US') || to_char(i.created_at, 'TZH:TZM')
)
)
FROM item i
WHERE i.pr_id = pr.id
),
'[]'::jsonb
);
-- One-time backfill of search tsvector for existing patron_request rows
UPDATE patron_request pr
SET search = to_tsvector(
pr.language,
COALESCE(pr.requester_req_id, '') || ' ' ||
COALESCE(pr.patron, '') || ' ' ||
COALESCE(pr.ill_request->'patronInfo'->>'givenName', '') || ' ' ||
COALESCE(pr.ill_request->'patronInfo'->>'surname', '') || ' ' ||
COALESCE(pr.ill_request->'patronInfo'->>'patronId', '') || ' ' ||
COALESCE(pr.ill_request->'bibliographicInfo'->>'title', '') || ' ' ||
COALESCE(pr.ill_request->'bibliographicInfo'->>'author', '') || ' ' ||
COALESCE(
(
SELECT string_agg(
COALESCE(item->>'item_id', '') || ' ' ||
COALESCE(item->>'barcode', '') || ' ' ||
COALESCE(item->>'call_number', ''),
' '
)
FROM jsonb_array_elements(pr.items) AS item
),
''
)
);
8 changes: 7 additions & 1 deletion broker/oapi/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ components:
lastActionResult:
type: string
description: Latest action status ("NEW", "PROCESSING", "SUCCESS", "PROBLEM", "ERROR")
items:
type: array
description: List of patron request items
items:
$ref: '#/components/schemas/PrItem'
required:
- id
- timestamp
Expand Down Expand Up @@ -1140,7 +1145,8 @@ paths:
Use this endpoint to retrieve patron requests.
Query parameter cql can be used to filter the results.
With cql you can use these fields state, side, requester_symbol, supplier_symbol, needs_attention,
has_notification, has_cost, has_unread_notification, service_type, service_level, created_at, needed_at, requester_req_id.
has_notification, has_cost, has_unread_notification, service_type, service_level, created_at, needed_at,
requester_req_id, title, patron, cql.serverChoice.
tags:
- patron-requests-api
parameters:
Expand Down
20 changes: 19 additions & 1 deletion broker/patron_request/api/api-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (a *PatronRequestApiHandler) PostPatronRequests(w http.ResponseWriter, r *h
return
}
dbreq := buildDbPatronRequest(&newPr, params.XOkapiTenant, creationTime, requesterReqId, illRequest)
pr, err := a.prRepo.CreatePatronRequest(ctx, (pr_db.CreatePatronRequestParams)(dbreq))
pr, err := a.prRepo.CreatePatronRequest(ctx, pr_db.CreatePatronRequestParams(dbreq))
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
Expand Down Expand Up @@ -548,6 +548,10 @@ func addNotFoundError(w http.ResponseWriter) {
}

func toApiPatronRequest(request pr_db.PatronRequest, illRequest iso18626.Request) proapi.PatronRequest {
items := []proapi.PrItem{}
for _, item := range request.Items {
items = append(items, toApiPrItem(item))
}
return proapi.PatronRequest{
Id: request.ID,
Timestamp: request.Timestamp.Time,
Expand All @@ -562,6 +566,7 @@ func toApiPatronRequest(request pr_db.PatronRequest, illRequest iso18626.Request
LastAction: toString(request.LastAction),
LastActionOutcome: toString(request.LastActionOutcome),
LastActionResult: toString(request.LastActionResult),
Items: &items,
}
}

Expand Down Expand Up @@ -691,6 +696,8 @@ func buildDbPatronRequest(
IllRequest: illRequest,
Tenant: getDbText(tenant),
RequesterReqID: getDbText(&requesterReqId),
Language: pr_db.LANGUAGE,
Items: []pr_db.PrItem{},
// LastAction, LastActionOutcome and LastActionResult are not set on creation
// they will be updated when the first action is executed.
}
Expand All @@ -716,6 +723,17 @@ func toApiItem(item pr_db.Item) proapi.PrItem {
}
}

func toApiPrItem(item pr_db.PrItem) proapi.PrItem {
return proapi.PrItem{
Id: item.ID,
Barcode: item.Barcode,
CallNumber: item.CallNumber,
ItemId: item.ItemID,
Title: item.Title,
CreatedAt: time.Time(item.CreatedAt),
}
}

func toApiNotification(notification pr_db.Notification) (proapi.PrNotification, error) {
var ackAt *time.Time
if notification.AcknowledgedAt.Valid {
Expand Down
14 changes: 14 additions & 0 deletions broker/patron_request/db/prcql.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (

"github.com/indexdata/cql-go/cql"
"github.com/indexdata/cql-go/pgcql"
"github.com/indexdata/go-utils/utils"
)

var LANGUAGE = utils.GetEnv("LANGUAGE", "english")

type FieldAllRecords struct{}

func (f *FieldAllRecords) GetColumn() string { return "" }
Expand Down Expand Up @@ -64,6 +67,15 @@ func handlePatronRequestsQuery(cqlString string, noBaseArgs int) (pgcql.Query, e
nf = pgcql.NewFieldDate()
def.AddField("needed_at", nf)

f = pgcql.NewFieldString().WithExact().WithColumn("ill_request->'bibliographicInfo'->>'title'")
def.AddField("title", f)

f = pgcql.NewFieldString().WithExact()
def.AddField("patron", f)

ftv := pgcql.NewFieldTsVector().WithLanguage(LANGUAGE).WithServerChoiceRel(cql.ALL).WithColumn("search")
def.AddField("cql.serverChoice", ftv)

var parser cql.Parser
query, err := parser.Parse(cqlString)
if err != nil {
Expand Down Expand Up @@ -118,6 +130,8 @@ func (q *Queries) ListPatronRequestsCql(ctx context.Context, db DBTX, arg ListPa
&i.LastAction,
&i.LastActionOutcome,
&i.LastActionResult,
&i.Language,
&i.Items,
&i.FullCount,
); err != nil {
return nil, err
Expand Down
13 changes: 13 additions & 0 deletions broker/patron_request/db/prmodels.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package pr_db

import (
"time"
)

type PatronRequestState string
type PatronRequestSide string
type PatronRequestAction string
Expand All @@ -10,3 +14,12 @@ const (
NotificationRejected NotificationReceipt = "REJECTED"
NotificationSeen NotificationReceipt = "SEEN"
)

type PrItem struct {
ID string `json:"id"`
Barcode string `json:"barcode"`
CallNumber *string `json:"call_number"`
Title *string `json:"title"`
ItemID *string `json:"item_id"`
CreatedAt time.Time `json:"created_at"`
}
2 changes: 2 additions & 0 deletions broker/patron_request/db/prrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (r *PgPrRepo) ListPatronRequests(ctx common.ExtendedContext, params ListPat
LastAction: r.LastAction,
LastActionOutcome: r.LastActionOutcome,
LastActionResult: r.LastActionResult,
Language: r.Language,
Items: r.Items,
})
}
} else {
Expand Down
40 changes: 36 additions & 4 deletions broker/patron_request/service/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1342,16 +1342,48 @@ func (r *MockPrRepo) UpdatePatronRequest(ctx common.ExtendedContext, params pr_d
if strings.Contains(params.ID, "error") || strings.Contains(params.RequesterReqID.String, "error") {
return pr_db.PatronRequest{}, errors.New("db error")
}
r.savedPr = pr_db.PatronRequest(params)
return pr_db.PatronRequest(params), nil
r.savedPr = pr_db.PatronRequest{
ID: params.ID,
Timestamp: params.Timestamp,
IllRequest: params.IllRequest,
State: params.State,
Side: params.Side,
Patron: params.Patron,
RequesterSymbol: params.RequesterSymbol,
SupplierSymbol: params.SupplierSymbol,
Tenant: params.Tenant,
RequesterReqID: params.RequesterReqID,
NeedsAttention: params.NeedsAttention,
LastAction: params.LastAction,
LastActionOutcome: params.LastActionOutcome,
LastActionResult: params.LastActionResult,
Language: params.Language,
}
return r.savedPr, nil
}

func (r *MockPrRepo) CreatePatronRequest(ctx common.ExtendedContext, params pr_db.CreatePatronRequestParams) (pr_db.PatronRequest, error) {
if strings.Contains(params.ID, "error") || strings.Contains(params.RequesterReqID.String, "error") {
return pr_db.PatronRequest{}, errors.New("db error")
}
r.savedPr = pr_db.PatronRequest(params)
return pr_db.PatronRequest(params), nil
r.savedPr = pr_db.PatronRequest{
ID: params.ID,
Timestamp: params.Timestamp,
IllRequest: params.IllRequest,
State: params.State,
Side: params.Side,
Patron: params.Patron,
RequesterSymbol: params.RequesterSymbol,
SupplierSymbol: params.SupplierSymbol,
Tenant: params.Tenant,
RequesterReqID: params.RequesterReqID,
NeedsAttention: params.NeedsAttention,
LastAction: params.LastAction,
LastActionOutcome: params.LastActionOutcome,
LastActionResult: params.LastActionResult,
Language: params.Language,
}
return r.savedPr, nil
}

func (r *MockPrRepo) GetLendingRequestBySupplierSymbolAndRequesterReqId(ctx common.ExtendedContext, symbol string, requesterReqId string) (pr_db.PatronRequest, error) {
Expand Down
Loading
Loading