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
1 change: 0 additions & 1 deletion broker/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ deps-update:
$(GO) mod tidy

deps-update-tools:
$(GO) get github.com/indexdata/xsd2goxsl@latest
$(GO) get github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
$(GO) mod tidy

Expand Down
6 changes: 5 additions & 1 deletion broker/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ replace (

require (
github.com/dustin/go-humanize v1.0.1
github.com/go-playground/validator/v10 v10.30.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/indexdata/cql-go v1.0.1-0.20260320114910-316aba36a2ce
github.com/indexdata/go-utils v0.0.0-20260218142542-28abe67711aa
github.com/indexdata/xsd2goxsl v1.3.0
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.9.1
github.com/lib/pq v1.12.0
Expand Down Expand Up @@ -60,18 +60,22 @@ require (
github.com/dprotaso/go-yit v0.0.0-20250512143907-c109d19d21e6 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
Expand Down
14 changes: 12 additions & 2 deletions broker/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand All @@ -68,6 +70,14 @@ github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMK
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
Expand All @@ -84,8 +94,6 @@ github.com/indexdata/cql-go v1.0.1-0.20260320114910-316aba36a2ce h1:3jQAjgancsKw
github.com/indexdata/cql-go v1.0.1-0.20260320114910-316aba36a2ce/go.mod h1:zmSHcE8JyK94EWZrV7VyjLr2QfRoj+EeEOttl9wm64U=
github.com/indexdata/go-utils v0.0.0-20260218142542-28abe67711aa h1:cXVO434D+Guc1a1jcIB/ga5xO5VyWx1He1R0gJov7MY=
github.com/indexdata/go-utils v0.0.0-20260218142542-28abe67711aa/go.mod h1:0sW6Szxv8GNU3LBtK6mgBKDEUnlovPfghiG9xi+i0R8=
github.com/indexdata/xsd2goxsl v1.3.0 h1:LZGBORHnO6olHBtvc6hQEefymdRuiM77FgtW4pCek7g=
github.com/indexdata/xsd2goxsl v1.3.0/go.mod h1:8sXNGyaUfpRCgSCamB5p9GQNLyPHF5N2MYBrhWSss8Y=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
Expand All @@ -108,6 +116,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=
github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 h1:Qj3hTcdWH8uMZDI41HNuTuJN525C7NBrbtH5kSO6fPk=
Expand Down
153 changes: 120 additions & 33 deletions broker/patron_request/api/api-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"

"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/indexdata/cql-go/cqlbuilder"
"github.com/indexdata/crosslink/broker/api"
Expand All @@ -29,6 +31,10 @@ type ActionTaskProcessor interface {
ProcessInvokeActionTask(ctx common.ExtendedContext, event events.Event) (events.Event, error)
}

var illRequestValidator = validator.New(validator.WithRequiredStructEnabled())
var brokerSymbol = utils.GetEnv("BROKER_SYMBOL", "ISIL:BROKER")
var errInvalidPatronRequest = errors.New("invalid patron request")

type PatronRequestApiHandler struct {
limitDefault int32
prRepo pr_db.PrRepo
Expand Down Expand Up @@ -198,11 +204,17 @@ func (a *PatronRequestApiHandler) PostPatronRequests(w http.ResponseWriter, r *h
return
}
newPr.RequesterSymbol = &symbol
dbreq, err := a.toDbPatronRequest(ctx, newPr, params.XOkapiTenant)
creationTime := pgtype.Timestamp{Valid: true, Time: time.Now()}
illRequest, requesterReqId, err := a.parseAndValidateIllRequest(ctx, &newPr, creationTime.Time)
if err != nil {
if errors.Is(err, errInvalidPatronRequest) {
addBadRequestError(ctx, w, err)
return
}
addInternalError(ctx, w, err)
return
}
dbreq := buildDbPatronRequest(&newPr, params.XOkapiTenant, creationTime, requesterReqId, illRequest)
pr, err := a.prRepo.CreatePatronRequest(ctx, (pr_db.CreatePatronRequestParams)(dbreq))
if err != nil {
var pgErr *pgconn.PgError
Expand Down Expand Up @@ -551,44 +563,40 @@ func toString(text pgtype.Text) *string {
return value
}

func (a *PatronRequestApiHandler) toDbPatronRequest(ctx common.ExtendedContext, request proapi.CreatePatronRequest, tenant *string) (pr_db.PatronRequest, error) {
creationTime := pgtype.Timestamp{Valid: true, Time: time.Now()}
var id string
func (a *PatronRequestApiHandler) parseAndValidateIllRequest(
ctx common.ExtendedContext,
request *proapi.CreatePatronRequest,
creationTime time.Time,
) (iso18626.Request, string, error) {
if request.RequesterSymbol == nil || *request.RequesterSymbol == "" {
return iso18626.Request{}, "", fmt.Errorf("%w: requesterSymbol must be specified", errInvalidPatronRequest)
}
reqSymbolType, reqSymbolValue, err := parseAgencySymbol(*request.RequesterSymbol)
if err != nil {
return iso18626.Request{}, "", fmt.Errorf("%w: requesterSymbol: %w", errInvalidPatronRequest, err)
}
var requesterReqId string
if request.Id != nil {
id = *request.Id
requesterReqId = *request.Id
} else {
prefix := strings.SplitN(*request.RequesterSymbol, ":", 2)[1]
hrid, err := a.prRepo.GetNextHrid(ctx, prefix)
hrid, err := a.prRepo.GetNextHrid(ctx, reqSymbolValue)
if err != nil {
return pr_db.PatronRequest{}, err
return iso18626.Request{}, "", err
}
id = hrid
}
var illRequest iso18626.Request
if request.IllRequest != nil {
illRequestBytes := utils.Must(json.Marshal(request.IllRequest))
err := json.Unmarshal(illRequestBytes, &illRequest)
if err != nil {
return pr_db.PatronRequest{}, err
}
illRequest.Header.Timestamp = utils.XSDDateTime{Time: creationTime.Time}
illRequest.Header.RequestingAgencyRequestId = id
requesterReqId = hrid
}
illRequest, err := parseAndValidateIllRequestPayload(
request.IllRequest,
reqSymbolType,
reqSymbolValue,
requesterReqId,
creationTime,
)
if err != nil {
return iso18626.Request{}, "", err
}

return pr_db.PatronRequest{
ID: id,
Timestamp: creationTime,
State: prservice.BorrowerStateNew,
Side: prservice.SideBorrowing,
Patron: getDbText(request.Patron),
RequesterSymbol: getDbText(request.RequesterSymbol),
SupplierSymbol: getDbText(nil),
IllRequest: illRequest,
Tenant: getDbText(tenant),
RequesterReqID: getDbText(&id),
// LastAction, LastActionOutcome and LastActionResult are not set on creation
// they will be updated when the first action is executed.
}, nil
return illRequest, requesterReqId, nil
}

func getId(id string) string {
Expand All @@ -608,6 +616,85 @@ func getDbText(value *string) pgtype.Text {
}
}

func parseAgencySymbol(symbol string) (string, string, error) {
scheme, value, ok := strings.Cut(symbol, ":")
if !ok || scheme == "" || value == "" {
return "", "", fmt.Errorf("expected format SCHEME:VALUE, got %q", symbol)
}
return scheme, value, nil
}

func parseAndValidateIllRequestPayload(
rawIllRequest map[string]interface{},
reqSymbolType string,
reqSymbolValue string,
requesterReqId string,
creationTime time.Time,
) (iso18626.Request, error) {
var illRequest iso18626.Request
if rawIllRequest == nil {
return iso18626.Request{}, fmt.Errorf("%w: missing required illRequest payload", errInvalidPatronRequest)
}
illRequestBytes, err := json.Marshal(rawIllRequest)
if err != nil {
return iso18626.Request{}, fmt.Errorf("%w: illRequest: %w", errInvalidPatronRequest, err)
}
err = json.Unmarshal(illRequestBytes, &illRequest)
if err != nil {
return iso18626.Request{}, fmt.Errorf("%w: illRequest: %w", errInvalidPatronRequest, err)
}
suppSymbolType, suppSymbolValue, err := parseAgencySymbol(brokerSymbol)
if err != nil {
return iso18626.Request{}, fmt.Errorf("invalid BROKER_SYMBOL %q: %w", brokerSymbol, err)
}
illRequest.Header.RequestingAgencyId = iso18626.TypeAgencyId{
AgencyIdType: iso18626.TypeSchemeValuePair{Text: reqSymbolType},
AgencyIdValue: reqSymbolValue,
}
illRequest.Header.SupplyingAgencyId = iso18626.TypeAgencyId{
AgencyIdType: iso18626.TypeSchemeValuePair{Text: suppSymbolType},
AgencyIdValue: suppSymbolValue,
}
illRequest.Header.Timestamp = utils.XSDDateTime{Time: creationTime}
illRequest.Header.RequestingAgencyRequestId = requesterReqId
if err = validateIllRequest(illRequest); err != nil {
return iso18626.Request{}, fmt.Errorf("%w: invalid illRequest: %w", errInvalidPatronRequest, err)
}
return illRequest, nil
}

func buildDbPatronRequest(
request *proapi.CreatePatronRequest,
tenant *string,
creationTime pgtype.Timestamp,
requesterReqId string,
illRequest iso18626.Request,
) pr_db.PatronRequest {
return pr_db.PatronRequest{
ID: requesterReqId,
Timestamp: creationTime,
State: prservice.BorrowerStateNew,
Side: prservice.SideBorrowing,
Patron: getDbText(request.Patron),
RequesterSymbol: getDbText(request.RequesterSymbol),
SupplierSymbol: getDbText(nil),
IllRequest: illRequest,
Tenant: getDbText(tenant),
RequesterReqID: getDbText(&requesterReqId),
// LastAction, LastActionOutcome and LastActionResult are not set on creation
// they will be updated when the first action is executed.
}
}

func validateIllRequest(request iso18626.Request) error {
requestForValidation := request
if requestForValidation.Header.MultipleItemRequestId == "" {
//schema workaround
requestForValidation.Header.MultipleItemRequestId = "#empty"
}
return illRequestValidator.Struct(requestForValidation)
}

func toApiItem(item pr_db.Item) proapi.PrItem {
return proapi.PrItem{
Id: item.ID,
Expand Down
Loading
Loading