@feathersjs/vuex
Version:
FeathersJS, Vue 3, and Nuxt for the artisan developer
430 lines (381 loc) • 14.1 kB
text/typescript
/*
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,
ServiceState,
assignTempId,
getId,
getQueryInfo,
PendingServiceMethodName,
PendingIdServiceMethodName,
} from '@feathersjs/vuex-commons'
import _omit from 'lodash/omit'
import _get from 'lodash/get'
import _isObject from 'lodash/isObject'
import { Id } from '@feathersjs/feathers'
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: PendingServiceMethodName): void {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
state[`is${uppercaseMethod}Pending`] = true
},
unsetPending(state, method: PendingServiceMethodName): void {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
state[`is${uppercaseMethod}Pending`] = false
},
setIdPending(state, payload: { method: PendingIdServiceMethodName; id: Id | Id[] }): void {
const { method, id } = payload
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
const isIdMethodPending = state[
`isId${uppercaseMethod}Pending`
] as ServiceState['isIdCreatePending']
// 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: { method: PendingIdServiceMethodName; id: Id | Id[] }): void {
const { method, id } = payload
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
const isIdMethodPending = state[
`isId${uppercaseMethod}Pending`
] as ServiceState['isIdCreatePending']
// 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: { method: PendingServiceMethodName; error: Error }): void {
const { method, error } = payload
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
state[`errorOn${uppercaseMethod}`] = Object.assign({}, serializeError(error))
},
clearError(state, method: PendingServiceMethodName): void {
const uppercaseMethod = method.charAt(0).toUpperCase() + method.slice(1)
state[`errorOn${uppercaseMethod}`] = null
},
}
}