Skip to content

Commit

Permalink
feat(api-service): Add user service (#46)
Browse files Browse the repository at this point in the history
* feat: create user script

* fix: set database name as singular

* build(dep): add class-transformer

* feat: domain persistence and usecase

* feat: remove profile image

* feat: user usecases

* feat: user port

* feat: user di token

* feat: remove T prefix

* feat: remove constructure and getter/setter

* feat: export functions

* fix: add new User function

* feat: add User service

* chore: add service path

* fix: resolve issues

* refactor: domain and service

---------

Co-authored-by: spicyzboss <supachai@spicyz.io>
  • Loading branch information
santhitak and spicyzboss authored Mar 17, 2024
1 parent 854f7b9 commit e536b18
Show file tree
Hide file tree
Showing 36 changed files with 330 additions and 80 deletions.
10 changes: 10 additions & 0 deletions db/scripts/create_user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE User (
user_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
created_at int NOT NULL,
updated_at int,
deleted_at int
);
9 changes: 9 additions & 0 deletions libs/api/shared/domain/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './entity';
export * from './exception';
export * from './nullable';
export * from './optional';
export * from './removable';
export * from './repository';
export * from './statusCode';
export * from './usecase';
export * from './validator';
13 changes: 13 additions & 0 deletions libs/api/shared/domain/src/common/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type FindOptions = {
includeRemoved?: boolean;
limit?: number;
offset?: number;
};

export type UpdateManyOptions = {
includeRemoved?: boolean;
};

export type RemoveOptions = {
disableSoftDeleting?: boolean;
};
8 changes: 8 additions & 0 deletions libs/api/shared/domain/src/common/usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface UseCase<Port, T> {
execute(port?: Port): Promise<T>;
}

export interface TransactionalUseCase<Port, T> extends UseCase<Port, T> {
onCommit?: (result: T, port: Port) => Promise<void>;
onRollback?: (error: Error, port: Port) => Promise<void>;
}
1 change: 1 addition & 0 deletions libs/api/shared/domain/src/entity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './user';
69 changes: 13 additions & 56 deletions libs/api/shared/domain/src/entity/user.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,39 @@
import { Entity } from '../common/entity';
import { IsDate, IsEmail, IsOptional, IsString, IsUrl } from 'class-validator';
import { CreateUserPayload } from '../model/createUserPayload';
import { Entity, Nullable } from '../common';
import { IsDate, IsOptional, IsString } from 'class-validator';
import { CreateUserPayload } from '../user';
import { typeid } from 'typeid-js';
import { Nullable } from '../common/nullable';

export class User extends Entity<string> {
@IsString()
private username: string;

@IsEmail()
private email: string;
public readonly username: string;

@IsString()
private firstName: string;
public readonly firstName: string;

@IsString()
private lastName: string;

@IsOptional()
@IsUrl()
private profileImage: string;
public readonly lastName: string;

@IsDate()
private createdAt: Date;
public readonly createdAt: Date;

@IsOptional()
@IsDate()
private updatedAt: Nullable<Date>;
public readonly updatedAt: Nullable<Date>;

@IsOptional()
@IsDate()
private deletedAt: Nullable<Date>;
public readonly deletedAt: Nullable<Date>;

constructor(payload: CreateUserPayload) {
private constructor(payload: CreateUserPayload) {
super();

this.username = payload.username;
this.email = payload.email;
this.id = typeid('user').toString();
this.firstName = payload.firstName;
this.lastName = payload.lastName;
this.profileImage = payload.profileImage;
this.email = payload.email;

this.id = typeid('user').toString();
this.createdAt = new Date();
this.updatedAt = null;
this.deletedAt = null;
}

getUsername(): string {
return this.username;
}

getEmail(): string {
return this.email;
}

getFirstName(): string {
return this.firstName;
}

getLastName(): string {
return this.lastName;
}

getProfileImage(): string {
return this.profileImage;
}

getCreatedAt(): Date {
return this.createdAt;
}

getUpdatedAt(): Nullable<Date> {
return this.updatedAt;
}

getDeletedAt(): Nullable<Date> {
return this.deletedAt;
public static new(payload: CreateUserPayload): User {
return new User(payload);
}
}
4 changes: 4 additions & 0 deletions libs/api/shared/domain/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './common';
export * from './entity';
export * from './user';
export * from './util';
19 changes: 0 additions & 19 deletions libs/api/shared/domain/src/model/createUserPayload.ts

This file was deleted.

1 change: 1 addition & 0 deletions libs/api/shared/domain/src/user/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './userUseCaseDto';
22 changes: 22 additions & 0 deletions libs/api/shared/domain/src/user/dto/userUseCaseDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { User } from '../../entity';
import { Exclude, Expose, plainToClass } from 'class-transformer';

@Exclude()
export class UserUseCaseDto {
@Expose()
public readonly username: string;

@Expose()
public readonly firstName: string;

@Expose()
public readonly lastName: string;

public static fromUser(user: User): UserUseCaseDto {
return plainToClass(UserUseCaseDto, user);
}

public static fromUsers(users: User[]): UserUseCaseDto[] {
return users.map(this.fromUser);
}
}
4 changes: 4 additions & 0 deletions libs/api/shared/domain/src/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './dto';
export * from './model';
export * from './port';
export * from './usecase';
12 changes: 12 additions & 0 deletions libs/api/shared/domain/src/user/model/createUserPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString } from 'class-validator';

export class CreateUserPayload {
@IsString()
public readonly username: string;

@IsString()
public readonly firstName: string;

@IsString()
public readonly lastName: string;
}
1 change: 1 addition & 0 deletions libs/api/shared/domain/src/user/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createUserPayload';
5 changes: 5 additions & 0 deletions libs/api/shared/domain/src/user/port/createUserPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CreateUserPort {
firstName: string;
lastName: string;
username: string;
}
4 changes: 4 additions & 0 deletions libs/api/shared/domain/src/user/port/findUserPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface FindUserPort {
id?: string;
username?: string;
}
5 changes: 5 additions & 0 deletions libs/api/shared/domain/src/user/port/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './createUserPort';
export * from './findUserPort';
export * from './removeUserPort';
export * from './updateUserPort';
export * from './userRepositoryPort';
3 changes: 3 additions & 0 deletions libs/api/shared/domain/src/user/port/removeUserPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface RemoveUserPort {
id: string;
}
5 changes: 5 additions & 0 deletions libs/api/shared/domain/src/user/port/updateUserPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface UpdateUserPort {
firstName: string;
lastName: string;
username: string;
}
12 changes: 12 additions & 0 deletions libs/api/shared/domain/src/user/port/userRepositoryPort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Optional, FindOptions } from '../../common';
import { User } from '../../entity';
import { FindUserPort } from './findUserPort';
import { RemoveUserPort } from './removeUserPort';

export interface UserRepositoryPort {
findUser(by: FindUserPort, options?: FindOptions): Promise<Optional<User>>;
countUsers(by: FindUserPort, options?: FindOptions): Promise<number>;
createUser(user: User): Promise<User>;
updateUser(user: User): Promise<Optional<User>>;
removeUser(by: RemoveUserPort): Promise<Optional<User>>;
}
5 changes: 5 additions & 0 deletions libs/api/shared/domain/src/user/usecase/createUserUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Optional, UseCase } from '../../common';
import { CreateUserPort } from '../port';
import { UserUseCaseDto } from '../dto';

export interface CreateUserUseCase extends UseCase<CreateUserPort, Optional<UserUseCaseDto>> {}
5 changes: 5 additions & 0 deletions libs/api/shared/domain/src/user/usecase/findUserUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Optional, UseCase } from '../../common';
import { FindUserPort } from '../port';
import { UserUseCaseDto } from '../dto';

export interface FindUserUseCase extends UseCase<FindUserPort, Optional<UserUseCaseDto>> {}
2 changes: 2 additions & 0 deletions libs/api/shared/domain/src/user/usecase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './findUserUseCase';
export * from './createUserUseCase';
23 changes: 23 additions & 0 deletions libs/api/shared/domain/src/util/assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Nullable } from '../common/nullable';
import { Optional } from '../common/optional';

export class Assert {
static isTrue(expression: boolean, exception: Error): void {
if (!expression) {
throw exception;
}
}

static isFalse(expression: boolean, exception: Error): void {
if (expression) {
throw exception;
}
}

static notEmpty<T>(value: Optional<Nullable<T>>, exception: Error): T {
if (value === null || value === undefined) {
throw exception;
}
return value;
}
}
1 change: 1 addition & 0 deletions libs/api/shared/domain/src/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './assert';
18 changes: 18 additions & 0 deletions libs/api/shared/service/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
13 changes: 13 additions & 0 deletions libs/api/shared/service/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "api-service",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/api/shared/service/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"tags": []
}
53 changes: 53 additions & 0 deletions libs/api/shared/service/src/user/usecase/createUserService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
Assert,
CreateUserPort,
CreateUserUseCase,
Exception,
FindUserPort,
Optional,
StatusCode,
User,
UserRepositoryPort,
UserUseCaseDto
} from '@producktivity/api-domain';

export class CreateUserService implements CreateUserUseCase {
constructor(private readonly userRepository: UserRepositoryPort) { }

public async execute(payload: CreateUserPort): Promise<Optional<UserUseCaseDto>> {
const isUserExist = await this.isUserExist(payload);
Assert.isFalse(isUserExist, Exception.new({ code: StatusCode.ENTITY_ALREADY_EXISTS }));

const user: User = User.new({
firstName: payload.firstName,
lastName: payload.lastName,
username: payload.lastName,
});

const createdUser = await this.createUser(user);

if (!createdUser) throw Exception.new({ code: StatusCode.INTERNAL_ERROR });

return UserUseCaseDto.fromUser(createdUser);
}

private async createUser(user: User): Promise<Optional<User>> {
try {
const createdUser = await this.userRepository.createUser(user);

return createdUser;
} catch (err) {
return;
}
}

private async isUserExist({ username }: Required<Pick<FindUserPort, 'username'>>): Promise<boolean> {
try {
const userCount = await this.userRepository.countUsers({ username });

return Boolean(userCount);
} catch (err) {
return false;
}
}
}
Loading

0 comments on commit e536b18

Please sign in to comment.