Skip to content

Commit

Permalink
feat(#639): add cache control to druxt views (#640)
Browse files Browse the repository at this point in the history
* feat(#639): add cache control to druxt views

* chore(#639): fix tests

* chore(#639): update tests

* chore(#639): update documentation and examples

* feat(#639): add cache controls to druxt vuex store

* feat(#639): add bypassCache setting to DruxtEntity

* chore(#639): update bypassCache behaviour

* chore(#639): update tests

* chore(#639): update documentation
  • Loading branch information
Decipher authored Jul 4, 2023
1 parent a8585cd commit 41cab3a
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 44 deletions.
7 changes: 7 additions & 0 deletions .changeset/angry-panthers-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"druxt-views": minor
---

feat(#639): added druxt/views/flushResults mutation
feat(#639): added bypassCache option to druxt/views/getResults action
feat(#639): added druxt.query.bypassCache option to DruxtView
6 changes: 6 additions & 0 deletions .changeset/few-pets-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"druxt": minor
---

feat(#639): added druxt/flushCollection and druxt/flushResource mutations
feat(#639): added bypassCache option to druxt/getCollection and druxt/getResource actions
5 changes: 5 additions & 0 deletions .changeset/khaki-glasses-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"druxt-entity": minor
---

feat(#639): add bypassCache druxt setting to DruxtEntity components.
105 changes: 95 additions & 10 deletions packages/druxt/src/stores/druxt.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,44 @@ const DruxtStore = ({ store }) => {

Vue.set(state.resources[type][id], prefix, resource)
},

/**
* @name flushCollection
* @mutator {object} flushCollection=collections Removes JSON:API collections from the Vuex state object.
* @param {flushCollectionContext} context
*
* @example @lang js
* // Flush all collections.
* this.$store.commit('druxt/flushCollection', {})
*
* // Flush target collection.
* this.$store.commit('druxt/flushCollection', { type, hash, prefix })
*/
flushCollection (state, { type, hash, prefix }) {
if (!type) Vue.set(state, 'collections', {})
else if (type && !hash && !prefix) Vue.set(state.collections, type, {})
else if (type && hash && !prefix) Vue.set(state.collections[type], hash, {})
else if (type && hash && prefix) Vue.set(state.collections[type][hash], prefix, {})
},

/**
* @name flushResource
* @mutator {object} flushResource=resources Removes JSON:API resources from the Vuex state object.
* @param {flushResourceContext} context
*
* @example @lang js
* // Flush all resources.
* this.$store.commit('druxt/flushResource', {})
*
* // Flush target resource.
* this.$store.commit('druxt/flushResource', { id, type, prefix, hash })
*/
flushResource (state, { type, id, prefix }) {
if (!type) Vue.set(state, 'resources', {})
else if (type && !id && !prefix) Vue.set(state.resources, type, {})
else if (type && id && !prefix) Vue.set(state.resources[type], id, {})
else if (type && id && prefix) Vue.set(state.resources[type][id], prefix, {})
}
},

/**
Expand All @@ -159,15 +197,16 @@ const DruxtStore = ({ store }) => {
* const resources = await this.$store.dispatch('druxt/getCollection', {
* type: 'node--article',
* query: new DrupalJsonApiParams().addFilter('status', '1'),
* bypassCache: false
* })
*/
async getCollection ({ commit, state }, { type, query, prefix }) {
async getCollection ({ commit, state }, { type, query, prefix, bypassCache = false }) {
// Generate a hash using query data excluding the 'fields' and 'include' data.
const queryObject = getDrupalJsonApiParams(query).getQueryObject()
const hash = query ? md5(JSON.stringify({ ...queryObject, fields: {}, include: [] })) : '_default'

// If collection hash exists, re-hydrate and return the data.
if (((state.collections[type] || {})[hash] || {})[prefix]) {
if (!bypassCache && ((state.collections[type] || {})[hash] || {})[prefix]) {
return {
...state.collections[type][hash][prefix],
// Hydrate resource data.
Expand Down Expand Up @@ -197,9 +236,13 @@ const DruxtStore = ({ store }) => {
* @return {object} The Drupal JSON:API resource.
*
* @example @lang js
* const resource = await this.$store.dispatch('druxt/getResource', { type: 'node--article', id })
* const resource = await this.$store.dispatch('druxt/getResource', {
* type: 'node--article',
* id,
* bypassCache: false
* })
*/
async getResource ({ commit, dispatch, state }, { type, id, query, prefix }) {
async getResource ({ commit, dispatch, state }, { type, id, query, prefix, bypassCache = false }) {
// Get the resource from the store if it's avaialble.
const storedResource = ((state.resources[type] || {})[id] || {})[prefix] ?
{ ...state.resources[type][id][prefix] }
Expand Down Expand Up @@ -257,7 +300,7 @@ const DruxtStore = ({ store }) => {
}

// Return if we have the full resource.
if ((storedResource || {})._druxt_full) {
if (!bypassCache && (storedResource || {})._druxt_full) {
return storedResource
}
const isFull = typeof (queryObject.fields || {})[type] !== 'string'
Expand All @@ -279,9 +322,13 @@ const DruxtStore = ({ store }) => {

// Request the resource from the DruxtClient if required.
let resource
if (!storedResource || fields) {
resource = await this.$druxt.getResource(type, id, getDrupalJsonApiParams(queryObject), prefix)
commit('addResource', { prefix, resource: { ...resource } })
if (bypassCache || !storedResource || fields) {
try {
resource = await this.$druxt.getResource(type, id, getDrupalJsonApiParams(queryObject), prefix)
commit('addResource', { prefix, resource: { ...resource } })
} catch(e) {
// Do nothing, just don't error.
}
}

// Build resource to be returned.
Expand Down Expand Up @@ -351,6 +398,40 @@ export { DruxtStore }
* }
*/

/**
* Parameters for the `flushCollection` mutation.
*
* @typedef {object} flushCollectionContext
*
* @param {string} type - The JSON:API collection resource type.
* @param {string} hash - An md5 hash of the query string.
* @param {string} [prefix] - (Optional) The JSON:API endpoint prefix or langcode.
*
* @example @lang js
* {
* type: 'node--page',
* hash: '_default',
* prefix: 'en'
* }
*/

/**
* Parameters for the `flushResource` mutation.
*
* @typedef {object} flushResourceContext
*
* @param {string} [type] - The JSON:API Resource type.
* @param {string} [id] - The Drupal resource UUID.
* @param {string} [prefix] - (Optional) The JSON:API endpoint prefix or langcode.
*
* @example @lang js
* {
* type: 'node--page',
* id: 'd8dfd355-7f2f-4fc3-a149-288e4e293bdd',
* prefix: 'en'
* }
*/

/**
* Parameters for the `getCollection` action.
*
Expand All @@ -359,11 +440,13 @@ export { DruxtStore }
* @param {string} type - The JSON:API collection resource type.
* @param {DruxtClientQuery} [query] - A correctly formatted JSON:API query string or object.
* @param {string} [prefix] - (Optional) The JSON:API endpoint prefix or langcode.
* @param {boolean} [bypassCache] - (Optional) Bypass the Vuex cached collection.
*
* @example @lang js
* {
* type: 'node--page',
* query: new DrupalJsonApiParams().addFilter('status', '1')
* query: new DrupalJsonApiParams().addFilter('status', '1'),
* bypassCache: false
* }
*/

Expand All @@ -376,12 +459,14 @@ export { DruxtStore }
* @param {string} id - The Drupal resource UUID.
* @param {DruxtClientQuery} [query] - A correctly formatted JSON:API query string or object.
* @param {string} [prefix] - (Optional) The JSON:API endpoint prefix or langcode.
* @param {boolean} [bypassCache] - (Optional) Bypass the Vuex cached resource.
*
* @example @lang js
* {
* type: 'node--page',
* id: 'd8dfd355-7f2f-4fc3-a149-288e4e293bdd',
* prefix: 'en'
* prefix: 'en',
* bypassCache: false
* }
*/

Expand Down
63 changes: 63 additions & 0 deletions packages/druxt/test/stores/druxt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ describe('DruxtStore', () => {
expect(mockAxios.get).toHaveBeenCalledTimes(2)
expect(storedResource).toStrictEqual(resource)
expect(storedResource).toStrictEqual(expected)

// Assert that:
// - Cache is bypassed
const bypassedResource = await store.dispatch('druxt/getResource', { ...mockPage.data, bypassCache: true })
delete resource._druxt_full
delete bypassedResource._druxt_full
expect(mockAxios.get).toHaveBeenCalledTimes(3)
expect(bypassedResource).toStrictEqual(resource)

// Assert that:
// - When bypassing cache, in case live data is unavailable, fallback to cache.
store.$druxt.getResource = jest.fn(() => { throw new Error() })
const fallback = await store.dispatch('druxt/getResource', { ...mockPage.data, bypassCache: true })
delete fallback._druxt_full
expect(mockAxios.get).toHaveBeenCalledTimes(3)
expect(fallback).toStrictEqual(bypassedResource)
})

test('getResource - filter', async () => {
Expand Down Expand Up @@ -282,4 +298,51 @@ describe('DruxtStore', () => {
await store.dispatch('druxt/getCollection', { type: 'node--page', query: {} })
expect(mockAxios.get).toHaveBeenCalledTimes(2)
})

test('flushCollection', async () => {
const type = 'node--page'
const hash ='_default'
const prefix = 'en'

// Ensure that the results state is populated.
const collection = await getMockCollection(type)
store.commit('druxt/addCollection', { collection, type, prefix, hash })
expect(store.state.druxt.collections[type][hash][prefix]).toStrictEqual(collection)

store.commit('druxt/flushCollection', { type, hash, prefix })
expect(store.state.druxt.collections[type][hash][prefix]).toStrictEqual({})

store.commit('druxt/flushCollection', { type, hash })
expect(store.state.druxt.collections[type][hash]).toStrictEqual({})

store.commit('druxt/flushCollection', { type })
expect(store.state.druxt.collections[type]).toStrictEqual({})

store.commit('druxt/flushCollection', {})
expect(store.state.druxt.collections).toStrictEqual({})

})

test('flushResource', async () => {
const type = 'node--page'
const prefix = 'en'

// Ensure that the results state is populated.
const resource = await getMockResource(type)
const id = resource.data.id
store.commit('druxt/addResource', { prefix, resource })
expect(store.state.druxt.resources[type][id][prefix]).toStrictEqual(resource)

store.commit('druxt/flushResource', { type, id, prefix })
expect(store.state.druxt.resources[type][id][prefix]).toStrictEqual({})

store.commit('druxt/flushResource', { type, id })
expect(store.state.druxt.resources[type][id]).toStrictEqual({})

store.commit('druxt/flushResource', { type })
expect(store.state.druxt.resources[type]).toStrictEqual({})

store.commit('druxt/flushResource', {})
expect(store.state.druxt.resources).toStrictEqual({})
})
})
41 changes: 35 additions & 6 deletions packages/entity/src/components/DruxtEntity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ export default {
},
},
mounted() {
// If static, re-fetch data allowing for cache-bypass.
// @TODO - Don't re-fetch in serverless configuration.
if (this.$store.app.context.isStatic) {
this.$fetch()
}
},
methods: {
/**
* Builds the query for the JSON:API request.
Expand Down Expand Up @@ -318,12 +326,22 @@ export default {
}
if (this.uuid && !this.value) {
// Check if we need to bypass cache.
let bypassCache = false
if (typeof (settings.query || {}).bypassCache === 'boolean') {
bypassCache = settings.query.bypassCache
}
// Build query.
const query = this.getQuery(settings)
// Execute the resquest.
const resource = await this.getResource({
id: this.uuid,
prefix: this.lang,
type: this.type,
query
query,
bypassCache
})
const entity = { ...(resource.data || {}) }
entity.included = resource.included
Expand All @@ -342,12 +360,20 @@ export default {
/**
* Component settings.
*/
settings: ({ $druxt, settings }, wrapperSettings) => {
settings: (context, wrapperSettings) => {
const { $druxt, settings } = context
// Start with the `nuxt.config.js` `druxt.settings.entity` settings and
// merge the Wrapper component settings on top.
let mergedSettings = merge($druxt.settings.entity || {}, wrapperSettings, { arrayMerge: (dest, src) => src })
let mergedSettings = merge(($druxt.settings || {}).entity || {}, wrapperSettings || {}, { arrayMerge: (dest, src) => src })
// Merge the DruxtEntity component `settings` property on top.
mergedSettings = merge(mergedSettings || {}, settings, { arrayMerge: (dest, src) => src })
mergedSettings = merge(mergedSettings || {}, settings || {}, { arrayMerge: (dest, src) => src })
// Evaluate the bypass cache function.
if (typeof (mergedSettings.query || {}).bypassCache === 'function') {
mergedSettings.query.bypassCache = !!mergedSettings.query.bypassCache(context)
}
// Currently only returning the query settings.
return {
query: mergedSettings.query || {},
Expand Down Expand Up @@ -485,7 +511,8 @@ export default {
* property.
*
* @typedef {object} ModuleSettings
* @param {object} query - Entity Query settings:
* @param {object} query - Entity query settings:
* @param {(boolean|function)} query.bypassCache - Whether to pull the data from the Vuex store or from the JSON:API.
* @param {(string[]|array[])} query.fields - An array or arrays of fields to filter from the JSON:API Resources.
* @param {string[]} query.include - An array of relationships to include alongside the JSON:API Resource.
* @param {boolean} query.schema - Whether to automatically detect fields to filter, per the Display mode.
Expand All @@ -495,11 +522,12 @@ export default {
* export default {
* druxt: {
* query: {
* bypassCache: ({ $store }) => $store.$auth.loggedIn,
* fields: [['title'], ['user--user', ['display_name']]],
* include: ['uid']
* schema: true,
* },
* }
* },
* }
*
* @example <caption>DruxtEntity component with settings</caption> @lang vue
Expand All @@ -509,6 +537,7 @@ export default {
* :uuid="uuid"
* :settings="{
* query: {
* bypassCache: true,
* fields: [['title'], ['user--user', ['display_name']]],
* include: ['uid']
* schema: true,
Expand Down
Loading

0 comments on commit 41cab3a

Please sign in to comment.