Skip to content

Commit

Permalink
[#408] Improve error messages (#410)
Browse files Browse the repository at this point in the history
* feat(#408): add DruxtClient error handling

* feat(#408): add error handling to DruxtModule

* chore(#408): add changeset

* feat(#408): update error handling

* chore(#408): update tests

* chore(#408): update test coverage

* chore(#408): update docs

* feat(#408): add warning for JSON:API extras

* chore(#408): update test coverage
  • Loading branch information
Decipher authored Dec 27, 2021
1 parent dc226c2 commit 7b749bd
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-gifts-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"druxt-entity": minor
---

Updated DruxtEntityForm error handling.
6 changes: 6 additions & 0 deletions .changeset/soft-garlics-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"druxt": minor
"druxt-router": minor
---

Added improved error handling.
117 changes: 92 additions & 25 deletions packages/druxt/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class DruxtClient {
// requests and errors.
if (options.debug) {
const log = this.log
// @TODO - Add test coverage.
this.axios.interceptors.request.use((config) => {
log.info(config.url)
return config
Expand Down Expand Up @@ -153,7 +154,15 @@ class DruxtClient {
}

if (Object.keys(permissions).length) {
throw new TypeError(`${res.data.meta.omitted.detail}\n\n Required permissions: ${Object.keys(permissions).join(', ')}.`)
const err = {
response: {
statusText: res.data.meta.omitted.detail,
data: {
errors: [{ detail: `Required permissions:\n - ${Object.keys(permissions).join('\n - ')}` }]
}
}
}
throw err
}
}
}
Expand Down Expand Up @@ -190,12 +199,63 @@ class DruxtClient {
}
)
} catch (err) {
response = (err.response || {}).data || err.message
this.error(err, { url: href })
}

return response
}

/**
* Throw a formatted error.
*
* @param {object} err - The error object
*
* @throws {Error} A formatted error.
*/
error(err, { url }) {
const title = [(err.response || {}).status, (err.response || {}).statusText].filter((s) => s).join(': ')
const meta = { url: [this.options.baseUrl, url].join('') }

// Build message.
let message = [title]

// Add meta information.
if (Object.values(meta).filter((o) => o)) {
message.push(Object.entries(meta).filter(([, v]) => v).map(([key, value]) => `${key.toUpperCase()}: ${value}`).join('\n'))
}

// Add main error details.
if (((((err.response || {}).data || {}).errors || [])[0] || {}).detail) {
message.push(err.response.data.errors[0].detail)
}

const error = Error(message.join('\n\n'))
error.response = err.response
throw error
}

/**
* Execute an Axios GET request, with permission checking and error handling.
*
* @param {string} url - The URL to GET.
* @param {object} options - An Axios options object.
*
* @returns {object} The Axios response.
*/
async get(url, options) {
try {
const res = await this.axios.get(url, options)

// Check that the response hasn't omitted data due to missing permissions.
this.checkPermissions(res)

return res
} catch(err) {
// Throw formatted error.
this.error(err, { url })
}
}

/**
* Get a collection of resources from the JSON:API server.
*
Expand All @@ -215,11 +275,8 @@ class DruxtClient {

const url = this.buildQueryUrl(href, query)

const res = await this.axios.get(url)

this.checkPermissions(res)

return res.data
const { data } = await this.get(url)
return data
}

/**
Expand Down Expand Up @@ -267,7 +324,15 @@ class DruxtClient {
return this.index[resource] ? this.index[resource] : false
}

let index = ((await this.axios.get(this.options.endpoint) || {}).data || {}).links
const url = this.options.endpoint
const { data } = await this.get(url)
let index = data.links

// Throw error if index is invalid.
if (typeof index !== 'object') {
const err = { response: { statusText: 'Invalid JSON:API endpoint' }}
this.error(err, { url })
}

// Remove Base URL from the resource URL.
const baseUrl = this.options.baseUrl
Expand All @@ -276,11 +341,19 @@ class DruxtClient {
return [key, value]
}))

// Use JSON API resource config to decorate the index.
// @TODO - Add test coverage
// Use JSON:API resource config to decorate the index.
// @TODO - Add test coverage.
if (index[this.options.jsonapiResourceConfig]) {
const resources = await this.axios.get(index[this.options.jsonapiResourceConfig].href)
for (const resourceType in resources.data.data) {
let resources = []

// Get JSON:API resource config if permissions setup correctly.
try {
resources = (await this.get(index[this.options.jsonapiResourceConfig].href)).data.data
} catch(err) {
this.log.warn(err.message)
}

for (const resourceType in resources) {
const resource = resources.data.data[resourceType]
const internal = resource.attributes.drupal_internal__id.split('--')

Expand Down Expand Up @@ -334,12 +407,8 @@ class DruxtClient {
}

const url = this.buildQueryUrl(`${href}/${id}/${related}`, query)
try {
const related = await this.axios.get(url)
return related.data
} catch (e) {
return false
}
const { data } = await this.get(url)
return data
}

/**
Expand All @@ -360,17 +429,14 @@ class DruxtClient {
}

let { href } = await this.getIndex(type)
// @TODO - Add test coverage.
if (!href) {
href = this.options.endpoint + '/' + type.replace('--', '/')
}

const url = this.buildQueryUrl(`${href}/${id}`, query)
try {
const resource = await this.axios.get(url)
return resource.data
} catch (e) {
return false
}
const { data } = await this.get(url)
return data
}

/**
Expand Down Expand Up @@ -402,9 +468,10 @@ class DruxtClient {
}
)
} catch (err) {
response = (err.response || {}).data || err.message
this.error(err)
}

// @TODO - Add test coverage.
return response
}
}
Expand Down
27 changes: 24 additions & 3 deletions packages/druxt/src/components/DruxtModule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ export default {
// Fetch configuration.
if ((this.$options.druxt || {}).fetchConfig) {
await this.$options.druxt.fetchConfig.call(this)
try {
await this.$options.druxt.fetchConfig.call(this)
} catch(err) {
return this.error(err)
}
}
// Build wrapper component object.
Expand Down Expand Up @@ -137,7 +141,11 @@ export default {
// Fetch resource.
if ((this.$options.druxt || {}).fetchData) {
await this.$options.druxt.fetchData.call(this, component.settings)
try {
await this.$options.druxt.fetchData.call(this, component.settings)
} catch(err) {
return this.error(err)
}
}
// Get scoped slots.
Expand All @@ -152,7 +160,7 @@ export default {
watch: {
model() {
if (this.component.props.value !== this.model) {
if (this.component.props && this.component.props.value !== this.model) {
this.component.props.value = this.model
// Only emit 'input' if using the default 'DruxtWrapper' component.
Expand All @@ -171,6 +179,19 @@ export default {
/** */
methods: {
/**
* Sets the component to render a DruxtDebug error message.
*/
error(err) {
this.component = {
is: 'DruxtDebug',
props: {
json: (err.response.data || {}).errors,
summary: [err.response.status, err.response.statusText].filter((s) => s).join(': ')
}
}
},
/**
* Get list of module wrapper components.
*
Expand Down
41 changes: 41 additions & 0 deletions packages/druxt/test/__snapshots__/client.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DruxtClient checkPermissions 1`] = `
Object {
"detail": "Required permissions:
- administer node fields",
}
`;

exports[`DruxtClient createResource 1`] = `
Object {
"detail": "subject: This value should not be null.",
"source": Object {
"pointer": "/data/attributes/subject",
},
"status": "422",
"title": "Unprocessable Entity",
}
`;

exports[`DruxtClient getIndex 1`] = `
Object {
"statusText": "Invalid JSON:API endpoint",
}
`;

exports[`DruxtClient getResource 1`] = `
Object {
"detail": "The \\"entity\\" parameter was not converted for the path \\"/jsonapi/node/article/{entity}\\" (route name: \\"jsonapi.node--article.individual\\")",
"links": Object {
"info": Object {
"href": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5",
},
"via": Object {
"href": "https://demo-api.druxtjs.org/en/jsonapi/node/article/missing",
},
},
"status": "404",
"title": "Not Found",
}
`;
Loading

0 comments on commit 7b749bd

Please sign in to comment.