Hi Moralis Community,
I have a quick question for you! Is there anyone at Moralis that can help me learn about Relational Databases and how Moralis Objects work w/ users?
My question is:
How can I associate Classes with each other? I was looking at Pointers
and Relations
but itâs a bit abstract to me.
- For example, how does a Book (object) get itâs ISBD (Relation) associated with it.
- Another one is how to associate 1 or more authors to a book (one-to-many relationship array).
- A further question is, how can I make the books ownable by only one User (object) at a time, but the user can own many books (one-to-many relationship array).
Context:
Iâm working on a Library App for practice on databases. So each book object gets itâs objectId in the
Book
Class (I get that and you can create a book)âŚbut if I wanted to make the Books ownable by a certain user in the database, thatâs what Iâm a bit stuck on.
This works: For
Pointers
, what I am doing is creating an ISBD class that makes it where each book must have a unique ISBD number, or else the book cannot be created.
What doesnât work: For
Relations
, what I am trying to do is create a way where many authors can be assigned to a book (i.e. as a selection), and allow many books to be âownableâ by a given User - so you could have 2 books, or 1 book, or 10 books, but I couldnât have any of those books in your âownableBooksâ class at the same time you are reviewing them.
Book Class:
Just a simple class with properties about the book. You will see there is a available
field which is a boolean that someone could click and that will take the book out. What I would love to do is wrap this into a function where as a user âtakes out a bookâ - it goes into their ownableBooks array and then that bookâs âavailabilityâ is set to false until the user puts it back.
ISBD Class: this works as a pointer param for the class Books
When you try to create a book with the same ISBD that exists in the database, it wonât allow it. This is a great use-case for filtering out inputs if a certain property is the same as other objects in the db.
The main issue: my Authors relationship doesnât work
It comes up as empty when I click on the View relation
for authorsTest
in the Book
class screenshot (below). Perhaps something is wrong in my code (further below) and my relationship isnât getting established correctly?
I believe I want to have something like this below, but Iâm confused how the Authors
would be âassignedâ to the Books
class so one book could have 1 or more authors.
The other issue: Users canât reserve their own books
Iâm hoping to be able to have an array of the books each user has, but I cannot seem to figure out how an objectId can be assigned to only one user programmatically in-app. For example, there are 10 books in the databaseâŚyou take out 2, there are only 8 books left and no one can take out those 2 books until you put them back.
+++
The App: (Code is below)
My Code: NEXTjs x TypeScript
import React, { useState, FC, ReactElement, useEffect } from 'react';
import styles from './App.module.css';
import { Button, Checkbox, Divider, Input, Radio, List } from 'antd';
import { useMoralis, useMoralisQuery } from "react-moralis";
import Router , { useRouter } from 'next/router';
import Moralis from 'moralis/types';
import {
CheckOutlined,
CloseOutlined,
PlusOutlined,
RedoOutlined,
PullRequestOutlined
} from '@ant-design/icons';
export const BookRegistrationApp: FC<{}> = (): ReactElement => {
const {
isAuthenticated,
user,
setUserData,
userError,
isUserUpdating,
refetchUserData,
Moralis,
} = useMoralis();
// State variables
const initialMoralisObjectArray: Moralis.Object[] = [];
const [publishers, setPublishers] = useState<Moralis.Object[] | null>(null);
const [authors, setAuthors] = useState<Moralis.Object[] | null>(null);
const [genres, setGenres] = useState<Moralis.Object[] | null>(null);
const [isbds, setIsbds] = useState<Moralis.Object[] | null>(null);
const [bookTitle, setBookTitle] = useState('');
const [bookYear, setBookYear] = useState('');
const [bookISBD, setBookISBD] = useState('');
const [bookPublisherInput, setBookPublisherInput] = useState('');
const [bookGenreInput, setBookGenreInput] = useState('');
const [bookAuthorInput, setBookAuthorInput] = useState('');
const [bookPublisher, setBookPublisher] = useState<Moralis.Object>();
const [bookGenre, setBookGenre] = useState<Moralis.Object>();
const [bookAuthors, setBookAuthors] = useState(initialMoralisObjectArray);
// ============================================== //
// METHOD #1: QUERY/FETCH DATA
// ============================================== //
// useEffect is called after the component is initially rendered and after every other render
useEffect(() => {
async function getFormChoices(): Promise<boolean> {
// This condition ensures data is updated only if needed
if (
publishers === null &&
authors === null &&
genres === null
) {
// Query all choices
for (let choiceObject of [
'Publisher',
'Author',
'Genre'
]) {
let newQuery: Moralis.Query = new Moralis.Query(choiceObject);
try {
let queryResults: Moralis.Object[] = await newQuery.find();
// Empty or invalid queries return as an empty array
// Set results to state variable
if (choiceObject === 'Publisher') {
setPublishers(queryResults);
} else if (choiceObject === 'Author') {
setAuthors(queryResults);
} else if (choiceObject === 'Genre') {
setGenres(queryResults);
}
} catch (error: any) {
// Error can be caused by lack of Internet Connection
alert(`Error! ${error.message}`);
return false;
}
}
}
return true;
}
getFormChoices();
}, [publishers, authors, genres]);
const fetchBooks = async function (): Promise<boolean> {
// Reading parse objects is done by using Moralis.Query
const moralisQuery: Moralis.Query = new Moralis.Query('Book');
try {
let books: Moralis.Object[] = await moralisQuery.find();
// Be aware that empty or invalid queries return as an empty array
// Set results to state variable
setBookAuthors(books);
return true;
} catch (error: any) {
// Error can be caused by lack of Internet connection
alert('Error!' + error.message);
return false;
}
};
// ============================================== //
// METHOD #2: BOOK CREATION FUNCTIONS
// ============================================== //
const createBook = async function(): Promise<boolean> {
try {
// these values come from state variables linked to screen form fields, retrieving the user choices as a complete Moralis.Object, when applicable;
const bookTitleValue: string = bookTitle;
const bookYearValue: number = Number(bookYear);
const bookISBDValue: string = bookISBD;
// Example; bookPublisher holds value from RadioButton.Group field with its options being every Publisher Moralis Object instance saved on server, which is queried on screen load via useEffect
const bookPublisherObject: Moralis.Object | undefined = bookPublisher;
const bookGenreObject: Moralis.Object | undefined = bookGenre;
// bookAuthors can be an array of Moralis.Objects, since the book may have more than one Author
const bookAuthorsObjects: Moralis.Object[] = bookAuthors;
// Creates a new Moralis Object Instance
let Book: Moralis.Object = new Moralis.Object('Book');
// Set data to Moralis Object
// Title field here:
Book.set('title', bookTitleValue);
// Simple number field
Book.set('year', bookYearValue);
// Set the book as automatically available
Book.set('available', true);
// ONE-TO-ONE (1:1)
// Check uniqueness of value before creating new ISBD object
let isbdQuery: Moralis.Query = new Moralis.Query('ISBD');
isbdQuery.equalTo('name', bookISBDValue);
let isbdQueryResult: Moralis.Object | undefined = await isbdQuery.first();
if (isbdQueryResult !== null && isbdQueryResult !== undefined) {
// if first returns a valid object instance, then means that there is at least one instance of ISBD w/ informed value
alert(
'Error! There is already an ISBD instance with this value!',
);
return false;
} else {
// Create a new ISBD object instance to create a 1:1 relation on saving
let ISBD: Moralis.Object = new Moralis.Object('ISBD');
ISBD.set('name', bookISBDValue);
ISBD = await ISBD.save();
// set the new object to the new book object ISBD field
Book.set('isbd', ISBD);
}
// ONE-TO-MANY (1:+)
// add direct object to field (Moralis will convert to pointer on save)
Book.set('publisher', bookPublisherObject);
// or add pointer to field
if (bookGenreObject !== undefined) {
Book.set('genre', bookGenreObject.toPointer());
}
// MANY-TO-MANY (+:+)
// create a new relation so data can be added
let authorsRelation: Moralis.Relation = Book.relation('authors');
// bookAuthorsObjects is an array of Moralis.Objects
// you can add to relation by adding the whole array or object by object
authorsRelation.add(bookAuthorsObjects);
// After setting values, save it on the server
try {
await Book.save();
// Success
alert('Success!');
Router.push('/04-relationships');
return true;
} catch (error: any) {
// Error can be caused by lack of Internet connection
alert(`Error! ${error.message}`);
return false;
}
} catch (error: any) {
// Error can be caused by lack of Internet connection
alert(`Error! ${error}`);
return false;
}
};
const reserveBook = async function (bookId: string, available: boolean): Promise<boolean> {
// Create a new to-do parse object instance and set todo id
let Book: Moralis.Object = new Moralis.Object('Book');
Book.set('objectId', bookId);
// Set new done value and save Moralis Object changes
Book.set('available', available);
try {
await Book.save();
// Success
alert('Success! Book checked-out!');
// Refresh todos list
fetchBooks();
return true;
} catch (error: any) {
// Error can be caused by lack of Internet connection
alert('Error!' + error.message);
return false;
}
};
const handlePressCheckboxAuthor = (author: Moralis.Object) => {
if (bookAuthors.includes(author)) {
setBookAuthors(bookAuthors.filter((bookAuthor) => bookAuthor !== author));
} else {
setBookAuthors(bookAuthors.concat([author]));
}
}
return (
<div>
<div className={styles.header}>
<p className={styles.header_text_bold}>{'React on Moralis'}</p>
<p className={styles.header_text}>{'React Relations'}</p>
</div>
<div className={styles.container}>
<h2 className={styles.heading}>New Book</h2>
<Divider/>
<div
style={{
display: "inline-grid",
width: "100%"
}}
>
<Input
className={styles.form_input}
value={bookTitle}
onChange={(event) => setBookTitle(event.target.value)}
placeholder="Title"
size="large"
/>
<Input
className={styles.form_input}
value={bookYear}
onChange={(event) => setBookYear(event.target.value)}
placeholder="Publishing Year"
size="large"
/>
<Input
className={styles.form_input}
value={bookISBD}
onChange={(event) => setBookISBD(event.target.value)}
placeholder="ISBD"
size="large"
/>
<Input
className={styles.form_input}
value={bookPublisherInput}
onChange={(event) => setBookPublisherInput(event.target.value)}
placeholder="Publisher"
size="large"
/>
<Input
className={styles.form_input}
value={bookGenreInput}
onChange={(event) => setBookGenreInput(event.target.value)}
placeholder="Genres (separated by comma)"
size="large"
/>
<Input
className={styles.form_input}
value={bookAuthorInput}
onChange={(event) => setBookAuthorInput(event.target.value)}
placeholder="Authors (separated by comma)"
size="large"
/>
</div>
{publishers !== null && (
<>
<h3 className={styles.subheading}>Publisher</h3>
<Radio.Group
onChange={(event) => setBookPublisher(event.target.value)}
value={bookPublisher}
>
<div>
{publishers.map((publisher: Moralis.Object, index: number) => (
<Radio
key={`${index}`}
value={publisher}>
{publisher.get('name')}
</Radio>
))}
</div>
</Radio.Group>
</>
)}
{genres !== null && (
<>
<h3 className={styles.subheading}>Genres</h3>
<Radio.Group
onChange={(event) => setBookGenre(event.target.value)}
value={bookGenre}
>
<div>
{genres.map((genre: Moralis.Object, index: number) => (
<Radio
key={`${index}`}
value={genre}
>
{genre.get('name')}
</Radio>
))}
</div>
</Radio.Group>
</>
)}
{authors !== null && (
<>
<h3 className={styles.subheading}>Author(s)</h3>
<>
{authors.map((author: Moralis.Object, index: number) => (
<div
key={`${index}`}
className={styles.checkbox_item}
>
<span className={styles.checkbox_text}>{author.get('name')}</span>
<Checkbox
onChange={(_e) => handlePressCheckboxAuthor(author)}
checked={bookAuthors.includes(author)}>
</Checkbox>
</div>
))}
{/* {authors.length < 1 && (
<div
className={styles.checkbox_item}
>
<span className={styles.checkbox_text}>hello2</span>
<Checkbox
onChange={(event) => setBookAuthors(event.target.value)}
>
</Checkbox>
</div>
)} */}
</>
</>
)}
<div className={styles.form_buttons}>
<Button
onClick={() => createBook()}
type="primary"
className={styles.form_button}
icon={<PlusOutlined /> }
size="large"
style={{
background: "#208AEC",
outline: "none",
height: 30,
border: "none",
color: "white",
fontWeight: "10",
fontSize: "12pt"
}}>
CREATE BOOK
</Button>
</div>
</div>
<div className={styles.container}>
<div style={{display: "flex"}}>
<h2
className={styles.heading}
style={{
position: "relative",
float: "left"
}}
>
Results
</h2>
<div
style={{
top: "50%",
transform: "translateY(30%)",
marginLeft: "20px"
}}
>
<button onClick={fetchBooks}>Refresh</button>
{console.log(bookAuthors)}
</div>
</div>
<Divider/>
<div
style={{
display: "inline-grid",
width: "100%"
}}
>
{bookAuthors !== null &&
bookAuthors !== undefined &&
bookAuthors.length > 0 && (
<List
dataSource={bookAuthors}
renderItem={(item: Moralis.Object) => (
<List.Item className={styles.todo_item}>
<h2
className={
item.get('available') === false
? `${styles.todo_text_done}`
: `${styles.todo_text}`
}
>
{/* Fetch the Title */}
<b>Title: </b>{item.get('title')}
</h2>
<p
className={
item.get('available') === false
? `${styles.todo_text_done}`
: `${styles.todo_text}`
}
>
<b>ISBD #: </b>
{/* {item?.get('isbd')} */}
{console.log(item.attributes.isbd.id)}
{console.log(item.attributes.isbd)}
</p>
<p
className={
item.get('available') === false
? `${styles.todo_text_done}`
: `${styles.todo_text}`
}
>
{/* Fetch the Date */}
{/* {JSON.stringify(item?.updatedAt.getMonth())}
{'/'}
{JSON.stringify(item?.updatedAt.getDate())}
{'/'}
{JSON.stringify(item?.updatedAt.getFullYear())} */}
<b>Last updated: </b>{item?.updatedAt.toLocaleDateString()}
{/* {console.log(item?.updatedAt)} */}
</p>
<div className={styles.flex_row}>
{/* Todo update button */}
{item.get('available') !== true && (
<Button
type="primary"
shape="circle"
className={styles.todo_button}
onClick={() => reserveBook(item.id, true)}
icon={
<PullRequestOutlined className={styles.todo_button_icon_done} />
}
> Request from Library</Button>
)}
{item.get('available') !== false && (
<Button
type="primary"
shape="circle"
className={styles.todo_button}
onClick={() => reserveBook(item.id, false)}
icon={
<CheckOutlined className={styles.todo_button_icon_done} />
}
>Take Out Book</Button>
)}
{/* Todo delete button */}
</div>
</List.Item>
)}
/>
)}
</div>
</div>
</div>
);
};
export default BookRegistrationApp;