Skip to content

Commit

Permalink
DEV : Get users sorting and filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
juliecoust committed Feb 14, 2024
1 parent c2c0e76 commit 5dd08d5
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 88 deletions.
64 changes: 52 additions & 12 deletions src/data/data-sources/sqlite/sqlite-user-data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,60 @@ export class SQLiteUserDataSource implements UserDataSource {
// }
async getAll(options: PreparedSearchOptions): Promise<SearchResult> {
// 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`
let sql = `SELECT *, (SELECT COUNT(*) FROM user`
const params: any[] = []
let filtering_sql = ""
const params_filtering: any[] = []
// Add filtering
if (options.filter.length > 0) {
filtering_sql += ` WHERE `;
// For each filter, add to filtering_sql and params_filtering
for (const filter of options.filter) {
// If value is undefined, null or empty, and operator =, set to is null
if (filter.value == undefined || filter.value == null || filter.value == "") {
if (filter.operator == "=") {
filtering_sql += filter.field + ` IS NULL`;
} else if (filter.operator == "!=") {
filtering_sql += filter.field + ` IS NOT NULL`;
}
}
// If value is true or false, set to 1 or 0
else if (filter.value == true) {
filtering_sql += filter.field + ` = 1`;
}
else if (filter.value == false) {
filtering_sql += filter.field + ` = 0`;
}
else {
filtering_sql += filter.field + ` ` + filter.operator + ` (?) `
params_filtering.push(filter.value)
}
filtering_sql += ` AND `;
}
// remove last AND
filtering_sql = filtering_sql.slice(0, -4);
}
// Add filtering_sql to sql
sql += filtering_sql
// Add params_filtering to params
params.push(...params_filtering)

sql += `) AS total_count FROM user`

// // 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 filtering_sql to sql
sql += filtering_sql
// Add params_filtering to params
params.push(...params_filtering)

// // Add sorting
// if (options.sort) {
// sql += ` ORDER BY ${options.sort}`; // Be cautious of SQL injection
// }
// Add sorting
if (options.sort_by.length > 0) {
sql += ` ORDER BY`;
for (const sort of options.sort_by) {
sql += ` ` + sort.sort_by + ` ` + sort.order_by + `,`;
}
// remove last ,
sql = sql.slice(0, -1);
}

// Add pagination
const page = options.page;
Expand Down Expand Up @@ -114,7 +154,7 @@ export class SQLiteUserDataSource implements UserDataSource {
user_creation_date: row.user_creation_date,
deleted: row.deleted
})),
total: rows[0].total_count
total: rows[0]?.total_count || 0
};
resolve(result);
}
Expand Down
51 changes: 35 additions & 16 deletions src/domain/entities/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,46 @@

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

// Raw data
export interface SearchOptions extends PaginedSearchOptions {
filter?: FilterSearchOptions[]; // Add filtering support
sort_by?: PreparedSortingSearchOptions[] | string; // Add sorting support
}

export interface PaginedSearchOptions {
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 PreparedSearchOptions {
// filter?: any;
// sort?: any; // sorting support
page: number; // pagination support, Default to page 1 if not specified

export interface FilterSearchOptions {
field: string;
operator: string; // TODO =, !=, >, >=, <, <=, IN, NOT IN, LIKE, NOT LIKE, BETWEEN, NOT BETWEEN
value: string | number | boolean | Date | null | undefined | any[];
}

// Prepared data
export interface PreparedSearchOptions extends PreparedPaginedSearchOptions {
filter: FilterSearchOptions[];
sort_by: PreparedSortingSearchOptions[];
}

export interface PreparedPaginedSearchOptions {
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 PreparedSortingSearchOptions {
sort_by: string;
order_by: string;
}

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
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 {
Expand Down
9 changes: 4 additions & 5 deletions src/domain/interfaces/repositories/user-repository.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@

import { AuthUserCredentialsModel, DecodedToken, ChangeCredentialsModel } from "../../entities/auth";
import { PreparedSearchOptions, SearchResult } from "../../entities/search";
import { PreparedPaginedSearchOptions, PreparedSearchOptions, PreparedSortingSearchOptions, SearchResult } from "../../entities/search";
import { UserRequesCreationtModel, UserResponseModel, UserRequestModel, UserUpdateModel, PublicUserModel, PrivateUserModel } from "../../entities/user";
export interface UserRepository {

formatSortBy(sort_by: string): PreparedSortingSearchOptions[];
changePassword(user_to_update: ChangeCredentialsModel): Promise<number>;
getUser(user: UserRequestModel): Promise<UserResponseModel | null>;
adminUpdateUser(user: UserUpdateModel): Promise<number>;
standardUpdateUser(user: UserUpdateModel): Promise<number>;
verifyUserLogin(user: AuthUserCredentialsModel): Promise<boolean>;
createUser(user: UserRequesCreationtModel): Promise<number>;
getUsers(options: PreparedSearchOptions): Promise<SearchResult>;
adminGetUsers(options: PreparedSearchOptions): Promise<SearchResult>;
standardGetUsers(options: PreparedSearchOptions): Promise<SearchResult>;
adminGetUsers(options: PreparedSearchOptions | PreparedPaginedSearchOptions): Promise<SearchResult>;
standardGetUsers(options: PreparedSearchOptions | PreparedPaginedSearchOptions): Promise<SearchResult>;
isAdmin(user_id: number): Promise<boolean>;
validUser(user: UserRequestModel): Promise<number>;
generateValidationToken(user: UserRequestModel): string;
Expand Down
4 changes: 2 additions & 2 deletions src/domain/interfaces/use-cases/user/get-all-users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SearchInfo, SearchOptions } from "../../../entities/search";
import { SearchInfo, PaginedSearchOptions } from "../../../entities/search";
import { UserResponseModel, UserUpdateModel } from "../../../entities/user";
export interface GetAllUsersUseCase {
execute(current_user: UserUpdateModel, options: SearchOptions): Promise<{ users: UserResponseModel[], search_info: SearchInfo }>;
execute(current_user: UserUpdateModel, options: PaginedSearchOptions): Promise<{ users: UserResponseModel[], search_info: SearchInfo }>;
}
5 changes: 5 additions & 0 deletions src/domain/interfaces/use-cases/user/search-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FilterSearchOptions, SearchInfo, SearchOptions } from "../../../entities/search";
import { UserResponseModel, UserUpdateModel } from "../../../entities/user";
export interface SearchUsersUseCase {
execute(current_user: UserUpdateModel, options: SearchOptions, filters: FilterSearchOptions[]): Promise<{ users: UserResponseModel[], search_info: SearchInfo }>;
}
88 changes: 52 additions & 36 deletions src/domain/repositories/user-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ 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";
import { PreparedSearchOptions, PreparedSortingSearchOptions, SearchResult } from "../entities/search";

export class UserRepositoryImpl implements UserRepository {
userDataSource: UserDataSource
userCrypto: CryptoWrapper
userJwt: JwtWrapper
VALIDATION_TOKEN_SECRET: string
RESET_PASSWORD_TOKEN_SECRET: string
order_by_allow_params: string[] = ["asc", "desc"]
filter_operator_allow_params: string[] = ["=", ">", "<", ">=", "<=", "<>", "IN", "LIKE", "BETWEEN"]

constructor(userDataSource: UserDataSource, userCrypto: CryptoWrapper, userJwt: JwtWrapper, VALIDATION_TOKEN_SECRET: string, RESET_PASSWORD_TOKEN_SECRET: string) {
this.userDataSource = userDataSource
Expand Down Expand Up @@ -85,45 +87,59 @@ export class UserRepositoryImpl implements UserRepository {
return nb_of_updated_user
}

adminGetUsers(options: PreparedSearchOptions): Promise<SearchResult> {
//can be filtered by ["user_id", "first_name", "last_name", "email", "valid_email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "deleted"]
async adminGetUsers(options: PreparedSearchOptions): Promise<SearchResult> {
//can be filtered by
const filter_params_admin = ["user_id", "first_name", "last_name", "email", "valid_email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "deleted"]
// Can be sort_by
const sort_param_admin = ["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)
return await this.getUsers(options, filter_params_admin, sort_param_admin, this.order_by_allow_params, this.filter_operator_allow_params)
}

standardGetUsers(options: PreparedSearchOptions): Promise<SearchResult> {
//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<SearchResult> {
//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)
async standardGetUsers(options: PreparedSearchOptions): Promise<SearchResult> {
//can be filtered by
const filter_params_restricted = ["user_id", "first_name", "last_name", "email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date", "valid_email", "deleted"] // Add valid_email and deleted to force default filter
// Can be sort_by
const sort_param_restricted = ["user_id", "first_name", "last_name", "email", "is_admin", "organisation", "country", "user_planned_usage", "user_creation_date"]

return result;
// If valid email or deleted dilter delet them
options.filter.filter(filter => filter.field === "valid_email" || filter.field === "deleted")

options.filter.push({ field: "valid_email", operator: "=", value: true });
options.filter.push({ field: "deleted", operator: "=", value: null });

return await this.getUsers(options, filter_params_restricted, sort_param_restricted, this.order_by_allow_params, this.filter_operator_allow_params)
}

formatSortBy(raw_sort_by: string): PreparedSortingSearchOptions[] {
// Split the raw_sort_by string by commas to get individual sorting statements
const prepared_sort_by = raw_sort_by.split(",").map(statement => {
// Split the statement by "(" to separate order_by and sort_by
const [order_by, sort_by] = statement.split("(");
// Extract the sort_by string and remove the closing ")"
const clean_sort_by = sort_by.slice(0, -1).toLowerCase();
// Return an object with sort_by and order_by keys if both are non-empty
if (clean_sort_by && order_by) {
return { sort_by: clean_sort_by, order_by: order_by.toLowerCase() };
}
// Otherwise, return null
return null;
}).filter(Boolean); // Filter out null values
return prepared_sort_by as PreparedSortingSearchOptions[];
}

private async getUsers(options: PreparedSearchOptions, filtering_params: string[], sort_by_params: string[], order_by_params: string[], filter_operator_params: string[]): Promise<SearchResult> {
// Filter options.sort_by by sorting params
options.sort_by = options.sort_by.filter(sort_by =>
sort_by_params.includes(sort_by.sort_by) && order_by_params.includes(sort_by.order_by)
);

// Filter options.filters by filtering params
options.filter = options.filter.filter(filter =>
filtering_params.includes(filter.field) && filter_operator_params.includes(filter.operator)
);
//TODO check value? or juste prepared statement after
return await this.userDataSource.getAll(options);
}

async getUser(user: UserRequestModel): Promise<UserResponseModel | null> {
Expand Down
15 changes: 14 additions & 1 deletion src/domain/use-cases/user/get-all-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ export class GetAllUsers implements GetAllUsersUseCase {
options.limit = 10
}

// Set filters to empty in get all case
options.filter = [];

// Check that options.sort_by is string and format it to PreparedSortingSearchOptions[]
if (options.sort_by) {
options.sort_by = this.userRepository.formatSortBy(options.sort_by as string);
}
// Default to sort by user_id ASC if not specified ou badly specified
if (!options.sort_by || options.sort_by.length === 0) {
options.sort_by = [{ sort_by: "user_id", order_by: "desc" }]
}

let result;
let users: UserResponseModel[] = [];

Expand All @@ -51,12 +63,13 @@ export class GetAllUsers implements GetAllUsersUseCase {
users = result.users.map(user => this.userRepository.toPublicUser(user));
}

// Prepare info for pagination
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
pages: Math.ceil(result.total / (options.limit || 1)) //total
};

return { search_info, users };
Expand Down
Loading

0 comments on commit 5dd08d5

Please sign in to comment.