Bug 22639: Add GET /config/search_mappings endpoint
This patch adds a new API endpoint to retrieve the search field
mappings configuration (search fields and their MARC mappings, grouped
by index).
The endpoint supports content negotiation:
- application/json (default)
- application/yaml (for export/import compatibility)
An optional 'type' query parameter allows filtering mappings by MARC
type (marc21 or unimarc).
Implementation notes:
- New controller: Koha::REST::V1::Config::SearchMappings
- Reuses Koha::SearchEngine::Elasticsearch::raw_elasticsearch_mappings
- Registers 'application/yaml' MIME type in Koha::REST::V1
- Permission: manage_search_engine_config
To test:
1. Apply this patch
2. Rebuild the API bundle:
$ yarn api:bundle
3. Restart Plack
4. Run:
$ prove t/db_dependent/api/v1/config_search_mappings.t
=> SUCCESS: Tests pass
5. Test manually:
$ curl -u user:pass /api/v1/config/search_mappings
=> SUCCESS: Returns JSON with search field mappings
$ curl -u user:pass -H 'Accept: application/yaml' \
/api/v1/config/search_mappings
=> SUCCESS: Returns YAML output
$ curl -u user:pass /api/v1/config/search_mappings?type=marc21
=> SUCCESS: Only marc21 mappings are returned
6. Sign off :-D
Signed-off-by: Laurence <laurence.rault@biblibre.com>
Signed-off-by: Thomas Klausner <domm@plix.at>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
975f0c3508
commit
caa52ddf91
|
|
@ -82,6 +82,9 @@ sub startup {
|
|||
$self->types->type( mij => 'application/marc-in-json' );
|
||||
$self->types->type( marc => 'application/marc' );
|
||||
|
||||
# YAML type
|
||||
$self->types->type( yaml => 'application/yaml' );
|
||||
|
||||
my $secret_passphrase = C4::Context->config('api_secret_passphrase');
|
||||
if ($secret_passphrase) {
|
||||
$self->secrets( [$secret_passphrase] );
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package Koha::REST::V1::Config::SearchMappings;
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
|
||||
use Koha::SearchEngine::Elasticsearch;
|
||||
|
||||
use Try::Tiny qw( catch try );
|
||||
use Encode;
|
||||
use YAML::XS;
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Methods
|
||||
|
||||
=head3 get
|
||||
|
||||
Controller method to retrieve search field mappings
|
||||
|
||||
=cut
|
||||
|
||||
sub get {
|
||||
my $c = shift->openapi->valid_input or return;
|
||||
|
||||
return try {
|
||||
my $type = $c->param('type');
|
||||
my $mappings = Koha::SearchEngine::Elasticsearch::raw_elasticsearch_mappings($type);
|
||||
|
||||
$c->respond_to(
|
||||
yaml => {
|
||||
status => 200,
|
||||
format => 'yaml',
|
||||
text => Encode::decode_utf8( YAML::XS::Dump($mappings) ),
|
||||
},
|
||||
any => {
|
||||
status => 200,
|
||||
openapi => $mappings,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
$c->unhandled_exception($_);
|
||||
};
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
/config/search_mappings:
|
||||
get:
|
||||
x-mojo-to: Config::SearchMappings#get
|
||||
operationId: getSearchMappings
|
||||
tags:
|
||||
- search_mappings
|
||||
summary: Get search field mappings
|
||||
produces:
|
||||
- application/json
|
||||
- application/yaml
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
description: MARC type filter
|
||||
required: false
|
||||
type: string
|
||||
enum:
|
||||
- marc21
|
||||
- unimarc
|
||||
responses:
|
||||
"200":
|
||||
description: Search field mappings
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"403":
|
||||
description: Access forbidden
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"500":
|
||||
description: |
|
||||
Internal server error. Possible `error_code` attribute values:
|
||||
|
||||
* `internal_server_error`
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"503":
|
||||
description: Under maintenance
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
x-koha-authorization:
|
||||
permissions:
|
||||
parameters: manage_search_engine_config
|
||||
|
|
@ -341,6 +341,8 @@ paths:
|
|||
$ref: ./paths/config_file_transports.yaml#/~1config~1file_transports
|
||||
"/config/file_transports/{file_transport_id}":
|
||||
$ref: "./paths/config_file_transports.yaml#/~1config~1file_transports~1{file_transport_id}"
|
||||
/config/search_mappings:
|
||||
$ref: ./paths/config_search_mappings.yaml#/~1config~1search_mappings
|
||||
"/deleted/biblios":
|
||||
$ref: "./paths/deleted_biblios.yaml#/~1deleted~1biblios"
|
||||
"/deleted/biblios/{biblio_id}":
|
||||
|
|
@ -1409,6 +1411,9 @@ tags:
|
|||
- description: "Manage search filters"
|
||||
name: search_filters
|
||||
x-displayName: Search filters
|
||||
- description: "Manage search field mappings"
|
||||
name: search_mappings
|
||||
x-displayName: Search mappings
|
||||
- description: "Manage SIP2 accounts"
|
||||
name: sip2_accounts
|
||||
x-displayName: SIP2 Accounts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
#!/usr/bin/env perl
|
||||
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Koha; if not, see <https://www.gnu.org/licenses>.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Test::NoWarnings;
|
||||
use Test::More tests => 5;
|
||||
use Test::Mojo;
|
||||
|
||||
use t::lib::TestBuilder;
|
||||
use t::lib::Mocks;
|
||||
|
||||
use YAML::XS;
|
||||
|
||||
use Koha::Database;
|
||||
use Koha::SearchFields;
|
||||
use Koha::SearchMarcMaps;
|
||||
|
||||
my $schema = Koha::Database->new->schema;
|
||||
my $builder = t::lib::TestBuilder->new;
|
||||
|
||||
my $t = Test::Mojo->new('Koha::REST::V1');
|
||||
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
|
||||
|
||||
subtest 'authorization' => sub {
|
||||
|
||||
plan tests => 4;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
my $authorized_patron = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 0 }
|
||||
}
|
||||
);
|
||||
$builder->build(
|
||||
{
|
||||
source => 'UserPermission',
|
||||
value => {
|
||||
borrowernumber => $authorized_patron->borrowernumber,
|
||||
module_bit => 3,
|
||||
code => 'manage_search_engine_config',
|
||||
},
|
||||
}
|
||||
);
|
||||
my $password = 'thePassword123';
|
||||
$authorized_patron->set_password( { password => $password, skip_validation => 1 } );
|
||||
|
||||
my $unauthorized_patron = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 0 }
|
||||
}
|
||||
);
|
||||
$unauthorized_patron->set_password( { password => $password, skip_validation => 1 } );
|
||||
|
||||
$t->get_ok( "//" . $unauthorized_patron->userid . ":$password@/api/v1/config/search_mappings" )->status_is(403);
|
||||
|
||||
$t->get_ok( "//" . $authorized_patron->userid . ":$password@/api/v1/config/search_mappings" )->status_is(200);
|
||||
|
||||
$schema->storage->txn_rollback;
|
||||
};
|
||||
|
||||
subtest 'get JSON (default)' => sub {
|
||||
|
||||
plan tests => 10;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
my $patron = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 3**2 }
|
||||
}
|
||||
);
|
||||
my $password = 'thePassword123';
|
||||
$patron->set_password( { password => $password, skip_validation => 1 } );
|
||||
my $userid = $patron->userid;
|
||||
|
||||
# Clear existing data
|
||||
Koha::SearchFields->search->delete;
|
||||
Koha::SearchMarcMaps->search->delete;
|
||||
$schema->resultset('SearchMarcToField')->search->delete;
|
||||
|
||||
# Create test mapping
|
||||
my $search_field = Koha::SearchFields->find_or_create(
|
||||
{
|
||||
name => 'title',
|
||||
label => 'Title',
|
||||
type => 'string',
|
||||
weight => 17,
|
||||
staff_client => 0,
|
||||
opac => 1,
|
||||
mandatory => 1,
|
||||
},
|
||||
{ key => 'name' }
|
||||
);
|
||||
|
||||
my $marc_map = Koha::SearchMarcMaps->find_or_create(
|
||||
{
|
||||
index_name => 'biblios',
|
||||
marc_type => 'marc21',
|
||||
marc_field => '245',
|
||||
}
|
||||
);
|
||||
|
||||
$search_field->add_to_search_marc_maps(
|
||||
$marc_map,
|
||||
{
|
||||
facet => 0,
|
||||
suggestible => 0,
|
||||
sort => 1,
|
||||
filter => '',
|
||||
}
|
||||
);
|
||||
|
||||
$t->get_ok("//$userid:$password@/api/v1/config/search_mappings")
|
||||
->status_is(200)
|
||||
->content_type_like(qr/json/)
|
||||
->json_has('/biblios/title')
|
||||
->json_is( '/biblios/title/label' => 'Title' )
|
||||
->json_is( '/biblios/title/type' => 'string' )
|
||||
->json_is( '/biblios/title/mandatory' => 1 )
|
||||
->json_is( '/biblios/title/opac' => 1 );
|
||||
|
||||
my $mapping = $t->tx->res->json->{biblios}{title}{mappings}[0];
|
||||
is( $mapping->{marc_field}, '245', 'marc_field is 245' );
|
||||
is( $mapping->{marc_type}, 'marc21', 'marc_type is marc21' );
|
||||
|
||||
$schema->storage->txn_rollback;
|
||||
};
|
||||
|
||||
subtest 'get YAML' => sub {
|
||||
|
||||
plan tests => 6;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
my $patron = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 3**2 }
|
||||
}
|
||||
);
|
||||
my $password = 'thePassword123';
|
||||
$patron->set_password( { password => $password, skip_validation => 1 } );
|
||||
my $userid = $patron->userid;
|
||||
|
||||
# Clear existing data
|
||||
Koha::SearchFields->search->delete;
|
||||
Koha::SearchMarcMaps->search->delete;
|
||||
$schema->resultset('SearchMarcToField')->search->delete;
|
||||
|
||||
# Create test mapping
|
||||
my $search_field = Koha::SearchFields->find_or_create(
|
||||
{
|
||||
name => 'author',
|
||||
label => 'Author',
|
||||
type => 'string',
|
||||
staff_client => 1,
|
||||
opac => 1,
|
||||
mandatory => 0,
|
||||
},
|
||||
{ key => 'name' }
|
||||
);
|
||||
|
||||
my $marc_map = Koha::SearchMarcMaps->find_or_create(
|
||||
{
|
||||
index_name => 'biblios',
|
||||
marc_type => 'marc21',
|
||||
marc_field => '100a',
|
||||
}
|
||||
);
|
||||
|
||||
$search_field->add_to_search_marc_maps(
|
||||
$marc_map,
|
||||
{
|
||||
facet => 1,
|
||||
suggestible => 0,
|
||||
sort => 0,
|
||||
filter => '',
|
||||
}
|
||||
);
|
||||
|
||||
$t->get_ok( "//$userid:$password@/api/v1/config/search_mappings" => { Accept => 'application/yaml' } )
|
||||
->status_is(200)
|
||||
->content_type_like(qr/yaml/);
|
||||
|
||||
my $body = $t->tx->res->text;
|
||||
my $mappings = YAML::XS::Load($body);
|
||||
|
||||
is( ref $mappings->{biblios}{author}, 'HASH', 'YAML contains author field' );
|
||||
is( $mappings->{biblios}{author}{label}, 'Author', 'YAML label is correct' );
|
||||
is( $mappings->{biblios}{author}{mappings}[0]{marc_field}, '100a', 'YAML marc_field is correct' );
|
||||
|
||||
$schema->storage->txn_rollback;
|
||||
};
|
||||
|
||||
subtest 'type filter' => sub {
|
||||
|
||||
plan tests => 8;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
my $patron = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 3**2 }
|
||||
}
|
||||
);
|
||||
my $password = 'thePassword123';
|
||||
$patron->set_password( { password => $password, skip_validation => 1 } );
|
||||
my $userid = $patron->userid;
|
||||
|
||||
# Clear existing data
|
||||
Koha::SearchFields->search->delete;
|
||||
Koha::SearchMarcMaps->search->delete;
|
||||
$schema->resultset('SearchMarcToField')->search->delete;
|
||||
|
||||
# Create test field with both marc21 and unimarc mappings
|
||||
my $search_field = Koha::SearchFields->find_or_create(
|
||||
{
|
||||
name => 'title',
|
||||
label => 'Title',
|
||||
type => 'string',
|
||||
staff_client => 1,
|
||||
opac => 1,
|
||||
mandatory => 0,
|
||||
},
|
||||
{ key => 'name' }
|
||||
);
|
||||
|
||||
my $marc21_map = Koha::SearchMarcMaps->find_or_create(
|
||||
{
|
||||
index_name => 'biblios',
|
||||
marc_type => 'marc21',
|
||||
marc_field => '245',
|
||||
}
|
||||
);
|
||||
|
||||
$search_field->add_to_search_marc_maps(
|
||||
$marc21_map,
|
||||
{
|
||||
facet => 0,
|
||||
suggestible => 0,
|
||||
sort => 0,
|
||||
filter => '',
|
||||
}
|
||||
);
|
||||
|
||||
my $unimarc_map = Koha::SearchMarcMaps->find_or_create(
|
||||
{
|
||||
index_name => 'biblios',
|
||||
marc_type => 'unimarc',
|
||||
marc_field => '200a',
|
||||
}
|
||||
);
|
||||
|
||||
$search_field->add_to_search_marc_maps(
|
||||
$unimarc_map,
|
||||
{
|
||||
facet => 0,
|
||||
suggestible => 1,
|
||||
sort => 0,
|
||||
filter => '',
|
||||
}
|
||||
);
|
||||
|
||||
# No filter — both mappings returned
|
||||
$t->get_ok("//$userid:$password@/api/v1/config/search_mappings")->status_is(200);
|
||||
|
||||
my $all_mappings = $t->tx->res->json->{biblios}{title}{mappings};
|
||||
is( scalar @$all_mappings, 2, 'Unfiltered returns both marc21 and unimarc mappings' );
|
||||
|
||||
# Filter by marc21
|
||||
$t->get_ok("//$userid:$password@/api/v1/config/search_mappings?type=marc21")->status_is(200);
|
||||
|
||||
my $marc21_mappings = $t->tx->res->json->{biblios}{title}{mappings};
|
||||
is( scalar @$marc21_mappings, 1, 'Filtered to marc21 returns 1 mapping' );
|
||||
is( $marc21_mappings->[0]{marc_type}, 'marc21', 'Returned mapping is marc21' );
|
||||
is( $marc21_mappings->[0]{marc_field}, '245', 'Returned mapping field is 245' );
|
||||
|
||||
$schema->storage->txn_rollback;
|
||||
};
|
||||
Loading…
Reference in New Issue