Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#408] Improve error messages #410

Merged
merged 9 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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