According to the backbone documentation about validation it states:
If validate returns an error, set and save will not continue, and the
model attributes will not be modified.
So the way I read that set or save should never run if the validation fails. But that is not the results I am getting. Even when validation fails it still sends the POST/PUT request. Am I reading the docs wrong or doing something incorrect in my code?
Here is my relevant code:
https://gist.github.com/80f6ef0099fbe96025dc
App.Models.Test = Backbone.Model.extend(
urlRoot: '/api/test'
validate: (attrs) ->
errors = []
if attrs.to is ''
errors.push
name: "to"
field: "js-to"
message: "You must enter a to address"
if attrs.subject is ''
errors.push
name: "subject"
field: "js-subject"
message: "You must enter a subject"
# Return our errors array if it isn't empty
errors if errors.length > 0
)
App.Views.Details = Backbone.View.extend(
initialize: ->
#model.bind "error", #error, this
events:
"click #js-save": "saveItem"
saveItem: (e) ->
e.preventDefault()
# Set the model then save it.
#model.set
subject: $("#js-subject").val()
message: $("#js-message").val()
mailbox_id: $("#js-from").val()
to: $("#js-to").val()
cc: $("#js-cc").val()
bcc: $("#js-bcc").val()
tags: App.Helpers.tagsToObject $('#js-tags').val()
scope: $('#js-scope').val()
attachments: attachments
#model.save null,
success: (model, response) =>
App.Helpers.showAlert "Success!", "Saved Successfully", "alert-success"
#next()
error: (model, response) ->
App.Helpers.showAlert "Error", "An error occurred while trying to save this item", "alert-error"
# Show the errors based on validation failure.
error: (model, error) ->
App.Helpers.displayValidationErrors error
You do this to save your model:
#model.save null,
success: -> ...
error: -> ...
That null is the source of your trouble, use {} and things will start behaving better; if you combine your #model.set and #model.save calls, things will be even better:
attrs =
subject: $("#js-subject").val()
#...
#model.save attrs,
success: -> ...
error: -> ...
A save call looks like this:
save model.save([attributes], [options])
[...]
The attributes hash (as in set) should contain the attributes you'd like to change
So passing a null for attributes means that you want to save the model as it is.
When you save a model, the validation is mostly left up to set, the code looks like this:
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
Your attrs will be null so set will not be called; however, if you let save handle your set, you will get the behavior you're after. If you passed the wait: true option, save would manually run the validation on the passed attributes:
if (options.wait) {
if (!this._validate(attrs, options)) return false;
...
}
The internal _validate method is a wrapper for validate that does some bookkeeping and error handling. You're not using wait: true so this doesn't apply to you but I thought it was worth mentioning anyway.
Consider a simple example with a model whose validate always fails. If you say this:
#model.on 'error', #error
#model.save attrs,
success: -> console.log 'AJAX success'
error: -> console.log 'AJAX error'
then #error will be called because save will end up calling set with some attributes and set will call validate. Demo: http://jsfiddle.net/ambiguous/HHQ2N/1/
But, if you say:
#model.save null, ...
the null will cause the set call to be skipped. Demo: http://jsfiddle.net/ambiguous/6pX2e/ (the AJAX here will fail).
Your #model.set call right before #model.save should be triggering your error handler but if you don't check what #model.set returns, execution will blindly continue on to the save call and talk to your server.
In summary, you have three things going on here:
You're not calling save they way you should be.
You're ignoring the #model.set return value and losing your chance to trap the validation errors.
Backbone's argument handling for save(null, ...) could be better but I don't know if it is worth the effort to handle a strange way of calling it.
You should combine your set/save pair into just a save or check what set returns.
Related
Is this possible to set custom GraphQL Error types for at least BAD_USER_INPUT code?
On the backend I've got an error type something like this:
throw {
type: 'ValidationError',
code: 422,
success: false,
message: `Something didn't work well`,
}
on Frontend I am still getting the same error type when some fields don't pass schema validation:
That's not really comfortable to take care of the both error formats on the frontend.
Is it possible to somehow:
Except errors and BAD_USER_INPUT receive data response 200 and check it by success?
Or maybe it's just possible to make my format of error (the one I'm sending form backend)?
I already wanted to make it using global error handler:
export default ({ graphQLErrors, networkError, operation, forward }, nuxtContext) => {
console.log('Global error handler')
console.log(graphQLErrors, networkError, operation, forward)
}
apollo: {
...
// setup a global error handler (see below for example)
errorHandler: '~/plugins/graphql/apollo-error-handler.js',
...
}
But it doesn't seem to work at all
Formatting errors can be achieved in Apollo Graphql server using formatError function. You can raise custom errors from your application from which the necessary error response can be constructed at formatError function.
In the view_submission type I set ack to clear the stack like this:
await submissionAck({ response_action: 'clear' } as any)
First question - why do I have to cast it to any? Without it code throws error
Argument of type '{ response_action: "clear"; }' is not assignable to parameter of type '(ViewUpdateResponseAction & void) | (ViewPushResponseAction & void) | (ViewClearResponseAction & void) | (ViewErrorsResponseAction & void) | undefined'.Type '{ response_action: "clear"; }' is not assignable to type 'ViewClearResponseAction & void'.
Type '{ response_action: "clear"; }' is not assignable to type 'void'.
Second question - the stack seems not to be cleared. When I submit modal for the first time it's okay, but if I try next time it throws:
[ERROR] bolt-app { Error: The receiver's `ack` function was called multiple times.
at ack (/home/ec2-user/metrics/node_modules/#slack/bolt/src/ExpressReceiver.ts:147:17)
at /home/ec2-user/metrics/app/actions.ts:43:17
at Generator.next (<anonymous>)
at /home/ec2-user/metrics/app/actions.ts:11:71
at new Promise (<anonymous>)
at __awaiter (/home/ec2-user/metrics/app/actions.ts:7:12)
at app.view (/home/ec2-user/metrics/app/actions.ts:40:70)
at process_1.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/App.ts:660:19)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:36:12)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.<anonymous> (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:201:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.exports.onlyViewActions (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:110:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at Object.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:39:10) code: 'slack_bolt_receiver_ack_multiple_error' }
Any ideas? That's how I call these views: (by the way 3rd question - why do I have to cast body to BlockAction? Otherwise it throws error that trigger_id doesn't exists)
app.action('modify', async ({ body, ack }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
await app.client.views.open({
trigger_id: (body as BlockAction).trigger_id,
token: 'token',
view: modificationModal,
})
app.view(
{
type: 'view_submission',
callback_id: 'yay',
},
async ({ body: submissionBody, ack: submissionAck, view }) => {
const receivedValues = submissionBody.view.state.values
await submissionAck({ response_action: 'clear' } as any)
},
)
})
})
I know that in the documentation stands:
view() requires a callback_id of type string or RegExp.
but that doesn't tell me much. What is that string? Is that a function? What should it do?
Sorry for noobish question and thanks for help!
I'll try to answer these in the reverse order, because I think that might make the most sense.
What is that string? Is that a function? What should it do? (referring to app.view())
When you create a Modal, you typically create it with a callback_id. You can see a description for that property in the documentation for a view payload.
That sentence is trying to say this is how you'd listen for a view submission for a view that was created with callback_id set to "some_callback_id":
app.view('some_callback_id', async () => {
/* listener logic goes here */
})
Note: You could also use a regular expression if you wanted the same function to handle view submissions for many views - views whose callback_ids all follow the same pattern. But the regular expression is a pretty advanced case that I don't think we should worry about it for now.
To create a Modal, you use the views.open method, and that's where you'll set the callback_id in the first place. I'm going to suggest an improvement. All Web API methods are available inside a listener as methods on the client argument. Then you don't need to worry about adding the token. Here's an example of using this:
// Add the `client` argument
app.action('modify', async ({ body, ack, client }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
// Remove `app.`
await client.views.open({
// Let's come back to this cast later
trigger_id: (body as BlockAction).trigger_id,
// Not sure what's in modificationModal, but to illustrate, I used a literal
view: {
// *** Setting the callback_id ***
callback_id: 'modify_submission',
title: {
type: 'plain_text',
text: 'Modify something'
},
blocks: [{ /* add your blocks here */ }],
},
})
})
})
Next, don't handle a view submission inside another listener. When you do that, each time the outer listener runs, you're (re)registering the view submission listener to run. So the first time it will run once, the second time it will run twice, the third time it will run three times. This explains why the stacktrace is telling you that ack() was called multiple times. Instead, just handle the view submission outside that listener. The "linking" information between the two interactions is the callback_id. Building off the previous example:
// Further down in the same file, at the same level as the previous code
// *** Using the callback_id we set previously ***
app.view('modify_submission', async ({ body, ack, view }) => {
const receivedValues = body.view.state.values
// Let's come back to this cast later
await ack({ response_action: 'clear' } as any)
})
Okay this should all work, but now let's talk about the casts. When you handle an action with app.action(), the body argument is typed as BlockAction | InteractiveMessage | DialogSubmitAction. Within these interfaces, BlockAction and InteractiveMessage do have a trigger_id property, but DialogSubmitAction does not. So as far as TypeScript is concerned, it can't be sure the property body.trigger_id exists. You might know that the action you're handling is a BlockAction (let's assume it is), but TypeScript does not! However, Bolt was built to allow users to give TypeScript some more information by using a generic parameter. Here's a part of the first example, modified to use the generic parameter.
import { BlockAction } from '#slack/bolt';
app.action<BlockAction>('modify', async ({ body, ack, client }) => {
// `body` is now typed as a BlockAction, and therefore body.trigger_id is a string
});
It's a pretty similar story for app.view() and generic parameters. This time, the body argument is of type ViewSubmitAction | ViewClosedAction. Using a clear response action doesn't make sense for a ViewClosedAction, so we need to constrain the type once again. That's right, the generic parameter is hooked up to more than just the type of body, it can actually change (constrain) any of the listener arguments! In this case, the generic parameter changes the type of ack().
import { ViewSubmitAction } from '#slack/bolt';
app.view<ViewSubmitAction>('modify_submission', async ({ body, ack, view }) => {
// No errors now
await ack({ response_action: 'clear' });
});
Final note: The way you wrote the view submission handler with a constraints object ({ type: 'view_submission', callback_id: 'yay' } does seem like you've given TypeScript enough information to constrain the types of the listener arguments. That actually would work for app.action({ type: 'block_actions', ... }, ...) because we defined ActionConstraints to be generic. This is an area where Bolt could be improved and all it would take is making ViewConstraints generic in the same way.
We have a site example.com behind ssl that runs a page with ApplePay.
We've got a server side that returns a Merchant Session that looks like the following:
{"epochTimestamp":1581975586106,"expiresAt":1581979186106,"merchantSessionIdentifier":"SSH8E666B0...","nonce":"1239e567","merchantIdentifier":"...8557220BAF491419A...","domainName":"example.com","displayName":"ApplePay","signature":"...20101310f300d06096086480165030402010500308..."}
We receive this response in session.onvalidatemerchant as a string and convert it to a Json Object and pass to session.completeMerchantValidation.
As a result we get the following error:
Code: "InvalidAccessError"
Message: "The object does not support the operation or argument"
We run the following code on our page:
.....
session.onvalidatemerchant = (event) => {
const validationURL = event.validationURL;
getApplePaySession(validationURL).then(function (response) {
try {
let resp = JSON.parse(response);
session.completeMerchantValidation(resp);
} catch (e) {
console.error(JSON.stringify(e));
}
});
};
....
Additional questions:
Is the object described above a "correct" Merchant Session opaque that needs to be passed to completeMerchantValidation or it's missing some fields?
Is this object needs to be passed as is or it needs to be base64 encoded?
Does it need to be wrapped into another object?
Any help or lead is greatly appreciated.
I have a little problem with DingoAPI and Vue.js when I'm trying to get my error message from response. I think the browser is replacing my custom message by the default one. Here is my code:
PHP script
if($request->readerId){
Return succes (this works properly)
else{
return $this->response->error(
'No reader',
400 //(or diffrent code)
);
}
Vue.js script
await axios.post(API_URL + 'card/', {
some data
}, {
headers: {
headers
},
}).then(({data}) => {
context.commit(SET_RESPONSE, data);
}).catch((error) => {
console.log(error.message);
throw error
})
When I'm trying to look on my message in the network tab I can see (so DingoAPI did it correctly):
{"message":"No reader","status_code":400}
But when I'm using console.log(error.message) or trying to show it on the page there is standard error message:
Request failed with status code 400
Is there a way to set error message with DingoAPI and catch it in my .js script?
Maybe I need to write my own custom exception?
What you want is access to the data of the response from your error variable.
console.log(error.response.data.message); // No reader
Otherwise you can log error.response to see the object:
console.log(error.response);
If you wonder why it's printing Request failed with status code 400:
The problem is when the console.log tries to output the error, the string representation is printed, not the object structure, so you do not see the .response property.
Source: https://github.com/axios/axios/issues/960#issuecomment-309287911
I successfully implemented client editors and a server side API.
Now I'm adding more validation at the server side, and besides returning the proper HTTP code (200 for OK, 4xx for other uses, 500 for errors, etc.) I want to return a list of validations that failed after the submission generated by Model.save().
I run it this way:
myModel.save({
success: function (a, operation, c) {...},
failure: function (a, operation, c) {...}
});
But if there was a failure, the operation object only have the response status and its statusText, all through
operation.error.status // i.e. 409
operation.error.statusText // "Conflict"
But server side a detail of the failing validations (mostly domain level ones) are being added to the response.
Is there a way I can get what the server sent as the body of the HTTP response to the PUT/POST submission?
Do I have to return it using a particular JSON structure?
EDIT:
I'm now returning this as the body of the HTTP Response (with code 4xx):
{
data: {/* the record serialized */},
success: false, // or true if everything went ok
message: "This failed because X and Y."
}
Thanks in advance.
For some reason Ext is not attaching the response content to the error object, but it triggers an exception event if there is a failure.
So what we did was to handle the "exception" event of the model's proxy, and then we will have access to the XHR response, being able to do whatever we want with it.
myModel.getProxy().on('exception', this.onProxyException, this);
The handler is as follows:
onProxyException : function (proxy, response, operation) {
var errors;
errors = Ext.JSON.decode(response.responseText).message;
/* Whatever is needed with the errors */
}
In this example we asume the errors come in JSON format, they could be a simple text string, which wouldn't require the use of decode().
According to this blog:
http://code.tonytuan.org/2013/07/extjs-get-server-response-in-modelsave.html
You can write code like this:
model.save({
success: function (record, operation) {
// json response from server
console.log(operation.response);
},
failure: function (record, operation) {
// undefined
console.log(operation.response);
// json response from server
console.log(operation.request.scope.reader.jsonData);
}
});
in reader block add: messageProperty: 'message'
from server return: success:false, message: 'error test'
from failure get error:
failure: function (records, operation) {
Ext.Msg.alert('error', operation.error);
}