@feathersjs/vuex
Version:
FeathersJS, Vue 3, and Nuxt for the artisan developer
378 lines (377 loc) • 16.1 kB
JavaScript
/*
eslint
@typescript-eslint/explicit-function-return-type: 0,
@typescript-eslint/no-explicit-any: 0,
no-var: 0
*/
import { serializeError } from 'serialize-error';
import { reactive, isReactive } from 'vue';
import { updateOriginal } from './utils.vue3';
import { models, assignTempId, getId, getQueryInfo, } from '@feathersjs/vuex-commons';
import _omit from 'lodash/omit';
import _get from 'lodash/get';
import _isObject from 'lodash/isObject';
export default function makeServiceMutations() {
function addItems(state, items) {
const { serverAlias, idField, tempIdField, modelName } = state;
const Model = _get(models, [serverAlias, modelName]);
const BaseModel = _get(models, [serverAlias, 'BaseModel']);
for (let item of items) {
const id = getId(item, idField);
const isTemp = id === null || id === undefined;
// If the response contains a real id, remove isTemp
if (id != null) {
delete item.__isTemp;
}
if (Model && !(item instanceof BaseModel) && !(item instanceof Model)) {
item = new Model(item, { commit: false });
}
if (isTemp) {
let tempId = item[tempIdField];
if (tempId == null) {
tempId = assignTempId(state, item);
}
item.__isTemp = true;
state.tempsById[tempId] = item;
}
else {
// Only add the id if it's not already in the `ids` list.
if (!state.ids.includes(id)) {
state.ids.push(id);
}
state.keyedById[id] = item;
}
}
}
function updateItems(state, items) {
const { idField, replaceItems, addOnUpsert, serverAlias, modelName } = state;
const Model = _get(models, [serverAlias, modelName]);
const BaseModel = _get(models, [state.serverAlias, 'BaseModel']);
for (let item of items) {
const id = getId(item, idField);
// If the response contains a real id, remove isTemp
if (id != null) {
delete item.__isTemp;
}
// Update the record
if (id !== null && id !== undefined) {
if (state.ids.includes(id)) {
// Completely replace the item
if (replaceItems) {
if (Model && !(item instanceof Model)) {
item = new Model(item);
}
state.keyedById[id] = item;
// Merge in changes
}
else {
/**
* If we have a Model class, calling new Model(incomingData) will call update
* the original record with the accessors and setupInstance data.
* This means that date objects and relationships will be preserved.
*
* If there's no Model class, just call updateOriginal on the incoming data.
*/
if (Model && !(item instanceof BaseModel) && !(item instanceof Model)) {
item = new Model(item);
}
else {
const original = state.keyedById[id];
updateOriginal(original, item);
}
}
// if addOnUpsert then add the record into the state, else discard it.
}
else if (addOnUpsert) {
state.ids.push(id);
state.keyedById[id] = item;
}
continue;
}
}
}
function mergeInstance(state, item) {
const { idField } = state;
const id = getId(item, idField);
const existingItem = state.keyedById[id];
if (existingItem) {
merge(existingItem, item);
}
}
function merge(state, { dest, source }) {
merge(dest, source);
}
return {
addItems,
updateItems(state, items) {
if (!Array.isArray(items)) {
throw new Error('You must provide an array to the `updateItems` mutation.');
}
updateItems(state, items);
},
mergeInstance,
merge,
addItem(state, item) {
addItems(state, [item]);
},
updateItem(state, item) {
updateItems(state, [item]);
},
// Promotes temp to "real" item:
// - adds _id to temp
// - removes __isTemp flag
// - migrates temp from tempsById to keyedById
updateTemp(state, { id, tempId }) {
const temp = state.tempsById[tempId];
if (temp) {
temp[state.idField] = id;
delete temp.__isTemp;
delete state.tempsbyId[tempId];
// If an item already exists in the store from the `created` event firing
// it will be replaced here
state.keyedById[id] = temp;
// Only add the id if it's not already in the `ids` list.
if (!state.ids.includes(id)) {
state.ids.push(id);
}
}
// Add _id to temp's clone as well if it exists
const Model = _get(models, [state.serverAlias, state.modelName]);
const tempClone = Model && Model.copiesById && Model.copiesById[tempId];
if (tempClone) {
tempClone[state.idField] = id;
Model.copiesById[id] = tempClone;
delete tempClone.__isTemp;
}
},
removeItem(state, item) {
const { idField } = state;
const idToBeRemoved = _isObject(item) ? getId(item, idField) : item;
const isIdOk = idToBeRemoved !== null && idToBeRemoved !== undefined;
const index = state.ids.findIndex(i => i === idToBeRemoved);
const Model = _get(models, `[${state.serverAlias}][${state.modelName}]`);
const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById;
if (isIdOk && index !== null && index !== undefined) {
state.ids = state.ids.filter((id, ind) => id && ind !== index);
delete state.keyedById[idToBeRemoved];
if (copiesById.hasOwnProperty(idToBeRemoved)) {
delete copiesById[idToBeRemoved];
}
}
},
// Removes temp records
removeTemps(state, tempIds) {
tempIds.forEach(id => {
const temp = state.tempsById[id];
if (temp) {
if (temp[state.idField]) {
delete temp.__isTemp;
}
}
});
state.tempsById = _omit(state.tempsById, tempIds);
},
removeItems(state, items) {
const { idField } = state;
if (!Array.isArray(items)) {
throw new Error('You must provide an array to the `removeItems` mutation.');
}
// Make sure we have an array of ids. Assume all are the same.
const containsObjects = items[0] && _isObject(items[0]);
const idsToRemove = containsObjects ? items.map(item => getId(item, idField)) : items;
const mapOfIdsToRemove = idsToRemove.reduce((map, id) => {
map[id] = true;
return map;
}, {});
const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath]);
const copiesById = state.keepCopiesInStore ? state.copiesById : Model.copiesById;
idsToRemove.forEach(id => {
delete state.keyedById[id];
if (copiesById.hasOwnProperty(id)) {
delete copiesById[id];
}
});
// Get indexes to remove from the ids array.
const mapOfIndexesToRemove = state.ids.reduce((map, id, index) => {
if (mapOfIdsToRemove[id]) {
map[index] = true;
}
return map;
}, {});
// Remove highest indexes first, so the indexes don't change
const indexesInReverseOrder = Object.keys(mapOfIndexesToRemove).sort((a, b) => {
if (a < b) {
return 1;
}
else if (a > b) {
return -1;
}
else {
return 0;
}
});
indexesInReverseOrder.forEach(indexInIdsArray => {
state.ids = state.ids.filter((id, index) => index !== indexInIdsArray);
});
},
clearAll(state) {
state.ids = [];
state.keyedById = {};
if (state.keepCopiesInStore) {
state.copiesById = {};
}
else {
const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath]);
Object.keys(Model.copiesById).forEach(k => {
delete Model.copiesById[k];
});
}
},
// Creates a copy of the record with the passed-in id, stores it in copiesById
createCopy(state, id) {
const { servicePath, keepCopiesInStore, serverAlias } = state;
const current = state.keyedById[id] || state.tempsById[id];
const Model = _get(models, [serverAlias, 'byServicePath', servicePath]);
let item;
if (Model) {
item = new Model(current, { clone: true });
}
else {
const existingClone = state.copiesById[id];
item = existingClone ? merge(existingClone, current) : merge({}, current);
}
if (keepCopiesInStore) {
state.copiesById[id] = item;
}
else {
// Make sure model has a copiesById
if (!Model.hasOwnProperty('copiesById')) {
Object.defineProperty(Model, 'copiesById', { value: {} });
}
// Since it won't be added to the store, make it a Vue object
if (!isReactive) {
item = reactive(item);
}
Model.copiesById[id] = item;
}
},
// Resets the copy to match the original record, locally
resetCopy(state, id) {
const { servicePath, keepCopiesInStore } = state;
const Model = _get(models, [state.serverAlias, 'byServicePath', servicePath]);
const copy = keepCopiesInStore
? state.copiesById[id]
: Model && _get(Model, ['copiesById', id]);
if (copy) {
const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id];
merge(copy, original);
}
},
// Deep assigns copy to original record, locally
commitCopy(state, id) {
const { servicePath, keepCopiesInStore } = state;
const Model = _get(models, [state.serverAlias, 'byServicePath', servicePath]);
const copy = keepCopiesInStore
? state.copiesById[id]
: Model && _get(Model, ['copiesById', id]);
if (copy) {
const original = copy[state.idField] != null ? state.keyedById[id] : state.tempsById[id];
merge(original, copy);
}
},
// Removes the copy from copiesById
clearCopy(state, id) {
const { keepCopiesInStore } = state;
const Model = _get(models, [state.serverAlias, 'byServicePath', state.servicePath]);
const copiesById = keepCopiesInStore ? state.copiesById : Model.copiesById;
if (copiesById[id]) {
delete copiesById[id];
}
},
/**
* Stores pagination data on state.pagination based on the query identifier
* (qid) The qid must be manually assigned to `params.qid`
*/
updatePaginationForQuery(state, { qid, response, query = {} }) {
const { data, total } = response;
const { idField } = state;
const ids = data.map(i => i[idField]);
const queriedAt = new Date().getTime();
const { queryId, queryParams, pageId, pageParams } = getQueryInfo({ qid, query }, response);
if (!state.pagination[qid]) {
state.pagination[qid] = {};
}
if (!query.hasOwnProperty('$limit') && response.hasOwnProperty('limit')) {
state.pagination.defaultLimit = response.limit;
}
if (!query.hasOwnProperty('$skip') && response.hasOwnProperty('skip')) {
state.pagination.defaultSkip = response.skip;
}
const mostRecent = {
query,
queryId,
queryParams,
pageId,
pageParams,
queriedAt,
total,
};
const qidData = state.pagination[qid] || {};
Object.assign(qidData, { mostRecent });
qidData[queryId] = qidData[queryId] || {};
const queryData = {
total,
queryParams,
};
Object.assign(qidData[queryId], queryData);
const pageData = {
[pageId]: { pageParams, ids, queriedAt },
};
Object.assign(qidData[queryId], pageData);
const newState = Object.assign({}, state.pagination[qid], qidData);
state.pagination[qid] = newState;
},
setPending(state, method) {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
state[`is${uppercaseMethod}Pending`] = true;
},
unsetPending(state, method) {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
state[`is${uppercaseMethod}Pending`] = false;
},
setIdPending(state, payload) {
const { method, id } = payload;
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
const isIdMethodPending = state[`isId${uppercaseMethod}Pending`];
// if `id` is an array, ensure it doesn't have duplicates
const ids = Array.isArray(id) ? [...new Set(id)] : [id];
ids.forEach(id => {
if (typeof id === 'number' || typeof id === 'string') {
isIdMethodPending.push(id);
}
});
},
unsetIdPending(state, payload) {
const { method, id } = payload;
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
const isIdMethodPending = state[`isId${uppercaseMethod}Pending`];
// if `id` is an array, ensure it doesn't have duplicates
const ids = Array.isArray(id) ? [...new Set(id)] : [id];
ids.forEach(id => {
const idx = isIdMethodPending.indexOf(id);
if (idx >= 0) {
delete isIdMethodPending[idx];
}
});
},
setError(state, payload) {
const { method, error } = payload;
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
state[`errorOn${uppercaseMethod}`] = Object.assign({}, serializeError(error));
},
clearError(state, method) {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1);
state[`errorOn${uppercaseMethod}`] = null;
},
};
}