Skip to content

Commit

Permalink
Feature: Clean architecture entities, controllers and services
Browse files Browse the repository at this point in the history
This commit implements the clean architecture concept.
The "features" folder has been introduced, containing
controllers, entities, and services for different modules.

Currently the event and user modules have been implemented with
basic fetch functions (get all and find by ID).

The ErrorHandler class is introduced for universal error handling.

- Added User, UserController and UserService
- Added Event, EventController and EventService
- Added ErrorHandler

(2024/12/14)
  • Loading branch information
yeahlowflicker committed Dec 14, 2024
1 parent 51c298f commit 07f9fbd
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 0 deletions.
113 changes: 113 additions & 0 deletions src/features/event/Controllers/EventController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import ErrorHandler from "../../../utils/ErrorHandler";
import { supabase } from "../../../utils/supabase";

import Event, { DBEvent } from '../Entities/Event';
import EventService from "../Services/EventService";


const EVENT_TABLE_NAME = "events"


export default class EventController {


/**
* Get an array of events
*
* @usage eventController.getEvents(<PARAMS>).then(
* (events: Array<Events>) => { ... }
* )
*
* @param {string} fields - The columns to retrieve (comma-separated)
* @param {string} orderBy - Which field to order by (leave blank if not needed)
* @param {boolean} orderDescending - Whether to order in descending order (defaults to false)
* @param {number} rangeStart - Starting index of fetch (defaults to 0)
* @param {number} rangeEnd - Ending index of fetch (defaults to 100)
*
* @returns {Array<Event>} - Array of events
*
* @see [https://supabase.com/docs/reference/javascript/order]
* @see [https://supabase.com/docs/reference/javascript/range]
*
* @author Henry C. (@yeahlowflicker)
*/
public async getEvents(
fields: string,
orderBy?: string,
orderDescending?: boolean,
rangeStart?: number,
rangeEnd?: number
) : Promise<Array<Event> | null> {

const query = supabase
.from(EVENT_TABLE_NAME)
.select(fields)
.returns<Array<DBEvent>>()

if (orderBy)
query.order(orderBy, { ascending: !orderDescending })

if (rangeStart && rangeEnd)
query.range(rangeStart, rangeEnd)

const { data, error } = await query

// Error handling
if (error) {
ErrorHandler.handleSupabaseError(error)
return null
}

// Initialize result array
const events : Array<Event> = []


// For each found DBEvent, convert to Event and append to result array
data.forEach((record: DBEvent) => {
events.push(
EventService.parseEvent(record)
)
})

return events
}



/**
* Find a single event by ID
*
* @usage eventController.FindEventByID(<PARAMS>).then(
* (event: Event) => { ... }
* )
*
* @param {string} eventID - Target event ID
* @param {string} fields - The columns to retrieve
*
* @returns {Event} - The target event entity (null if not found)
*
* @author Henry C. (@yeahlowflicker)
*/
public async findEventByID(eventID: string, fields?: string) : Promise<Event | null> {

const { data, error } = await supabase
.from(EVENT_TABLE_NAME)
.select(fields)
.eq("id", eventID)
.returns<DBEvent>()
.limit(1)
.single()

// Error handling
if (error) {
ErrorHandler.handleSupabaseError(error)
return null
}

// Type conversion: DBEvent -> Event
const event : Event = EventService.parseEvent(data)

return event
}

}
22 changes: 22 additions & 0 deletions src/features/event/Entities/Event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Database } from "../../../utils/database.types";

/**
* This is a dummy-type inherited from the generated Supabase type
*/
export type DBEvent = Database['public']['Tables']['events']['Row'];


export default class Event {

public id: number = 0;
public name: string = "";
public type: number = 0;
public description: string = "";
public startTime: string = "";
public endTime: string = "";
public location: string = "";
public fee: number = 0;
public userID: string = "";
public createdAt: string = "";

}
22 changes: 22 additions & 0 deletions src/features/event/Services/EventService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Event, { DBEvent } from "../Entities/Event"

export default class EventService {

public static parseEvent(record: DBEvent) : Event {
const event = new Event()

event.id = record.id
event.name = record.name ? record.name : ""
event.type = record.type ? record.type : 0
event.description = record.description ? record.description : ""
event.startTime = record.start_time ? record.start_time : ""
event.endTime = record.end_time ? record.end_time : ""
event.location = record.location ? record.location : ""
event.fee = record.fee ? record.fee : 0
event.userID = record.user_id
event.createdAt = record.created_at

return event
}

}
113 changes: 113 additions & 0 deletions src/features/user/Controllers/UserController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import ErrorHandler from "../../../utils/ErrorHandler";
import { supabase } from "../../../utils/supabase";

import User, { DBUser } from '../Entities/User';
import UserService from '../Services/UserService';


const USER_TABLE_NAME = "members"


export default class UserController {

/**
* Get an array of users
*
* @usage userController.getUsers(<PARAMS>).then(
* (users: Array<Users>) => { ... }
* )
*
* @param {string} fields - The columns to retrieve (comma-separated)
* @param {string} orderBy - Which field to order by (leave blank if not needed)
* @param {boolean} orderDescending - Whether to order in descending order (defaults to false)
* @param {number} rangeStart - Starting index of fetch (defaults to 0)
* @param {number} rangeEnd - Ending index of fetch (defaults to 100)
*
* @returns {Array<User>} - Array of users
*
* @see [https://supabase.com/docs/reference/javascript/order]
* @see [https://supabase.com/docs/reference/javascript/range]
*
* @author Henry C. (@yeahlowflicker)
*/
public async getUsers(
fields: string,
orderBy?: string,
orderDescending?: boolean,
rangeStart?: number,
rangeEnd?: number
) : Promise<Array<User> | null> {

const query = supabase
.from(USER_TABLE_NAME)
.select(fields)
.returns<Array<DBUser>>()

if (orderBy)
query.order(orderBy, { ascending: !orderDescending })

if (rangeStart && rangeEnd)
query.range(rangeStart, rangeEnd)

const { data, error } = await query

// Error handling
if (error) {
ErrorHandler.handleSupabaseError(error)
return null
}

// Initialize result array
const users : Array<User> = []

// For each found DBUser, convert to User and append to result array
data.forEach((record: DBUser) => {
users.push(
UserService.parseUser(record)
)
})

return users
}



/**
* Find a single user by ID
*
* @usage userController.FindUserByID(<PARAMS>).then(
* (user: User) => { ... }
* )
*
* @param {string} userID - Target user ID
* @param {string} fields - The columns to retrieve
*
* @returns {User} - The target user entity (null if not found)
*
* @author Henry C. (@yeahlowflicker)
*/
public async findUserByID(userID: string, fields?: string) : Promise<User | null> {

const { data, error } = await supabase
.from(USER_TABLE_NAME)
.select(fields)
.eq("uuid", userID)
.returns<DBUser>()
.limit(1)
.single()


// Error handling
if (error) {
ErrorHandler.handleSupabaseError(error)
return null
}

// Type conversion: DBUser -> User
const user : User = UserService.parseUser(data)

return user
}


}
33 changes: 33 additions & 0 deletions src/features/user/Entities/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Database } from "../../../utils/database.types";

/**
* This is a dummy-type inherited from the generated Supabase type
*/
export type DBUser = Database['public']['Tables']['members']['Row'];


/**
* The User entity model
*
* @author Henry C. (@yeahlowflicker)
*/
export default class User {
public id: string = ""
public username: string = ""
public email: string = ""
public phone: string = ""
public avatar: string = ""
public profileBackground: string = ""
public joinedAt: Date = new Date()
public identity: number = 0
public department: string = ""
public grade: string = ""
public bio: string = ""


public convertIdentity(): string {
switch (this.identity) {
default: return "用戶"
}
}
}
24 changes: 24 additions & 0 deletions src/features/user/Services/UserService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import User, { DBUser } from "../Entities/User";

export default class UserService {

/**
* Convert a user from default Supabase type to User entity
*
* @param {DBUser} record - The record retrieved from Supabase
* @returns {User} - Converted user entity
*
* @author Henry C. (@yeahlowflicker)
*/
public static parseUser(record: DBUser) : User {
const user = new User()
user.id = record.uuid
user.username = record.name
user.email = record.fk_email
user.identity = record.fk_identity
user.avatar = record.avatar

return user
}

}
17 changes: 17 additions & 0 deletions src/utils/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PostgrestError } from "@supabase/supabase-js";

/**
* A universal error handler class.
*
* This will be called by all controllers and is useful for general
* error-handling logic.
*
* @author Henry C. (@yeahlowflicker)
*/
export default class ErrorHandler {

public static handleSupabaseError(error: PostgrestError) {
console.error(error)
}

}

0 comments on commit 07f9fbd

Please sign in to comment.