Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/rest/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import express, { Express } from 'express';
import { Controller } from '../shared/libs/rest/controller/controller.interface.js';
import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception-filter.interface.js';
import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js';
import { getFullServerPath } from '../shared/helpers/common.js';

@injectable()
export class RestApplication {
Expand Down Expand Up @@ -57,6 +58,11 @@ export class RestApplication {
'/upload',
express.static(this.config.get('UPLOAD_FILE_DIRECTORY')),
);

this.server.use(
'/static',
express.static(this.config.get('STATIC_DIRECTORY_PATH')),
);
}

private async _initControllers() {
Expand Down Expand Up @@ -91,7 +97,7 @@ export class RestApplication {
await this.__initExceptionFilters();

this.logger.info(
`🚀 Server started on http://localhost:${this.config.get('PORT')}`,
`🚀 Server started on ${getFullServerPath(this.config.get('HOST'), this.config.get('PORT'))}`,
);

this.logger.info('Database initialization');
Expand Down
5 changes: 5 additions & 0 deletions src/rest/rest.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../shared/libs/database-client/index.js';
import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception-filter.interface.js';
import { AppExceptionFilter } from '../shared/libs/rest/exception-filter/app-exception-filter.js';
import { PathTransformer } from '../shared/libs/transform/path-transformer.js';

export function createRestContainer() {
const restApplicationContainer = new Container();
Expand All @@ -32,6 +33,10 @@ export function createRestContainer() {
.bind<ExceptionFilter>(Component.ExceptionFilter)
.to(AppExceptionFilter)
.inSingletonScope();
restApplicationContainer
.bind(Component.PathTransformer)
.to(PathTransformer)
.inSingletonScope();

return restApplicationContainer;
}
4 changes: 4 additions & 0 deletions src/shared/helpers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ export function fillDTO<T, V>(dto: ClassConstructor<T>, plainObject: V) {
export function createErrorObject(message: string) {
return { error: message };
}

export function getFullServerPath(host: string, port: number) {
return `http://${host}:${port}`;
}
14 changes: 14 additions & 0 deletions src/shared/libs/config/rest.config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type RestSchema = {
DB_NAME: string;
UPLOAD_FILE_DIRECTORY: string;
JWT_SECRET: string;
HOST: string;
STATIC_DIRECTORY_PATH: string;
};

convict.addFormats(validator);
Expand Down Expand Up @@ -70,4 +72,16 @@ export const restSchema = convict<RestSchema>({
env: 'JWT_SECRET',
default: null,
},
HOST: {
doc: 'Host name',
format: String,
env: 'HOST',
default: null,
},
STATIC_DIRECTORY_PATH: {
doc: 'Static directory path',
format: String,
env: 'STATIC_DIRECTORY_PATH',
default: null,
},
});
12 changes: 10 additions & 2 deletions src/shared/libs/rest/controller/base-controller.abstract.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { injectable } from 'inversify';
import { inject, injectable } from 'inversify';
import { Controller } from './controller.interface.js';
import { Response, Router } from 'express';
import { Route } from '../types/route.interface.js';
import { StatusCodes } from 'http-status-codes';
import { Logger } from '../../logger/index.js';
import expressAsyncHandler from 'express-async-handler';
import { Component } from '../../../types/container.js';
import { PathTransformer } from '../../transform/path-transformer.js';

const DEFAULT_CONTENT_TYPE = 'application/json';

Expand All @@ -16,6 +18,9 @@ export abstract class BaseController implements Controller {
this._router = Router();
}

@inject(Component.PathTransformer)
protected readonly pathTransformer: PathTransformer;

get router(): Router {
return this._router;
}
Expand All @@ -37,7 +42,10 @@ export abstract class BaseController implements Controller {
}

public send<T>(res: Response, statusCode: number, data: T) {
res.type(DEFAULT_CONTENT_TYPE).status(statusCode).json(data);
const modifiedData = this.pathTransformer.execute(
data as Record<string, unknown>,
);
res.type(DEFAULT_CONTENT_TYPE).status(statusCode).json(modifiedData);
}

public created<T>(res: Response, data: T) {
Expand Down
12 changes: 8 additions & 4 deletions src/shared/libs/rest/middleware/upload-file.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { NextFunction, Request, Response } from 'express';
import { Middleware } from '../index.js';
import multer, { diskStorage } from 'multer';
import { extension } from 'mime-types';
import * as crypto from 'node:crypto';
import { Middleware } from './middleware.interface.js';

export class UploadFileMiddleware implements Middleware {
constructor(
private readonly fieldName: string,
private readonly uploadDirectory: string,
private uploadDirectory: string,
private fieldName: string,
) {}

execute(req: Request, res: Response, next: NextFunction): void {
public async execute(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
const storage = diskStorage({
destination: this.uploadDirectory,
filename: (_req, file, callback) => {
Expand Down
62 changes: 62 additions & 0 deletions src/shared/libs/transform/path-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { inject, injectable } from 'inversify';
import { Logger } from '../logger/index.js';
import { Component } from '../../types/container.js';
import { Config, RestSchema } from '../config/index.js';
import {
DEFAULT_STATIC_IMAGES,
STATIC_RESOURCE_FIELDS,
} from './transformer.const.js';
import { getFullServerPath } from '../../helpers/common.js';

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

@injectable()
export class PathTransformer {
constructor(
@inject(Component.Logger) private readonly logger: Logger,
@inject(Component.Config) private readonly config: Config<RestSchema>,
) {
this.logger.info('PathTransformer was created');
}

private hasDefaultImage(value: string) {
return DEFAULT_STATIC_IMAGES.includes(value);
}

private isStaticProperty(property: string) {
return STATIC_RESOURCE_FIELDS.includes(property);
}

public execute(data: Record<string, unknown>) {
const stack = [data];
while (stack.length > 0) {
const current = stack.pop();
for (const key in current) {
if (Object.hasOwn(current, key)) {
const value = current[key];

if (isObject(value)) {
stack.push(value);
continue;
}

if (this.isStaticProperty(key) && typeof value === 'string') {
const staticPath = '/static';
const uploadPath = '/upload';
const serverHost = this.config.get('HOST');
const serverPort = this.config.get('PORT');
const rootPath = this.hasDefaultImage(value)
? staticPath
: uploadPath;
current[key] =
`${getFullServerPath(serverHost, serverPort)}${rootPath}/${value}`;
}
}
}
}

return data;
}
}
3 changes: 3 additions & 0 deletions src/shared/libs/transform/transformer.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DEFAULT_STATIC_IMAGES = ['default-avatar.jpg'];

export const STATIC_RESOURCE_FIELDS = ['avatar', 'imagePreview', 'images'];
7 changes: 6 additions & 1 deletion src/shared/modules/comment/comment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CommentDto } from './dto/comment-dto.js';
import { CommentEntity } from './comment.entity.js';
import { DocumentType, types } from '@typegoose/typegoose';
import { OfferServiceInterface } from '../offer/offer-service.interface.js';
import { DISPLAY_COMMENT_COUNT } from './comment.const.js';

@injectable()
export class CommentService implements CommentServiceInterface {
Expand All @@ -31,7 +32,11 @@ export class CommentService implements CommentServiceInterface {
public async findByOfferId(
offerId: string,
): Promise<DocumentType<CommentEntity>[]> {
return this.commentModel.find({ offerId }).populate('author');
return this.commentModel
.find({ offerId })
.limit(DISPLAY_COMMENT_COUNT)
.sort({ date: -1 })
.exec();
}

public async deleteByOfferId(offerId: string): Promise<number> {
Expand Down
1 change: 1 addition & 0 deletions src/shared/modules/comment/comment.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DISPLAY_COMMENT_COUNT = 50;
2 changes: 1 addition & 1 deletion src/shared/modules/offer/dto/offer-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class CreateOfferDto {
@IsArray({ message: OfferDTOValidationMessage.goods.isArray })
goods!: Amenity[];

@IsObject({ message: OfferDTOValidationMessage.host.isObject })
@IsString({ message: OfferDTOValidationMessage.host.isObject })
host!: string;

@IsInt({ message: OfferDTOValidationMessage.commentsCount.isInt })
Expand Down
32 changes: 28 additions & 4 deletions src/shared/modules/offer/offer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { DocumentExistsMiddleware } from '../../libs/rest/middleware/document-ex
import { DocumentExists } from '../../libs/rest/types/document-exists.interface.js';
import { PrivateRouteMiddleware } from '../../libs/rest/middleware/private-route.middleware.js';
import { UserServiceInterface } from '../user/user-service.interface.js';
import { CommentRDO } from '../comment/rdo/comment-rdo.js';
import { MAX_DISPLAY_OFFERS_COUNT } from './offer.const.js';

export class OfferController extends BaseController {
constructor(
Expand Down Expand Up @@ -62,6 +64,20 @@ export class OfferController extends BaseController {
new DocumentExistsMiddleware('offerId', 'offer', this.offerService),
],
});
this.addRoute({
path: '/premium',
method: HttpMethod.Get,
handler: this.showPremiumOffers,
});
this.addRoute({
path: '/:offerId',
method: HttpMethod.Get,
handler: this.getDetailOffers,
middlewares: [
new ValidateObjectIdMiddleware('offerId'),
new DocumentExistsMiddleware('offerId', 'offer', this.offerService),
],
});
this.addRoute({
path: '/:offerId/comments',
method: HttpMethod.Get,
Expand All @@ -74,7 +90,8 @@ export class OfferController extends BaseController {
}

public async index(req: Request, res: Response) {
const offers = await this.offerService.find();
const limit = Number(req.params.limit) || MAX_DISPLAY_OFFERS_COUNT;
const offers = await this.offerService.find(limit);
let favoritesIds: string[] = [];

if (req.tokenPayload) {
Expand Down Expand Up @@ -105,11 +122,18 @@ export class OfferController extends BaseController {

const result = await this.offerService.createOffer({
...req.body,
userId: req.tokenPayload.id,
host: req.tokenPayload.id,
});
this.created(res, fillDTO(OfferRDO, result));
}

public async getDetailOffers(req: Request, res: Response) {
const offer = await this.offerService.findOfferById(
req.params.offerId as string,
);
this.ok(res, fillDTO(OfferRDO, offer));
}

public async update(req: Request, res: Response) {
const updateOffer = await this.offerService.updateById(
req.params.id as string,
Expand Down Expand Up @@ -144,8 +168,8 @@ export class OfferController extends BaseController {
public async getComments(req: Request, res: Response) {
const offerId = req.params.offerId;

const comments = this.commentService.findByOfferId(offerId as string);
const comments = await this.commentService.findByOfferId(offerId as string);

this.ok(res, fillDTO(OfferRDO, comments));
this.ok(res, fillDTO(CommentRDO, comments));
}
}
2 changes: 1 addition & 1 deletion src/shared/modules/offer/offer-service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export interface OfferServiceInterface {
incCommentCount(offerId: string): Promise<DocumentType<OfferEntity> | null>;
decCommentCount(offerId: string): Promise<DocumentType<OfferEntity> | null>;
findPremiumOffersByCity(city: City): Promise<DocumentType<OfferEntity>[]>;
find(): Promise<DocumentType<OfferEntity>[]>;
find(limit: number): Promise<DocumentType<OfferEntity>[]>;
}
7 changes: 5 additions & 2 deletions src/shared/modules/offer/offer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DocumentType, types } from '@typegoose/typegoose';
import { CommentEntity } from '../comment/comment.entity.js';
import { Types } from 'mongoose';
import { DocumentExists } from '../../libs/rest/types/document-exists.interface.js';
import { DISPLAY_PREMIUM_OFFERS_COUNT } from './offer.const.js';

@injectable()
export class OfferService implements OfferServiceInterface, DocumentExists {
Expand Down Expand Up @@ -63,15 +64,17 @@ export class OfferService implements OfferServiceInterface, DocumentExists {
.exec();
}

async find(): Promise<DocumentType<OfferEntity>[]> {
return this.offerModel.find().populate('host').exec();
async find(limit: number): Promise<DocumentType<OfferEntity>[]> {
return this.offerModel.find().populate('host').limit(limit).exec();
}

async findPremiumOffersByCity(
city: City,
): Promise<DocumentType<OfferEntity>[]> {
return this.offerModel
.find({ city, isPremium: true })
.sort({ date: -1 })
.limit(DISPLAY_PREMIUM_OFFERS_COUNT)
.populate('host')
.exec();
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/modules/offer/offer.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DISPLAY_PREMIUM_OFFERS_COUNT = 3;
export const MAX_DISPLAY_OFFERS_COUNT = 60;
8 changes: 2 additions & 6 deletions src/shared/modules/offer/offer.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
prop,
Ref,
} from '@typegoose/typegoose';
import { Amenity, City, HousingType } from '../../types/offer.js';
import { Amenity, City, HousingType, Location } from '../../types/offer.js';
import { UserEntity } from '../user/user.entity.js';

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
Expand All @@ -17,10 +17,6 @@ export interface OfferEntity extends defaultClasses.Base {}
timestamps: true,
},
})
class Location {
public latitude!: number;
public longitude!: number;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class OfferEntity extends defaultClasses.TimeStamps {
Expand Down Expand Up @@ -104,7 +100,7 @@ export class OfferEntity extends defaultClasses.TimeStamps {
@prop({ required: true })
public commentsCount!: number;

@prop({ _id: false, type: () => Location, required: true })
@prop({ _id: false, required: true })
public location!: Location;
}

Expand Down
Loading
Loading