Skip to content

Commit 666709a

Browse files
author
jimmypalelil
committed
feat(rec-suggestions): added api and suggestion box ui with minimal styling
1 parent 4cb2e25 commit 666709a

File tree

66 files changed

+3150
-57
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3150
-57
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,5 @@ backend-el/*.iml
263263
k6_results/
264264
/public/backend/coverage
265265
/public/frontend/coverage
266+
admin/frontend/coverage/**/*
267+
admin/backend/coverage/**/*

admin/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ system.
77

88
- [Doc link](docs/auth/auth.md) - Details about the authentication
99
implementation using BC Gov SSO (Keycloak)
10+
11+
## Data Fetching
12+
13+
This project uses
14+
[TanStack React Query](https://tanstack.com/query/latest/docs/framework/react/overview)
15+
for data fetching and caching.
16+
The `QueryClientProvider` is set up in `src/App.tsx`.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileOverview
3+
* Script to copy Prisma-generated files (.js and .d.ts) containing functions and types to
4+
* a target directory
5+
*/
6+
7+
/**
8+
* Target directory where files will be copied
9+
*/
10+
const TARGET_DIR = "./src/prisma-generated-sql";
11+
12+
import { execSync } from "child_process";
13+
import { join } from "path";
14+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "fs";
15+
16+
// Get the installation location of @prisma/client
17+
const output = execSync("npm list --depth 1 --parseable @prisma/client", {
18+
encoding: "utf8",
19+
});
20+
21+
// Find the specific line containing @prisma/client path
22+
const prismaClientLine = output
23+
.split("\n")
24+
.find((line) => line.includes("@prisma/client"));
25+
26+
if (!prismaClientLine) {
27+
throw new Error("@prisma/client not found");
28+
}
29+
30+
// Construct the path to Prisma's SQL files
31+
const prismaClientPath = join(
32+
prismaClientLine.replace(/@prisma\/client$/, ""),
33+
".prisma",
34+
"client",
35+
"sql",
36+
);
37+
38+
// Create target directory if it doesn't exist
39+
if (!existsSync(TARGET_DIR)) {
40+
mkdirSync(TARGET_DIR, { recursive: true });
41+
}
42+
43+
/**
44+
* Copy relevant files to target directory
45+
* Includes only .js and .d.ts files, excluding edge-specific files
46+
*/
47+
readdirSync(prismaClientPath)
48+
.filter(
49+
(file) =>
50+
(file.endsWith(".js") || file.endsWith(".d.ts")) &&
51+
!file.includes(".edge."),
52+
)
53+
.forEach((file) => {
54+
const sourcePath = join(prismaClientPath, file);
55+
const targetPath = join(TARGET_DIR, file);
56+
57+
copyFileSync(sourcePath, targetPath);
58+
});

admin/backend/package-lock.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

admin/backend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
1818
"test": "vitest --exclude e2e",
1919
"test:cov": "vitest run --coverage --exclude e2e",
20-
"test:e2e": "vitest --dir e2e"
20+
"test:e2e": "vitest --dir e2e",
21+
"copy-prisma-generated-sql": "prisma generate --sql && node build-scripts/copy-prisma-generated-sql.js"
2122
},
2223
"dependencies": {
2324
"@nestjs/cli": "^11.0.0",
@@ -29,6 +30,8 @@
2930
"@nestjs/swagger": "^11.2.0",
3031
"@nestjs/terminus": "^11.0.0",
3132
"@prisma/client": "^6.8.2",
33+
"class-transformer": "^0.5.1",
34+
"class-validator": "^0.14.0",
3235
"helmet": "^8.1.0",
3336
"nest-winston": "^1.10.2",
3437
"passport": "^0.7.0",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SELECT name, rec_resource_id,
2+
recreation_resource_type,
3+
recreation_resource_type_code,
4+
district_description
5+
FROM recreation_resource_search_view
6+
WHERE similarity(name, $1) > 0.1
7+
OR similarity(rec_resource_id, $1) > 0.3
8+
ORDER BY GREATEST(
9+
similarity(name, $1),
10+
similarity(rec_resource_id, $1)
11+
) DESC
12+
LIMIT 30;

admin/backend/src/app.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { AppService } from "./app.service";
33
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
44
import { AuthGuard } from "@nestjs/passport";
55
import {
6-
AUTH_RST_ADMIN_ROLE,
76
AUTH_STRATEGY,
87
AuthRoles,
98
AuthRolesGuard,
9+
RecreationResourceAuthRole,
1010
ROLE_MODE,
1111
} from "./auth";
1212

@@ -18,7 +18,7 @@ export class AppController {
1818
@Get()
1919
@ApiBearerAuth(AUTH_STRATEGY.KEYCLOAK)
2020
@UseGuards(AuthGuard(AUTH_STRATEGY.KEYCLOAK), AuthRolesGuard)
21-
@AuthRoles([AUTH_RST_ADMIN_ROLE], ROLE_MODE.ALL)
21+
@AuthRoles([RecreationResourceAuthRole.RST_ADMIN], ROLE_MODE.ALL)
2222
getHello(): string {
2323
return this.appService.getHello();
2424
}

admin/backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { HealthController } from "./health.controller";
77
import { PassportModule } from "@nestjs/passport";
88
import { AuthModule } from "./auth";
99
import { PrismaService } from "./prisma.service";
10+
import { RecreationResourceModule } from "@/recreation-resource/recreation-resource.module";
1011

1112
@Module({
1213
imports: [
1314
ConfigModule.forRoot({ isGlobal: true }),
1415
PassportModule,
1516
AuthModule,
1617
TerminusModule,
18+
RecreationResourceModule,
1719
],
1820
controllers: [AppController, HealthController],
1921
providers: [AppService, PrismaService],

admin/backend/src/app.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
66
import helmet from "helmet";
77
import { VersioningType } from "@nestjs/common";
88
import { AUTH_STRATEGY } from "./auth";
9+
import { AllExceptionsFilter } from "@/common/filters/all-exceptions.filter";
10+
import { globalValidationPipe } from "@/config/validation-pipe.config";
911

1012
/**
11-
*
13+
* Bootstrap function to initialize the NestJS application.
1214
*/
1315
export async function bootstrap() {
1416
const app: NestExpressApplication =
@@ -20,6 +22,11 @@ export async function bootstrap() {
2022
app.set("trust proxy", 1);
2123
app.enableShutdownHooks();
2224
app.setGlobalPrefix("api");
25+
app.useGlobalPipes(globalValidationPipe);
26+
27+
// global filters
28+
app.useGlobalFilters(new AllExceptionsFilter());
29+
2330
app.enableVersioning({
2431
type: VersioningType.URI,
2532
prefix: "v",
@@ -28,7 +35,7 @@ export async function bootstrap() {
2835
.setTitle("Recreation Sites and Trails BC Admin API")
2936
.setDescription("RST Admin API documentation")
3037
.setVersion("1.0")
31-
.addTag("recreation-resource")
38+
.addTag("recreation-resource-admin")
3239
.addBearerAuth(
3340
{
3441
name: "Authorization",

admin/backend/src/auth/auth.constants.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// Metadata keys for decorators
22
export const AUTH_ROLES_KEY = "roles";
33

4-
export const AUTH_RST_ADMIN_ROLE = "rst-admin";
4+
/**
5+
* Enum for authentication roles.
6+
*/
7+
export enum RecreationResourceAuthRole {
8+
RST_ADMIN = "rst-admin",
9+
RST_VIEWER = "rst-viewer",
10+
}
511

612
// Role validation modes
713
export const ROLE_MODE = {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { GenericErrorResponseDto } from "./generic-error-response.dto"; // Import the generic DTO
3+
import { ValidationErrorDetailDto } from "./validation-error-detail.dto";
4+
5+
export class BadRequestResponseDto extends GenericErrorResponseDto {
6+
@ApiProperty({
7+
description:
8+
"An array of detailed validation errors specific to this bad request.",
9+
type: [ValidationErrorDetailDto], // Reference the nested DTO
10+
example: [
11+
{
12+
field: "email",
13+
messages: ["email must be an email"],
14+
},
15+
{
16+
field: "password",
17+
messages: [
18+
"password should not be empty",
19+
"password must be longer than or equal to 6 characters",
20+
],
21+
},
22+
],
23+
})
24+
details: ValidationErrorDetailDto[];
25+
26+
@ApiProperty({
27+
type: "integer",
28+
enum: [400],
29+
})
30+
declare readonly statusCode = 400;
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { HttpStatus } from "@nestjs/common";
3+
4+
export const ErrorHttpStatusCodes = [
5+
HttpStatus.BAD_REQUEST,
6+
HttpStatus.UNAUTHORIZED,
7+
HttpStatus.FORBIDDEN,
8+
HttpStatus.NOT_FOUND,
9+
HttpStatus.INTERNAL_SERVER_ERROR,
10+
] as const;
11+
12+
export class GenericErrorResponseDto {
13+
@ApiProperty({
14+
description: "The HTTP status code of the error response.",
15+
example: 404,
16+
enum: ErrorHttpStatusCodes,
17+
})
18+
statusCode: number;
19+
20+
@ApiProperty({
21+
description: "A general message describing the error.",
22+
example: "Resource Not Found",
23+
})
24+
message: string; // Keep this as string if your generic filter always produces a string message here
25+
26+
@ApiProperty({
27+
description: "The error type or short description of the HTTP status.",
28+
example: "Not Found",
29+
})
30+
error?: string; // Optional, can be derived from status code
31+
32+
@ApiProperty({
33+
description: "The timestamp of when the error occurred (ISO 8601 format).",
34+
example: "2025-06-13T21:13:23.000Z", // Example based on current time
35+
})
36+
timestamp: string;
37+
38+
@ApiProperty({
39+
description: "The request path that caused the error.",
40+
example: "/api/v1/users/invalid-input",
41+
})
42+
path: string;
43+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
3+
export class ValidationErrorDetailDto {
4+
@ApiProperty({
5+
description: "The name of the field that failed validation.",
6+
example: "name",
7+
})
8+
field: string;
9+
10+
@ApiProperty({
11+
description: "An array of error messages for the field.",
12+
type: [String],
13+
example: [
14+
"name must be longer than or equal to 3 characters",
15+
"name should not be empty",
16+
],
17+
})
18+
messages: string[];
19+
}

0 commit comments

Comments
 (0)