diff --git a/broker/README.md b/broker/README.md index 01f5899e..2fb67463 100644 --- a/broker/README.md +++ b/broker/README.md @@ -66,39 +66,39 @@ Note that for all modes, the broker attaches Directory information about the sup Configuration is provided via environment variables: -| Name | Description | Default value | -|--------------------------|---------------------------------------------------------------------------------------|-------------------------------------------| -| `HTTP_PORT` | Server port | `8081` | -| `DB_TYPE` | Database type | `postgres` | -| `DB_USER` | Database user | `crosslink` | -| `DB_PASSWORD` | Database password | `crosslink` | -| `DB_HOST` | Database host | `localhost` | -| `DB_DATABASE` | Database name | `crosslink` | -| `DB_PORT` | Database port | `25432` | -| `LOG_LEVEL` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG` | `INFO` | -| `ENABLE_JSON_LOG` | Should JSON log format be enabled | `false` | -| `BROKER_MODE` | Default broker mode if not configured for a peer: `opaque` or `transparent` | `opaque` | -| `BROKER_SYMBOL` | Symbol for the broker when in the `opaque` mode | `ISIL:BROKER` | -| `REQ_AGENCY_INFO` | Should `request/requestingAgencyInfo` be populated from Directory | `true` | -| `SUPPLIER_INFO` | Should `request/supplierInfo` be populated from Directory | `true` | -| `RETURN_INFO` | Should `returnInfo` be populated from Directory for supplier `Loaned` message | `true` | -| `VENDOR_NOTE` | Should `note` field be prepended with `Vendor: {vendor}` text | `true` | -| `SUPPLIER_SYMBOL_NOTE` | Should `note` field be prepended with a `Supplier: {symbol}` text, `opaque` mode only | `true` | -| `OFFERED_COSTS` | Should `deliveryCosts` be transferred to `offeredCosts` for ReShare vendor requesters | `false` | -| `NOTE_FIELD_SEP` | Separator for fields (e.g. Vendor) prepended to the note | `, ` | -| `CLIENT_DELAY` | Delay duration for outgoing ISO18626 messages | `0ms` | -| `SHUTDOWN_DELAY` | Delay duration for graceful shutdown (in-flight connections) | `15s` | -| `MAX_MESSAGE_SIZE` | Max accepted ISO18626 message size | `100KB` | -| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock` or `sru` | `mock` | -| `SRU_URL` | Comma separated list of URLs when `HOLDINGS_ADAPTER` is `sru` | `http://localhost:8081/sru` | -| `DIRECTORY_ADAPTER` | Directory lookup method: `mock` or `api` | `mock` | -| `DIRECTORY_API_URL` | Comma separated list of URLs when `DIRECTORY_ADAPTER` is `api` | `http://localhost:8081/directory/entries` | -| `PEER_REFRESH_INTERVAL` | Peer refresh interval (via Directory lookup) | `5m` | -| `MOCK_CLIENT_URL` | Mocked peer URLs value when `DIRECTORY_ADAPTER` is `mock` | `http://localhost:19083/iso18626` | -| `API_PAGE_SIZE` | Default value for the `limit` query parameter when paging the API | `10` | -| `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` | +| Name | Description | Default value | +|---------------------------|---------------------------------------------------------------------------------------|-------------------------------------------| +| `HTTP_PORT` | Server port | `8081` | +| `DB_TYPE` | Database type | `postgres` | +| `DB_USER` | Database user | `crosslink` | +| `DB_PASSWORD` | Database password | `crosslink` | +| `DB_HOST` | Database host | `localhost` | +| `DB_DATABASE` | Database name | `crosslink` | +| `DB_PORT` | Database port | `25432` | +| `LOG_LEVEL` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG` | `INFO` | +| `ENABLE_JSON_LOG` | Should JSON log format be enabled | `false` | +| `BROKER_MODE` | Default broker mode if not configured for a peer: `opaque` or `transparent` | `opaque` | +| `BROKER_SYMBOL` | Symbol for the broker when in the `opaque` mode | `ISIL:BROKER` | +| `REQ_AGENCY_INFO` | Should `request/requestingAgencyInfo` be populated from Directory | `true` | +| `SUPPLIER_INFO` | Should `request/supplierInfo` be populated from Directory | `true` | +| `RETURN_INFO` | Should `returnInfo` be populated from Directory for supplier `Loaned` message | `true` | +| `VENDOR_NOTE` | Should `note` field be prepended with `Vendor: {vendor}` text | `true` | +| `SUPPLIER_SYMBOL_NOTE` | Should `note` field be prepended with a `Supplier: {symbol}` text, `opaque` mode only | `true` | +| `OFFERED_COSTS` | Should `deliveryCosts` be transferred to `offeredCosts` for ReShare vendor requesters | `false` | +| `NOTE_FIELD_SEP` | Separator for fields (e.g. Vendor) prepended to the note | `, ` | +| `CLIENT_DELAY` | Delay duration for outgoing ISO18626 messages | `0ms` | +| `SHUTDOWN_DELAY` | Delay duration for graceful shutdown (in-flight connections) | `15s` | +| `MAX_MESSAGE_SIZE` | Max accepted ISO18626 message size | `100KB` | +| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock` or `sru` | `mock` | +| `SRU_URL` | Comma separated list of URLs when `HOLDINGS_ADAPTER` is `sru` | `http://localhost:8081/sru` | +| `DIRECTORY_ADAPTER` | Directory lookup method: `mock` or `api` | `mock` | +| `DIRECTORY_API_URL` | Comma separated list of URLs when `DIRECTORY_ADAPTER` is `api` | `http://localhost:8081/directory/entries` | +| `PEER_REFRESH_INTERVAL` | Peer refresh interval (via Directory lookup) | `5m` | +| `MOCK_CLIENT_URL` | Mocked peer URLs value when `DIRECTORY_ADAPTER` is `mock` | `http://localhost:19083/iso18626` | +| `API_PAGE_SIZE` | Default value for the `limit` query parameter when paging the API | `10` | +| `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` | # Build diff --git a/broker/api/api-handler.go b/broker/api/api-handler.go index 4c252b27..34998d6e 100644 --- a/broker/api/api-handler.go +++ b/broker/api/api-handler.go @@ -321,12 +321,12 @@ func (a *ApiHandler) PostPeers(w http.ResponseWriter, r *http.Request) { addInternalError(ctx, w, err) return } - _, err = a.illRepo.GetPeerById(ctx, newPeer.ID) + _, err = a.illRepo.GetPeerById(ctx, newPeer.Id) if err != nil && !errors.Is(err, pgx.ErrNoRows) { addInternalError(ctx, w, err) return } else if err == nil { - addBadRequestError(ctx, w, fmt.Errorf("ID %v is already used", newPeer.ID)) + addBadRequestError(ctx, w, fmt.Errorf("ID %v is already used", newPeer.Id)) return } for _, s := range newPeer.Symbols { @@ -668,7 +668,7 @@ func addNotFoundError(w http.ResponseWriter) { func toApiEvent(event events.Event) oapi.Event { api := oapi.Event{ - ID: event.ID, + Id: event.ID, Timestamp: event.Timestamp.Time, IllTransactionID: event.IllTransactionID, EventType: string(event.EventType), @@ -685,7 +685,7 @@ func toApiEvent(event events.Event) oapi.Event { func toApiLocatedSupplier(r *http.Request, sup ill_db.LocatedSupplier) oapi.LocatedSupplier { return oapi.LocatedSupplier{ - ID: sup.ID, + Id: sup.ID, IllTransactionID: sup.IllTransactionID, SupplierID: sup.SupplierID, SupplierSymbol: sup.SupplierSymbol, @@ -705,7 +705,7 @@ func toApiLocatedSupplier(r *http.Request, sup ill_db.LocatedSupplier) oapi.Loca func toApiIllTransaction(r *http.Request, trans ill_db.IllTransaction) oapi.IllTransaction { api := oapi.IllTransaction{ - ID: trans.ID, + Id: trans.ID, Timestamp: trans.Timestamp.Time, } api.RequesterSymbol = getString(trans.RequesterSymbol) @@ -761,7 +761,7 @@ func toApiPeer(peer ill_db.Peer, symbols []ill_db.Symbol, branchSymbols []ill_db } return oapi.Peer{ - ID: peer.ID, + Id: peer.ID, Symbols: list, Name: peer.Name, Url: peer.Url, @@ -811,7 +811,7 @@ func toDbPeer(peer oapi.Peer) ill_db.Peer { httpHeaders = *peer.HttpHeaders } db := ill_db.Peer{ - ID: peer.ID, + ID: peer.Id, Name: peer.Name, Url: peer.Url, Vendor: peer.Vendor, diff --git a/broker/migrations/016_add_option_for_hrid.down.sql b/broker/migrations/016_add_option_for_hrid.down.sql new file mode 100644 index 00000000..61a7ae9a --- /dev/null +++ b/broker/migrations/016_add_option_for_hrid.down.sql @@ -0,0 +1 @@ +DROP FUNCTION get_next_hrid; \ No newline at end of file diff --git a/broker/migrations/016_add_option_for_hrid.up.sql b/broker/migrations/016_add_option_for_hrid.up.sql new file mode 100644 index 00000000..a6304f68 --- /dev/null +++ b/broker/migrations/016_add_option_for_hrid.up.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION get_next_hrid(prefix text) RETURNS varchar AS $$ +BEGIN + EXECUTE format('CREATE SEQUENCE IF NOT EXISTS %I START 1', LOWER(prefix) || '_hrid_seq'); + RETURN UPPER(prefix) || '-' || nextval(LOWER(prefix) || '_hrid_seq')::TEXT; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index c44aba9d..77b288ba 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -207,262 +207,265 @@ components: Event: type: object properties: - ID: + id: type: string description: Unique identifier of the event - Timestamp: + timestamp: type: string format: date-time description: Timestamp of the event - IllTransactionID: + illTransactionID: type: string description: ID of the ILL transaction (if applicable) - EventType: + eventType: type: string description: Type of the event - EventName: + eventName: type: string description: Name of the event - EventStatus: + eventStatus: type: string description: Status of the event - EventData: + eventData: type: object description: Data associated with the event additionalProperties: true - ResultData: + resultData: type: object description: Result data of the event additionalProperties: true - ParentID: + parentID: type: string description: Parent event ID required: # List required fields if any - - ID - - IllTransactionID - - Timestamp - - EventType - - EventName - - EventStatus + - id + - illTransactionID + - timestamp + - eventType + - eventName + - eventStatus IllTransaction: type: object properties: - ID: + id: type: string description: Unique identifier for the ILL transaction - Timestamp: + timestamp: type: string format: date-time description: Timestamp of the transaction - RequesterSymbol: + requesterSymbol: type: string description: Symbol of the requesting institution - RequesterID: + requesterID: type: string description: ID of the requesting institution - LastRequesterAction: + lastRequesterAction: type: string description: Last action performed by the requester - PrevRequesterAction: + prevRequesterAction: type: string description: Previous action performed by the requester - SupplierSymbol: + supplierSymbol: type: string description: Symbol of the supplying institution - RequesterRequestID: + requesterRequestID: type: string description: ID of the request from the requester's side - SupplierRequestID: + supplierRequestID: type: string description: ID of the request from the supplier's side - LastSupplierStatus: + lastSupplierStatus: type: string description: Last status update from the supplier - PrevSupplierStatus: + prevSupplierStatus: type: string description: Previous status update from the supplier - IllTransactionData: + illTransactionData: type: object description: Result data of the event additionalProperties: true - EventsLink: + eventsLink: type: string description: Link to Ill Transaction events - LocatedSuppliersLink: + locatedSuppliersLink: type: string description: Link to located Suppliers - RequesterPeerLink: + requesterPeerLink: type: string description: Link to requester Peer required: - - ID - - Timestamp - - RequesterSymbol - - RequesterID - - LastRequesterAction - - PrevRequesterAction - - SupplierSymbol - - RequesterRequestID - - SupplierRequestID - - LastSupplierStatus - - PrevSupplierStatus - - IllTransactionData - - EventsLink - - LocatedSuppliersLink - - RequesterPeerLink + - id + - timestamp + - requesterSymbol + - requesterID + - lastRequesterAction + - prevRequesterAction + - supplierSymbol + - requesterRequestID + - supplierRequestID + - lastSupplierStatus + - prevSupplierStatus + - illTransactionData + - eventsLink + - locatedSuppliersLink + - requesterPeerLink Peer: type: object properties: - ID: + id: type: string description: Unique identifier for the peer - Symbols: + symbols: type: array items: type: string description: Unique symbol representing the peer - BranchSymbols: + branchSymbols: type: array items: type: string description: Symbols of peer branches - Name: + name: type: string description: Name of the peer - Url: + url: type: string description: Network URL of the peer - RefreshPolicy: + refreshPolicy: type: string enum: [never, transaction] description: Policy for refreshing peer information (never, transaction) - RefreshTime: + refreshTime: type: string format: date-time description: Timestamp of refresh - LoansCount: + loansCount: type: integer format: int32 description: Count of loans - BorrowsCount: + borrowsCount: type: integer format: int32 description: Count of borrows - Vendor: + vendor: type: string description: Vendor of the ISO18626 implementation, e.g "Alma", "ReShare" - BrokerMode: + brokerMode: type: string enum: [opaque, transparent, translucent] description: Broker mode, e.g "opaque", "transparent" or "translucent" - CustomData: + customData: type: object description: Custom data of peer additionalProperties: true - HttpHeaders: + httpHeaders: type: object description: HTTP headers to be sent with requests to the peer additionalProperties: type: string required: - - ID - - Symbols - - Name - - Url - - RefreshPolicy - - Vendor - - BrokerMode + - id + - symbols + - name + - url + - refreshPolicy + - vendor + - brokerMode LocatedSupplier: type: object properties: - ID: + id: type: string description: Generate ID - IllTransactionID: + illTransactionID: type: string description: Ill Transaction ID - SupplierID: + supplierID: type: string description: Supplier ID from peer table - SupplierSymbol: + supplierSymbol: type: string description: Supplier symbol to use for communication - Ordinal: + ordinal: type: integer format: int32 description: Ordinal number for ordering - SupplierStatus: + supplierStatus: type: string description: Supplier status, possible values (new, selected, skipped) - PrevAction: + prevAction: type: string description: Previous requester action - PrevStatus: + prevStatus: type: string description: Previous supplier transaction status - LastAction: + lastAction: type: string description: Latest requester action - LastStatus: + lastStatus: type: string description: Latest supplier transaction status - LocalID: + localID: type: string description: Item local ID - PrevReason: + prevReason: type: string description: Previous requester reason - LastReason: + lastReason: type: string description: Latest requester reason - SupplierRequestID: + supplierRequestID: type: string description: Supplier request ID - SupplierPeerLink: + supplierPeerLink: type: string description: Link to supplier Peer required: - - ID - - IllTransactionID - - SupplierID - - SupplierSymbol - - Ordinal - - SupplierPeerLink + - id + - illTransactionID + - supplierID + - supplierSymbol + - ordinal + - supplierPeerLink PatronRequest: type: object properties: - ID: + id: type: string description: Unique identifier of the patron request - Timestamp: + timestamp: type: string format: date-time description: Timestamp of the patron reuqest - IllRequest: + illRequest: type: object description: JSON of ISO18626 request - State: + state: type: string description: Patron request state - Side: + side: type: string description: Patron request side - borrowing or lending - Patron: + patron: type: string description: User who requested item - RequesterSymbol: + requesterSymbol: type: string description: Requester symbol - SupplierSymbol: + supplierSymbol: type: string description: Supplier symbol + requesterRequestId: + type: string + description: Requester patron request ID required: - - ID - - Timestamp - - State - - Side - - IllRequest + - id + - timestamp + - state + - side + - illRequest PatronRequests: type: object required: @@ -480,28 +483,23 @@ components: CreatePatronRequest: type: object properties: - ID: + id: type: string description: Unique identifier of the patron request - Timestamp: - type: string - format: date-time - description: Timestamp of the patron reuqest - IllRequest: - type: string + illRequest: + type: object description: JSON of ISO18626 request - Patron: + patron: type: string description: User who requested item - RequesterSymbol: + requesterSymbol: type: string description: Requester symbol - SupplierSymbol: + supplierSymbol: type: string description: Supplier symbol required: - - ID - - Timestamp + - illRequest ExecuteAction: type: object diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 0bf2d278..dc2730c9 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "net/http" + "strings" "sync" + "time" "github.com/google/uuid" "github.com/indexdata/crosslink/broker/api" @@ -115,7 +117,11 @@ func (a *PatronRequestApiHandler) PostPatronRequests(w http.ResponseWriter, r *h return } newPr.RequesterSymbol = &symbol - dbreq := toDbPatronRequest(newPr, params.XOkapiTenant) + dbreq, err := a.toDbPatronRequest(ctx, newPr, params.XOkapiTenant) + if err != nil { + addInternalError(ctx, w, err) + return + } pr, err := a.prRepo.SavePatronRequest(ctx, (pr_db.SavePatronRequestParams)(dbreq)) if err != nil { addInternalError(ctx, w, err) @@ -341,14 +347,15 @@ func addNotFoundError(w http.ResponseWriter) { func toApiPatronRequest(request pr_db.PatronRequest, illRequest iso18626.Request) proapi.PatronRequest { return proapi.PatronRequest{ - ID: request.ID, - Timestamp: request.Timestamp.Time, - State: string(request.State), - Side: string(request.Side), - Patron: toString(request.Patron), - RequesterSymbol: toString(request.RequesterSymbol), - SupplierSymbol: toString(request.SupplierSymbol), - IllRequest: utils.Must(common.StructToMap(illRequest)), + Id: request.ID, + Timestamp: request.Timestamp.Time, + State: string(request.State), + Side: string(request.Side), + Patron: toString(request.Patron), + RequesterSymbol: toString(request.RequesterSymbol), + SupplierSymbol: toString(request.SupplierSymbol), + IllRequest: utils.Must(common.StructToMap(illRequest)), + RequesterRequestId: toString(request.RequesterReqID), } } @@ -360,14 +367,35 @@ func toString(text pgtype.Text) *string { return value } -func toDbPatronRequest(request proapi.CreatePatronRequest, tenant *string) pr_db.PatronRequest { +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 + if request.Id != nil { + id = *request.Id + } else { + prefix := strings.SplitN(*request.RequesterSymbol, ":", 2)[1] + hrid, err := a.prRepo.GetNextHrid(ctx, prefix) + if err != nil { + return pr_db.PatronRequest{}, err + } + id = hrid + } var illRequest []byte if request.IllRequest != nil { - illRequest = []byte(*request.IllRequest) + illRequest = utils.Must(json.Marshal(request.IllRequest)) + var isoRequest iso18626.Request + err := json.Unmarshal(illRequest, &isoRequest) + if err != nil { + return pr_db.PatronRequest{}, err + } + isoRequest.Header.Timestamp = utils.XSDDateTime{Time: creationTime.Time} + isoRequest.Header.RequestingAgencyRequestId = id + illRequest = utils.Must(json.Marshal(isoRequest)) } + return pr_db.PatronRequest{ - ID: getId(request.ID), - Timestamp: pgtype.Timestamp{Valid: true, Time: request.Timestamp}, + ID: id, + Timestamp: creationTime, State: prservice.BorrowerStateNew, Side: prservice.SideBorrowing, Patron: getDbText(request.Patron), @@ -375,7 +403,7 @@ func toDbPatronRequest(request proapi.CreatePatronRequest, tenant *string) pr_db SupplierSymbol: getDbText(request.SupplierSymbol), IllRequest: illRequest, Tenant: getDbText(tenant), - } + }, nil } func getId(id string) string { diff --git a/broker/patron_request/api/api-handler_test.go b/broker/patron_request/api/api-handler_test.go index f5e324a4..39f22803 100644 --- a/broker/patron_request/api/api-handler_test.go +++ b/broker/patron_request/api/api-handler_test.go @@ -2,13 +2,16 @@ package prapi import ( "bytes" + "context" "encoding/json" "errors" "net/http" "net/http/httptest" + "strconv" "strings" "testing" + "github.com/google/uuid" "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" @@ -84,7 +87,7 @@ func TestGetPatronRequestsWithLimits(t *testing.T) { func TestPostPatronRequests(t *testing.T) { handler := NewPrApiHandler(new(PrRepoError), mockEventBus, common.NewTenant(""), 10) - toCreate := proapi.PatronRequest{ID: "1", RequesterSymbol: &symbol} + toCreate := proapi.PatronRequest{Id: "1", RequesterSymbol: &symbol} jsonBytes, err := json.Marshal(toCreate) assert.NoError(t, err, "failed to marshal patron request") req, err := http.NewRequest("POST", "/", bytes.NewBuffer(jsonBytes)) @@ -98,7 +101,7 @@ func TestPostPatronRequests(t *testing.T) { func TestPostPatronRequestsMissingSymbol(t *testing.T) { handler := NewPrApiHandler(new(PrRepoError), mockEventBus, common.NewTenant(""), 10) - toCreate := proapi.PatronRequest{ID: "1"} + toCreate := proapi.PatronRequest{Id: "1"} jsonBytes, err := json.Marshal(toCreate) assert.NoError(t, err, "failed to marshal patron request") req, err := http.NewRequest("POST", "/", bytes.NewBuffer(jsonBytes)) @@ -261,9 +264,26 @@ func TestPostPatronRequestsIdActionErrorParsing(t *testing.T) { assert.Contains(t, rr.Body.String(), "unexpected EOF") } +func TestToDbPatronRequest(t *testing.T) { + handler := NewPrApiHandler(new(PrRepoError), mockEventBus, common.NewTenant(""), 10) + ctx := common.CreateExtCtxWithArgs(context.Background(), &common.LoggerArgs{}) + id := uuid.NewString() + + pr, err := handler.toDbPatronRequest(ctx, proapi.CreatePatronRequest{Id: &id, RequesterSymbol: &symbol}, nil) + assert.NoError(t, err) + assert.Equal(t, id, pr.ID) + assert.True(t, pr.Timestamp.Valid) + + pr, err = handler.toDbPatronRequest(ctx, proapi.CreatePatronRequest{RequesterSymbol: &symbol}, nil) + assert.NoError(t, err) + assert.Equal(t, "REQ-1", pr.ID) + assert.True(t, pr.Timestamp.Valid) +} + type PrRepoError struct { mock.Mock pr_db.PgPrRepo + counter int64 } func (r *PrRepoError) WithTxFunc(ctx common.ExtendedContext, fn func(repo pr_db.PrRepo) error) error { @@ -292,6 +312,10 @@ func (r *PrRepoError) DeletePatronRequest(ctx common.ExtendedContext, id string) } return errors.New("DB error") } +func (r *PrRepoError) GetNextHrid(ctx common.ExtendedContext, prefix string) (string, error) { + r.counter++ + return strings.ToUpper(prefix) + "-" + strconv.FormatInt(r.counter, 10), nil +} type MockEventBus struct { mock.Mock diff --git a/broker/patron_request/db/prrepo.go b/broker/patron_request/db/prrepo.go index 8431caf7..c1f30b14 100644 --- a/broker/patron_request/db/prrepo.go +++ b/broker/patron_request/db/prrepo.go @@ -1,6 +1,8 @@ package pr_db import ( + "strings" + "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/repo" "github.com/jackc/pgx/v5/pgtype" @@ -13,6 +15,7 @@ type PrRepo interface { SavePatronRequest(ctx common.ExtendedContext, params SavePatronRequestParams) (PatronRequest, error) DeletePatronRequest(ctx common.ExtendedContext, id string) error GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx common.ExtendedContext, supplierSymbol string, requesterReId string) (PatronRequest, error) + GetNextHrid(ctx common.ExtendedContext, prefix string) (string, error) } type PgPrRepo struct { @@ -82,3 +85,7 @@ func (r *PgPrRepo) GetPatronRequestBySupplierSymbolAndRequesterReqId(ctx common. }) return row.PatronRequest, err } + +func (r *PgPrRepo) GetNextHrid(ctx common.ExtendedContext, prefix string) (string, error) { + return r.queries.GetNextHrid(ctx, r.GetConnOrTx(), strings.ToUpper(prefix)) +} diff --git a/broker/sqlc/pr_query.sql b/broker/sqlc/pr_query.sql index 07d24cd6..410831e4 100644 --- a/broker/sqlc/pr_query.sql +++ b/broker/sqlc/pr_query.sql @@ -35,4 +35,7 @@ WHERE id = $1; SELECT sqlc.embed(patron_request) FROM patron_request WHERE supplier_symbol = $1 AND requester_req_id = $2 -LIMIT 1; \ No newline at end of file +LIMIT 1; + +-- name: GetNextHrid :one +SELECT get_next_hrid($1)::TEXT AS hrid; \ No newline at end of file diff --git a/broker/sqlc/pr_schema.sql b/broker/sqlc/pr_schema.sql index 191fa4c2..75b25beb 100644 --- a/broker/sqlc/pr_schema.sql +++ b/broker/sqlc/pr_schema.sql @@ -10,4 +10,11 @@ CREATE TABLE patron_request supplier_symbol VARCHAR, tenant VARCHAR, requester_req_id VARCHAR -); \ No newline at end of file +); + +CREATE OR REPLACE FUNCTION get_next_hrid(prefix VARCHAR) RETURNS VARCHAR AS $$ +BEGIN + EXECUTE format('CREATE SEQUENCE IF NOT EXISTS %I START 1', LOWER(prefix) || '_hrid_seq'); + RETURN UPPER(prefix) || '-' || nextval(LOWER(prefix) || '_hrid_seq')::TEXT; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/broker/test/api/api-handler_test.go b/broker/test/api/api-handler_test.go index c42acdef..2f7a136b 100644 --- a/broker/test/api/api-handler_test.go +++ b/broker/test/api/api-handler_test.go @@ -107,7 +107,7 @@ func TestGetEvents(t *testing.T) { assert.GreaterOrEqual(t, len(resp.Items), 1) assert.GreaterOrEqual(t, resp.About.Count, int64(1)) assert.GreaterOrEqual(t, resp.About.Count, int64(len(resp.Items))) - assert.Equal(t, eventId, resp.Items[0].ID) + assert.Equal(t, eventId, resp.Items[0].Id) body = getResponseBody(t, "/events?ill_transaction_id=not-exists") err = json.Unmarshal(body, &resp) @@ -255,7 +255,7 @@ func TestGetIllTransactionsId(t *testing.T) { var resp oapi.IllTransaction err := json.Unmarshal(body, &resp) assert.NoError(t, err) - assert.Equal(t, illId, resp.ID) + assert.Equal(t, illId, resp.Id) assert.Equal(t, getLocalhostWithPort()+"/events?ill_transaction_id="+url.PathEscape(illId), resp.EventsLink) assert.Equal(t, getLocalhostWithPort()+"/located_suppliers?ill_transaction_id="+url.PathEscape(illId), resp.LocatedSuppliersLink) @@ -274,7 +274,7 @@ func TestGetLocatedSuppliers(t *testing.T) { err := json.Unmarshal(body, &resp) assert.NoError(t, err) assert.GreaterOrEqual(t, len(resp.Items), 1) - assert.Equal(t, resp.Items[0].ID, locSup.ID) + assert.Equal(t, resp.Items[0].Id, locSup.ID) assert.GreaterOrEqual(t, resp.About.Count, int64(len(resp.Items))) body = getResponseBody(t, "/located_suppliers?ill_transaction_id=not-exists") @@ -306,7 +306,7 @@ func TestBrokerCRUD(t *testing.T) { var tran oapi.IllTransaction err = json.Unmarshal(body, &tran) assert.NoError(t, err) - assert.Equal(t, illId, tran.ID) + assert.Equal(t, illId, tran.Id) assert.Equal(t, getLocalhostWithPort()+"/broker/events?ill_transaction_id="+url.PathEscape(illId), tran.EventsLink) assert.Equal(t, getLocalhostWithPort()+"/broker/located_suppliers?ill_transaction_id="+url.PathEscape(illId), tran.LocatedSuppliersLink) @@ -318,7 +318,7 @@ func TestBrokerCRUD(t *testing.T) { body = httpGet(t, "/broker/ill_transactions/"+illId+"?requester_symbol="+url.QueryEscape("ISIL:DK-DIKU"), "", http.StatusOK) err = json.Unmarshal(body, &tran) assert.NoError(t, err) - assert.Equal(t, illId, tran.ID) + assert.Equal(t, illId, tran.Id) assert.Equal(t, 1, len(httpGetTrans(t, "/broker/ill_transactions", "diku", http.StatusOK))) @@ -335,7 +335,7 @@ func TestBrokerCRUD(t *testing.T) { err = json.Unmarshal(body, &resp) assert.NoError(t, err) assert.Len(t, resp.Items, 1) - assert.Equal(t, illId, resp.Items[0].ID) + assert.Equal(t, illId, resp.Items[0].Id) peer := apptest.CreatePeer(t, illRepo, "ISIL:LOC_OTHER", "") locSup := apptest.CreateLocatedSupplier(t, illRepo, illId, peer.ID, "ISIL:LOC_OTHER", string(iso18626.TypeStatusLoaned)) @@ -345,13 +345,13 @@ func TestBrokerCRUD(t *testing.T) { err = json.Unmarshal(body, &supps) assert.NoError(t, err) assert.Len(t, supps.Items, 1) - assert.Equal(t, locSup.ID, supps.Items[0].ID) + assert.Equal(t, locSup.ID, supps.Items[0].Id) body = httpGet(t, "/broker/located_suppliers?ill_transaction_id="+url.QueryEscape(illId), "diku", http.StatusOK) err = json.Unmarshal(body, &supps) assert.NoError(t, err) assert.Len(t, supps.Items, 1) - assert.Equal(t, locSup.ID, supps.Items[0].ID) + assert.Equal(t, locSup.ID, supps.Items[0].Id) body = httpGet(t, "/broker/located_suppliers?requester_req_id="+url.QueryEscape(reqReqId), "ruc", http.StatusOK) err = json.Unmarshal(body, &supps) @@ -370,19 +370,19 @@ func TestBrokerCRUD(t *testing.T) { err = json.Unmarshal(body, &events) assert.NoError(t, err) assert.Len(t, events.Items, 1) - assert.Equal(t, eventId, events.Items[0].ID) + assert.Equal(t, eventId, events.Items[0].Id) body = httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(reqReqId)+"&requester_symbol="+url.QueryEscape("ISIL:DK-DIKU"), "", http.StatusOK) err = json.Unmarshal(body, &events) assert.NoError(t, err) assert.Len(t, events.Items, 1) - assert.Equal(t, eventId, events.Items[0].ID) + assert.Equal(t, eventId, events.Items[0].Id) body = httpGet(t, "/broker/events?ill_transaction_id="+url.QueryEscape(illId), "diku", http.StatusOK) err = json.Unmarshal(body, &events) assert.NoError(t, err) assert.Len(t, events.Items, 1) - assert.Equal(t, eventId, events.Items[0].ID) + assert.Equal(t, eventId, events.Items[0].Id) body = httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(reqReqId), "ruc", http.StatusOK) err = json.Unmarshal(body, &events) @@ -399,7 +399,7 @@ func TestPeersLinks(t *testing.T) { for i := 0; i < 2*int(api.LIMIT_DEFAULT); i++ { peer := "ISIL:DK-PEER" + strconv.Itoa(i) toCreate := oapi.Peer{ - ID: uuid.New().String(), + Id: uuid.New().String(), Name: peer, Url: "https://url.com", Symbols: []string{peer}, @@ -440,8 +440,8 @@ func TestPeersNoHeaders(t *testing.T) { assert.NoError(t, err) // Delete peer - httpRequest(t, "DELETE", "/peers/"+respPeer.ID, nil, "", http.StatusNoContent) - httpRequest(t, "DELETE", "/peers/"+respPeer.ID, nil, "", http.StatusNotFound) + httpRequest(t, "DELETE", "/peers/"+respPeer.Id, nil, "", http.StatusNoContent) + httpRequest(t, "DELETE", "/peers/"+respPeer.Id, nil, "", http.StatusNotFound) } func TestPeersCRUD(t *testing.T) { @@ -458,7 +458,7 @@ func TestPeersCRUD(t *testing.T) { loanCount := int32(5) borrowCount := int32(10) toCreate := oapi.Peer{ - ID: uuid.New().String(), + Id: uuid.New().String(), Name: "Peer", Url: "https://url.com", Symbols: []string{"ISIL:PEER"}, @@ -475,7 +475,7 @@ func TestPeersCRUD(t *testing.T) { var respPeer oapi.Peer err = json.Unmarshal(body, &respPeer) assert.NoError(t, err) - assert.Equal(t, toCreate.ID, respPeer.ID) + assert.Equal(t, toCreate.Id, respPeer.Id) assert.Equal(t, "diku", (*toCreate.HttpHeaders)["X-Okapi-Tenant"]) var respPeers oapi.Peers @@ -483,7 +483,7 @@ func TestPeersCRUD(t *testing.T) { body = getResponseBody(t, "/peers?cql="+url.QueryEscape("symbol any ISIL:PEER")) err = json.Unmarshal(body, &respPeers) assert.NoError(t, err) - assert.Equal(t, toCreate.ID, respPeers.Items[0].ID) + assert.Equal(t, toCreate.Id, respPeers.Items[0].Id) assert.GreaterOrEqual(t, len(respPeers.Items), 1) assert.Equal(t, "Peer", respPeers.Items[0].Name) assert.Equal(t, "ISIL:PEER", respPeers.Items[0].Symbols[0]) @@ -519,11 +519,11 @@ func TestPeersCRUD(t *testing.T) { jsonBytes, err = json.Marshal(toCreate) assert.NoError(t, err) - body = httpRequest(t, "PUT", "/peers/"+toCreate.ID, jsonBytes, "", http.StatusOK) + body = httpRequest(t, "PUT", "/peers/"+toCreate.Id, jsonBytes, "", http.StatusOK) err = json.Unmarshal(body, &respPeer) assert.NoError(t, err) - assert.Equal(t, toCreate.ID, respPeer.ID) + assert.Equal(t, toCreate.Id, respPeer.Id) assert.Equal(t, "Updated", respPeer.Name) assert.Len(t, respPeer.Symbols, 2) assert.Equal(t, 2, len(*respPeer.BranchSymbols)) @@ -533,8 +533,8 @@ func TestPeersCRUD(t *testing.T) { assert.Equal(t, int32(10), *respPeer.LoansCount) assert.Equal(t, int32(15), *respPeer.BorrowsCount) // Get peer - respPeer = getPeerById(t, toCreate.ID) - assert.Equal(t, toCreate.ID, respPeer.ID) + respPeer = getPeerById(t, toCreate.Id) + assert.Equal(t, toCreate.Id, respPeer.Id) assert.Equal(t, "Updated", respPeer.Name) assert.Equal(t, int32(10), *respPeer.LoansCount) assert.Equal(t, int32(15), *respPeer.BorrowsCount) @@ -561,20 +561,20 @@ func TestPeersCRUD(t *testing.T) { err = json.Unmarshal(body, &respPeers) assert.NoError(t, err) assert.GreaterOrEqual(t, len(respPeers.Items), 1) - assert.Equal(t, toCreate.ID, respPeers.Items[0].ID) + assert.Equal(t, toCreate.Id, respPeers.Items[0].Id) assert.NotNil(t, respPeers.Items[0].CustomData) assert.Equal(t, "v1", (*respPeers.Items[0].CustomData)["name"]) assert.Equal(t, "v2", (*respPeers.Items[0].CustomData)["email"]) assert.Equal(t, "http://localhost:1234", (*respPeers.Items[0].HttpHeaders)["X-Okapi-Url"]) // Delete peer - httpRequest(t, "DELETE", "/peers/"+toCreate.ID, nil, "", http.StatusNoContent) - httpRequest(t, "DELETE", "/peers/"+toCreate.ID, nil, "", http.StatusNotFound) + httpRequest(t, "DELETE", "/peers/"+toCreate.Id, nil, "", http.StatusNoContent) + httpRequest(t, "DELETE", "/peers/"+toCreate.Id, nil, "", http.StatusNotFound) // Check no peers left respPeers = getPeers(t) for _, p := range respPeers.Items { - assert.NotEqual(t, toCreate.ID, p.ID) + assert.NotEqual(t, toCreate.Id, p.Id) } } @@ -650,7 +650,7 @@ func TestGetPeersDbError(t *testing.T) { } func TestPostPeersDbError(t *testing.T) { toCreate := oapi.Peer{ - ID: uuid.New().String(), + Id: uuid.New().String(), Name: "Peer", Url: "https://url.com", Symbols: []string{"ISIL:PEER"}, diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 6f9218ac..aa14d48e 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -89,16 +89,13 @@ func TestCrud(t *testing.T) { SupplierUniqueRecordId: "WILLSUPPLY_LOANED", }, } - jsonBytes, err := json.Marshal(request) - assert.NoError(t, err) - illMessage := string(jsonBytes) + id := uuid.NewString() newPr := proapi.CreatePatronRequest{ - ID: uuid.NewString(), - Timestamp: time.Now(), + Id: &id, SupplierSymbol: &supplierSymbol, RequesterSymbol: &requesterSymbol, Patron: &patron, - IllRequest: &illMessage, + IllRequest: utils.Must(common.StructToMap(request)), } newPrBytes, err := json.Marshal(newPr) assert.NoError(t, err, "failed to marshal patron request") @@ -109,10 +106,9 @@ func TestCrud(t *testing.T) { err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, newPr.ID, foundPr.ID) + assert.Equal(t, *newPr.Id, foundPr.Id) assert.True(t, foundPr.State != "") assert.Equal(t, string(prservice.SideBorrowing), foundPr.Side) - assert.Equal(t, newPr.Timestamp.YearDay(), foundPr.Timestamp.YearDay()) assert.Equal(t, *newPr.RequesterSymbol, *foundPr.RequesterSymbol) assert.Equal(t, *newPr.SupplierSymbol, *foundPr.SupplierSymbol) assert.Equal(t, *newPr.Patron, *foundPr.Patron) @@ -125,14 +121,14 @@ func TestCrud(t *testing.T) { assert.NoError(t, err, "failed to unmarshal patron request") assert.Equal(t, int64(1), foundPrs.About.Count) - assert.Equal(t, newPr.ID, foundPrs.Items[0].ID) + assert.Equal(t, *newPr.Id, foundPrs.Items[0].Id) // GET by id - thisPrPath := basePath + "/" + newPr.ID + thisPrPath := basePath + "/" + *newPr.Id respBytes = httpRequest(t, "GET", thisPrPath+queryParams, []byte{}, 200) err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, newPr.ID, foundPr.ID) + assert.Equal(t, *newPr.Id, foundPr.Id) // GET actions by PR id test.WaitForPredicateToBeTrue(func() bool { @@ -191,16 +187,13 @@ func TestActionsToCompleteState(t *testing.T) { SupplierUniqueRecordId: "return-" + supplierSymbol + "::WILLSUPPLY_LOANED", }, } - jsonBytes, err := json.Marshal(request) - assert.NoError(t, err) - illMessage := string(jsonBytes) + id := uuid.NewString() newPr := proapi.CreatePatronRequest{ - ID: uuid.NewString(), - Timestamp: time.Now(), + Id: &id, SupplierSymbol: &supplierSymbol, RequesterSymbol: &requesterSymbol, Patron: &patron, - IllRequest: &illMessage, + IllRequest: utils.Must(common.StructToMap(request)), } newPrBytes, err := json.Marshal(newPr) assert.NoError(t, err, "failed to marshal patron request") @@ -211,8 +204,8 @@ func TestActionsToCompleteState(t *testing.T) { err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, newPr.ID, foundPr.ID) - requesterPrPath := basePath + "/" + newPr.ID + assert.Equal(t, *newPr.Id, foundPr.Id) + requesterPrPath := basePath + "/" + *newPr.Id queryParams := "?side=borrowing&symbol=" + *foundPr.RequesterSymbol // Wait till action available @@ -231,10 +224,10 @@ func TestActionsToCompleteState(t *testing.T) { // Find supplier patron request test.WaitForPredicateToBeTrue(func() bool { - supPr, _ := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, newPr.ID) + supPr, _ := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, *newPr.Id) return supPr.ID != "" }) - supPr, err := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, newPr.ID) + supPr, err := prRepo.GetPatronRequestBySupplierSymbolAndRequesterReqId(appCtx, supplierSymbol, *newPr.Id) assert.NoError(t, err) assert.NotNil(t, supPr.ID) @@ -349,14 +342,14 @@ func TestActionsToCompleteState(t *testing.T) { respBytes = httpRequest(t, "GET", requesterPrPath+queryParams, []byte{}, 200) err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, newPr.ID, foundPr.ID) + assert.Equal(t, *newPr.Id, foundPr.Id) assert.Equal(t, string(prservice.BorrowerStateCompleted), foundPr.State) // Check supplier patron request done respBytes = httpRequest(t, "GET", supplierPrPath+supQueryParams, []byte{}, 200) err = json.Unmarshal(respBytes, &foundPr) assert.NoError(t, err, "failed to unmarshal patron request") - assert.Equal(t, supPr.ID, foundPr.ID) + assert.Equal(t, supPr.ID, foundPr.Id) assert.Equal(t, string(prservice.LenderStateCompleted), foundPr.State) }