Skip to content

Commit

Permalink
Merge pull request #85 from jembi/feat/add-server-config-id-to-user
Browse files Browse the repository at this point in the history
Feat/add server config id to user
  • Loading branch information
jacob-khoza-symb authored Sep 25, 2024
2 parents 0ff6ea0 + f93c72d commit 766e869
Show file tree
Hide file tree
Showing 15 changed files with 179 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "server_config_id" TEXT;

-- AddForeignKey
ALTER TABLE "user" ADD CONSTRAINT "user_server_config_id_fkey" FOREIGN KEY ("server_config_id") REFERENCES "server_config"("id") ON DELETE SET NULL ON UPDATE CASCADE;
11 changes: 7 additions & 4 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ model shlink {
}

model user {
id String @id @default(cuid()) @db.VarChar(43)
user_id String @unique
patient_id String
shlink shlink[]
id String @id @default(cuid()) @db.VarChar(43)
user_id String @unique
patient_id String
server_config_id String?
server_config server_config? @relation(fields: [server_config_id], references: [id])
shlink shlink[]
@@index([id], name: "idx_user_id")
Expand All @@ -84,6 +86,7 @@ model server_config {
refresh_time DateTime @default(now())
access_token_response String
shlink_endpoints shlink_endpoint[]
users user[]
@@index([id], name: "idx_server_config_id")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function GET(
`Getting an endpoint data of user id: ${shlink.getUserId()} with share link id: ${params.id}, endpoint id: ${params.endpointId} and ticket id: ${ticketId}`,
);
const patient = await getPatientDataUseCase(
{ repo: serverConfigRepo },
{ repo: serverConfigRepo, userRepo },
{ user: user },
);

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/v1/users/[id]/ips/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function GET(
`Retrieving patient summary data from FHIR with user: ${JSON.stringify(user)}`,
);
const result = await getPatientDataUseCase(
{ repo: serverConfigRepo },
{ repo: serverConfigRepo, userRepo },
{ user },
);

Expand Down
25 changes: 23 additions & 2 deletions src/app/api/v1/users/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { POST } from '@/app/api/v1/users/route';
import { getUserProfile } from '@/app/utils/authentication';
import { handleApiValidationError } from '@/app/utils/error-handler';
import { mapEntityToModel } from '@/mappers/server-config-mapper';
import { mapDtoToModel, mapModelToDto } from '@/mappers/user-mapper';
import { HapiFhirServiceFactory } from '@/services/hapi-fhir-factory';
import { IHapiFhirService } from '@/services/hapi-fhir.interface';
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('POST /api/users', () => {

const mockRoute = '/api/v1/users';
let mockService: jest.Mocked<IHapiFhirService>;
(getUserProfile as jest.Mock).mockResolvedValue(true);
(getUserProfile as jest.Mock).mockResolvedValue({ email: '[email protected]' });

HapiFhirServiceFactory.getService = jest.fn().mockReturnValue(mockService);

Expand All @@ -74,7 +75,13 @@ describe('POST /api/users', () => {
(mapDtoToModel as jest.Mock).mockReturnValue(mockUserModel);
(addUserUseCase as jest.Mock).mockResolvedValue(mockUserModel);
(mapModelToDto as jest.Mock).mockReturnValue(mockUserDto);
(searchPatientUseCase as jest.Mock).mockResolvedValue('patient id');
(searchPatientUseCase as jest.Mock).mockResolvedValue({
patient: { id: 'patient id' },
serverConfig: mapEntityToModel({
id: 'server-config-id',
endpoint_url: 'http://test.com',
}),
});

const request = mockRequest(mockCreateUserDto);
const response = await POST(request);
Expand All @@ -88,6 +95,13 @@ describe('POST /api/users', () => {

it('should handle validation errors and return status 400', async () => {
const error = new Error('Validation error');
(searchPatientUseCase as jest.Mock).mockResolvedValue({
patient: { id: 'patient id' },
serverConfig: mapEntityToModel({
id: 'server-config-id',
endpoint_url: 'http://test.com',
}),
});
(addUserUseCase as jest.Mock).mockRejectedValue(error);
(handleApiValidationError as jest.Mock).mockReturnValue(
NextResponse.json({ message: 'Validation error' }, { status: 400 }),
Expand All @@ -109,6 +123,13 @@ describe('POST /api/users', () => {

it('should handle unexpected errors and return status 500', async () => {
const error = new Error('Unexpected error');
(searchPatientUseCase as jest.Mock).mockResolvedValue({
patient: { id: 'patient id' },
serverConfig: mapEntityToModel({
id: 'server-config-id',
endpoint_url: 'http://test.com',
}),
});
(addUserUseCase as jest.Mock).mockRejectedValue(error);
(handleApiValidationError as jest.Mock).mockReturnValue(
NextResponse.json({ message: 'Unexpected error' }, { status: 500 }),
Expand Down
9 changes: 7 additions & 2 deletions src/app/api/v1/users/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ export async function POST(request: Request) {
try {
unstable_noStore();
const { email } = await getUserProfile(request);
const patientId = await searchPatientUseCase(
const data = await searchPatientUseCase(
{ repo: serverConfigRepo },
{ patientId: dto.patientId, email },
);
dto.patientId = patientId;
dto = {
...dto,
patientId: data.patient.id,
serverConfigId: data.serverConfig.getId(),
};
logger.info(JSON.stringify(dto));
const model = mapDtoToModel(dto as UserDto);
const newUser = await addUserUseCase({ repo }, { user: model });
return NextResponse.json(mapModelToDto(newUser), { status: 201 });
Expand Down
1 change: 1 addition & 0 deletions src/domain/dtos/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
export class CreateUserDto {
userId: string;
patientId: string;
serverConfigId?: string;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/domain/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,27 @@ export class UserModel extends BaseModel {
private userId: string,
private patientId: string,
private id?: string,
private serverConfigId?: string,
) {
super(
z.object({
userId: z.string().min(1),
patientId: z.string().min(1),
id: z.string().optional(),
serverConfigId: z.string().optional(),
}),
);
this.validate();
}

getServerConfigId(): string | undefined {
return this.serverConfigId;
}

setServerConfigId(serverConfigId: string): void {
this.serverConfigId = serverConfigId;
}

getId(): string | undefined {
return this.id;
}
Expand Down
1 change: 1 addition & 0 deletions src/entities/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { BaseEntity } from './base-entity';
export class UserEntity extends BaseEntity {
user_id: string;
patient_id: string;
server_config_id?: string;
}
15 changes: 13 additions & 2 deletions src/mappers/user-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ export const mapEntityToModel = (
userEntity: UserEntity,
): UserModel | undefined => {
return userEntity
? new UserModel(userEntity.user_id, userEntity.patient_id, userEntity.id)
? new UserModel(
userEntity.user_id,
userEntity.patient_id,
userEntity.id,
userEntity.server_config_id,
)
: undefined;
};

Expand All @@ -18,6 +23,7 @@ export const mapModelToEntity = (
id: userModel.getId(),
user_id: userModel.getUserId(),
patient_id: userModel.getPatientId(),
server_config_id: userModel.getServerConfigId(),
}
: undefined;
};
Expand All @@ -34,6 +40,11 @@ export const mapModelToDto = (userModel: UserModel): UserDto | undefined => {

export const mapDtoToModel = (userDto: UserDto): UserModel | undefined => {
return userDto
? new UserModel(userDto.userId, userDto.patientId, userDto.id)
? new UserModel(
userDto.userId,
userDto.patientId,
userDto.id,
userDto.serverConfigId,
)
: undefined;
};
6 changes: 3 additions & 3 deletions src/services/hapi-fhir-factory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ServerConfigEntity } from '@/entities/server_config';
import { ServerConfigModel } from '@/domain/models/server-config';

import { IHapiFhirService } from './hapi-fhir.interface';
import { HapiFhirService } from './hapi-fhir.service';

export class HapiFhirServiceFactory {
static getService(serverConfig: ServerConfigEntity): IHapiFhirService {
return new HapiFhirService(serverConfig.endpoint_url);
static getService(serverConfig: ServerConfigModel): IHapiFhirService {
return new HapiFhirService(serverConfig.getEndpointUrl());
}
}
29 changes: 22 additions & 7 deletions src/usecases/patient/get-patient-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { UserModel } from '@/domain/models/user';
import { IServerConfigRepository } from '@/infrastructure/repositories/interfaces/server-config-repository';
import { IUserRepository } from '@/infrastructure/repositories/interfaces/user-repository';
import {
ExternalDataFetchError,
HapiFhirService,
Expand All @@ -16,6 +17,7 @@ jest.mock('@/services/hapi-fhir.service', () => ({

describe('getPatientDataUseCase', () => {
let mockRepo: jest.Mocked<IServerConfigRepository>;
let mockUserRepo: jest.Mocked<IUserRepository>;
let mockUser: jest.Mocked<UserModel>;
let mockHapiFhirService: jest.Mocked<HapiFhirService>;

Expand All @@ -26,6 +28,7 @@ describe('getPatientDataUseCase', () => {

mockUser = {
getPatientId: jest.fn().mockReturnValue('test-patient-id'),
getServerConfigId: jest.fn().mockReturnValue('server-config-id'),
} as any;

mockHapiFhirService = {
Expand All @@ -41,7 +44,10 @@ describe('getPatientDataUseCase', () => {
mockRepo.findMany.mockResolvedValue([]); // No server config

await expect(
getPatientDataUseCase({ repo: mockRepo }, { user: mockUser }),
getPatientDataUseCase(
{ repo: mockRepo, userRepo: mockUserRepo },
{ user: mockUser },
),
).rejects.toThrow(new ExternalDataFetchError('Missing Config error.'));
});

Expand All @@ -54,7 +60,10 @@ describe('getPatientDataUseCase', () => {
);

await expect(
getPatientDataUseCase({ repo: mockRepo }, { user: mockUser }),
getPatientDataUseCase(
{ repo: mockRepo, userRepo: mockUserRepo },
{ user: mockUser },
),
).rejects.toThrow(new ExternalDataFetchError('Unfullfilled request'));
});

Expand All @@ -65,20 +74,23 @@ describe('getPatientDataUseCase', () => {
mockHapiFhirService.getPatientData.mockResolvedValue(null);

await expect(
getPatientDataUseCase({ repo: mockRepo }, { user: mockUser }),
getPatientDataUseCase(
{ repo: mockRepo, userRepo: mockUserRepo },
{ user: mockUser },
),
).rejects.toThrow(new ExternalDataFetchError('Unfullfilled request'));
});

it('should return patient data if service fetches data successfully', async () => {
const mockPatientData = { id: 'test-patient-id', name: 'John Doe' };

mockRepo.findMany.mockResolvedValue([
{ endpoint_url: 'http://test-url.com' },
{ endpoint_url: 'http://test-url.com', id: 'server-config-id' },
]);
mockHapiFhirService.getPatientData.mockResolvedValue(mockPatientData);

const result = await getPatientDataUseCase(
{ repo: mockRepo },
{ repo: mockRepo, userRepo: mockUserRepo },
{ user: mockUser },
);

Expand All @@ -92,12 +104,15 @@ describe('getPatientDataUseCase', () => {
it('should handle errors gracefully', async () => {
const error = new Error('Test error');
mockRepo.findMany.mockResolvedValue([
{ endpoint_url: 'http://test-url.com' },
{ endpoint_url: 'http://test-url.com', id: 'server-config-id' },
]);
mockHapiFhirService.getPatientData.mockRejectedValue(error);

await expect(
getPatientDataUseCase({ repo: mockRepo }, { user: mockUser }),
getPatientDataUseCase(
{ repo: mockRepo, userRepo: mockUserRepo },
{ user: mockUser },
),
).rejects.toThrow(new ExternalDataFetchError('Unfullfilled request'));

expect(mockHapiFhirService.getPatientData).toHaveBeenCalledWith(
Expand Down
Loading

0 comments on commit 766e869

Please sign in to comment.