From c2c0e7689ac2bee7ebee414d7c3c2c79b876ad05 Mon Sep 17 00:00:00 2001 From: juliecoust Date: Mon, 12 Feb 2024 15:35:19 +0100 Subject: [PATCH] DEV : Get users pagination --- .../sqlite/sqlite-user-data-source.ts | 88 +++++++++++++++---- .../data-sources/database-wrapper.ts | 2 +- .../data-sources/user-data-source.ts | 3 +- src/domain/entities/search.ts | 31 +++++++ .../repositories/user-repository.ts | 6 +- .../use-cases/user/get-all-users.ts | 5 +- src/domain/repositories/user-repository.ts | 41 ++++++++- src/domain/use-cases/user/get-all-users.ts | 58 ++++++++++-- src/presentation/routers/user-router.ts | 2 +- 9 files changed, 205 insertions(+), 31 deletions(-) create mode 100644 src/domain/entities/search.ts diff --git a/src/data/data-sources/sqlite/sqlite-user-data-source.ts b/src/data/data-sources/sqlite/sqlite-user-data-source.ts index bfdd1a1..0389b09 100644 --- a/src/data/data-sources/sqlite/sqlite-user-data-source.ts +++ b/src/data/data-sources/sqlite/sqlite-user-data-source.ts @@ -2,6 +2,7 @@ import { UserRequesCreationtModel, UserRequestModel, UserResponseModel, UserUpda import { AuthUserCredentialsModel, } from "../../../domain/entities/auth"; import { UserDataSource } from "../../interfaces/data-sources/user-data-source"; import { SQLiteDatabaseWrapper } from "../../interfaces/data-sources/database-wrapper"; +import { PreparedSearchOptions, SearchResult } from "../../../domain/entities/search"; // const DB_TABLE = "user" export class SQLiteUserDataSource implements UserDataSource { @@ -40,26 +41,81 @@ export class SQLiteUserDataSource implements UserDataSource { }); }) } - async getAll(): Promise { - const sql = "SELECT * from user" + // async getAll(): Promise { + // let sql = `SELECT * FROM user;` + // return await new Promise((resolve, reject) => { + // this.db.all(sql, (err, rows) => { + // if (err) { + // reject(err); + // } else { + // const result = rows.map(row => ({ + // user_id: row.user_id, + // first_name: row.first_name, + // last_name: row.last_name, + // email: row.email, + // valid_email: row.valid_email == 1 ? true : false, + // is_admin: row.is_admin == 1 ? true : false, + // organisation: row.organisation, + // country: row.country, + // user_planned_usage: row.user_planned_usage, + // user_creation_date: row.user_creation_date, + // deleted: row.deleted + // })); + // resolve(result); + // } + // }); + // }) + // } + async getAll(options: PreparedSearchOptions): Promise { + // Get the limited rows and the total count of rows // WHERE your_condition + let sql = `SELECT *, (SELECT COUNT(*) FROM user) AS total_count FROM user` + const params: any[] = [] + + // // Add filtering + // if (options.filter) { + // const params = options.filter.map(() => '(?)').join(','); + // const placeholders = params.map(() => '(?)').join(','); + // sql += ` WHERE LIKE ?`; // Replace 'someColumn' with the actual column + // } + + // // Add sorting + // if (options.sort) { + // sql += ` ORDER BY ${options.sort}`; // Be cautious of SQL injection + // } + + // Add pagination + const page = options.page; + const limit = options.limit; + const offset = (page - 1) * limit; + sql += ` LIMIT (?) OFFSET (?)`; + params.push(limit, offset); + + // Add final ; + sql += `;` + return await new Promise((resolve, reject) => { - this.db.all(sql, (err, rows) => { + this.db.all(sql, params, (err, rows) => { if (err) { reject(err); } else { - const result = rows.map(row => ({ - user_id: row.user_id, - first_name: row.first_name, - last_name: row.last_name, - email: row.email, - valid_email: row.valid_email == 1 ? true : false, - is_admin: row.is_admin == 1 ? true : false, - organisation: row.organisation, - country: row.country, - user_planned_usage: row.user_planned_usage, - user_creation_date: row.user_creation_date, - deleted: row.deleted - })); + if (rows === undefined) resolve({ users: [], total: 0 }); + console.log("rows", rows) + const result: SearchResult = { + users: rows.map(row => ({ + user_id: row.user_id, + first_name: row.first_name, + last_name: row.last_name, + email: row.email, + valid_email: row.valid_email == 1 ? true : false, + is_admin: row.is_admin == 1 ? true : false, + organisation: row.organisation, + country: row.country, + user_planned_usage: row.user_planned_usage, + user_creation_date: row.user_creation_date, + deleted: row.deleted + })), + total: rows[0].total_count + }; resolve(result); } }); diff --git a/src/data/interfaces/data-sources/database-wrapper.ts b/src/data/interfaces/data-sources/database-wrapper.ts index 382bf9d..16206b5 100644 --- a/src/data/interfaces/data-sources/database-wrapper.ts +++ b/src/data/interfaces/data-sources/database-wrapper.ts @@ -8,7 +8,7 @@ export interface SQLiteDatabaseWrapper { //get(sql: string, params: any, callback?: (this: Statement, err: Error | null, row: T) => void): this; //get(sql: string, ...params: any[]): this; - all(sql: string, callback?: (this: any, err: Error | null, rows: any[]) => void): void; + all(sql: string, params: any, callback?: (this: any, err: Error | null, rows: any[]) => void): void; //all(sql: string, params: any, callback?: (this: Statement, err: Error | null, rows: T[]) => void): this; //all(sql: string, ...params: any[]): this; diff --git a/src/data/interfaces/data-sources/user-data-source.ts b/src/data/interfaces/data-sources/user-data-source.ts index 2483eaa..04ead70 100644 --- a/src/data/interfaces/data-sources/user-data-source.ts +++ b/src/data/interfaces/data-sources/user-data-source.ts @@ -1,9 +1,10 @@ import { UserRequesCreationtModel, UserRequestModel, UserUpdateModel, UserResponseModel } from "../../../domain/entities/user"; import { AuthUserCredentialsModel } from "../../../domain/entities/auth"; +import { PreparedSearchOptions, SearchResult } from "../../../domain/entities/search"; export interface UserDataSource { create(user: UserRequesCreationtModel): Promise; - getAll(): Promise; + getAll(options: PreparedSearchOptions): Promise; updateOne(user: UserUpdateModel): Promise; getOne(user: UserRequestModel): Promise; getUserLogin(email: string): Promise; diff --git a/src/domain/entities/search.ts b/src/domain/entities/search.ts new file mode 100644 index 0000000..f428aaa --- /dev/null +++ b/src/domain/entities/search.ts @@ -0,0 +1,31 @@ +//import { UserRequestModel } from "./user"; + +import { UserResponseModel } from "./user"; + +export interface SearchOptions { + // filter?: string; // Add filtering support + // sort?: string; // Add sorting support + page?: number; // Add pagination support, Default to page 1 if not specified + limit?: number; // Set limit for pagination, Default to 10 items per page if not specified +} +export interface PreparedSearchOptions { + // filter?: any; + // sort?: any; // sorting support + page: number; // pagination support, Default to page 1 if not specified + limit: number; // Set limit for pagination, Default to 10 items per page if not specified +} +// export interface User_PreparedSearchOptions extends PreparedSearchOptions { +// filter: UserRequestModel; // filtering support +// } +export interface SearchInfo { + total: number; // total number of items + limit: number; // items per page + total_on_page: number; // total number of items on current page + page: number; // current page + pages: number; // total number of pages +} + +export interface SearchResult { + users: UserResponseModel[], + total: number +} \ No newline at end of file diff --git a/src/domain/interfaces/repositories/user-repository.ts b/src/domain/interfaces/repositories/user-repository.ts index b1a4375..031f0b7 100644 --- a/src/domain/interfaces/repositories/user-repository.ts +++ b/src/domain/interfaces/repositories/user-repository.ts @@ -1,14 +1,18 @@ import { AuthUserCredentialsModel, DecodedToken, ChangeCredentialsModel } from "../../entities/auth"; +import { PreparedSearchOptions, SearchResult } from "../../entities/search"; import { UserRequesCreationtModel, UserResponseModel, UserRequestModel, UserUpdateModel, PublicUserModel, PrivateUserModel } from "../../entities/user"; export interface UserRepository { + changePassword(user_to_update: ChangeCredentialsModel): Promise; getUser(user: UserRequestModel): Promise; adminUpdateUser(user: UserUpdateModel): Promise; standardUpdateUser(user: UserUpdateModel): Promise; verifyUserLogin(user: AuthUserCredentialsModel): Promise; createUser(user: UserRequesCreationtModel): Promise; - getUsers(): Promise; + getUsers(options: PreparedSearchOptions): Promise; + adminGetUsers(options: PreparedSearchOptions): Promise; + standardGetUsers(options: PreparedSearchOptions): Promise; isAdmin(user_id: number): Promise; validUser(user: UserRequestModel): Promise; generateValidationToken(user: UserRequestModel): string; diff --git a/src/domain/interfaces/use-cases/user/get-all-users.ts b/src/domain/interfaces/use-cases/user/get-all-users.ts index e4a3218..652f90e 100644 --- a/src/domain/interfaces/use-cases/user/get-all-users.ts +++ b/src/domain/interfaces/use-cases/user/get-all-users.ts @@ -1,4 +1,5 @@ -import { UserResponseModel } from "../../../entities/user"; +import { SearchInfo, SearchOptions } from "../../../entities/search"; +import { UserResponseModel, UserUpdateModel } from "../../../entities/user"; export interface GetAllUsersUseCase { - execute(): Promise; + execute(current_user: UserUpdateModel, options: SearchOptions): Promise<{ users: UserResponseModel[], search_info: SearchInfo }>; } \ No newline at end of file diff --git a/src/domain/repositories/user-repository.ts b/src/domain/repositories/user-repository.ts index 582d799..18cb7b0 100644 --- a/src/domain/repositories/user-repository.ts +++ b/src/domain/repositories/user-repository.ts @@ -5,6 +5,7 @@ import { AuthUserCredentialsModel, ChangeCredentialsModel, DecodedToken } from " import { UserResponseModel, UserRequesCreationtModel, UserRequestModel, UserUpdateModel, PublicUserModel, PrivateUserModel } from "../entities/user"; import { UserRepository } from "../interfaces/repositories/user-repository"; import { JwtWrapper } from "../../infra/auth/jwt-wrapper"; +import { PreparedSearchOptions, SearchOptions, SearchResult } from "../entities/search"; export class UserRepositoryImpl implements UserRepository { userDataSource: UserDataSource @@ -84,8 +85,44 @@ export class UserRepositoryImpl implements UserRepository { return nb_of_updated_user } - async getUsers(): Promise { - const result = await this.userDataSource.getAll() + adminGetUsers(options: PreparedSearchOptions): Promise { + //can be filtered by ["user_id", "first_name", "last_name", "email", "valid_email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "deleted"] + //const prepared_options = this.prepare_options(options) + + return this.userDataSource.getAll(options) + } + + standardGetUsers(options: PreparedSearchOptions): Promise { + //can be filtered by ["user_id", "first_name", "last_name", "email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date"] + //const prepared_options = this.prepare_options(options) + // if option + // prepared_options.filter.deleted = undefined; + // prepared_options.filter.valid_email = true; + return this.userDataSource.getAll(options) + } + + // prepare_options(options: SearchOptions): PreparedSearchOptions { + // const preparedOptions: PreparedSearchOptions = { + // // filter: options.filter || {}, // Use the provided filter or an empty object if not specified + // // sort: options.sort || [], // Use the provided sort or an empty array if not specified + // page: options.page, + // limit: options.limit + // } + // return preparedOptions; + // } + + async getUsers(options: SearchOptions): Promise { + //can be filtered by ["user_id", "first_name", "last_name", "email", "valid_email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "deleted"] + //can be ordred by ["user_id", "first_name", "last_name", "email", "valid_email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "deleted"] + console.log("options", options) + const preparedOptions: PreparedSearchOptions = { + // filter: [], + // sort: [], + page: options.page || 1, // Add pagination support, Default to page 1 if not specified + limit: options.limit || 10 // Set limit for pagination, Default to 10 items per page if not specified + } + const result = await this.userDataSource.getAll(preparedOptions) + return result; } diff --git a/src/domain/use-cases/user/get-all-users.ts b/src/domain/use-cases/user/get-all-users.ts index ceb040b..40bc67c 100644 --- a/src/domain/use-cases/user/get-all-users.ts +++ b/src/domain/use-cases/user/get-all-users.ts @@ -1,4 +1,5 @@ -import { UserResponseModel } from "../../entities/user"; +import { PreparedSearchOptions, SearchInfo, SearchOptions } from "../../entities/search"; +import { UserResponseModel, UserUpdateModel } from "../../entities/user"; import { UserRepository } from "../../interfaces/repositories/user-repository"; import { GetAllUsersUseCase } from "../../interfaces/use-cases/user/get-all-users"; @@ -8,13 +9,56 @@ export class GetAllUsers implements GetAllUsersUseCase { this.userRepository = userRepository } - async execute(): Promise { - // TODO + async execute(current_user: UserUpdateModel, options: SearchOptions): Promise<{ users: UserResponseModel[], search_info: SearchInfo }> { // User should not be deleted - //if (await this.userRepository.isDeleted(userAuth.user_id)) throw new Error("User is deleted"); + if (await this.userRepository.isDeleted(current_user.user_id)) throw new Error("User is deleted"); - const result = await this.userRepository.getUsers() - const publicUsers = result.map(user => this.userRepository.toPublicUser(user)) - return publicUsers + if (options.page) { + // Check that options.page is int + if (options.page && isNaN(parseInt(options.page.toString()))) throw new Error("Page must be a number"); + // Cast options.page to int + if (options.page) options.page = parseInt(options.page.toString()); + // Check options.page to be positive + if (options.page && options.page < 1) throw new Error("Page too low"); + } else { + // Default to page 1 if not specified + options.page = 1 + } + + if (options.limit) { + // Check that options.limit is int + if (options.limit && isNaN(parseInt(options.limit.toString()))) throw new Error("Limit must be a number"); + // Cast options.limit to int + if (options.limit) options.limit = parseInt(options.limit.toString()); + // Check options.limit to be positive + if (options.limit && options.limit < 0) throw new Error("Limit too low"); + } else { + // Default to 10 items per page if not specified + options.limit = 10 + } + + let result; + let users: UserResponseModel[] = []; + + // if admin, can search for deleted users + if (await this.userRepository.isAdmin(current_user.user_id)) { + result = await this.userRepository.adminGetUsers(options as PreparedSearchOptions); + users = result.users; + //const total =result.total; // Total number of users matching the filter + } else { + result = await this.userRepository.standardGetUsers(options as PreparedSearchOptions); + //const total =result.total; // Total number of users matching the filter + users = result.users.map(user => this.userRepository.toPublicUser(user)); + } + + const search_info: SearchInfo = { + total: result.total, + limit: options.limit || 10, // Default to 10 items per page if not specified + total_on_page: users.length, + page: options.page || 1, // Default to page 1 if not specified + pages: Math.ceil(result.total / (options.page || 1)) //total + }; + + return { search_info, users }; } } \ No newline at end of file diff --git a/src/presentation/routers/user-router.ts b/src/presentation/routers/user-router.ts index 0958dc7..36a6206 100644 --- a/src/presentation/routers/user-router.ts +++ b/src/presentation/routers/user-router.ts @@ -23,7 +23,7 @@ export default function UsersRouter( router.get('/', middlewareAuth.auth, async (req: Request, res: Response) => { try { - const users = await getAllUsersUseCase.execute() + const users = await getAllUsersUseCase.execute((req as CustomRequest).token, { ...req.query }); res.status(200).send(users) } catch (err) { console.log(err)