update one object at a time (and maybe related objects)
delete one object at a time (and maybe related objects)
However, you will often have multiple READ methods: get one article, list all articles, list all articles by date, get all articles by author, search results, etc.
For our Contacts model, we will keep it simple. We want:
one method to create one contact. It will take a name and an email, as one object
one method to delete one contact. It will take an id
one method to update one contact. It will take an id, and either a name, an email, or both
one method to read one contact. It will receive an id
one method to read all contact, with options to order them by name, or by email
That's 5 methods total. We already know their signatures, thanks to the description above. Here are the signatures:
Note: the above is not valid Javascript. This is not actually valid anything. It looks a bit like ES6, a bit like Typescript, a bit like Haskell, and hopefully conveys 3 informations:
what the function is (async or sync)
what the function needs, in terms of arguments. I'm using ? to signify the argument is optional, and | to signify options (e.g., a|b means a or b).
what the function returns
We already have a skeleton of getContactsList. We only need to implement the order by logic.
/** * retrieves the contacts from the database * @param{string} orderBy an optional string that is either "name" or "email" * @returns{array} the list of contacts */constgetContactsList=async (orderBy) => {let statement =`SELECT contact_id AS id, name, email FROM contacts`switch(orderBy){case'name': statement+=` ORDER BY name`; break;case'email': statement+=` ORDER BY email`; break;default: break; }constrows=awaitdb.all(statement)return rows}
As you see, we construct the statement differently, depending on the orderBy argument.
Let's implement them very basically:
// back/src/db.js...constcreateContact=async (props) => {const { name,email } = propsconstresult=awaitdb.run(SQL`INSERT INTO contacts (name,email) VALUES (${name}, ${email})`);constid=result.stmt.lastIDreturn id }constdeleteContact=async (id) => {constresult=awaitdb.run(SQL`DELETE FROM contacts WHERE contact_id = ${id}`);if(result.stmt.changes ===0){returnfalse }returntrue }constupdateContact=async (id, props) => {const { name,email } = propsconstresult=awaitdb.run(SQL`UPDATE contacts SET email=${email} WHERE contact_id = ${id}`);if(result.stmt.changes ===0){returnfalse }returntrue }constgetContact=async (id) => {constcontactsList=awaitdb.all(SQL`SELECT contact_id AS id, name, email FROM contacts WHERE contact_id = ${id}`);constcontact= contactsList[0]return contact }/** * retrieves the contacts from the database * @param{string} orderBy an optional string that is either "name" or "email" * @returns{array} the list of contacts */constgetContactsList=async (orderBy) => {let statement =`SELECT contact_id AS id, name, email FROM contacts`switch(orderBy){case'name': statement+=` ORDER BY name`; break;case'email': statement+=` ORDER BY email`; break;default: break; }constrows=awaitdb.all(statement)return rows }constcontroller= { createContact, deleteContact, updateContact, getContact, getContactsList }return controller
We can try them. Open back/src/index.js, comment out everything inside ofstart`, and write instead:
...conststart=async () => {constcontroller=awaitinitializeDatabase()/** ... previous code commmented out **/constid=awaitcontroller.createContact({name:"Brad Putt",email:"brad@pet.com"})constcontact=awaitcontroller.getContact(id)console.log("------\nmy newly created contact\n",contact)// await controller.updateContact(id, {name:"Brad Pitt"})awaitcontroller.updateContact(id, {email:"brad@pitt.com"})constupdated_contact=awaitcontroller.getContact(id)console.log("------\nmy updated contact\n",updated_contact)console.log("------\nlist of contacts before\n",awaitcontroller.getContactsList())awaitcontroller.deleteContact(id)console.log("------\nlist of contacts after deleting\n",awaitcontroller.getContactsList())}...
Run it, observe the output in your terminal.
One confusing part might be this result.stmt stuff that is going on from time to time. This is returned from sqlite's run method, and has this shape:
{ stmt:{ lastID:number, changes:number }}
lastID contains the last ID operated on, and changes contains a number representing the number of changes.
Take some time, try to understand what those methods are doing. Try thinking how you would, for example, implement a getContactByName or getContactByEmail function.
Do take a moment, because they're going to get more complex right after this. We need to do error checking, and make sure we receive the right arguments. While none of that is going to be difficult, those checks will decrease the readability of the code.
I will also add documentation, in the jsdoc format. This format is a comment, over the function, that explains what it does. This also pops up in some editors (like VSCode) when using the function.
Ready? Let's add checks:
// back/src/db.jsconstinitializeDatabase=async () => {constdb=awaitsqlite.open('./db.sqlite');/** * creates a contact * @param{object} props an object with keys `name` and `email` * @returns{number} the id of the created contact (or an error if things went wrong) */constcreateContact=async (props) => {if(!props ||!props.name ||!props.email){thrownewError(`you must provide a name and an email`) }const { name,email } = propstry{constresult=awaitdb.run(SQL`INSERT INTO contacts (name,email) VALUES (${name}, ${email})`);constid=result.stmt.lastIDreturn id }catch(e){thrownewError(`couldn't insert this combination: `+e.message) } }/** * deletes a contact * @param{number} id the id of the contact to delete * @returns{boolean} `true` if the contact was deleted, an error otherwise */constdeleteContact=async (id) => {try{constresult=awaitdb.run(SQL`DELETE FROM contacts WHERE contact_id = ${id}`);if(result.stmt.changes ===0){thrownewError(`contact "${id}" does not exist`) }returntrue }catch(e){thrownewError(`couldn't delete the contact "${id}": `+e.message) } }/** * Edits a contact * @param{number} id the id of the contact to edit * @param{object} props an object with at least one of `name` or `email` */constupdateContact=async (id, props) => {if (!props ||!(props.name ||props.email)) {thrownewError(`you must provide a name or an email`); }const { name,email } = props;try {let statement ="";if (name && email) { statement =SQL`UPDATE contacts SET email=${email}, name=${name} WHERE contact_id = ${id}`; } elseif (name) { statement =SQL`UPDATE contacts SET name=${name} WHERE contact_id = ${id}`; } elseif (email) { statement =SQL`UPDATE contacts SET email=${email} WHERE contact_id = ${id}`; }constresult=awaitdb.run(statement);if (result.stmt.changes ===0) {thrownewError(`no changes were made`); }returntrue; } catch (e) {thrownewError(`couldn't update the contact ${id}: `+e.message); } }/** * Retrieves a contact * @param{number} id the id of the contact * @returns{object} an object with `name`, `email`, and `id`, representing a contact, or an error */constgetContact=async (id) => {try{constcontactsList=awaitdb.all(SQL`SELECT contact_id AS id, name, email FROM contacts WHERE contact_id = ${id}`);constcontact= contactsList[0]if(!contact){thrownewError(`contact ${id} not found`) }return contact }catch(e){thrownewError(`couldn't get the contact ${id}: `+e.message) } }/** * retrieves the contacts from the database * @param{string} orderBy an optional string that is either "name" or "email" * @returns{array} the list of contacts */constgetContactsList=async (orderBy) => {try{let statement =`SELECT contact_id AS id, name, email FROM contacts`switch(orderBy){case'name': statement+=` ORDER BY name`; break;case'email': statement+=` ORDER BY email`; break;default: break; }constrows=awaitdb.all(statement)if(!rows.length){thrownewError(`no rows found`) }return rows }catch(e){thrownewError(`couldn't retrieve contacts: `+e.message) } }constcontroller= { createContact, deleteContact, updateContact, getContact, getContactsList }return controller}
Re-run the software. The output in the terminal should not have changed; however, if you try illegal things, it should not work.
Try to make the software crash by changing the arguments in index.js.
For example, let's try to add two users with the same email:
// back/src/index.jsawaitcontroller.createContact({name:"Brad Putt",email:"brad@pet.com"})awaitcontroller.createContact({name:"Brad Putt",email:"brad@pet.com"})console.log("------\nlist of contacts after deleting\n",awaitcontroller.getContactsList())
We should get an error. Once we've verified our controller works, we can restore index.js to what it was, and uncomment the previous code.