Skip to content

Commit

Permalink
Merge pull request #80 from jembi/feat/shlink-filters
Browse files Browse the repository at this point in the history
Feat/shlink filters
  • Loading branch information
godchiSymbionix authored Sep 23, 2024
2 parents 943445c + 4a84169 commit 6154ce1
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 7 deletions.
5 changes: 5 additions & 0 deletions public/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@
"description": "A string representing the share link's encoded url.",
"example": "5AMl62z2XDmgrh2XsI2O"
},
"active": {
"type": "boolean",
"description": "A boolean indicating whether the share link is active.",
"example": true
},
"passwordRequired": {
"type": "boolean",
"description": "A boolean indicating whether the share link has a passcode or not.",
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ const logger = new LogHandler(__dirname);

export async function GET(request) {
logger.info('API connected successfully');
return NextResponse.json({ message: 'API Health Check' }, { status: 200 });
return NextResponse.json(
{ message: 'API Health Check' },
{ status: 200 },
);
}
18 changes: 16 additions & 2 deletions src/app/api/v1/share-links/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';

import { getUserProfile, validateUser } from '@/app/utils/authentication';
import { handleApiValidationError } from '@/app/utils/error-handler';
import { validateSHLinkStatusParameter } from '@/app/utils/validate';
import { container, SHLinkRepositoryToken } from '@/container';
import { CreateSHLinkDto, SHLinkDto } from '@/domain/dtos/shlink';
import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository';
Expand Down Expand Up @@ -63,7 +64,14 @@ export async function POST(request: Request) {
* /api/v1/share-links:
* get:
* tags: [Share Links]
* description: Get share links.
* description: Get share links
* parameters:
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive, expired]
* description: Filter share links by status.
* responses:
* 200:
* description: Gets all the signed in user's share links.
Expand All @@ -75,13 +83,19 @@ export async function POST(request: Request) {
* $ref: '#/components/schemas/SHLinkMini'
*/
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
let status: string | null = searchParams.get('status')?.toLowerCase() || null

try {
unstable_noStore();

validateSHLinkStatusParameter({status});

const { id } = await getUserProfile(request);

logger.info(`Getting all share links by user with user id: ${id}`);

const newShlink = await getSHLinkUseCase({ repo }, { user_id: id });
const newShlink = await getSHLinkUseCase({ repo }, { user_id: id, status });
return NextResponse.json(
newShlink.map((shlink) => mapModelToMiniDto(shlink)),
{ status: 200 },
Expand Down
7 changes: 6 additions & 1 deletion src/app/utils/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { ExternalDataFetchError } from '@/services/hapi-fhir.service';
import { SHLinkValidationError } from '@/usecases/shlinks/validate-shlink';

import { AuthenticationError } from './authentication';
import { ParameterValidationError } from './validate';
import {
BAD_REQUEST,
PRECONDITION_FAILED,
SERVER_ERROR,
} from '../constants/http-constants';


export function handleApiValidationError(error: Error, logger:LogHandler) {
logger.error(error);

Expand All @@ -21,6 +21,11 @@ export function handleApiValidationError(error: Error, logger:LogHandler) {
{ error: BAD_REQUEST, detail: error },
{ status: 422 },
);
}else if (error instanceof ParameterValidationError) {
return NextResponse.json(
{ error: BAD_REQUEST, detail: error },
{ status: 422 }
)
} else if (error instanceof ExternalDataFetchError) {
return NextResponse.json(
{ error: PRECONDITION_FAILED, detail: error.message },
Expand Down
27 changes: 27 additions & 0 deletions src/app/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';

export class ParameterValidationError extends Error {
constructor(
message: string,
public errors: Record<string, string[]>,
) {
super(message);
this.name = 'ParameterValidationError';
}
}

interface SHLinkParameters {
status?: string;
}

export const validateSHLinkStatusParameter = (parameters: SHLinkParameters) => {
const statusSchema = z.enum(['expired', 'active', 'inactive']).nullable();

const result = statusSchema.safeParse(parameters.status || null);

if (!result.success) {
throw new ParameterValidationError('Invalid status parameter', {
status: result.error.errors.map((err) => err.message),
});
}
};
5 changes: 5 additions & 0 deletions src/domain/dtos/shlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export class SHLinkDto extends CreateSHLinkDto {
* type: string
* description: A string representing the share link's encoded url.
* example: 5AMl62z2XDmgrh2XsI2O
* active:
* type: boolean
* description: A boolean indicating whether the share link is active.
* example: true
* passwordRequired:
* type: boolean
* description: A boolean indicating whether the share link has a passcode or not.
Expand All @@ -143,6 +147,7 @@ export class SHLinkMiniDto {
passwordRequired?: boolean;
name: string;
url: string;
active: boolean
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/mappers/shlink-mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ describe('mapModelToMiniDto', () => {
passwordRequired: true,
url: 'http://localhost:3000/viewer#shlink:/eyJsYWJlbCI6Im5hbWUiLCJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvYXBpL3YxL3NoYXJlLWxpbmtzL2xpbmstaWQiLCJmbGFnIjoiUCJ9',
name: 'name',
active: true,
files: [
{
location: `${EXTERNAL_URL}/api/v1/share-links/link-id/endpoints/endpoint1-id?ticket=${ticket}`,
Expand Down Expand Up @@ -211,6 +212,7 @@ describe('mapModelToMiniDto', () => {
passwordRequired: true,
name: 'name',
expiryDate: date,
active: true,
files: undefined,
url: 'http://localhost:3000/viewer#shlink:/eyJsYWJlbCI6Im5hbWUiLCJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvYXBpL3YxL3NoYXJlLWxpbmtzL2xpbmstaWQiLCJmbGFnIjoiUCJ9',
});
Expand Down Expand Up @@ -258,6 +260,7 @@ describe('mapModelToMiniDto', () => {
name: 'name',
url: 'http://localhost:3000/viewer#shlink:/eyJsYWJlbCI6Im5hbWUiLCJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvYXBpL3YxL3NoYXJlLWxpbmtzL2xpbmstaWQiLCJmbGFnIjoiUCJ9',
expiryDate: date,
active: true,
files: [
{
location: `${EXTERNAL_URL}/api/v1/share-links/link-id/endpoints/endpoint-id?ticket=undefined`,
Expand Down
1 change: 1 addition & 0 deletions src/mappers/shlink-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const mapModelToMiniDto = (
expiryDate: shlinkModel.getConfigExp(),
passwordRequired: !!shlinkModel.getConfigPasscode(),
url: encodeSHLink(shlinkModel),
active: shlinkModel.getActive(),
files: files?.map((x) => {
return {
location: `${EXTERNAL_URL}/api/v1/share-links/${shlinkModel.getId()}/endpoints/${x.getId()}?ticket=${ticket}`,
Expand Down
40 changes: 39 additions & 1 deletion src/usecases/shlinks/get-shlink.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @jest-environment node
*/
import { SHLinkModel } from '@/domain/models/shlink';
import { SHLinkEntity } from '@/entities/shlink';
import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository';
Expand Down Expand Up @@ -44,6 +47,7 @@ describe('getSHLinkUseCase', () => {
config_exp: new Date('2024-06-01T00:00:00Z'),
},
];

mockModels = [
new SHLinkModel(
mockUserId,
Expand All @@ -66,7 +70,7 @@ describe('getSHLinkUseCase', () => {
'2',
),
];
// Set up mock implementations

(mockRepo.findMany as jest.Mock).mockResolvedValue(mockSHLinkEntities);
});

Expand All @@ -89,4 +93,38 @@ describe('getSHLinkUseCase', () => {

expect(result).toEqual([]);
});

it('should filter by active status when status is "active"', async () => {
await getSHLinkUseCase(mockContext, { user_id: mockUserId, status: 'active' });

expect(mockRepo.findMany).toHaveBeenCalledWith({
user_id: mockUserId,
active: true,
});
});

it('should filter by inactive status when status is "inactive"', async () => {
await getSHLinkUseCase(mockContext, { user_id: mockUserId, status: 'inactive' });

expect(mockRepo.findMany).toHaveBeenCalledWith({
user_id: mockUserId,
active: false,
});
});

it('should filter by expired status when status is "expired"', async () => {
const dateBeforeNow = new Date();
jest.useFakeTimers().setSystemTime(dateBeforeNow); // Mock the current date

await getSHLinkUseCase(mockContext, { user_id: mockUserId, status: 'expired' });

expect(mockRepo.findMany).toHaveBeenCalledWith({
user_id: mockUserId,
config_exp: {
lt: dateBeforeNow,
},
});

jest.useRealTimers();
});
});
18 changes: 16 additions & 2 deletions src/usecases/shlinks/get-shlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ import { mapEntityToModel } from '@/mappers/shlink-mapper';

export const getSHLinkUseCase = async (
context: { repo: ISHLinkRepository },
data: { user_id: string },
data: { user_id: string; status?: string },
): Promise<SHLinkModel[]> => {
const entities = await context.repo.findMany({ user_id: data.user_id });

let logicOperator = {};

const statusLogicMap = {
inactive: { active: false },
active: { active: true },
expired: { config_exp: { lt: new Date() } },
};

logicOperator = statusLogicMap[data.status] || {}

const entities = await context.repo.findMany({
user_id:data.user_id,
...logicOperator
});

return entities.map((x) => mapEntityToModel(x));
};

0 comments on commit 6154ce1

Please sign in to comment.