Skip to content

API: Fix tags update on updating a note #2649

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

Merged
merged 8 commits into from
Mar 13, 2020
Merged

API: Fix tags update on updating a note #2649

merged 8 commits into from
Mar 13, 2020

Conversation

jyuvaraj03
Copy link
Contributor

@jyuvaraj03 jyuvaraj03 commented Mar 3, 2020

Fixes #941

Before:
before-2020-03-04-002019

After:
after-2020-03-04-000934

The issue as mentioned in #941 was that the tags were not updated. This was due to the fact that in PUT request method the tags were not explicitly set unlike how the POST request method was handled in the action_notes function.

The fix I have given makes the necessary changes. I have also written unit tests for the code. Looking forward to any comments or suggestions!

@jyuvaraj03
Copy link
Contributor Author

@PackElend label me please


if (!note) throw new ErrorNotFound();

let newNote = Object.assign({}, note, request.bodyJson(this.readonlyProperties('PUT')));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this interface so please help me understand. Is the PUT command intended to update an existing note? Is this really a new note, or an updated one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the PUT command is intended to update an existing note. Perhaps, the variable name newNote was a bit confusing? I will change it to updatedNote.

if (!note) throw new ErrorNotFound();

let newNote = Object.assign({}, note, request.bodyJson(this.readonlyProperties('PUT')));
newNote = await Note.save(newNote, { userSideValidation: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the effect of user-side validation here?

Copy link
Contributor Author

@jyuvaraj03 jyuvaraj03 Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code I have written here was actually refactored from the defaultAction_ function which was previously called in the case of PUT methods.

To be honest, I do not exactly know what user-side validation does here. But since it was already present in the code that was executed (before this fix), I decided to leave it untampered in order not to break any functionality.

const tag3 = await Tag.save({ title: 'mon étiquette 3' });
const note = await Note.save({
title: 'ma note un',
tags: `${tag1.title},${tag2.title}`,
Copy link
Contributor

@miciasto miciasto Mar 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this add the tags correclty? Perhaps in the test you can start checking if the note is set up as you intended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does add the tags in the note (checked in the desktop app). But I haven't checked the note in the unit test. I will rewrite the unit test to make sure it tests the notes rather than the response.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you have the response checks, they are still useful. So just add the new checks, so it checks both.

tags: `${tag1.title},${tag2.title},${tag3.title}`,
}));
expect(response3.tags === `${tag1.title},${tag2.title},${tag3.title}`).toBe(true);
}));
});
Copy link
Contributor

@miciasto miciasto Mar 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the user accidently sets a tag name that doesn't exist? Is it possible to do via this interface? If it is, then we should add a test for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user sets a tag name that doesn't exist via this interface, it creates the tags and then adds it to the note. I forgot to write a test case for that. I will fix the unit test.

Copy link
Contributor

@miciasto miciasto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this testing, you are checking the API response to confirm if the tags are set. In the issue, it looks like the responses were OK, but the actual database was not updated.

Should you also check the actual tags on the note note after each PUT (using Tag and/or Note methods to directly interrogate the associated tags)? I'm not sure the test is covering the issue.

@jyuvaraj03
Copy link
Contributor Author

Should you also check the actual tags on the note note after each PUT (using Tag and/or Note methods to directly interrogate the associated tags)? I'm not sure the test is covering the issue.

Yes, you were right. I should have checked the actual tags on the notes themselves. I have since rewritten the unit tests to do this.

@jyuvaraj03
Copy link
Contributor Author

I have done all the requested changes. Please review :)

@tessus tessus added the api Joplin API label Mar 6, 2020
Copy link
Contributor

@miciasto miciasto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jyuvaraj03, just a few more comments, all minor except one.

Comment on lines 347 to 348
expect(tagIds1[0]).toBe(tag1.id);
expect(tagIds1[1]).toBe(tag3.id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this might work, there is no guarantee the tags will be returned in this order. Please modify so it is not dependent on the order.

@@ -451,6 +451,23 @@ class Api {
return note;
}

if (request.method === 'PUT') {
const note = await Note.load(id);
const requestBody = JSON.parse(request.body);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency we should change the name of 'requestBody' to match what is used in the 'POST' code (just above).

Also there is no need to create this so early in the function, so please move it closer to where it is first used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there is no need to create this so early in the function, so please move it closer to where it is first used.

I have made this change. But, can I know why it is important to not create it early in the function?


if (requestBody.tags) {
const tagTitles = requestBody.tags.split(',');
await Tag.setNoteTagsByTitles(updatedNote.id, tagTitles);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatedNote.id can be just id

Comment on lines 460 to 461
let updatedNote = Object.assign({}, note, request.bodyJson(this.readonlyProperties('PUT')));
updatedNote = await Note.save(updatedNote, { userSideValidation: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned you borrowed this from the default handler. So rather than duplicate the code here, why not call the default handler to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't think about that! I'll implement it now.

@jyuvaraj03
Copy link
Contributor Author

Completed all the requested changes. Please review!

@laurent22
Copy link
Owner

@mic704b, I've sent you an invite to be a collaborator on the project, so when you think this one is ready feel free to merge it.

Please use the syntax Platform: Fixes|Resolves #xxx: Comment. The comment will appear in the changelog so it should be something not too technical (indeed the title of this PR would be good). So for example Api: Resolves #941: Fix tags update on updating a note

@jyuvaraj03 jyuvaraj03 requested a review from miciasto March 8, 2020 09:08
@jyuvaraj03
Copy link
Contributor Author

Sorry, I accidentally requested a review! Please review at your own pace.

@miciasto
Copy link
Contributor

miciasto commented Mar 8, 2020

@mic704b, I've sent you an invite to be a collaborator on the project, so when you think this one is ready feel free to merge it.

Thank you @laurent22! I am honoured to be invited.

@miciasto
Copy link
Contributor

miciasto commented Mar 8, 2020

@jyuvaraj03 looks good, nearly there.

@jyuvaraj03
Copy link
Contributor Author

Please add a test case for when tags are an empty string. It is not expected, but we could receive it, so we should test we handle it properly.

Thanks for pointing it out. That was an excellent catch! I hadn't handled that case. When the tags in the request is empty, the tags associated with the note do not get deleted like intended. Instead, they stay the same.

So I handled it by checking if the tags property is explicitly given as '' during the API request. If so, all the existing tags are removed from the corresponding note. If the tags property is not mentioned during the request, no change is made to the tags in the note.

Comment on lines 369 to 375
const response4 = await api.route('PUT', `notes/${note.id}`, null, JSON.stringify({
tags: '',
}));
const tagIds4 = await NoteTag.tagIdsByNoteId(note.id);
expect(response4.tags === '').toBe(true);
expect(tagIds4.length === 0).toBe(true);
}));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test case, the tags property is mentioned explicitly with a ''. So, all the tags associated with the note are removed from the note.

Comment on lines 361 to 367
const response3 = await api.route('PUT', `notes/${note.id}`, null, JSON.stringify({
title: 'Some other title',
}));
const tagIds3 = await NoteTag.tagIdsByNoteId(note.id);
expect(tagIds3.length === 2).toBe(true);
expect(tagIds3.includes(tag2.id)).toBe(true);
expect(tagIds3.includes(newTag.id)).toBe(true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case checks the case when the tags property is not mentioned during the request. So, the tags associated with the note does not differ from the previous test case.

Also, I haven't checked response3.tags because it is undefined when tags property isn't mentioned during the API request. So, I skipped that check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it is undefined when tags property isn't mentioned

That can be tested too! If it wasn't returned as undefined, then it would be a problem.

Copy link
Contributor

@miciasto miciasto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, a few more minor comments.

One more question: does the API documentation need update? The documentation says PUT on a note is to set note properties, but doesn't list tags as a property. Perhaps it should be added to the list, or the description changed to something like Sets the properties and tags of .... What do you think?

If it needs change, please do include it on this PR.

Comment on lines 462 to 467
if (requestNote.tags === '') {
await Tag.setNoteTagsByTitles(id, '');
} else if (requestNote.tags) {
const tagTitles = requestNote.tags.split(',');
await Tag.setNoteTagsByTitles(id, tagTitles);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be reduced to a single if statement? I don't think you need two branches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just realized two branches weren't necessary. I changed the code to use just a single if.

const tagIds4 = await NoteTag.tagIdsByNoteId(note.id);
expect(response4.tags === '').toBe(true);
expect(tagIds4.length === 0).toBe(true);
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, for future maintenance, it would be better to separate the test cases into 3 or 4 smaller test cases rather than one big one. This also helps avoid using the 1,2,3,4 variable names too. Sometimes it is too inefficient, but by default it is preferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restructured all the test cases into separate ones. But, I'm not sure if I did it right. I have used lengthy test case descriptions for some test cases. Please review and let me know if I'm doing it right.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Comment on lines 361 to 367
const response3 = await api.route('PUT', `notes/${note.id}`, null, JSON.stringify({
title: 'Some other title',
}));
const tagIds3 = await NoteTag.tagIdsByNoteId(note.id);
expect(tagIds3.length === 2).toBe(true);
expect(tagIds3.includes(tag2.id)).toBe(true);
expect(tagIds3.includes(newTag.id)).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it is undefined when tags property isn't mentioned

That can be tested too! If it wasn't returned as undefined, then it would be a problem.

@laurent22
Copy link
Owner

One more question: does the API documentation need update? The documentation says PUT on a note is to set note properties, but doesn't list tags as a property. Perhaps it should be added to the list, or the description changed to something like Sets the properties and tags of .... What do you think?

That's a good point, the doc should be updated. To do so @jyuvaraj03 you can update command-apidoc.js, then you can verify the output from /CliClient using npm start -- apidoc.

In fact, ideally we should support all the special properties that the POST method supports, such as body_html, image_data_url, crop_rect, etc. for consistency. That could be done with a bit of refactoring, but it's not needed for this PR.

@jyuvaraj03
Copy link
Contributor Author

One more question: does the API documentation need update? The documentation says PUT on a note is to set note properties, but doesn't list tags as a property. Perhaps it should be added to the list, or the description changed to something like Sets the properties and tags of .... What do you think?

You're right. So, I updated the doc. I added the tags property to the list of properties.

@jyuvaraj03
Copy link
Contributor Author

jyuvaraj03 commented Mar 12, 2020

That's a good point, the doc should be updated. To do so @jyuvaraj03 you can update command-apidoc.js, then you can verify the output from /CliClient using npm start -- apidoc.

Yes, I did update the command-apidoc.js file. But, I have a small question. These were the lines of code I changed in that file:

tableFields.push({
	name: 'tags',
	type: Database.enumId('fieldType', 'text'),
	description: 'Comma-separated list of tags. eg. `tag1,tag2`.',
});

I just uncommented them in this commit. These lines already existed, but you had commented them out. Was there a reason you did that? Am I missing something?


In fact, ideally we should support all the special properties that the POST method supports, such as body_html, image_data_url, crop_rect, etc. for consistency. That could be done with a bit of refactoring, but it's not needed for this PR.

I would like to work on implementing these (possibly in another PR). How should I go about it? Should I create an issue for this and then submit a PR?

Also, @PackElend can you please @mention Laurent here because he was the one who commented those lines out, and I would like to know why he did that.

@laurent22
Copy link
Owner

Ok I think it's ready to merge now, thanks @jyuvaraj03 and thanks @mic704b for reviewing!

@laurent22 laurent22 merged commit cda8372 into laurent22:master Mar 13, 2020
@miciasto
Copy link
Contributor

I think it's ready to merge now

Definitely.

Good work @jyuvaraj03

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Joplin API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API : Tags not updated when updating a note
5 participants