@oarepo/invenio-api-vuex
A set of vuex store modules and route utils to work with Invenio 3
REST API.
Installation
yarn add @oarepo/invenio-api-vuex
import Vue from 'vue'
import InvenioAPI from '@oarepo/invenio-api-vuex'
Vue.use(Vuex)
const store = new Vuex.Store({})
Vue.use(InvenioAPI, {
store: store,
apiURL: '/api'
})
Using it together with vuex preloader
yarn add @oarepo/vuex-preloader
To use the library together with url preloader (recommended way),
add the following configuration to your routes:
import { routerCollectionList, routerCollection, routerRecord } from '@oarepo/invenio-api-vuex'
export default [
{
path: '/',
redirect: '/collections'
},
routerCollectionList({
path: '/collections',
component: Collections
}),
routerRecord({
path: '/:collectionId/:recordId',
component: Record
}),
routerCollection({
path: '/:collectionId',
component: Collection,
props: query()
})
]
See src/router.js for details
Using it together with vue query synchronizer
To simplify facet & query handling, record collection can be used together with
@oarepo/vue-query-synchronizer
. Add props: query()
into router path
as above and see the example at src/components/Collection.vue
Store modules
Store modules are implemented with vuex-class-modules
.
Collections module
Use this store module only if invenio-oarepo-ui
is installed on the backend.
This module returns a list of contained data models/collections together
with extra information about the collections, such as:
- name
- description
- urls and identifiers
- validation rules (serialized marshmallow, WIP)
- json schema (WIP)
This module is registered under the name 'oarepoCollectionList' and is
not loaded automatically unless registered in the router.
import { routerCollectionList } from '@oarepo/invenio-api-vuex'
routes = [
routerCollectionList({
name: 'root',
path: '/',
component: MyApp,
children: [...]
})
]
To skip automatic loading and load it manually when needed,
invoke oarepoCollectionList/load
action on the store.
Usage in components
{
computed: {
...mapState({
collections: state => state.oarepoCollectionList.collections
})
}
methods: {
...mapActions([
'oarepoCollectionList.reload',
])
reload () {
this['oarepoCollectionList.reload']()
}
}
}
@Component({})
class MyComponent extends Vue {
get collections() {
return this.$oarepo.collections.collections
}
reload() {
return this.$oarepo.collections.reload()
}
}
Template:
<list>
<ul v-for="collection in collections" :key="collection.code">
<p>{{ collection.title['en-us'] }}</p>
<p>{{ collection.description['en-us'] }}</p>
</ul>
</list>
See https://github.com/oarepo/invenio-oarepo-ui module for data structure details.
State/Getters
state
One of State.INVALID
, State.LOADING
, State.LOADED
collections
An array of descriptions of collections received from the server
visibleCollections
An array of descriptions of collections received from the server
that are not marked as hidden
Actions
load
, reload
Fetches the list of collections from the server
Collection module
Collection module is responsible for loading a single collection of
records (listing of records) and creating a new record. The module
is mapped at oarepoCollection
Usage
{
computed: {
...mapState({
records: state => state.oarepoCollection.records,
facets: state => state.oarepoCollection.facets,
})
}
methods: {
...mapActions([
'oarepoCollection.load',
])
load (collectionId) {
this['oarepoCollection.load']({collectionId})
}
}
}
@Component({})
class MyComponent extends Vue {
get items() {
return this.$oarepo.collection.items
}
get facets() {
return this.$oarepo.collection.facets
}
load(collectionId) {
return this.$oarepo.collection.load({collectionId})
}
}
State/Getters
state
One of State.INVALID
, State.LOADING
, State.LOADED
collectionId
ID of the currently loaded collection
records
A list of loaded records as returned from the elasticsearch. Each record looks like:
{
"created":"2019-12-01T23:48:10.851166+00:00",
"id":"32861b6d-1655-4c61-9914-80c5f9a19838",
"links":{
"self":"https://restaurovani.vscht.cz/api/drafts/restorations/objects/32861b6d-1655-4c61-9914-80c5f9a19838"
},
"metadata":{
"$schema":"https://restaurovani.vscht.cz/schemas/draft/krokd/restoration-object-v1.0.0.json",
},
"revision":31,
"updated":"2019-12-12T12:38:25.138961+00:00"
}
facets
A list of loaded facets. Each facet looks like:
{
"element": {},
"facets": [
{
"count": 73,
"label": "textil",
"value": "textil"
},
{
"count": 11,
"label": "sklo",
"value": "sklo"
},
{
"count": 92,
"label": "kovy",
"value": "kovy"
},
{
"count": 18,
"label": "keramika",
"value": "keramika"
}
],
"label": "category",
"code": "category",
"type": "buckets"
}
See Configuration section of this readme for details how to configure
extractors and translators.
totalPages
Total pages available
response
The last axios response without any modifications
queryParams
Query params (after ?) passed to the API call
reloadNeeded
This state parameter works together with vuex-preloader. Set it to true to force
reloading of the state on the next route change
Actions
load({query, collectionId})
Fetches apiURL/${collectionId}?{query}
reload
Reloads the state
transform
Transforms the remembered response and refills facets
and records
. This action
is useful for example when locale is changed to translate facets to the new locale.
create({metadata, storeModule})
Calls HTTP post on collection url to create a new record. Returns the created item.
If storeModule is specified, the loaded record is fed to the storeModule (instance
of RecordModule type). Returns json of the created record.
This call sets the reloadNeeded
state so that the store is reloaded on the next
route change when vuex-preloader is used (in case the created record falls into
the current page).
RecordModule
Record module is responsible for loading, updating, deleting a single record.
The module is mapped at oarepoRecord
Usage
{
computed: {
...mapState({
metadata: state => state.oarepoRecord.metadata,
links: state => state.oarepoRecord.links,
created: state => state.oarepoRecord.created,
updated: state => state.oarepoRecord.updated,
record: state => state.oarepoRecord.record,
})
}
methods: {
...mapActions([
'oarepoRecord.load',
])
load (collectionId, recordId) {
this['oarepoCollection.load']({collectionId, recordId})
}
}
}
@Component({})
class MyComponent extends Vue {
get metadata() {
return this.$oarepo.record.metadata
}
load(collectionId, recordId) {
return this.$oarepo.record.load({collectionId, recordId})
}
}
State/Getters
state
One of State.INVALID
, State.LOADING
, State.LOADED
collectionId
ID of the currently loaded collection
recordId
ID of the currently loaded record
metadata
The metadata
section of the response
links
The links
section of the response
created
Created timestamp
updated
Updated timestamp
record
Raw, unparsed record
Actions
load({collectionId, recordId})
Loads the record and returns it
reload
Reloads the last loaded record
patch(json)
Issues patch command on the repository. json
is a json-patch
payload
save
Saves (via HTTP POST) the current record and persists any modifications
Configuration
import InvenioAPI from '@oarepo/invenio-api-vuex'
Vue.use(InvenioAPI, {
store,
apiURL: '/api',
defaultPageSize: 10,
i18n (x) { return x },
defaultFacetOptions: new FacetOptions({}),
facetOptions: {},
defaultRecordPreprocessors: new CallbackList(),
recordPreprocessors: {},
defaultListRecordPreprocessors: new CallbackList(),
listRecordPreprocessors: {},
collectionListMixins: [],
collectionMixins: [],
recordMixins: [],
})
Config Parameters
defaultPageSize
Page size to use when no ?size is given
i18n
A function taking a string (key) and converting it to translated msg
defaultFacetOptions
, facetOptions
Facet options to be used for facet extraction and processing. For a collection
with id collectionId
facetOptions[collectionId]
are tried and if undefined
defaultFacetOptions
are used.
defaultRecordPreprocessors
, recordPreprocessors
Preprocessors called when a record is fetched from repository. For a collection
with id collectionId
recordPreprocessors[collectionId]
are tried and if undefined
defaultRecordPreprocessors
are used.
Preprocessor is a function with a signature preprocess(response.data, { record: RecordModule })
defaultListRecordPreprocessors
, listRecordPreprocessors
Preprocessors called when a listing of records is fetched from repository.
For a collection
with id collectionId
listRecordPreprocessors[collectionId]
are tried and
if undefined defaultListRecordPreprocessors
are used.
Preprocessor is a function with a signature
preprocess(record, { collection: CollectionModule })
Facet processing
Facet processing takes place in two phases.
During the first phase, for each node of aggregations
tree a facet extractor
is called to extract facets.
During the second phase all the facets might be modified by facet preprocessors,
for example to add human-readable labels to facet values.
The configuration of this process is contained in FacetOptions
class.
FacetOptions
A class containing options for handling collection facets:
class FacetOptions {
facetExtractors = new CallbackList(bucketExtractor)
facetPreprocessors = new CallbackList(facetTranslator)
translation = {
defaultTranslateTitles: TranslationOptions.NO_PREFIX,
defaultTranslateValues: TranslationOptions.NO_TRANSLATION,
translateTitles: {},
translateValues: {}
}
constructor ({
parent,
facetExtractors, facetPreprocessors,
defaultTranslateTitles, defaultTranslateValues,
translateTitles, translateValues
}) {}
}
The extractor decides if the node represents a facet or not and returns its representation.
It is a function with signature extractor({ node, path })
, node
being the current
node in the json tree and path a reversed array of {code, element}
(root comes last),
where code
is the key under which the element
is stored.
If the extractor decides that the node
represents facet, it returns it in the form of:
return {
type: "facet type",
code: path[0].code,
element: node,
facets: [
{
label: 'London',
value: 'london',
count: 30
},
]
}
If the extractor does not recognize the element as facet bearer, it returns undefined
.
The default implementation contains a bucketExtractor. To add yours, create new FacetOptions
:
new FacetOptions({
facetExtractors: [myFacetExtractor1, myFacetExtractor2]
})
new FacetOptions({
facetExtractors: myFacetExtractor
})
Facet preprocessors
Facet preprocessor is a function with signature
facetPreprocessor(facets, { facetOptions, i18n, collection })
. It should transform
the facets array (with items returned by the extractors) and return the transformed array.
The default implementation converts facet codes into labels as described below.
To add your own preprocessors, create an instance of FacetOptions with the preprocessors:
new FacetOptions({
facetPreprocessors: [myFacetPreprocessor1, myFacetPreprocessor2]
})
new FacetOptions({
facetPreprocessors: myFacetPreprocessor
})
Translation
This block configures the default facetTranslator
. Using config.i18n(key)
,
the translator converts facet code and facet values into a human readable string.
The description below will use the following facet as returned from facet extractor:
facet = {
type: "buckets",
code: 'cities',
element: {},
facets: [
{
value: 'london',
count: 30
},
]
}
Options:
defaultTranslateTitles, translateTitles
translateTitles[facet.code]
specifies how facet titles will be obtained.
If it is not defined, defaultTranslateTitles
setting will be used.
Default is TranslationOptions.TRANSLATE
.
It can be set to either TranslationOptions.NO_TRANSLATION, TranslationOptions.TRANSLATE
or an arbitrary string.
If set to TranslationOptions.NO_TRANSLATION, the translated facet will look like:
facet = {
label: 'cities',
}
TranslationOptions.TRANSLATE:
facet = {
label: i18n('cities'),
}
arbitrary string ('abc.'):
facet = {
label: i18n('abc.cities'),
}
defaultTranslateValues, translateValues
Works similarly as titles, but for facet value labels.
translateValues[facet.code]
specifies how facet titles will be obtained.
If it is not defined, defaultTranslateValues
setting will be used.
Default is TranslationOptions.NO_TRANSLATION
.
It can be set to either TranslationOptions.NO_TRANSLATION,
TranslationOptions.TRANSLATE, TranslationOptions.FACET_CODE_PREFIX
or an arbitrary string.
If set to TranslationOptions.NO_TRANSLATION, the translated facet will look like:
facet = {
code: 'cities',
facets: [
{
value: 'london',
label: 'london',
count: 30
}
],
}
TranslationOptions.TRANSLATE:
facet = {
code: 'cities',
facets: [
{
value: 'london',
label: i18n('london'),
count: 30
}
],
}
TranslationOptions.FACET_CODE_PREFIX:
facet = {
code: 'cities',
facets: [
{
value: 'london',
label: i18n('cities.london'),
count: 30
}
],
}
arbitrary string ('abc.'):
facet = {
code: 'cities',
facets: [
{
value: 'london',
label: i18n('abc.london'),
count: 30
}
],
}
Mixins
The provided stores can be extended with custom mixin classes to add extra
properties/mutations/actions. To do so, provide functions that return a new
mixin class inherited from the provided superclass. This pattern is described
at https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
An example:
import { Action, Mutation } from 'vuex-class-modules'
Vue.use(InvenioAPI, {
store,
apiURL: '/api',
collectionListMixins: [
(superclass) => class extends superclass {
num = 1
@Mutation
setNumber(num) {
this.num = num
}
@Action
async numAction() {
return new Promise((resolve) => {
setTimeout(() => {
this.num = 3
resolve()
}, 10)
})
}
}
],
})
Now, num
is defined as a getter on $oarepo.collectionList and mutations/actions
are added to the store module.