Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): integrate OpenAPI client generator for frontend #286

Merged
merged 1 commit into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/.analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
dir: ${{ matrix.dir }}
node_version: "22"
sonar_args: >
-Dsonar.exclusions=**/coverage/**,**/node_modules/**,**/*spec.ts,**/*test.ts,**/*test.tsx
-Dsonar.exclusions=**/coverage/**,**/node_modules/**,**/*spec.ts,**/*test.ts,**/*test.tsx,src/service/recreation-resource/**
-Dsonar.organization=bcgov-sonarcloud
-Dsonar.projectKey=bcgov-sonarcloud_nr-rec-resources-${{ matrix.dir }}
-Dsonar.sources=src
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
*/coverage



Expand All @@ -221,6 +222,7 @@ env.bak/
venv.bak/
# End of https://www.toptal.com/developers/gitignore/api/node,java,python,go
.idea
.run
*.key
*.pem
*.pub
Expand All @@ -239,3 +241,10 @@ venv.bak/
# Playwright
playwright-report/
test-results/

# Ignoring all the extra files generated by openapi-generator-cli
frontend/src/service/recreation-resource/.gitignore
frontend/src/service/recreation-resource/.npmignore
frontend/src/service/recreation-resource/.openapi-generator-ignore
frontend/src/service/recreation-resource/git_push.sh
frontend/src/service/recreation-resource/.openapi-generator
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,57 @@ npm install
npm run dev
```

### **OpenAPI/Swagger Documentation**

The backend uses NestJS's built-in Swagger module (@nestjs/swagger) to
automatically generate OpenAPI documentation from TypeScript decorators.

#### **Swagger Decorators**

Decorators are used throughout the codebase to provide metadata for API
documentation:

- @ApiTags() - Groups related endpoints together
- @ApiOperation() - Describes what an endpoint does
- @ApiResponse() - Documents possible response types
- @ApiProperty() - Documents DTO properties and their types

Example usage in a controller:

```tsx
@ApiTags("parks")
@Controller("parks")
export class ParksController {
@Get()
@ApiOperation({ summary: "Get all parks" })
@ApiResponse({
status: 200,
description: "List of parks returned",
type: [ParkDto],
})
findAll(): Promise<ParkDto[]> {
return this.parksService.findAll();
}
}
```

#### **Accessing Generated Documentation**

When running the backend server, Swagger UI is available at:

`http://localhost:3000/api/docs`

The raw OpenAPI specification can be accessed at:

`http://localhost:3000/api/docs-json`

This documentation is automatically generated from the TypeScript code and is
used to:

- Provide interactive API documentation through Swagger UI
- Generate TypeScript client types using openapi-generator
- Ensure API consistency and type safety across the frontend and backend

### Frontend

Create an `.env` file in the `frontend` directory using the example in
Expand All @@ -75,6 +126,32 @@ npm run dev

Navigate to `http://localhost:3000` in your web browser to view the application.

### Generate Client Library

#### Prerequisites

Install Java Development Kit (JDK) 17:

```bash
brew install openjdk@17
```

#### Generate TypeScript Axios Client

Run the following command to generate the TypeScript client library from your
OpenAPI specification:

```bash
npx openapi-generator-cli generate -i http://localhost:3000/api/docs-json -g typescript-axios -o src/service/recreation-resource --skip-validate-spec
```

This command will:

- Generate TypeScript client code using Axios
- Use the OpenAPI spec from your local NestJS server which should be running on
port **3000**
- Output the generated code to `src/service/recreation-resource` directory

## Pre-commit hooks

Pre-commit is set up to run checks for linting, formatting, and secrets.
Expand Down
1 change: 0 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PaginatedRecreationResourceDto } from "./paginated-recreation-resouce.dto";
import { RecreationResourceDto } from "./recreation-resource.dto";

const recResourceArrayResolved: RecreationResourceDto[] = [
new RecreationResourceDto(),
new RecreationResourceDto(),
new RecreationResourceDto(),
new RecreationResourceDto(),
];

describe("PaginatedRecreationResourceDto", () => {
it("should validate the structure of paginated response", () => {
const paginatedResponse = new PaginatedRecreationResourceDto();
paginatedResponse.data = recResourceArrayResolved;
paginatedResponse.total = 4;
paginatedResponse.page = 1;
paginatedResponse.limit = 10;

expect(paginatedResponse).toHaveProperty("data");
expect(paginatedResponse).toHaveProperty("total");
expect(paginatedResponse).toHaveProperty("page");
expect(Array.isArray(paginatedResponse.data)).toBe(true);
expect(typeof paginatedResponse.total).toBe("number");
expect(typeof paginatedResponse.page).toBe("number");
});

it("should handle empty data array", () => {
const emptyPaginatedResponse = new PaginatedRecreationResourceDto();
emptyPaginatedResponse.data = [];
emptyPaginatedResponse.total = 0;
emptyPaginatedResponse.page = 1;
emptyPaginatedResponse.limit = 10;

expect(emptyPaginatedResponse.data).toHaveLength(0);
expect(emptyPaginatedResponse.total).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApiProperty } from "@nestjs/swagger";
import { RecreationResourceDto } from "./recreation-resource.dto";

export class PaginatedRecreationResourceDto {
@ApiProperty({ type: [RecreationResourceDto] })
data: RecreationResourceDto[];

@ApiProperty()
total: number;

@ApiProperty()
page: number;

@ApiProperty()
limit: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
RecreationActivityDto,
RecreationStatusDto,
RecreationResourceDto,
} from "./recreation-resource.dto";

describe("Recreation DTOs", () => {
describe("RecreationActivityDto", () => {
it("should create a valid RecreationActivityDto instance", () => {
const activity = new RecreationActivityDto();
activity.recreation_activity_code = "HIKING";
activity.description = "Hiking trails available for all skill levels";

expect(activity instanceof RecreationActivityDto).toBeTruthy();
expect(activity.recreation_activity_code).toBeDefined();
expect(activity.description).toBeDefined();
});
});

describe("RecreationStatusDto", () => {
it("should create a valid RecreationStatusDto", () => {
const status = new RecreationStatusDto();
status.status_code = "CLOSED";
status.comment = "Temporary closure due to weather conditions";
status.description = "The facility is currently closed to visitors";

expect(status.status_code).toBeDefined();
expect(status.description).toBeDefined();
});

it("should allow null comment", () => {
const status: RecreationStatusDto = {
status_code: "OPEN",
comment: null,
description: "The facility is open",
};

expect(status.comment).toBeNull();
});
});

describe("RecreationResourceDto", () => {
it("should create a valid RecreationResourceDto", () => {
const resource: RecreationResourceDto = {
rec_resource_id: "rec-123-abc",
name: "Evergreen Valley Campground",
description:
"A scenic campground nestled in the heart of Evergreen Valley",
site_location: "123 Forest Road, Mountain View, CA 94043",
recreation_activity: [
{
recreation_activity_code: "HIKING",
description: "Hiking trails available for all skill levels",
},
],
recreation_status: {
status_code: "OPEN",
comment: null,
description: "The facility is open",
},
};

expect(resource.rec_resource_id).toBeDefined();
expect(resource.name.length).toBeGreaterThanOrEqual(1);
expect(resource.name.length).toBeLessThanOrEqual(100);
expect(Array.isArray(resource.recreation_activity)).toBeTruthy();
expect(resource.recreation_status).toBeDefined();
});

it("should allow null description", () => {
const resource: RecreationResourceDto = {
rec_resource_id: "rec-123-abc",
name: "Test Resource",
description: null,
site_location: "Test Location",
recreation_activity: [],
recreation_status: {
status_code: "OPEN",
comment: null,
description: "Open",
},
};

expect(resource.description).toBeNull();
});
});
});
67 changes: 52 additions & 15 deletions backend/src/recreation-resource/dto/recreation-resource.dto.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
import { ApiProperty } from "@nestjs/swagger";

export class RecreationActivityDto {
@ApiProperty({
description: "Unique code identifying the recreation activity",
example: "HIKING",
})
recreation_activity_code: string;

@ApiProperty({
description: "Detailed description of the activity",
example: "Hiking trails available for all skill levels",
})
description: string;
}

export class RecreationStatusDto {
@ApiProperty({
description: "Status code of the resource",
})
status_code: string;

@ApiProperty({
description: "Additional status information",
example: "Temporary closure due to weather conditions",
nullable: true,
})
comment: string;

@ApiProperty({
description: "Detailed status description",
example: "The facility is currently closed to visitors",
})
description: string;
}

export class RecreationResourceDto {
@ApiProperty({
description: "The ID of the Recreation Resource",
description: "Unique identifier of the Recreation Resource",
example: "rec-123-abc",
format: "uuid",
})
rec_resource_id: string;

@ApiProperty({
description: "The name of the Recreation Resource",
description: "Official name of the Recreation Resource",
example: "Evergreen Valley Campground",
minLength: 1,
maxLength: 100,
})
name: string;

@ApiProperty({
description: "The description of the Recreation Resource",
description: "Detailed description of the Recreation Resource",
example: "A scenic campground nestled in the heart of Evergreen Valley",
nullable: true,
})
description: string;

@ApiProperty({
description: "The location of the Recreation Resource",
description: "Physical location of the Recreation Resource",
example: "123 Forest Road, Mountain View, CA 94043",
})
site_location: string;

@ApiProperty({
description: "The list of available activities at the Recreation Resource",
description: "List of recreational activities available at this resource",
type: [RecreationActivityDto],
})
recreation_activity: {
recreation_activity_code: string;
description: string;
}[];
recreation_activity: RecreationActivityDto[];

@ApiProperty({
description: "The status of the Recreation Resource",
description: "Current operational status of the Recreation Resource",
type: RecreationStatusDto,
})
recreation_status: {
status_code: string;
comment: string;
description: string;
};
recreation_status: RecreationStatusDto;
}
Loading
Loading