Skip to content

Instantly share code, notes, and snippets.

@win0err
Created November 9, 2020 10:04
Show Gist options
  • Save win0err/feb98166b011f772c0bd6cd3215280ab to your computer and use it in GitHub Desktop.
Save win0err/feb98166b011f772c0bd6cd3215280ab to your computer and use it in GitHub Desktop.
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;
}
}
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,
};
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;
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