In a first instance, we need to create a new migration script for contacts. Put it in back/migrations, call it 003-images.sql:
---------------------------------------------------------------------------------- Up--------------------------------------------------------------------------------ALTERTABLE contacts RENAME TO old_contacts;CREATETABLEcontacts ( contact_id INTEGERPRIMARY KEY AUTOINCREMENT,nameTEXT, email TEXT, author_id TEXT,dateTEXT,imageTEXT,FOREIGN KEY(author_id) REFERENCES users(auth0_sub));-- copy old data in new table-- all previous contacts are for the first userINSERT INTO contacts (contact_id, name, email, date, author_id) SELECT contact_id, name, email, date, author_id FROM old_contacts;
-- remove previous tableDROPTABLE old_contacts;---------------------------------------------------------------------------------- Down--------------------------------------------------------------------------------ALTERTABLE contacts RENAME TO old_contacts;CREATETABLEcontacts ( contact_id INTEGERPRIMARY KEY AUTOINCREMENT,nameTEXT, email TEXT, author_id TEXT,dateTEXT,FOREIGN KEY(author_id) REFERENCES users(auth0_sub));-- delete new contacts:DELETEFROM old_contacts WHERE author_id ="fakeId";-- copy old data to previous tableINSERT INTO contacts (contact_id, name, email, date, author_id) SELECT contact_id, name, email, date, author_id FROM old_contacts;
DROPTABLE old_contacts;
We're going to add the new property in db.js and controller.js. Since we're beginning to have a lot of properties, having a dozen ifs in updates can be very cumbersome. Let's create a little function that helps us. Put it in db.js:
// back/src/db.js.../** * * Joins multiple statements. Useful for `WHERE x = 1 AND y = 2`, where the number of arguments is variable. * * Usage: * * joinSQLStatementKeys( ["name", "age", "email"], { email:"x@y.c", name="Z"}, ", ") * * * Will return an SQL statement corresponding to the string: * * name="Z", email="x@y.c" * * * @param{Array} keys an array of strings representing the properties you want to join * @param{Object} values an object containing the values * @param{string} delimiter a string to join the parts with * @param{string} keyValueSeparator a string to join the parts with * @returns{Statement} an SQL Statement object */constjoinSQLStatementKeys= (keys, values, delimiter , keyValueSeparator='=') => {return keys.map(propName => {constvalue= values[propName];if (value !==null&&typeof value !=="undefined") {returnSQL``.append(propName).append(keyValueSeparator).append(SQL`${value}`); }returnfalse; }).filter(Boolean).reduce((prev, curr) =>prev.append(delimiter).append(curr));};...constinitializeDatabase=async () => {.../** * creates a contact * @param{object} props an object with keys `name`, `email`, `image`, and `author_id` * @returns{number} the id of the created contact (or an error if things went wrong) */constcreateContact=async props => {if (!props ||!props.name ||!props.email ||!props.author_id) {thrownewError(`you must provide a name, an email, an author_id`); }const { name,email,author_id,image } = props;...constresult=awaitdb.run( SQL`INSERT INTO contacts (name,email, date, image, author_id) VALUES (${name}, ${email}, ${date}, ${image}, ${author_id})`
);... };/** * Edits a contact * @param{number} contact_id the id of the contact to edit * @param{object} props an object with at least one of `name`,`email` or `image`, and `author_id` */constupdateContact=async (contact_id, props) => {if ( (!props ||!(props.name ||props.email ||props.image),!props.author_id) ) {thrownewError(`you must provide a name, or email, or image, and an author_id` ); }try {constpreviousProps=awaitgetContact(contact_id)constnewProps= {...previousProps,...props }conststatement=SQL`UPDATE contacts SET `.append(joinSQLStatementKeys( ["name","email","image"], newProps,", " ) ).append(SQL` WHERE `).append(joinSQLStatementKeys( ["contact_id","author_id"], { contact_id:contact_id, author_id:props.author_id }," AND " ) );constresult=awaitdb.run(statement);if (result.stmt.changes ===0) {thrownewError(`no changes were made`); }returntrue; } catch (e) {thrownewError(`couldn't update the contact ${contact_id}: `+e.message); } };...constgetContact=async id => {try {constcontactsList=awaitdb.all(SQL`SELECT contact_id AS id, name, email, image, author_id FROM contacts WHERE contact_id = ${id}` );... };...constgetContactsList=async props => {...try { const statement = SQL`SELECT contact_id AS id, name, email, date, image, author_id FROM contacts WHERE ${orderBy} > ${startingId}`;
Done! Let's see how to handle uploads now
Sending Uploads From React
We will:
add a file field to the create and update form
get the data from that field on submit
change the method of fetch for update and create so it uses POST (this is necessary for uploads).
prepare an image field to receive the property
All fairly simple, so we're not going to waste a lot of time in explanations. We should still try to understand how a file input works:
onSubmit= evt => {evt.preventDefault() // stop the page from refreshingconstinput=evt.target.fileField// instead of input.value, a file field has a `files` propertyconstfilesList=input.files // fileList is like an array, but not really an arrayconstfilesArray= [...fileList] // we transform it into an array// we now have an array of files, to use as we wish}render(){return <formonSubmit={this.onSubmit}> <inputtype="text"name="textField"/> <inputtype="file"multiplename="fileField"/> </form>}
the onSubmit above can be re-written more simply as
onSubmit= evt => {evt.preventDefault() // stop the page from refreshingconstfilesArray= [...evt.target.fileField.files]}
If the field has only one file (multiple not set), then we can do:
onSubmit= evt => {evt.preventDefault() // stop the page from refreshingconstfile=evt.target.fileField.files[0]}
Lastly, the way to send files through fetch is like so:
onSubmit= evt => {evt.preventDefault() // stop the page from refreshingconstfilesArray= [...evt.target.fileField.files]constbody=newFormData();filesArray.map((file, index) =>body.append(`files[${index}]`, file))body.append('text',evt.target.textField.value)fetch('http://localhost:3000/upload-path', { method:'POST', body })}
For both the update and create form:
add a file input field called "contact_image_input" <input type="file" name="contact_image_input"/>
in each form's onSubmit
capture the files array
change fetch's method to POST
append the body
Then, in <Contact>, we'll add a div to show the image
(don't forget to also pass image={contact.image} when you use <Contact/>)
Here is a summary of the changes:
// front/src/App.js...updateContact=async (id, props) => {...let body =null;if(props.image){ body =newFormData();body.append(`image`,props.image)/** * NOTE: * If you were uploading several files, you'd do: * body.append(`image[0]`, image1) * body.append(`image[1]`, image2) * ... **/ }constresponse=awaitfetch(url, { method:'POST', body, headers: { Authorization:`Bearer ${auth0Client.getIdToken()}` } });... }...createContact=async props => {try {...const { name,email,image } = props;...let body =null;if(image){ body =newFormData();body.append(`image`, image) }constresponse=awaitfetch(url, { method:'POST', body, headers: { Authorization:`Bearer ${auth0Client.getIdToken()}` } });... }...onSubmit= evt => {...// get the filesconstimage=evt.target.contact_image_input.files[0]// create the contact from mail and emailthis.createContact({ name, email, image });... };...renderCreateForm= () => {return (... <inputtype="file"name="contact_image_input" />... ); };...renderContactPage= ({ match }) => {...return ( <Contact ...image={contact.image} ... /> );... }
note: we're uploading the images in /back, so it's natural that we serve the images from there. Thus, the //localhost:8080/images/ in image sources
Your front-end is ready to send files
Receiving Uploads on Express
We will:
set Multer to receive images, and change some methods to POST.
store the images somewhere public, and get the path
send that path to the database
Doing file uploads by hand is tricky, so we'll use Multer. If you've followed all steps, we already installed it previously. If not, move to back and run
Then, let's add it to the two methods. Add it afterisLoggedIn (no need to process the image if the user isn't logged in). Don't forget to change gets to posts!
note: You'll notice that when you create or edit a contact, you need to refresh to see the image. It's not immediately there because we aren't changing the browser-side representation of the contact. This is not very elegant, but also not essential, and we can think about it later.
You might want to ignore the images uploaded by adding **/*public/images to your .gitignore. It's not necessary, and you should do it depending if you feel your images are part of your core website or not.