Created
November 9, 2020 10:04
-
-
Save win0err/feb98166b011f772c0bd6cd3215280ab to your computer and use it in GitHub Desktop.
Generic Vuex Modules (https://habr.com/ru/company/odin_ingram_micro/blog/526094/)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from 'axios'; | |
// parameterize :: replace substring in string by template | |
// parameterize :: Object -> String -> String | |
// parameterize :: {userId: '123'} -> '/users/:userId/activate' -> '/users/123/activate' | |
const parameterize = (url, urlParameters) => Object.entries(urlParameters) | |
.reduce( | |
(a, [key, value]) => a.replace(`:${key}`, value), | |
url, | |
); | |
// responsesToCollection :: Array -> Array | |
// responsesToCollection :: [{data: [1, 2]}, {data: [3, 4]}] -> [1, 2, 3, 4] | |
const responsesToCollection = responses => responses.reduce((a, v) => a.concat(v.data), []); | |
// getContentRangeSize :: String -> Integer | |
// getContentRangeSize :: "Content-Range: items 0-137/138" -> 138 | |
const getContentRangeSize = header => +/(\w+) (\d+)-(\d+)\/(\d+)/g.exec(header)[4]; | |
// getCollectionAndTotal :: Object -> Object | |
// getCollectionAndTotal :: { data, headers } -> { collection, total } | |
const getCollectionAndTotal = ({ data, headers }) => ({ | |
collection: data, | |
total: headers['Content-Range'] && getContentRangeSize(headers['Content-Range']), | |
}) | |
export default class BaseRepository { | |
constructor(entity, version = 'v1') { | |
this.entity = entity; | |
this.version = version; | |
} | |
get endpoint() { | |
return `/${this.version}/${this.entity}`; | |
} | |
async query({ | |
method = 'GET', | |
nestedEndpoint = '', | |
urlParameters = {}, | |
queryParameters = {}, | |
data = undefined, | |
headers = {}, | |
}) { | |
const url = parameterize(`${this.endpoint}${nestedEndpoint}`, urlParameters); | |
const result = await axios({ | |
method, | |
url, | |
headers, | |
data, | |
params: queryParameters, | |
}); | |
return result; | |
} | |
async getTotal(urlParameters, queryParameters = {}) { | |
const { headers } = await this.query({ | |
queryParameters: { ...queryParameters, limit: 1 }, | |
urlParameters, | |
}); | |
if (!headers['Content-Range']) { | |
throw new Error('Content-Range header is missing'); | |
} | |
return getContentRangeSize(headers['Content-Range']); | |
} | |
async list(queryParameters, urlParameters) { | |
const result = await this.query({ urlParameters, queryParameters }); | |
return { | |
...getCollectionAndTotal(result), | |
params: queryParameters, | |
}; | |
} | |
async listAll(queryParameters = {}, urlParameters, chunkSize = 100) { | |
const params = { | |
...queryParameters, | |
offset: 0, | |
limit: chunkSize, | |
}; | |
const requests = []; | |
const total = await this.getTotal(urlParameters, queryParameters); | |
while (params.offset < total) { | |
requests.push( | |
this.query({ | |
urlParameters, | |
queryParameters: params, | |
}), | |
); | |
params.offset += chunkSize; | |
} | |
const result = await Promise.all(requests); | |
return { | |
total, | |
params: { | |
...queryParameters, | |
offset: 0, | |
limit: total, | |
}, | |
collection: responsesToCollection(result), | |
}; | |
} | |
async create(requestBody, urlParameters) { | |
const { data } = await this.query({ | |
method: 'POST', | |
urlParameters, | |
data: requestBody, | |
}); | |
return data; | |
} | |
async get(id = '', urlParameters, queryParameters = {}) { | |
const { data } = await this.query({ | |
method: 'GET', | |
nestedEndpoint: `/${id}`, | |
urlParameters, | |
queryParameters, | |
}); | |
return data; | |
} | |
async update(id = '', requestBody, urlParameters) { | |
const { data } = await this.query({ | |
method: 'PUT', | |
nestedEndpoint: `/${id}`, | |
urlParameters, | |
data: requestBody, | |
}); | |
return data; | |
} | |
async delete(id = '', requestBody, urlParameters) { | |
const { data } = await this.query({ | |
method: 'DELETE', | |
nestedEndpoint: `/${id}`, | |
urlParameters, | |
data: requestBody, | |
}); | |
return data; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
applyTo, | |
assocPath, | |
clone, | |
findIndex, | |
identity, | |
ifElse, | |
is, | |
update, | |
map, | |
path, | |
prepend, | |
} from 'ramda'; | |
import { | |
mergeArraysBy, | |
typeIs, | |
} from '~utils'; | |
/** replaceState is universal mutation function | |
* Usage: Import it inside store file, and then just set as mutation "replace" | |
* | |
* commit({ | |
* type: 'replace', // sets mutation type. Value should match set key | |
* inside: true, // false by default. Switches mutation to collection logic. | |
* obj: 'webhooks', // refers mutated state property (can be an array) | |
* when: propEq('id', id), // If defined applies conditional logic | |
* value: webhook, // Value to be set. May take transform function | |
* }) | |
* | |
* Api: | |
* value - Any | Function - if Function, understand it as transformation. | |
* It takes current value as an argument and should return new one. | |
* | |
* { !inside, !when } - default - applies mutation to state[obj] | |
* { inside, !when } - map - mutates to each element of collection (f.e. transform) | |
* { !inside, when } - cond - mutates if #.when(current value) -> true | |
* { inside, when } - update - mutates specific elements inside collection | |
* | |
* */ | |
export const replaceState = (state, { obj, value, when, inside = false }) => { | |
let data = typeIs('Array', obj) ? path(obj, state) : clone(state[obj]); | |
const val = d => ifElse(is(Function), applyTo(d), identity)(value); | |
// Regular replacement | |
if (!inside && !when) data = val(data); | |
// replace item in collection | |
if (inside && when) { | |
const idx = findIndex(when, data); | |
data = update(idx, val(data[idx]), data); | |
} | |
// transform each inside collection | |
if (inside && !when) data = map(val, data); | |
// conditionally replace | |
if (!inside && when) data = ifElse(when, val, identity)(data); | |
if (typeIs('Array', obj)) { | |
state[obj[0]] = assocPath(obj, data, state)[obj[0]]; | |
} else { | |
state[obj] = data; | |
} | |
}; | |
export const expandState = (state, { obj, value, expandBy }) => { | |
if (expandBy) { | |
state[obj] = mergeArraysBy(expandBy, state[obj], value); | |
} else { | |
state[obj] = state[obj].concat(value); | |
} | |
}; | |
export const prependState = (state, { obj, value }) => { | |
state[obj] = prepend(value, state[obj]); | |
}; | |
export default { | |
expandState, | |
replaceState, | |
prependState, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import BaseRepository from './BaseRepository'; | |
import StoreFactory from './StoreFactory'; | |
const createRepository = (endpoint, repositoryExtension = {}) => { | |
const repository = new BaseRepository(endpoint, 'v1'); | |
return Object.assign(repository, repositoryExtension); | |
} | |
const ResourceFactory = ( | |
store, | |
{ | |
name, | |
endpoint, | |
repositoryExtension = {}, | |
storeExtension = () => ({}), | |
}, | |
) => { | |
const repository = createRepository(endpoint, repositoryExtension); | |
const module = StoreFactory(repository, storeExtension(repository)); | |
store.registerModule(name, module); | |
} | |
export default ResourceFactory; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
clone, | |
is, | |
mergeDeepRight, | |
} from 'ramda'; | |
const keyBy = (pk, collection) => { | |
const keyedCollection = {}; | |
collection.forEach( | |
item => keyedCollection[item[pk]] = item, | |
); | |
return keyedCollection; | |
} | |
const replaceState = (state, { obj, value }) => { | |
const data = clone(state[obj]); | |
state[obj] = is(Function, value) ? value(data) : value; | |
}; | |
const updateItemInCollection = (id, item) => collection => { | |
collection[id] = item; | |
return collection | |
}; | |
const removeItemFromCollection = id => collection => { | |
delete collection[id]; | |
return collection | |
}; | |
const inc = v => ++v; | |
const dec = v => --v; | |
export const createStore = (repository, primaryKey = 'id') => ({ | |
namespaced: true, | |
state: { | |
collection: {}, | |
currentId: '', | |
total: 0, | |
}, | |
getters: { | |
collection: ({ collection }) => Object.values(collection), | |
total: ({ total }) => total, | |
current: ({ collection, currentId }) => collection[currentId], | |
}, | |
mutations: { | |
replace: replaceState, | |
}, | |
actions: { | |
async list({ commit }, attrs = {}) { | |
const { queryParameters = {}, urlParameters = {} } = attrs; | |
console.log('repository', repository) | |
const result = await repository.list(queryParameters, urlParameters); | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: keyBy(primaryKey, result.collection), | |
}); | |
commit({ | |
obj: 'total', | |
type: 'replace', | |
value: result.total, | |
}); | |
return result; | |
}, | |
async listAll({ commit }, attrs = {}) { | |
const { | |
queryParameters = {}, | |
urlParameters = {}, | |
chunkSize = 100, | |
} = attrs; | |
const result = await repository.listAll(queryParameters, urlParameters, chunkSize) | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: keyBy(primaryKey, result.collection), | |
}); | |
commit({ | |
obj: 'total', | |
type: 'replace', | |
value: result.total, | |
}); | |
return result; | |
}, | |
async get({ commit, getters }, attrs = {}) { | |
const { urlParameters = {}, queryParameters = {} } = attrs; | |
const id = urlParameters[primaryKey]; | |
try { | |
const item = await repository.get( | |
id, | |
urlParameters, | |
queryParameters, | |
); | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: updateItemInCollection(id, item), | |
}); | |
commit({ | |
obj: 'currentId', | |
type: 'replace', | |
value: id, | |
}); | |
} catch (e) { | |
commit({ | |
obj: 'currentId', | |
type: 'replace', | |
value: '', | |
}); | |
throw e; | |
} | |
return getters.current; | |
}, | |
async create({ commit, getters }, attrs = {}) { | |
const { data, urlParameters = {} } = attrs; | |
const createdItem = await repository.create(data, urlParameters); | |
const id = createdItem[primaryKey]; | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: updateItemInCollection(id, createdItem), | |
}); | |
commit({ | |
obj: 'total', | |
type: 'replace', | |
value: inc, | |
}); | |
commit({ | |
obj: 'current', | |
type: 'replace', | |
value: id, | |
}); | |
return getters.current; | |
}, | |
async update({ commit, getters }, attrs = {}) { | |
const { data, urlParameters = {} } = attrs; | |
const id = urlParameters[primaryKey]; | |
const item = await repository.update(id, data, urlParameters); | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: updateItemInCollection(id, item), | |
}); | |
commit({ | |
obj: 'current', | |
type: 'replace', | |
value: id, | |
}); | |
return getters.current; | |
}, | |
async delete({ commit }, attrs = {}) { | |
const { urlParameters = {}, data } = attrs; | |
const id = urlParameters[primaryKey]; | |
await repository.delete(id, urlParameters, data); | |
commit({ | |
obj: 'collection', | |
type: 'replace', | |
value: removeItemFromCollection(id), | |
}); | |
commit({ | |
obj: 'total', | |
type: 'replace', | |
value: dec, | |
}); | |
}, | |
}, | |
}); | |
const StoreFactory = (repository, extension = {}) => { | |
const genericStore = createStore( | |
repository, | |
extension.primaryKey || 'id', | |
); | |
['state', 'getters', 'actions', 'mutations'].forEach( | |
part => { | |
genericStore[part] = mergeDeepRight( | |
genericStore[part], | |
extension[part] || {}, | |
); | |
} | |
) | |
return genericStore; | |
}; | |
export default StoreFactory; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment