Skip to content

Commit

Permalink
Addd a better api-front contract declaration
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Naulleau committed Dec 17, 2024
1 parent ec83a46 commit 7198f0c
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 215 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,46 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ReportListingVM, ReportRetrievalVM } from 'shared-models';
import { ReportsContextRestContract } from 'shared-models';
import { AttachReportFileUseCase } from 'src/reports-context/business-logic/use-cases/report-attach-file/attach-report-file';
import { DeleteReportAttachedFileUseCase } from 'src/reports-context/business-logic/use-cases/report-file-deletion/delete-report-attached-file';
import { GenerateReportFileUrlUseCase } from 'src/reports-context/business-logic/use-cases/report-file-url-generation/generate-report-file-url';
import { ListReportsUseCase } from 'src/reports-context/business-logic/use-cases/report-listing/list-reports.use-case';
import { RetrieveReportUseCase } from 'src/reports-context/business-logic/use-cases/report-retrieval/retrieve-report.use-case';
import { UpdateReportUseCase } from 'src/reports-context/business-logic/use-cases/report-update/update-report.use-case';
import { ChangeRuleValidationStateUseCase } from 'src/reports-context/business-logic/use-cases/rule-validation-state-change/change-rule-validation-state.use-case';
import {
reportsControllerRoute,
reportsEndpointRelativePaths,
ReportsEndpoints,
} from 'shared-models';
IController,
IControllerPaths,
} from 'src/shared-kernel/adapters/primary/nestjs/controller';
import {
ChangeRuleValidationStateDto,
ReportUpdateDto,
} from './dto/report-update.dto';
import { GenerateReportFileUrlUseCase } from 'src/reports-context/business-logic/use-cases/report-file-url-generation/generate-report-file-url';
import { DeleteReportAttachedFileUseCase } from 'src/reports-context/business-logic/use-cases/report-file-deletion/delete-report-attached-file';

export type ReportsEndpoints = typeof ReportsEndpoints;
type IReportController = Pick<
IController<ReportsContextRestContract>,
| 'listReports'
| 'retrieveReport'
| 'updateReport'
| 'updateRule'
| 'attachFile'
| 'generateFileUrl'
| 'deleteAttachedFile'
>;

export type IReportController = {
[K in keyof ReportsEndpoints]: (
params: ReportsEndpoints[K]['Params'],
body: K extends 'attachFile'
? Express.Multer.File
: ReportsEndpoints[K]['Body'],
) => Promise<ReportsEndpoints[K]['Response']>;
const baseRoute: ReportsContextRestContract['basePath'] = 'api/reports';
const endpointsPaths: IControllerPaths<ReportsContextRestContract> = {
retrieveReport: ':id',
listReports: '',
updateReport: ':id',
updateRule: 'rules/:ruleId',
attachFile: ':id/files/upload-one',
generateFileUrl: ':reportId/files/:fileName',
deleteAttachedFile: ':id/files/:fileName',
};

@Controller(reportsControllerRoute)
@Controller(baseRoute)
export class ReportsController implements IReportController {
constructor(
private readonly listReportsUseCase: ListReportsUseCase,
Expand All @@ -51,60 +61,71 @@ export class ReportsController implements IReportController {
private readonly deleteReportAttachedFileUseCase: DeleteReportAttachedFileUseCase,
) {}

@Get(reportsEndpointRelativePaths.listReports)
async listReports(): Promise<ReportListingVM> {
@Get(endpointsPaths.listReports)
async listReports() {
return this.listReportsUseCase.execute();
}

@Get(reportsEndpointRelativePaths.retrieveReport)
@Get(endpointsPaths.retrieveReport)
async retrieveReport(
@Param() params: ReportsEndpoints['retrieveReport']['Params'],
): Promise<ReportRetrievalVM | null> {
@Param()
params: ReportsContextRestContract['endpoints']['retrieveReport']['params'],
) {
const { id } = params;
return this.retrieveReportUseCase.execute(id);
}

@Put(reportsEndpointRelativePaths.updateReport)
@Put(endpointsPaths.updateReport)
async updateReport(
@Param() { id }: ReportsEndpoints['updateReport']['Params'],
@Param()
{ id }: ReportsContextRestContract['endpoints']['updateReport']['params'],
@Body() dto: ReportUpdateDto,
): Promise<void> {
) {
await this.updateReportUseCase.execute(id, dto);
}

@Put(reportsEndpointRelativePaths.updateRule)
@Put(endpointsPaths.updateRule)
async updateRule(
@Param() { ruleId }: ReportsEndpoints['updateRule']['Params'],
@Param()
{ ruleId }: ReportsContextRestContract['endpoints']['updateRule']['params'],
@Body() dto: ChangeRuleValidationStateDto,
): Promise<void> {
) {
await this.changeRuleValidationStateUseCase.execute(ruleId, dto.validated);
}

@Post(reportsEndpointRelativePaths.attachFile)
@Post(endpointsPaths.attachFile)
@UseInterceptors(FileInterceptor('file'))
async attachFile(
@Param() { id }: ReportsEndpoints['attachFile']['Params'],
@Param()
{ id }: ReportsContextRestContract['endpoints']['attachFile']['params'],
@UploadedFile() file: Express.Multer.File,
): Promise<void> {
) {
return this.attachReportFileUseCase.execute(
id,
file.originalname,
file.buffer,
);
}

@Get(reportsEndpointRelativePaths.generateFileUrl)
@Get(endpointsPaths.generateFileUrl)
async generateFileUrl(
@Param()
{ reportId, fileName }: ReportsEndpoints['generateFileUrl']['Params'],
): Promise<string> {
{
reportId,
fileName,
}: ReportsContextRestContract['endpoints']['generateFileUrl']['params'],
) {
return this.generateReportFileUrlUseCase.execute(reportId, fileName);
}

@Delete(reportsEndpointRelativePaths.deleteAttachedFile)
@Delete(endpointsPaths.deleteAttachedFile)
async deleteAttachedFile(
@Param() { id, fileName }: ReportsEndpoints['deleteAttachedFile']['Params'],
): Promise<void> {
@Param()
{
id,
fileName,
}: ReportsContextRestContract['endpoints']['deleteAttachedFile']['params'],
) {
return this.deleteReportAttachedFileUseCase.execute(id, fileName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('Delete Report Attached File Use Case', () => {
expect(reportAttachedFileRepository.files).toEqual({});
});

it("doesn't delete the uploaded file if it's not metadata cannot be removed", async () => {
it("doesn't delete the uploaded file if its metadata cannot be removed", async () => {
reportAttachedFileRepository.deleteError = new Error('Failed to delete');
await expect(deleteFile()).rejects.toThrow(
reportAttachedFileRepository.deleteError,
Expand All @@ -55,7 +55,7 @@ describe('Delete Report Attached File Use Case', () => {
]);
});

it("doesn't remos a file's metadata if file deletion failed", async () => {
it("doesn't remove a file's metadata if file deletion failed", async () => {
reportFileService.deleteError = new Error('Failed to delete');
await expect(deleteFile()).rejects.toThrow(reportFileService.deleteError);
expect(Object.values(reportAttachedFileRepository.files)).toEqual([
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/shared-kernel/adapters/primary/nestjs/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RestContract } from 'shared-models/models/endpoints/common';

export type IController<C extends RestContract> = {
[K in keyof C['endpoints']]: (
params: C['endpoints'][K]['params'],
body: C['endpoints'][K]['body'] extends FormData
? Express.Multer.File
: C['endpoints'][K]['body'],
) => Promise<C['endpoints'][K]['response']>;
};

export type IControllerPaths<C extends RestContract> = {
[K in keyof C['endpoints']]: C['endpoints'][K]['path'];
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,27 @@ export const Login = () => {
};

return (
<form onSubmit={authenticateUser} className="w-1/2 m-auto">
<Input
label="Email"
id="username"
nativeInputProps={{
name: "username",
type: "email",
}}
/>
<Input
label="Mot de passe"
id="password"
nativeInputProps={{
name: "password",
type: "password",
}}
/>
<Button type="submit">Se connecter</Button>
</form>
<div id="login-layout" className="h-full flex place-items-center">
<form onSubmit={authenticateUser} className="w-1/2 m-auto">
<Input
label="Email"
id="username"
nativeInputProps={{
name: "username",
type: "email",
}}
/>
<Input
label="Mot de passe"
id="password"
nativeInputProps={{
name: "password",
type: "password",
}}
/>
<Button type="submit">Se connecter</Button>
</form>
</div>
);
};

Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/src/layout/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AppHeader } from "./AppHeader";

export const PageLayout: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div className="flex flex-col h-screen w-full">
<div className="flex flex-col h-screen">
<AppHeader />
<main className="flex-grow flex">
<div className="flex-grow">{children}</div>
Expand Down
Loading

0 comments on commit 7198f0c

Please sign in to comment.