Skip to content

Commit

Permalink
Merge pull request #12 from Roger13579/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
y0000ga authored May 8, 2024
2 parents d3948bb + 72d8b42 commit 250ea3f
Show file tree
Hide file tree
Showing 16 changed files with 1,172 additions and 88 deletions.
15 changes: 10 additions & 5 deletions src/controller/productController.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Request } from 'express';
import { BaseController } from './baseController';
import { CustomResponseType } from '../types/customResponseType';
import { ResponseObject } from '../utils/responseObject';
import { ProductService } from '../service/productService';
import { NewProductDto } from '../dto/newProductDto';
import { NewProductVo } from '../vo/newProductVo';
import { TCreateProductsReq } from '../types/product.type';
import { TCreateProductsReq, TGetProductsReq } from '../types/product.type';
import { ProductFilterDTO } from '../dto/productFilterDto';
import { GetProductVo } from '../vo/getProductVo';

class ProductController extends BaseController {
private readonly productService = new ProductService();

public getProducts = async (req: Request): Promise<ResponseObject> => {
const products = await this.productService.findProducts();
public getProducts = async (
req: TGetProductsReq,
): Promise<ResponseObject> => {
const productFilterDto = new ProductFilterDTO(req);
const { page, limit } = productFilterDto.getFilter;
const info = await this.productService.findProducts(productFilterDto);
return this.formatResponse(
CustomResponseType.OK_MESSAGE,
CustomResponseType.OK,
products,
new GetProductVo(info, page, limit),
);
};

Expand Down
135 changes: 135 additions & 0 deletions src/dto/productFilterDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { IUser } from '../models/user';
import {
MovieGenre,
ProductSortBy,
ProductType,
RecommendWeightRange,
TGetProductsReq,
} from '../types/product.type';
import { AccountType } from '../types/user.type';
import { throwError } from '../utils/errorHandler';
import { CustomResponseType } from '../types/customResponseType';
import {
checkDateOrder,
parseBoolean,
parseDate,
parsePositiveInteger,
parseValidEnums,
} from '../utils/common';

export class ProductFilterDTO {
public readonly title?: string;
public readonly types?: ProductType[];
public readonly genres?: MovieGenre[];
public readonly vendors?: string[];
public readonly theaters?: string[];
public readonly isPublic?: boolean;
public readonly isLaunched?: boolean;
public readonly startAtFrom?: Date;
public readonly startAtTo?: Date;
public readonly recommendWeights?: number[];
public readonly sellStartAtFrom?: Date;
public readonly sellStartAtTo?: Date;
public readonly priceMax?: number;
public readonly priceMin?: number;
public readonly tags?: string[];
public readonly page?: number;
public readonly limit?: number;
public readonly sortBy?: string;
public readonly accountType: AccountType = AccountType.member;

get getFilter() {
return this;
}

constructor(req: TGetProductsReq) {
const {
title,
types,
genres,
vendors,
theaters,
isLaunched,
isPublic,
startAtFrom,
startAtTo,
sellStartAtFrom,
recommendWeights,
sellStartAtTo,
priceMax,
priceMin,
tags,
page,
limit,
sortBy,
} = req.query;

if ((req.user as IUser)?.accountType === AccountType.admin) {
this.accountType = AccountType.admin;
}

// number
this.limit = parsePositiveInteger('limit', limit);
this.priceMax = parsePositiveInteger('priceMax', priceMax);
this.priceMin = parsePositiveInteger('priceMin', priceMin);
this.page = parsePositiveInteger('page', page);

// string
this.title = title;

// validate
this.types = parseValidEnums('type', ProductType, types);
this.genres = parseValidEnums('genre', MovieGenre, genres);

const validRecommendWeights: number[] = [];
recommendWeights?.split(',').forEach((weight) => {
const weightNum = parsePositiveInteger('recommendWeight', weight);
if (
weightNum &&
Object.values(RecommendWeightRange).indexOf(weightNum) > -1
) {
validRecommendWeights.push(weightNum);
}
});

if (validRecommendWeights.length > 0) {
this.recommendWeights = validRecommendWeights;
}

if (sortBy) {
const isValidSortBy =
Object.keys(ProductSortBy).indexOf(sortBy.trim().replace('-', '')) >= 0;
if (isValidSortBy) {
this.sortBy = sortBy as ProductSortBy;
} else {
throwError(
CustomResponseType.INVALID_PRODUCT_FILTER_MESSAGE +
`: sortBy 不得為 ${sortBy}`,
CustomResponseType.INVALID_PRODUCT_FILTER,
);
}
}

// array
this.vendors = vendors?.split(',');
this.theaters = theaters?.split(',');
this.tags = tags?.split(',');

// boolean
this.isLaunched = parseBoolean('isLaunched', isLaunched);
this.isPublic = parseBoolean('isPublic', isPublic);

// time
this.startAtTo = parseDate('startAtTo', startAtTo);
this.startAtFrom = parseDate('startAtFrom', startAtFrom);
this.sellStartAtFrom = parseDate('sellStartAtFrom', sellStartAtFrom);
this.sellStartAtTo = parseDate('sellStartAtTo', sellStartAtTo);
// 確認時間順序
checkDateOrder(
{ prop: 'sellStartAtFrom', value: this.sellStartAtFrom },
{ prop: 'sellStartAtTo', value: this.sellStartAtTo },
{ prop: 'startAtFrom', value: this.startAtFrom },
{ prop: 'startAtTo', value: this.startAtTo },
);
}
}
32 changes: 30 additions & 2 deletions src/middleware/userVerify.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import { Request, Response, NextFunction } from 'express';
import { Response, NextFunction } from 'express';
import { throwError } from '../utils/errorHandler';
import { CustomResponseType } from '../types/customResponseType';
import jwt, { TokenExpiredError } from 'jsonwebtoken';
import { UserRepository } from '../repository/userRepository';
import { UserReq } from '../types/common.type';

/**
* @description 只取得身分,不限制行為
*/
export const UserCheck = async (
req: UserReq,
res: Response,
next: NextFunction,
) => {
const userRepository = new UserRepository();

let token: string = '';
const authorization = req.headers.authorization;
try {
if (authorization && authorization.startsWith('Bearer ')) {
token = authorization.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRETS as any);
const user = await userRepository.findById((payload as any).id);
if (user) {
req.user = user;
}
}
return next();
} catch (err) {
return next(err);
}
};

export const UserVerify = async (
req: Request,
req: UserReq,
res: Response,
next: NextFunction,
) => {
Expand Down
8 changes: 5 additions & 3 deletions src/models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface IProduct extends Document, ITimestamp {
isPublic: boolean;
isLaunched: boolean;
tags?: [string];
photoPath: string;
photoPath: string | null;
notifications?: [string];
highlights?: [string];
introduction?: string;
Expand Down Expand Up @@ -148,10 +148,12 @@ const schema = new Schema<IProduct>(
min: 1,
max: 10,
required: true,
select: true, // 只有管理者可以看到
},
isPublic: {
type: Boolean,
required: true,
select: true, // 只有管理者可以看到
},
isLaunched: {
type: Boolean,
Expand All @@ -175,8 +177,8 @@ const schema = new Schema<IProduct>(
],
},
photoPath: {
type: String,
default: '',
type: String || null,
default: null,
trim: true,
},
notifications: {
Expand Down
2 changes: 1 addition & 1 deletion src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface IUser extends Document, ITimestamp {
thirdPartyId: string;
thirdPartyType: string;
isThirdPartyVerified: boolean;
accountType: string;
accountType: AccountType;
status: Status;
groups: [Schema.Types.ObjectId];
collects: [Schema.Types.ObjectId];
Expand Down
74 changes: 72 additions & 2 deletions src/repository/productRepository.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,84 @@
import { NewProductDto } from '../dto/newProductDto';
import { ProductFilterDTO } from '../dto/productFilterDto';
import ProductModel, { IProduct } from '../models/product';
import { AccountType } from '../types/user.type';

export class ProductRepository {
private createProductFilter(productFilterDto: ProductFilterDTO) {
const {
title,
types,
genres,
vendors,
theaters,
isLaunched,
isPublic,
startAtFrom,
startAtTo,
sellStartAtFrom,
recommendWeights,
sellStartAtTo,
priceMax,
priceMin,
tags,
} = productFilterDto.getFilter;
const titleRegex = title ? new RegExp(title) : undefined;
return {
...(titleRegex && { title: { $regex: titleRegex } }),
...(types && { type: { $in: types } }),
...(genres && { genre: { $in: genres } }),
...(vendors && { vendor: { $in: vendors } }),
...(theaters && { theater: { $in: theaters } }),
...(recommendWeights && { recommendWeight: { $in: recommendWeights } }),
...(isLaunched !== undefined && { isLaunched }),
...(isPublic !== undefined && { isPublic }),
...((startAtFrom || startAtTo) && {
startAt: {
...(startAtFrom && { $lte: startAtFrom }),
...(startAtTo && { $gte: startAtTo }),
},
}),
...((sellStartAtFrom || sellStartAtTo) && {
sellStartAt: {
...(sellStartAtFrom && { $lte: sellStartAtFrom }),
...(sellStartAtTo && { $gte: sellStartAtTo }),
},
}),
...((priceMax || priceMin) && {
price: {
...(priceMin && { $lte: priceMin }),
...(priceMax && { $gte: priceMax }),
},
}),
...(tags && { tags: { $in: tags } }),
};
}

public async createProducts(
newProductsDto: NewProductDto,
): Promise<IProduct[] | void> {
return ProductModel.insertMany(newProductsDto.getNewProducts);
}

public async findProducts(): Promise<IProduct[]> {
return ProductModel.find({});
public async findProducts(
productFilterDto: ProductFilterDTO,
): Promise<IProduct[]> {
const { page, limit, sortBy, accountType } = productFilterDto.getFilter;
const filter = this.createProductFilter(productFilterDto);
const selection =
accountType === AccountType.admin ? '' : '-recommendWeight -isPublic';
const options = {
...(page && limit && { skip: (page - 1) * limit }),
...(limit && { limit }),
sort: sortBy || '-createdAt',
};
return ProductModel.find(filter, selection, options);
}

public async countProducts(
productFilterDto: ProductFilterDTO,
): Promise<number> {
const filter = this.createProductFilter(productFilterDto);
return ProductModel.countDocuments(filter);
}
}
Loading

0 comments on commit 250ea3f

Please sign in to comment.