Architecture - COS-301/graduates GitHub Wiki
-
Project Overview
- 1.1 Project Architecture
- 1.1.1 Architecture Overview
- 1.1.2 Architecture Motivation
- 1.2 Project Layout
- 1.2.1 High Level Overview
- 1.2.2 Apps
- 1.2.3 Libs
- 1.2.4 Prisma
- 1.2.5 Tools
- 1.1 Project Architecture
- Design Architecture
- UI Architecture
-
Service Architecture
- 4.1 NestJS CQRS
- 4.1.1 Commands
- 4.1.2 Events
- 4.1.3 Queries
- 4.1.2 Putting it all together
- 4.2 File Structure
- 4.3 Note on bundling
- 4.1 NestJS CQRS
-
Data Architecture
- 5.1 SQL Format
- 5.2 Users of the system
- 5.3 User Permissions
- 5.3.1 ADMIN Permissions
- 5.3.2 STUDENT Permissions
- 5.3.3 COMPANY Permissions
- 5.4 User Profiles
-
API Architecture
- 6.1 Layers
- 6.1.1 API Layer
- 6.1.2 Service Layer
- 6.1.3 Repository layer
- 6.1 Layers
-
DevOps Architecture
- 7.1 Git-Actions
- 7.2 Subsystems
- Testing Architecture
The software architecture of a system details the structure of the system with regards to: the interfacing, interaction among its components and subsystems and helps developers to design a system. The architecture of a system provides a crucial representation of a system that allows everyone that is a part of the system to have a joint understanding of the system.
The project will follow a N-tier architecture pattern, it consists of Presentation Layer, API Layer, Service Layer and Data Layer. The client interacts with the service layer which will then generate requests which will then be pushed through the various layers which will then process the request and then respond back to the presentation layer which will then be viewed by the client.
The N-Tier architecture was chosen due to its simplistic concepts and segregation of functions. Furthermore due to the feature driven methodology each of the roles in the team could be allocated to a specific layer in the architecture pattern. For example service engineers could focus on service engineer, UI and design engineers could focus on the presentation layer and data engineers could focus on the data layer. Each role then defines an interface that is used by the layer below it, hiding unnecessary implementation details from the other layers. This allows for better cohesion of application functionality, and decouples unrelated functionality, by grouping functionality on a layer-by-layer basis.
There are four top level folders where most of the application development will take place.
We will be using the micro libs approach, meaning our library types would fall under feature/data-access/UI/util categories which will be further explained below, but if more information is need you can access it at: https://nx.dev/structure/library-types
-
Apps
- Should configure any dependencies and are responsible for linking libraries.
- This folder should not contain any components, services, controller or any other backend logic.
-
Libs
- Will contain components, services, controllers and other utilities.
-
Prisma
- Configurations relating to the database and contains the relevant database schemas.
-
Tools
- Contains generation and other utility scripts.
The Apps folder will be responsible for any dependency configuration and library linking needed by the application. It should not contain any code relating to any services or business logic.
The apps folder will have the following structure:
βββ apps
β βββ api
β βββ src
β βββ app
| βββ app.module.ts // API related features will be imported here
β βββ assets
β βββ environments
β βββ client
β βββ src
β βββ app
| βββ app.module.ts // Client related features will be imported here
β βββ assets
β βββ environments
β βββ client-e2e // end-to-end testing
The libs folder will contain the controllers, services and utilities used by the application.
The libs folder will have the following structure: Note for team, it is important we enforce this structure well, and also maybe communicate this to other teams to reduce confusion and rework if work is being done in the wrong directories. If we can enforce this from the start we will have a solid base structure.
βββ libs
β βββ api
β βββ shell
β βββ src
β βββ feature
β βββ lib
β βββ api-shell-feature.module.ts // Used to export feature libraries, which will be accessed by files in the apps folder
β βββ example-section // Section within the application ex. Registration which encapsulated both Login and Signup Features
β βββ api
β βββ feature-name
β βββ api
β βββ feature
β βββ src
β βββ lib
β βββ feature-name.spec.ts // Feature related testing
β βββ feature-name.controller.ts // Feature related API endpoints go here
β βββ feature-name.module.ts // Used to import controller and export library
β βββ shared // For libraries that might be reused by other developers (can go on any level that you feel makes sense)
β βββ repository // Repositories for database data
β βββ data-access
β βββ src
β βββ lib
β βββ data-access.spec.ts // Data access related testing
β βββ data-access.ts // Data access logic goes here
β βββ shared // For libraries that might be reused by other developers
β βββ services // Responsible for data access and retrieval, also responsible for business logic
β βββ feature
β βββ src
β βββ lib // Service Architecture File Structure Section
β βββ commands
β βββ queries
β βββ events
β βββ models
β βββ interfaces
β βββ name.service.ts // Responsible for executing the Commands and Queries
β βββ shared // For libraries that might be reused by other developers
β βββ client
β βββ section-within-application
β βββ feature-related-to-section // Indicate feature name
β βββ src
β βββ lib
β βββ feature.component.scss
β βββ feature.component.spec.ts
β βββ feature.component.ts
β βββ feature.component.html
β βββ feature.module.ts
β βββ feature-routing.module.ts
Shared libraries should contain who owns that code. It is the owners of the code's responsibility to maintain and make their service available under the shared libs for others to make use of it.
The Prisma folder will be responsible for the configurations and structure relating to the database. It contains the relevant database schemas.
The prisma folder will have the following structure:
βββ prisma
β βββ migrations
β βββ dateAndId_changemade
β βββ migration.sql //Contains code representing the the sql added
β βββ schema.prisma //Code detailing the entire system schema
β βββ seed.ts //Code for authorization seeding
β βββ sow.ts //Code for authorization sowing
The Tools folder will contain generation and other utility scripts.
The tools folder will have the following structure:
βββ tools
β βββ generators
β βββ scripts
β βββ merge-reports.js // Code for jest coverage of merge reports
For global styling that is constant throughout the solution, we will be using the styles.scss
file located in graduates/apps/client/src/
This will also be the place where the global color pallet of the solution will reside
A very important aspect of our client is that is has to be Stateful. To ensure this we will be using NGXS and itβs 4 main concepts. Below is a very brief example of how the 4 concepts of NGXS work, but for more examples and more difficult features of NGXS state management, the documentation has a lot of examples and go into further depth.
This is a global state, dispatcher for actions as well as a selector. In context with our solution, the store will be responsible for making the calls to the API. In order to dispatch actions, you need to include the store in your desired component/service and then do a dispatch to the function you need
/*
food.actions.ts
*/
export class AddFood {
static readonly type = '[Shop] Add Food;
constructor(public name: string) {}
}
import { Store } from '@ngxs/store';
import { AddFood } from './food.actions';
@Component({ ... })
export class ShopComponent {
constructor(private store: Store) {}
addFood(name: string) {
this.store.dispatch(new AddFood(name));
}
}
A usefull feature of Store is the Snapshop. Calling
store.snapshot()
will return all the current values of the store.
These should be seen as commands that trigger events or could also be a result of a previous event. Each action will contain a type
as a unique identifier
Example:
export class buyFood {
static readonly type = '[Shop] Buy Food';
}
If we want to include extra data in a action it happens as follows:
export class buyBread {
static readonly type = '[Shop] Buy Food';
constructor(public type: string, public breadCount: number) {}
}
In the above example type refers to what type of bread should be bought and breadCount would be the amount of bread that should be bought
This is a class definition of the state, along with decorators that are going to describe the metadata and actions.
@State<ZooStateModel>({
name: 'zoo',
defaults: {
feed: false
}
})
@Injectable()
export class ZooState {
constructor(private zooService: ZooService) {}
}
Our state will listen to actions via a decorator called @Action
. This decorator accepts an action class. Important concepts around State are calling actions, setting the state. Note NGXS supports Async actions which is very useful. Actions can also dispatch other actions.
These will be functions that 'slice' a part of the state from our global state. PLEASE follow the pattern of keeping READ and WRITE separate.
You can select a piece of data from Store using @Select
and passing in the a state class, function or selector.
import { Select } from '@ngxs/store';
import { ZooState, ZooStateModel } from './zoo.state';
@Component({ ... })
export class ShopComponent {
// Read name of the state from the state class
@Select(ShopState) foods$: Observable<string[]>;
// memoized milk selector to only return milk
@Select(ShopState.milk) milk$: Observable<string[]>;
// Function
@Select(state => state.shop.foods) foods$: Observable<string[]>;
// Read name of the state from the parameter
@Select() shop$: Observable<ShopStateModel>;
}
Store also a select
function which will come in handy when you cannot declare statically with @select
.
Other important features of Selects to keep in mind are Lazy selectors, joining of selectors.
The service layer for this project is where all the relevant business logic will live and will also be responsible for bridging the gap between the controllers(API) level and the database repositories. This layer will follow the CQRS pattern, also known as the Command and Query Responsibility Segregation pattern. The main goal of this pattern is to separate read and update operations on some data store, in the context of our application this will be our database. The read operations on the data store will be represented by queries and the update operations will be represented as commands. Queries are responsible for generating so called DTO entities, also known as Data Transfer Objects. To make these queries and commands more reactive these will then be used in the context of events.
An example of the CQRS pattern in a layered architecture is given by:
But as mentioned before this is not a very reactive architecture, so we introduce the notion of events to allow the system the ability to respond and create "events".
The NestJS framework provides a module to aid in the implementation of this pattern. This is module allows us to create commands and queries which can then be dispatched from the services layer or directly from the controllers if needed.
Each action to be performed by the system is captured in a command. A command will then in turn be consumed by a command handler which is responsible for executing the command. To create a command we need the following:
- Command - Capture the data that will be extracted when the command is dispatched. (has a 1-1 relationship with a command handler)
- Command Handler - This is where the logic will live when handling the specific command. (has a 1-1 relationship with a command)
- Controller/Service - Responsible for dispatching the command.
This will be illustrated at the hand of an example: Our command will be a create user command, which will cause a new item to be added to our users repository.
/*
users.service.ts
*/
@Injectable()
export class UsersService {
constructor(private commandBus: CommandBus) {} // Constructor takes the command bus(responsible for consuming commands)
async createUser(userDto: UserDto) {
return this.commandBus.execute(
new CreateUserCommand(userDto.name, userDto.email, userDto.password)
);
}
}
The above service will dispatch a createUserCommand when called. Let's take a look at how the command looks:
/*
create-user.command.ts
*/
export class CreateUserCommand {
constructor(
public readonly userName: string,
public readonly userEmail: string,
public readonly userPassword: string
) {}
}
The Command Bus was said to execute a command, the execution is delegated to a Command Handler. So now we need to write a command handler for our Create User Command which is where the actual data manipulation will happen.
/*
create-user.handler.ts
*/
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(private repository: UserRepository) {}
async execute(command: CreateUserCommand) : Promise<User> {
// Destruct data from command object
const { userName, userEmail, userPassword } = command;
// Create new user object from User model
const user = new User();
user.name = userName;
user.email = userEmail;
user.password = userPassword;
return this.repository.save(user);
}
}
The decorator at the top of the file(@CommandHandler) tells NestJS that this handler is available to execute any create user commands. We use the user repository to insert our new user.
Furthermore we might need to react to any changes caused by commands so this is where we introduce events.
Events allow us to make our application more reactive, by reacting to changes. Much like how commands are dispatched by the service layer or sometimes even the controllers layer, events are dispatched by either Models or the EventBus. To continue using our above example for commands, we already have the User model available to us. In order for a model to dispatch an event, it needs to inherit from the AggregateRoot class.
/*
user.model.ts || user.entity.ts
*/
export class User extends AggregateRoot {
constructor() {
super();
}
}
Now that our User model has been setup to work with events, it is now possible for our command to emit an event. To create an event we need the following:
- Event Class - Holds our data that we want to extract when handling the event (has a 1-1 relationship with an event handler)
- Event Handler Class - This is where the logic will live when handling the specific event. (has a 1-1 relationship with an event)
- Event Publisher || Model - Something that will dispatch the events, this is usually done in the model class, but can be manually executed by the EventBus class. (1-n relationship with events)
We want to emit a createUser event so we can react to that event and send the user an email welcoming them to our imaginary application. To allow for this we will make some modifications to our CreateUserHandler.
/*
create-user.handler.ts
*/
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(
private repository: UserRepository,
private publisher: EventPublisher
) {}
async execute(command: CreateUserCommand) : Promise<User> {
// Destruct data from command object
const { userName, userEmail, userPassword } = command;
// Link our User Model type with the EventPublisher to allow for it to respond to the user events
const UserModel = this.publisher.mergeObjectContext(
await this.repository.save(User);
);
// Create new user using the type reference returned by merging the EventPublisher and User type
const user = new UserModel();
user.name = userName;
user.email = userEmail;
user.password = userPassword;
// Link create user event with the object
user.apply(new CreateUserEvent(user.name, user.email));
// Commit the event, will publish all registered events
user.commit();
return user;
}
}
Now we need to create an actual Create User Event to capture our user data such as their name and email.
/*
create-user.event.ts
*/
export class CreateUserEvent {
constructor(
public readonly userName: string,
public readonly userEmail: string,
) {}
}
Now we need to add the logic to handle the event:
/*
create-user-event.handler.ts
*/
@EventsHandler(CreateUserEvent)
export class CreateUserEventHandler
implements IEventHandler<CreateUserEvent> {
handle(event: CreateUserEvent) {
const { userName, userEmail } = event;
sendWelcomeEmail(userName, userEmail);
}
}
This should make it clear of how to implement a reactive system using the NestJS event system.
Queries will be used when data needs to read from the data store. Queries follow a similar architecture to Commands and Events.
To create a query we need the following:
- Query - Capture the data that will be extracted when the query is dispatched. (has a 1-1 relationship with a command handler)
- Query Handler - This is where the logic will live when handling the specific query. (has a 1-1 relationship with a command)
- Controller/Service - Responsible for dispatching the query.
This will be illustrated using an example, where we will implement a query to get a user by their email from our data store.
/*
get-user.query.ts
*/
export class GetUserByEmailQuery {
constructor(
public readonly userEmail
) {}
}
/*
get-user-by-email.handler.ts
*/
@QueryHandler(GetUserByEmailQuery)
export class GetUserByEmailHandler implements IQueryHandler<GetUserByEmailQuery> {
constructor(private readonly repository: UserRepository) {}
async execute(query: GetUserByEmailQuery) {
const { userEmail } = query;
return this.repository.find(userEmail);
}
}
This now allows us to call this query in the service/controller level.
Now all of these different commands, queries and events need to be registered in a module file so NestJS can construct its dependency graph.
/*
registration.module.ts // Responsible for user login and signup
*/
// ...other imports
import { CommandHandlers } from './commands/handlers'; // For our commands
import { EventHandlers } from './events/handlers'; // For our events
import { QueryHandlers } from './queries/handlers'; // For our queries
@Module({
imports: [CqrsModule],
providers: [
UserRegistrationService,
...CommandHandlers,
...EventHandlers,
...QueryHandlers,
],
})
export class RegistrationModule {}
This is where we register our command, query and event handlers and bundle them in to a module for organisation.
These functions can now be called from the controller layer:
/*
registration.controller.ts
*/
// This is an example using a REST API not graphql as being used in the project at this moment in time
@Controller('registration')
export class RegistrationController {
constructor(
private userRegistrationService: UserRegistrationService
) {}
@Post('/signup')
async createNewUser(@Body() dto: NewUserDto) {
return this.userRegistrationService.createUser(dto);
}
@Get('/user/:email')
async findUserByEmail(@Param('email') email: string): Promise<User> {
return this.userRegistrationService.getUserByEmail(email);
}
}
This is the service layer which will fire the commands and queries to the handlers.
/*
user-registration.service.ts
*/
@Injectable()
export class UserRegistration {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: queryBus
) {}
async createUser(dto: NewUserDto) {
return this.commandBus.execute(
new CreateUserCommand(dto.userName, dto.userEmail, dto.userPassword)
);
}
async getUserByEmail(email: string) {
return this.queryBus.execute(
new GetUserByEmailQuery(email)
);
}
}
Each service layer file will have three directories for each of the CQRS pattern participants.
βββ lib
β βββ commands
β βββ handlers // Contains the command handlers
β βββ impl // Contains the command objects
β βββ queries
β βββ handlers // Contains the query handlers
β βββ impl // Contains the query objects
β βββ events
β βββ handlers // Contains the event handlers
β βββ impl // Contains the event objects
β βββ models // Used for events, will inherit from the Aggregate Root class
β βββ interfaces // Contains DTO entities if necessary
These services will then be called by the controller layer, the controller layer will directly interact with the commands and queries which will then be executed by the api query resolver function.
Just a quick note on how to bundle everything, it would be a good idea to place a module(*.module.ts) file in the service directory, in the same directory as the service file. Then update the index.ts file in the directory one level up from the current one to export this module.
βββ src
β βββ lib
β βββ commands
β βββ handlers // Contains the command handlers
β βββ impl // Contains the command objects
β βββ queries
β βββ handlers // Contains the query handlers
β βββ impl // Contains the query objects
β βββ events
β βββ handlers // Contains the event handlers
β βββ impl // Contains the event objects
β βββ models // Used for events, will inherit from the Aggregate Root class
β βββ interfaces // Contains DTO entities if necessary
β βββ index.ts // Change export to say export * from './lib/api-registration-service.module';
The format of the service module file can be seen in the Putting it all together. This module file should then be imported into a feature bundle alongside the controller and data-access files and should be added to the imports array in the module object.
The database for the Graduates Portal will serve as a resource where one can file and record the creation of data pertaining to the various users of the system. It will allow the entry, update, editing and reporting of all the data regarding the portal.
The structure of the database has been modelled after a 3 Tier Architecture, as it allows for development, maintenance, logic, data access, data storage, and user interface of the systemβs database to be worked on independently as separate modules. This modular approach is beneficial for the Mini-Project due to the nature in which different teams work on separate features within the database and then come together at the end of the project to integrate all the features together. This modular approach helps simplify the independent workflow and helps make the integration of the many projects with the database much less complicated.
The 3 Tier Architecture pattern in which the systemβs database portion is modelled after consists of three fundamental layers, namely, the client, server and database layers.
This Architectural Pattern ensures that the users of the system will not have to work with the database directly. This is due to the server layer that separates the client and the database. The server will accept requests from the client, which it will then pass on to the database, the database will then proceed to handle these requests and subsequently sends a response back to the server which then passes the response back to the client.
The main benefits and thus, reasons this Database Architecture Pattern was chosen was due to its ability to separate development of different features within the database. It also allowed for a more simplified integration of features through the database as well as an indirect communication between the Client and the Database, which is important for efficiency, simplicity and safety for all users of the system.
The basic file structure and the configuration relating to the database may be viewed under prisma.
Where the structure and functionality of the Database will be outlined below.
SQL will be used in the creation and updating of all tables of data. An example of what the format of most of the SQL files can be seen below:
--Creating a βUserβ Table
CREATE TABLE βUserβ (
βidβ SERIAL NOT NULL,
βemailβ SERIAL NOT NULL,
βnameβ TEXT,
CONSTRAINT βUser_pkeyβ PIMARY KEY (βidβ)
);
--Creating a Unique Index for the userβs email address
CREATE UNIQUE INDEX βUser_email_keyβ ON βUserβ(βemailβ);
Students will not be the only users of the graduateβs portal, therefore, provisions in the database need to be made for all users, these different users include:
ADMIN
STUDENT
COMPANY
All the permissions for the system should not be accessible by all users, hence why permissions are required for each user. The different permissions that each type of user has access to is outlined below:
- ADD_USER: to add a new user to the database.
- ADD_USER_COMPANY_USER: to be able to add users which are part of a company to the database.
- EDIT_PROFILE: to edit any users profile in the database.
- REMOVE_PROFILE: to remove any users profile from the database.
- VIEW_PROFILE: to view any userβs profile.
- POST_BLOG_ADMIN: to put a blog post to the blog page.
- EDIT_BLOG_ADMIN: to edit a blog post on the blog page.
- REMOVE_BLOG_ADMIN: to remove a blog post from the blog page.
- EDIT_STORY: to edit any userβs story.
- REMOVE_STORY: to remove any userβs story.
- VIEW_STORY: to view any userβs story.
- VIEW_STORY_COUNT: to see the view count of any userβs story.
- VIEWED_BY_STORY: to see anyone that has viewed a userβs story.
- ADD_PERMISSIONS: to give any permissions to a user.
- ADD_PERMISSIONS_EDIT: to give the permission to edit anything to a user.
- ADD_PERMISSIONS_VIEW: to give the permission to view anything to a user.
- ADD_PERMISSION_REMOVE: to give the permission to remove anything to a user.
- ADD_PERMISSION_$PERMISSION$: to give the permission - $PERMISSION$ - to a user.
- REMOVE_PERMISSIONS: to remove any permissions from a user.
- REMOVE_PERMISSIONS_EDIT: to remove the permission to edit anything from a user.
- REMOVE_PERMISSIONS_VIEW: to remove the permission to view anything from a user.
- REMOVE_PERMISSION_REMOVE: to remove the permission to remove anything from a user.
- REMOVE_PERMISSION_$PERMISSION$: to remove the permission - $PERMISSION$ - from a user.
- VIEW_PERMISSIONS: to view any permissions of a user.
- VIEW _PERMISSIONS_EDIT: to view the permission to edit anything of a user.
- VIEW _PERMISSIONS_VIEW: to view the permission to view anything of a user.
- VIEW _PERMISSION_REMOVE: to view the permission to remove anything of a user.
- VIEW _PERMISSION_$PERMISSION$: to view the permission - $PERMISSION$ - of a user.
- EDIT_PROFILE_USER: to edit a studentβs own profile.
- REMOVE_PROFILE_USER: to remove a studentβs own profile.
- VIEW_PROFILE_COMPANY: to view the profile of a company.
- POST_STORY: to post a story to a studentβs own profile.
- EDIT_STORY_USER: to edit a studentβs own story.
- REMOVE_STORY_USER: to remove a studentβs own story from their profile.
- VIEW_STORY_COUNT_USER: to see the story view count of the studentsβ own stories.
- VIEWED_BY_STORY_USER: to see who viewed a studentβs own story.
- VIEW_STORY_COMPANY: to view the stories of companies.
- EDIT_PROFILE_COMPANY: to edit a companyβs own profile.
- EDIT_PROFILE_COMPANY_USERS: to edit the usersβ profiles that fall under the company.
- REMOVE_PROFILE_COMPANY: to remove a companyβs own profile.
- REMOVE_PROFILE_COMPANY_USERS: to remove the usersβ profiles that fall under the company.
- VIEW_PROFILE_STUDENT: to view the profile of a student.
- VIEW_PROFILE_COMPANY_USERS: to view the profiles of users that fall under the company.
- POST_STORY_COMPANY: to post a story to a companyβs own profile.
- EDIT_STORY_COMPANY: to edit a companyβs own story.
- EDIT_STORY_COMPANY_USERS: to edit a userβs story that falls under the company.
- REMOVE_STORY_COMPANY: to remove a companyβs own story from their profile.
- REMOVE_STORY_COMPANY_USERS: to remove a userβs story that falls under the company.
- VIEW_STORY_COUNT_COMPANY: to see the story view count of the companyβs own stories.
- VIEW_STORY_COUNT_COMPANY_USERS: to see the story view count of the usersβ stories that fall under the company.
- VIEWED_BY_STORY_COMPANY: to see who viewed a companyβs own story.
- VIEWED_BY_STORY_COMPANY_USERS: to see who viewed a userβs story that falls under the company.
- VIEW_STORY_STUDENT: to view the stories of students.
- VIEW_STORY_COMPANY_USERS: to view the stories of users that fall under the company.
Since each user will have their own profile, it makes sense to create a database entry for each userβs profile information. The type of user and their respective profile information that will be depicted in the database representation is as shown below:
user (the table that holds the information of each registered user in the system)
- user_id (Primary Key, unique id used to differentiate each user)
- email
- password
- password_salt (for hashing)
- name
- date_of_birth
- company_id (Foreign Key, an id used to determine which company (if the user is part of a company) the user is a part of)
- date_created
- suspended (a flag to determine if the user's account has been suspended)
- validated
user_profile (the table used to hold information for the user's accounts' profile)
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's profile to use)
- studentNumber
- profile_picture
- bio (a short description of the user)
- employmentStatus
- openToOffers
user_tag
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's tag to use)
- tag (Primary Key, used to determine defining features of the user)
user_social_media
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's social media to use)
- type (Primary Key, the type of Social Media)
- link (to the user's specific social media)
user_location
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's location to use)
- location (Primary Key)
user_email
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's email to use)
- contact email (Primary Key)
user_degree
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user)
- degreeType
- degreeName
user_permission (a table used to determine which permissions a user has)
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's permissions to use)
- permission_type (Primary Key)
- permission_category (Primary Key)
- permission_tenant (Primary Key)
user_validation (the table used to validate a user's account)
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's validation to use)
- validated (a flag to determine if the user's account has been validated or not)
user_scouted (the table used to determine if a user has been scouted by a company)
- user_id_scout (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's scouting to use)
- scouted (a flag to determine if a user has been scouted or not)
user_role (the table to determine what roles a user has)
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's role to use)
- role (Primary Key, the role of the user)
role_permission (table for information about users roles)
- role (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's role to use)
- permission_type (Primary Key)
- permission_category (Primary Key)
- permission_tenant (Primary Key)
user_permissions (table for information about users roles)
- userid (Primary Key/Foreign Key, a unique id used to differentiate the user)
- permission_type (Primary Key)
- permission_category (Primary Key)
- permission_tenant (Primary Key)
UserProfileFile (table for information about users roles)
- userid (Primary Key/Foreign Key, a unique id used to differentiate the user)
- fileId
- filePath
- fileCategory
- fileExtension
student_profile (the table that holds the studentβs information)
- u_num (Primary Key of the userβs number in the database)
- UserID (Foreign Key of ID in the βUserβ table, used to differentiate each student)
- profile_picture (A headshot of the student)
- first_name
- last_name
- dob (The date of birth of the student)
- cellphone_number
- bio (A short paragraph that the user can use to describe themselves and/or any accomplishments they may like to highlight)
- notable_achievements (Foreign Key of the achievement in the βachievementsβ table -expanded below- to show the achievements of the student)
- story (Foreign Key of story_ID in the βstoryβ table -expanded below-, to link the student to any stories they may post)
short (the table that holds the information and metadata of the stories posted by users)
- story_ID (Primary Key, an ID used to differentiate each story)
- user_id (Foreign Key of User_ID in βstudent_profileβ)
- description
- link
- thumbnail
- date_posted
- archived (a flag used to determine if the story has been archived)
short_tag
- story_id (Primary Key, Foreign Key, used to differentiate each story and determine which user posted story to use)
- tag (Primary Key)
short report (a table used to determine if a story has been reported)
- story_id (Primary Key, Foreign Key, used to differentiate each story and determine which user posted story to use)
- user_id (Primary Key/Foreign Key, a unique id used to differentiate the user as well as determine which user's story to use)
- reason (the reason given for reporting the story)
notifications (the table that holds the information about notifications for users)
- notification_id (Primary Key, used to differentiate each unique notification)
- user_id_from (Foreign Key, used to show which user sent the notification)
- user_id_from (Foreign Key, used to show which user to send the notification to)
- data (the actual data of the notification)
- date (the date and time the notification was sent)
- seen (a flag to determine if the user has viewed the notification or not)
blog (the table used to hold information about blogs users have posted)
- id (a unique id used to differentiate blogs)
- user_id (Foreign Key used to determine which user the blog belongs to)
- title
- content
- date
- archived (a flag used to determine if the blog is archived)
blog_media (the table used to store the media of user posted blogs)
- blog_id (Primary Key/Foreign Key, an id used to differentiate each blog and to determine which user posted blog is being used)
- media (Primary Key, used to differentiate between the different media used in the blogs)
blog_comment (the table used to store information on the comments left on user blogs)
- comment_id (Primary Key, a unique id used to differentiate each comment)
- blog_id (Foreign Key, used to determine which blog the comment pertains to)
- user_id (Foreign Key used to determine which user left the comment)
- date_posted
The API architecture is devised into 3 main layers, which specify where their specific files and applications will be stored in the repository.These layers are namely:
This layer contains the resolvers, these resolvers are used to resolve the data that is required and is also used to present the required data by using GraphQL. These resolvers do this by calling the Service layer.
This is the layer where all the CQRS services are located. They are the starting point for all the dispatched CQRS queries, events and commands. The services can however not do this directly to the databases, they resolve this problem by calling the Repository Layer.
This layer is responsible for the connection between the Service layer and the databases. Therefore only the Repository layer elements will be able to communicate with the databases and acquire/send data to the various databases.
https://nx.dev/ci/monorepo-ci-github-actions
Image taken from CI/CD team documentation
- Unit Testing Phase
- Integration Testing Phase
- End to End Testing Phase
- Usability Testing Phase
- Libraries and modules are written and implemented, afterwards tests need to be written by the involved developer(s).
- Upon completed implementation and written tests, the tester will review the modules and libraries and determine whether the tests are adequate or if more tests are needed, also providing Quality Assurance.
- Once reviewed and the tests are proper and complete, the tester will log the results of each test using the protocol defined on the next page.
- No module or library may leave the current phase or be pushed until a tester has reviewed and logged the results for the test.
- Testers will coordinate to effectively test the systemβs features from start to end, ensuring the expected functionality is achieved.
- More details on steps to follow in next Sprints.
- Testers will coordinate to test the UI and feel of the system, ensuring it delivers aesthetical as well as functional quality and testing the systemβs usability.
- More details on steps to follow in next Sprints.
Commit Hash: 42eeff
Status: Passed/Failed
Date: yyyy/mm/dd
Tester(s): Bob Ross, Ben Stiller
Developer(s): Timbur Lee
Description: Commit Title if passed and Error explanation if failed (short)
1. Pass
- i. Log the test with the given format.
- ii. Move the segment to the next phase if applicable.
2. Fail
- i. Log the test with the given format.
- ii. Open a GitHub Issue with the same format and titled βFailed Testβ.
- iii. Notify the involved developer(s).
- iv. Await valid and passed test results then close the GitHub Issue (only allowed by testers).
- v. Follow βPassβ step.