Skip to content

Commit

Permalink
feat(api): add repository module
Browse files Browse the repository at this point in the history
  • Loading branch information
Mefjus committed Mar 21, 2021
1 parent be64e64 commit 0e56240
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 51 deletions.
1 change: 0 additions & 1 deletion packages/api/src/emailTemplates/emailTemplates.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export interface WelcomeTemplateProps extends BaseEmailTemplatesProps {
firstName: string;
link: string;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/emailTemplates/welcomeTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import mjml2html from 'mjml';

import { WelcomeTemplateProps } from './emailTemplates.types';

const welcomeTemplate = ({ firstName, link, clientUrl }: WelcomeTemplateProps) => {
const welcomeTemplate = ({ link, clientUrl }: WelcomeTemplateProps) => {
const { html, errors } = mjml2html(
`
<mjml>
Expand All @@ -25,7 +25,7 @@ const welcomeTemplate = ({ firstName, link, clientUrl }: WelcomeTemplateProps) =
</mj-text>
<mj-image src='${clientUrl}/email-images/paper-plane.png' height='160px' width='160px' alt="paper plane image" />
<mj-text font-size="24px">
Hi <b>${firstName}</b>,
Hi,
</mj-text>
<mj-text>
It’s look like that administrator invites you to group<br/> in Smart Gate system. Click on the button to<br/> create your account.
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/modules/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { InvitationModule } from './invitation/invitation.module';
import { MailerModule } from './mailer/mailer.module';
import { PasswordResetModule } from './password-reset/password-reset.module';
import { RefreshTokenModule } from './refresh-token/refresh-token.module';
import { RepositoryModule } from './repository/repository.module';
import { UsersModule } from './users/users.module';

@Module({
Expand All @@ -28,8 +29,10 @@ import { UsersModule } from './users/users.module';
limit: config.rateLimiter.minTime,
}),
}),
RepositoryModule,
InvitationModule,
PasswordResetModule,
RepositoryModule,
],
})
export class AppModule {}
16 changes: 16 additions & 0 deletions packages/api/src/modules/database/entities/base.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm';

export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;

@CreateDateColumn({
type: 'timestamp',
})
public createdAt: number;

@CreateDateColumn({
type: 'timestamp',
})
public updatedAt: number;
}
13 changes: 3 additions & 10 deletions packages/api/src/modules/database/entities/invitation.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity } from 'typeorm';

import { Role } from '../../auth/role.enum';
import { BaseEntity } from './base.entity';

@Entity('invitations')
export class InvitationEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;

export class InvitationEntity extends BaseEntity {
@Column({
type: 'varchar',
})
Expand All @@ -24,9 +22,4 @@ export class InvitationEntity {
enum: Role,
})
public roles: Array<Role>;

@CreateDateColumn({
type: 'timestamp',
})
public createdAt: number;
}
13 changes: 3 additions & 10 deletions packages/api/src/modules/database/entities/refreshToken.entity.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, ManyToOne } from 'typeorm';

import { BaseEntity } from './base.entity';
// eslint-disable-next-line import/no-cycle
import { UserEntity } from './user.entity';

@Entity('refresh_tokens')
export class RefreshTokenEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;

export class RefreshTokenEntity extends BaseEntity {
@Column({
type: 'timestamp',
})
Expand All @@ -21,9 +19,4 @@ export class RefreshTokenEntity {

@Column({ type: 'uuid' })
public userId: string;

@CreateDateColumn({
type: 'timestamp',
})
public createdAt: number;
}
25 changes: 3 additions & 22 deletions packages/api/src/modules/database/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Column, Entity, Index, OneToMany } from 'typeorm';

import { Role } from '../../auth/role.enum';
import { BaseEntity } from './base.entity';
// eslint-disable-next-line import/no-cycle
import { RefreshTokenEntity } from './refreshToken.entity';

@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;

export class UserEntity extends BaseEntity {
@Column({
unique: true,
})
Expand Down Expand Up @@ -47,14 +38,4 @@ export class UserEntity {

@OneToMany(() => RefreshTokenEntity, (token) => token.user)
public refreshTokens: Promise<[RefreshTokenEntity]>;

@CreateDateColumn({
type: 'timestamp',
})
public createdAt: number;

@CreateDateColumn({
type: 'timestamp',
})
public updatedAt: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddBaseEntity1616345466388 implements MigrationInterface {
name = 'AddBaseEntity1616345466388';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "invitations" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now()',
);
await queryRunner.query(
'ALTER TABLE "refresh_tokens" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now()',
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "refresh_tokens" DROP COLUMN "updatedAt"');
await queryRunner.query('ALTER TABLE "invitations" DROP COLUMN "updatedAt"');
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { CacheModule, Module } from '@nestjs/common';

import { MailerModule } from '../mailer/mailer.module';
import { UsersModule } from '../users/users.module';
import { RepositoryModule } from '../repository/repository.module';
import { PasswordResetConfigModule } from './config/password-reset-config.module';
import { PasswordResetController } from './password-reset.controller';
import { PasswordResetService } from './password-reset.service';

@Module({
imports: [UsersModule, CacheModule.register(), MailerModule, PasswordResetConfigModule],
imports: [CacheModule.register(), MailerModule, PasswordResetConfigModule, RepositoryModule],
providers: [PasswordResetService],
controllers: [PasswordResetController],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { v4 as uuidV4 } from 'uuid';

import { urlEncodedParams } from '../../utils';
import { MailerService } from '../mailer/mailer.service';
import { UsersService } from '../users/users.service';
import { UserRepository } from '../repository/user.repository';
import { PasswordResetConfigService } from './config/password-reset-config.service';

@Injectable()
export class PasswordResetService {
constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly usersService: UsersService,
private readonly userRepository: UserRepository,
private readonly mailerService: MailerService,
private readonly passwordResetConfigService: PasswordResetConfigService,
) {}
Expand All @@ -22,7 +22,7 @@ export class PasswordResetService {
private readonly passwordResetTtl = this.passwordResetConfigService.getTtl();

async createAndSend(email: string): Promise<string> {
const { firstName } = await this.usersService.findOneByEmail(email);
const { firstName } = await this.userRepository.findOneByEmailOrFail(email);
const generatedUuid = uuidV4();
await this.cacheManager.set(email, generatedUuid, { ttl: this.passwordResetTtl });

Expand Down Expand Up @@ -50,6 +50,6 @@ export class PasswordResetService {

const salt = bcrypt.genSaltSync();
const hashPassword = bcrypt.hashSync(password, salt);
await this.usersService.updatePassword(email, hashPassword);
await this.userRepository.updatePassword(email, hashPassword);
}
}
84 changes: 84 additions & 0 deletions packages/api/src/modules/repository/base.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Type } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeepPartial, FindManyOptions, FindOneOptions, Repository } from 'typeorm';

import { BaseEntity } from '../database/entities/base.entity';

export interface BaseRepositoryType<T> {
readonly repository: Repository<T>;

create: (dataToCreate: DeepPartial<T>) => Promise<T>;

findById: (id: string) => Promise<T | undefined>;
findByIdOrFail: (id: string) => Promise<T>;

findOne: (options: FindOneOptions) => Promise<T | undefined>;
findOneOrFail: (options: FindOneOptions) => Promise<T>;

find: (options: FindManyOptions) => Promise<T[]>;

update: (id: string, dataToUpdate: DeepPartial<T>) => Promise<T>;
}

type Constructor<T> = new (...args: never[]) => T;

export function BaseRepository<T extends BaseEntity>(
entityType: Constructor<T>,
): Type<BaseRepositoryType<T>> {
class BaseRepositoryHost implements BaseRepositoryType<T> {
@InjectRepository(entityType)
public readonly repository: Repository<T>;

async create(dataToCreate: DeepPartial<T>): Promise<T> {
try {
return this.repository.save(dataToCreate);
} catch (err) {
throw new Error(`Cannot create entity with data ${dataToCreate}`);
}
}

async findById(id: string): Promise<T | undefined> {
return this.repository.findOne({ where: { id } });
}

async findByIdOrFail(id: string): Promise<T> {
try {
return this.repository.findOneOrFail({ where: { id } });
} catch (err) {
throw new Error(`Entity with id: '${id}' dose not exists!`);
}
}

async findOne(options: FindOneOptions): Promise<T | undefined> {
return this.repository.findOne(options);
}

async findOneOrFail(options: FindOneOptions): Promise<T> {
return this.repository.findOneOrFail(options);
}

async find(options: FindManyOptions): Promise<T[]> {
return this.repository.find(options);
}

async update(id: string, dataToUpdate: DeepPartial<T>): Promise<T> {
const entityToUpdate: DeepPartial<T> = {
id,
...dataToUpdate,
updatedAt: new Date(),
};

try {
return await this.repository.save(entityToUpdate);
} catch (err) {
throw new Error(
`Entity with id: '${id}' cannot be updated with data: '${JSON.stringify(
entityToUpdate,
)}'`,
);
}
}
}

return BaseRepositoryHost;
}
4 changes: 4 additions & 0 deletions packages/api/src/modules/repository/invitation.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { InvitationEntity } from '../database/entities/invitation.entity';
import { BaseRepository } from './base.repository';

export class InvitationRepository extends BaseRepository(InvitationEntity) {}
13 changes: 13 additions & 0 deletions packages/api/src/modules/repository/repository.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import entities from '../database/entities';
import { InvitationRepository } from './invitation.repository';
import { UserRepository } from './user.repository';

@Module({
imports: [TypeOrmModule.forFeature(entities)],
providers: [UserRepository, InvitationRepository],
exports: [UserRepository, InvitationRepository],
})
export class RepositoryModule {}
17 changes: 17 additions & 0 deletions packages/api/src/modules/repository/user.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { UserEntity } from '../database/entities/user.entity';
import { BaseRepository } from './base.repository';

export class UserRepository extends BaseRepository(UserEntity) {
async findOneByEmail(email: string): Promise<UserEntity | undefined> {
return this.findOne({ where: { email } });
}

async findOneByEmailOrFail(email: string): Promise<UserEntity> {
return this.findOneOrFail({ where: { email } });
}

async updatePassword(email: string, hashPassword: string): Promise<UserEntity> {
const { id } = await this.findOneByEmailOrFail(email);
return this.update(id, { password: hashPassword });
}
}

0 comments on commit 0e56240

Please sign in to comment.