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

Richard/docs/images #36

Merged
merged 9 commits into from
Aug 27, 2021
Merged

Richard/docs/images #36

merged 9 commits into from
Aug 27, 2021

Conversation

chomosuke
Copy link
Owner

@chomosuke chomosuke commented Aug 25, 2021

I've decided to separate images upload from card update, considering that we might want to upload the image while user is still filling out other details. To avoid garbage images that isn't attach to any business card building up on the database and taking space, we can prune the images for a user every time that user uploads an image.

This means I'll also need to:

  • modify card schema to include the image url instead of the actual buffer
  • modify user schema so that it includes another array containing images along with their ids (could be objectId)

related to #30

I've realized that we can prune on every new image upload
@chomosuke chomosuke added the documentation Improvements or additions to documentation label Aug 25, 2021
@chomosuke chomosuke added this to the Sprint 1 milestone Aug 25, 2021
@chomosuke chomosuke self-assigned this Aug 25, 2021
@shangzhel
Copy link
Collaborator

shangzhel commented Aug 25, 2021

The card JSON sent over the network does not need to have a 1-to-1 correspondence to the database Card schema. If we don't want garbage images building up in MongoDB, we can simply embed the image data as a buffer inside each Card. When a Card gets erased in MongoDB, the image buffer goes along with it.

In regards to the consideration for uploading ahead of time, when we are restricting image sizes to be under 1 MB, it will take mere seconds to upload. Now that I have given it more thought, it seems more dangerous to split image and card update requests, because there can be a race condition where either the request to upload the image arrives at the server or the request to create a card does, but not both, and there will be inconsistencies. Requiring all card update information to travel within a single request avoids this: either the entire request fails, or the entire request succeeds.

The image get endpoint is still useful, because GET /api/me will not transmit the card image data so it must still be done separately.

@chomosuke
Copy link
Owner Author

chomosuke commented Aug 25, 2021

@shangzhel I don't think that's the case.

First of all, an 1mb image taking mere seconds to upload might not always be a valid assumption, because internet speed such as 50kbps exist in places such as New Zealand.

Secondly, if we were to enforce that each imagine has to be bounded to a card then yes, we will have inconsistency that'll break the requirements. However, this need not be the case. For that if everything is structured as I've proposed, a card update can not succeed without the imagine upload, because it won't have the imagine URL, which can only be obtained in the response by the server after the success of the image upload. And if the imagine upload succeeds without the card update, nothing will happen, the previous imagine, or lack there of will be retained and we'll just have one imagine on the server not bounded to any card, which will be pruned at the next imagine upload.

@shangzhel
Copy link
Collaborator

But can you ensure that the image upload and card create/update operations are atomic?
Consider concurrent sessions, a successful sequence would look like this:

Time Session 1 Session 2 Resources on server
0 Upload image, gets url1 url1
1 Create card with url1, succeeds card1:url1
2 Upload image, prune, nothing unused, gets url2 card1:url1 url2
3 Create card with url2, succeeds card1:url1 card2:url2

But there can be a race condition, consider the following sequence of requests:

Time Session 1 Session 2 Resources on server
0 Upload image, gets url1 url1
1 Upload image, prune, url1 unused, replaced with url2 url2
2 Create card with url1, fails because url1 got deleted url2
3 Create card with url2, succeeds card1:url2

Copy link
Collaborator

@shangzhel shangzhel left a comment

Choose a reason for hiding this comment

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

Some adjustments needed to fit the conventions that the other endpoints use. Refer to the code comments.

docs/api/Card.ts Outdated
@@ -9,6 +9,7 @@ export interface Card {
email: string;
jobTitle: string;
company: string;
imageUrl?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of keeping an entire URL, it can keep just an ObjectId to the image in the other array.

Copy link
Collaborator

Choose a reason for hiding this comment

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

May I know which other array you are referring to?
@shangzhel

Copy link
Owner Author

Choose a reason for hiding this comment

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

The array no longer exist as now there's a 1 to 1 relationship between the image and the cards, I've modify the API accordingly


Response body:
```ts
import type { ObjectId } from './ObjectId';
Copy link
Collaborator

Choose a reason for hiding this comment

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

ObjectId is unused here, no need to import it.

import type { ObjectId } from './ObjectId';

interface ImagePostResponse {
imageUrl: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be an Id instead of a URL.

}
```

# /api/image/imageId
Copy link
Collaborator

@shangzhel shangzhel Aug 26, 2021

Choose a reason for hiding this comment

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

Use the route parameter syntax.

@chomosuke
Copy link
Owner Author

chomosuke commented Aug 26, 2021

@shangzhel This is a very valid scenario that I did not consider before.

There are three way that I can think of to address this.

First is to make image and card update a single request, as you've proposed.

Second is to set a time stamp on images to say, prevent the image from getting pruned within a day.

Third is that, if when attempting to update card, the server complain image not found. Default to another end point which upload the image and update the card on a single POST.

Tell me what you think is best (maybe justify) and I'll just do that.

Also regarding to your requested changes about /api/image/imageId, I thought we wanted to keep the route a get request so that we can simply feed the url into the src property in whatever image element on the frontend we'll use.

@waltervan00
Copy link
Collaborator

Also regarding to your requested changes about /api/image/imageId, I thought we wanted to keep the route a get request so that we can simply feed the url into the src property in whatever image element on the frontend we'll use.

When we were discussing image upload, I remembered that the binary data of the image would be stored on the database. Is it possible to generate a url for you to open the image with an external browser? What if we could turn binary data sent from the backend into a url to display on in the img element?

If we let the image be transmitted as a binary buffer over the api, will our image validation library still be able to validate it? If so we can create an image validation middleware for endpoints #23 and #25. Patching a cards details would be an overwrite of its contents in this situation.

Alternatively I've been reading on the transactions feature offered by Mongoose: https://mongoosejs.com/docs/transactions.html
You could store in back-end cache each user's editing transaction. It may provide the atomicity we need:

  1. User starts a transaction
  2. User edits details
  3. User saves details, patch request is sent.
  4. Server saves new card details under transaction.
  5. Server closes the transaction

1a. User already has a previous transaction when starting a new one

  1. Abort previous transaction and start a new one

2a. User uploads image on front end

  1. POST image request is stored in database under transaction

4a. Image has been uploaded from 2a.

  1. Card on database is linked to the saved image under the transaction

*a. User closes page

  1. Transaction remains in server

*b. User cancels transaction

  1. Transaction is aborted.

I'm slightly worried about Heroku's environment between the two api calls. How often does it clean out its deployment environment? If we have to consider more permanent transaction management solutions this may not be valid.

@waltervan00
Copy link
Collaborator

waltervan00 commented Aug 26, 2021

We could defer this issue to Sprint 2 once we have at least the basic functions down. Then we can have a more focused attempt at it. When we have some front-end developed, we also have to identify how to insert the user's image files into the front end interface (allowing for binary encoding).

@chomosuke
Copy link
Owner Author

@waltervan00

Yes, and yes, that's exactly what I'm saying for /api/image/imageId, that would be the url we feed into the img element, or used to open the image in a new window (though have to be the same browser as we need to cookie for authentication).

Yes, and very likely we will.

One problem with the transaction is that, overtime we'll have unaborted transaction garbages on the server, not to mention that managing those transaction between all the different endpoint would be difficult to say the least as far as I can imagine.

@shangzhel
Copy link
Collaborator

@chomosuke

set a time stamp on images to say, prevent the image from getting pruned within a day.

Default to another end point which upload the image and update the card on a single POST.

The more complicated or split APIs get the more difficult it is to get right. This is because REST APIs by design are stateless, which means we are not allowed to assume that any sequence of more than one requests actually completes. This is why I propose each API completes exactly one action.

Also regarding to your requested changes about /api/image/imageId, I thought we wanted to keep the route a get request so that we can simply feed the url into the src property in whatever image element on the frontend we'll use.

I had forgotten about this, but it is not the best either way so I will adjust my code comment to propose a different change.

@waltervan00

You could store in back-end cache each user's editing transaction.

That's not how transactions work. MongoDB transactions are database-wide. This means once a transaction starts, no other transaction can begin, regardless of whether it is the same user or otherwise. If our server begins a transaction and the client disconnects, it becomes a denial-of-service attack while the server waits for the next message from the client. There is also no way to "store a transaction". They are not documents, transactions is a MongoDB function, not data.

@chomosuke
Copy link
Owner Author

@shangzhel

Fair enough, I will move forward with the first way.

@waltervan00
Copy link
Collaborator

waltervan00 commented Aug 26, 2021

Fair enough, I will move forward with the first way.

@shangzhel
Will this affect the processes #23 and #25 and /api/card/ POST, PATCH? We can implement a middleware for validating / processing the image binary -- a common feature between these api calls. And each endpoint can work as if overwriting or creating a new card.

@chomosuke
Copy link
Owner Author

@waltervan00 now I'm thinking that I might have to take over #23 and #25.

Or alternatively, I'll wait for you to finish writing, and then I'll start working on it, which might be a bit inefficient.

Copy link
Collaborator

@shangzhel shangzhel left a comment

Choose a reason for hiding this comment

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

Just minor changes left, then it should be good to go.

@@ -0,0 +1 @@
type Image = string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Needs to export the type, and add a newline at the end of the file.

docs/api/card.md Outdated
@@ -1,12 +1,16 @@
# /api/card
## PUT
- Authenticated
- 413 Payload Too Large if image string is larger than 1 MB
Copy link
Collaborator

Choose a reason for hiding this comment

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

Verifying only the image string will require first processing the entire request body which can itself DoS the server. Instead we should limit the total request size for all requests. The server can do this with a middleware. Instead of noting this here, state this restriction in docs/api/README.md.

docs/api/card.md Outdated

Request body:
```ts
import type { Card } from './Card';

type CardPatchRequest = Pick<Card, 'id'> & Partial<Omit<Card, 'id'>>;
interface CardPatchRequest extends Pick<Card, 'id'> & Partial<Omit<Card, 'id' | 'hasImage'>> {
image?: Image;
Copy link
Collaborator

Choose a reason for hiding this comment

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

There needs to be a way to indicate the intention to "remove image". I think it will be best to assign null to image, so the type needs to be Image | null instead of just Image.

@@ -0,0 +1,8 @@
# /api/image/:cardId
Copy link
Collaborator

Choose a reason for hiding this comment

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

Either change the endpoint path to /api/images/:cardId to match the file name or rename the file to image.md.

docs/api/card.md Outdated
@@ -34,12 +38,15 @@ Response body: None
## PATCH
- Authenticated
- 410 Gone if card does not exist.
- 413 Payload Too Large if image string is larger than 1 MB
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above, instead of noting this here, state this restriction for all requests in docs/api/README.md.

@waltervan00
Copy link
Collaborator

@waltervan00 now I'm thinking that I might have to take over #23 and #25.

Or alternatively, I'll wait for you to finish writing, and then I'll start working on it, which might be a bit inefficient.

@chomosuke
I can first write up the Mongoose code first, take this time to research how the user is able to get the images from their computer into the UI.

@shangzhel
Copy link
Collaborator

With window.showOpenFilePicker

Copy link
Collaborator

@shangzhel shangzhel 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 to me.

Copy link
Collaborator

@waltervan00 waltervan00 left a comment

Choose a reason for hiding this comment

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

lgtm

@chomosuke chomosuke merged commit b73e47d into docs Aug 27, 2021
@chomosuke chomosuke deleted the richard/docs/images branch August 27, 2021 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants