Frost Web Annotations Example
- Clone the repo
- use
npm startto launch the website
danger
Please Don't forget to fill the firebaseConfig object in src/database/frost.ts
The Data Structure
Entities
Students
- The node path is defined inside the FrostEntity decorator
- The node has 4 properties
- name
- year
- birthday
- A DateSerializer is used on the
birthdayproperty - The property
excludeExampleis decorated with Exclude, so it will not be serialized and inserted in the database.
src/database/student.ts
@FrostEntity({ collectionPath: "/testing/students" })
export class Student extends FrostObject<Student> {
name?: string;
year?: "FRESHMAN" | "SOPHOMORE" | "JUNIOR" | "SENIOR";
email?: string;
@Exclude()
excludeExample?: string;
@DateSerializer<Student>()
birthday?: Date;
...
...
.
.
}
@FrostNode({
entity: Student,
})
export class StudentApi extends FrostApi<Student> {}
Professors
- The node path is defined inside the FrostEntity decorator
- The node has 4 properties
- name
- department
src/database/professor.ts
@FrostEntity({collectionPath:'/testing/professor'})
export class Professor extends FrostObject<Professor>{
name?:string;
department?:string;
email?:string;
...
}
@FrostNode({
entity: Professor,
})
export class ProfessorApi extends FrostApi<Professor> {}
Courses
- The node path is defined inside the FrostEntity decorator
- The node has 4 properties
- name
- department
- duration: (enum)
- difficultyLevel: (enum)
src/database/course.ts
export enum Duration {
FULL_YEAR = 24,
FULL_SEMESTER = 12,
HALF_SEMESTER = 6,
}
@FrostEntity({collectionPath:'/testing/courses'})
export class Course extends FrostObject<Course> {
name?:string;
difficultyLevel?: "INTRODUCTORY" | "INTERMEDIATE" | "UPPER_INTERMEDIATE" | "ADVANCED_PLACEMENT";
duration?: Duration; // in weeks
department?: string;
...
}
@FrostNode({
entity: Course,
})
export class CourseApi extends FrostApi<Course> {}
Clubs
- The node path is defined inside the FrostEntity decorator
- The node has 4 properties
- name
- roomId
- type: (enum)
src/database/club.ts
@FrostEntity({collectionPath:'/testing/clubs'})
export class Club extends FrostObject<Club> {
name?: string;
type?: "STEM" | "SPORTS" | "CREATIVE";
roomId?: string;
...
}
@FrostNode({
entity: Club,
})
export class ClubApi extends FrostApi<Club> {}
Relations
One to One
Club Supervisor (Professor <--> Club)
- Each Club has one supervisor and each professor supervises only one club.
- Relation Name:
CLUB_SUPERVISORconstant. should be passed to the decorator on both sides. - Relation Type:
RelationTypes.ONE_TO_ONE. should be passed to the decorator at least on one side. Professor:clubproperty is decorated with the Relation decorator.type: ()=> CLubis passed to the decorator and it's the same as the type of the property since it's "One to One"
Club:supervisorproperty is decorated with the Relation decorator.type: ()=> Professoris passed to the decorator and it's the same as the type of the property since it's "One to One"
src/database/professor.ts
import {CLUB_SUPERVISOR} from "./const.ts"
@FrostEntity({collectionPath:'/testing/professor'})
export class Professor extends FrostObject<Professor>{
...
@Relation({ name: CLUB_SUPERVISOR, type: () => Club,relation:RelationTypes.ONE_TO_ONE })
club?: () => Club
...
}
src/database/club.ts
import {CLUB_SUPERVISOR} from "./const.ts"
@FrostEntity({collectionPath:'/testing/clubs'})
export class Club extends FrostObject<Club> {
...
@Relation({ name: CLUB_SUPERVISOR, type: () => Professor,relation:RelationTypes.ONE_TO_ONE })
supervisor?: () => Professor
...
}
One to Many
Club Members (Club <--> Student)
- Each Club has multiple students (members) and each student is allowed to participate in one club only
- Relation Name:
CLUB_STUDENTSconstant. should be passed to the decorator on both sides. - Relation Type:
RelationTypes.ONE_TO_MANY. should be passed to the decorator at least on one side. Club: (Main)studentsproperty is decorated with the Relation decorator.type: ()=> Studentis passed to the decorator while the type of the property is()=> Student[]since theClubis the main (one) side and theStudentare the many side.- Also, Since the Club is the Main side; then
master: trueshould be passed to the relation decorator.
Student: (Secondary)clubproperty is decorated with the Relation decorator.type: ()=> Clubis passed to the decorator and the type of the property is also()=> Clubsince theStudentis the secondary (many) side and theClubis main (one) side.
info
Tip: master: true should be in the Relation decorator that decorates the property with the Array type
src/database/club.ts
import {CLUB_STUDENTS} from './consts.ts'
@FrostEntity({collectionPath:'/testing/clubs'})
export class Club extends FrostObject<Club> {
...
@Relation({ name: CLUB_STUDENTS, type: () => Student,relation: RelationTypes.ONE_TO_MANY, master: true })
students?: () => Student[]
...
}
src/database/student.ts
import {CLUB_STUDENTS} from './consts.ts'
export class Student extends FrostObject<Student> {
...
@Relation({ name: CLUB_STUDENTS, type: () => Club, relation: RelationTypes.ONE_TO_MANY })
club?: () => Club;
..
}
Professors' Courses (Professor <--> Course)
- Each Course is taught by one professor but one professor teaches multiple courses.
- Relation Name:
CLUB_STUDENTSconstant. should be passed to the decorator on both sides. - Relation Type:
RelationTypes.ONE_TO_MANY. should be passed to the decorator at least on one side. Professor:(Main)coursesproperty is decorated with the Relation decorator.type: ()=> Courseis passed to the decorator while the type of the property is()=> Course[]since theProfessoris the main (one) side and theCourseis the many side.- Also, Since the Club is the Main side; then
master: trueshould be passed to the relation decorator.
Course:(Secondary)professorproperty is decorated with the Relation decorator.type: ()=> Professoris passed to the decorator and the type of the property is also()=> Professorsince theCourseis the secondary (many) side and theProfessoris main (one) side.
info
Tip: master: true should be in the Relation decorator that decorates the property with the Array type
src/database/professor.ts
import {PROFESSOR_COURSES} from './consts.ts'
@FrostEntity({collectionPath:'/testing/professor'})
export class Professor extends FrostObject<Professor>{
...
@Relation({ name: PROFESSOR_COURSES , type: () => Course, relation:RelationTypes.ONE_TO_MANY, master:true})
courses?: () => Course[]
...
}
src/database/course.ts
import {PROFESSOR_COURSES} from './consts.ts'
export class Course extends FrostObject<Course> {
...
@Relation(
{
name: PROFESSOR_COURSES,
type: () => Professor,
relation: RelationTypes.ONE_TO_MANY
}
)
professor?:() => Professor
...
}
Many to Many
Students' Courses (Student <--> Course)
- Each Course is audited by multiple students and each student audits multiple courses
- Relation Name:
STUDENT_COURSESconstant. should be passed to the decorator on both sides. - Relation Type:
RelationTypes.MANY_TO_MANY. should be passed to the decorator at least on one side. Student:coursesproperty is decorated with the Relation decorator.type: ()=> Courseis passed to the decorator while the type of the property is()=> Course[]since it's "Many to Many"
Course:studentsproperty is decorated with the Relation decorator.type: ()=> Studentis passed to the decorator while the type of the property is()=> Student[]since it's "Many to Many"
src/database/student.ts
import {STUDENT_COURSES} from './consts.ts'
export class Student extends FrostObject<Student> {
...
@Relation({
name: STUDENT_COURSES,
relation: RelationTypes.MANY_TO_MANY,
type: () => Course,
})
courses?: () => Course[];
...
}
src/database/course.ts
import {STUDENT_COURSES} from './consts.ts'
export class Course extends FrostObject<Course> {
...
@Relation({ name: STUDENT_COURSES, type: () => Student })
students?: () => Student[]
...
}
Brief Explanation on flow
Initialization
- The firebase configuration object is passed as the first argument to the Frost.initialize function.
- The APIs are passed in a map as the second argument. If the keys for the APIs are specified then you can access the APIs by the name you desire
- The Function Returns a FrostApp Instance containing the APIs instances and the
firebaseAppinstance. - On
Line 8: the firebase DB instance is retrieved by passing thefirebaseAppinstance to the getDatabase() function. this instance will be used in the update() function in Mock Data
src/database/frost.ts
export const FrostApp = Frost.initialize(firebaseConfig, {
CourseApi,
ClubApi,
ProfessorApi,
StudentApi,
})
export const fireDB = getDatabase(FrostApp.firebaseApp)
Rendering
We have 3 lists:
- Courses List
- Clubs List
- Students List
Each list will contain cards displaying the data for each item.
The students list is empty by default. Each card in the Clubs and Courses List will have
Students ListButton. When this button is clicked; then the students for said course or club will be displayed in the list.The Rendering is managed by native DOM Manipulation and Custom Web Elements (Cards and Buttons from Paper UI).
We Have an observer for each list
- Clubs and Courses: Their observers are from the observeMany() function in their respective FrostApi.
- Students: Their Observer is manually created form a RX Subject. and the Emitting of the data to this observer is managed through the other observers and the
onclickevents.
src/index.ts
...
/*
* When Clicked set the data in the selected variable
* and Emit the new students list
*/
function handleStudentsListClick(data: Club | Course){
selected = { type: data instanceof Club ? "club":"course", id: data.id };
studentsSubject.next(data.students?.() ?? []);
}
/*
* Courses Observer
* (No Constraints Passed , So it listens to all Courses)
* included with each course is the connected students and professor
*/
FrostApp.CourseApi.observeMany({
include: ["professor", "students"],
}).subscribe((data) => {
/*
* When the data changes the coursesList div is modified
*/
coursesList.replaceChildren(...data.map((course)=>courseCard(course,handleStudentsListClick)));
/*
* if the selected course changes then emit the new students list
* if empty then emit an empty student list
*/
if (selected && selected.type === "course" ) {
if(!data.length) studentsSubject.next([])
else studentsSubject.next(data.find((value)=>value.id === selected?.id)?.students?.() ?? []);
}
});
/*
* Clubs Observer
* (No Constraints Passed , So it listens to all Clubs)
* included with each club is the connected students and supervisor
*/
FrostApp.ClubApi.observeMany({ include: ["supervisor", "students"] }).subscribe(
(data) => {
/*
* When the data changes the clubsList div is modified
*/
clubsList.replaceChildren(...data.map((club)=>clubCard(club,handleStudentsListClick)));
/*
* if the selected club changes then emit the new students list
* if empty then emit an empty student list
*/
if (selected && selected.type === "club" ) {
if(!data.length) studentsSubject.next([])
else studentsSubject.next(data.find((value)=>value.id === selected?.id)?.students?.() ?? []);
}
},
);
studentsSubject.subscribe((data) => {
/*
* When the data changes the studentsList div is modified
*/
studentsList.replaceChildren(...data.map(studentCard));
});
...
Mock Data
- Some Initial data is generated and Added to the database. (
src/database/mock-data.tscontains the frost operations to add the data) - Extra Students can be added. (mock data generated using Faker.Js)
src/index.ts
/*
* Sets The initial Mock Data (Clubs,Courses,Professors, Some Students)
*/
mockBtn.onclick = () => {
setData();
};
/*
* Adds Extra Mock Students Data
*/
mockStudentsBtn.onclick = () => {
addStudents();
};