Skip to content

Feat/829 pdf backend #937

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

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/.aws-deployer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ jobs:
keycloak_realm: ${{ secrets.KEYCLOAK_REALM }}
keycloak_client_id: ${{ secrets.KEYCLOAK_CLIENT_ID }}
keycloak_issuer: ${{ secrets.KEYCLOAK_ISSUER }}
dam_url: ${{ secrets.DAM_URL }}
dam_private_key: ${{ secrets.DAM_PRIVATE_KEY }}
dam_user: ${{ secrets.DAM_USER }}
dam_rst_pdf_collection_id: ${{ secrets.DAM_RST_PDF_COLLECTION_ID }}
run: |
terragrunt run-all ${{ inputs.command }} --terragrunt-non-interactive

Expand All @@ -128,6 +132,10 @@ jobs:
keycloak_realm: ${{ secrets.KEYCLOAK_REALM }}
keycloak_client_id: ${{ secrets.KEYCLOAK_CLIENT_ID }}
keycloak_issuer: ${{ secrets.KEYCLOAK_ISSUER }}
dam_url: ${{ secrets.DAM_URL }}
dam_private_key: ${{ secrets.DAM_PRIVATE_KEY }}
dam_user: ${{ secrets.DAM_USER }}
dam_rst_pdf_collection_id: ${{ secrets.DAM_RST_PDF_COLLECTION_ID }}
run: |
terragrunt output -json > outputs.json
cat outputs.json
Expand Down
5 changes: 5 additions & 0 deletions admin/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE=postgres
POSTGRES_SCHEMA=rst
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DATABASE}?schema=${POSTGRES_SCHEMA}
AWS_EMF_ENVIRONMENT=Local
DAM_URL=https://dam.lqc63d-test.nimbus.cloud.gov.bc.ca
DAM_PRIVATE_KEY=<private-key>
DAM_USER=<user>
DAM_RST_PDF_COLLECTION_ID=739
25 changes: 17 additions & 8 deletions admin/backend/package-lock.json

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

5 changes: 4 additions & 1 deletion admin/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/terminus": "^11.0.0",
"@prisma/client": "^6.8.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"axios": "^1.9.0",
"@nestjs/terminus": "^11.0.0",
"form-data": "^4.0.3",
"helmet": "^8.1.0",
"nest-winston": "^1.10.2",
"passport": "^0.7.0",
Expand All @@ -48,6 +50,7 @@
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^3.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
select array_agg(public.st_asgeojson(rmfg.geometry)) as spatial_feature_geometry,
public.st_asgeojson(rsp.geometry) as site_point_geometry
from rst.recreation_map_feature rmf
inner join rst.recreation_map_feature_geom rmfg using (rmf_skey)
left join rst.recreation_site_point rsp on rmf.rec_resource_id = rsp.rec_resource_id
where rmf.rec_resource_id = $1
and rmf.retirement_date is null
group by rsp.geometry;
2 changes: 2 additions & 0 deletions admin/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PassportModule } from "@nestjs/passport";
import { AuthModule } from "./auth";
import { PrismaService } from "./prisma.service";
import { RecreationResourceModule } from "@/recreation-resource/recreation-resource.module";
import { ResourceDocsModule } from "./resource-docs/resource-docs.module";

@Module({
imports: [
Expand All @@ -16,6 +17,7 @@ import { RecreationResourceModule } from "@/recreation-resource/recreation-resou
AuthModule,
TerminusModule,
RecreationResourceModule,
ResourceDocsModule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be better to have this imported under RecreationResourceModule

],
controllers: [AppController, HealthController],
providers: [AppService, PrismaService],
Expand Down
107 changes: 107 additions & 0 deletions admin/backend/src/dam-api/dam-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import axios from "axios";
import { createHash } from "crypto";
import { Readable } from "stream";
import FormData from "form-data";

declare const window: any;
const NodeFormData =
// eslint-disable-next-line @typescript-eslint/no-require-imports
typeof window === "undefined" ? require("form-data") : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for? window will always be undefined right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a technique used to test, because running vitest with form data we can instantiate while testing.


const damUrl = `${process.env.DAM_URL}/api/?`;
const private_key = process.env.DAM_PRIVATE_KEY ?? "";
const user = process.env.DAM_USER ?? "";
const pdfCollectionId = process.env.DAM_RST_PDF_COLLECTION_ID ?? "";

function sign(query) {
return createHash("sha256").update(`${private_key}${query}`).digest("hex");
}

function createFormData(params) {
const queryString = new URLSearchParams(params).toString();
const signature = sign(queryString);
const formData =
typeof window === "undefined" ? new NodeFormData() : new FormData();
formData.append("query", queryString);
formData.append("sign", signature);
formData.append("user", user);
return formData;
}

async function axiosPost(formData) {
return await axios
.post(damUrl, formData, {
headers: formData.getHeaders(),
})
.then((res) => {
return res.data;
})
.catch((err) => {
throw err;
});
}

export async function createResource() {
const params: any = {
user,
function: "create_resource",
resource_type: 1,
archive: 0,
};
const formData = createFormData(params);

return await axiosPost(formData);
}

export async function getResourcePath(resource: string) {
const params: any = {
user,
function: "get_resource_all_image_sizes",
resource,
};
const formData = createFormData(params);

return await axiosPost(formData);
}

export async function addResourceToCollection(resource: string) {
const params: any = {
user,
function: "add_resource_to_collection",
resource,
collection: pdfCollectionId,
};
const formData = createFormData(params);

return await axiosPost(formData);
}

export async function uploadFile(ref: string, file: Express.Multer.File) {
const params: any = {
user,
function: "upload_multipart",
ref,
no_exif: 1,
revert: 0,
};
const stream = Readable.from(file.buffer);

const formData = createFormData(params);
formData.append("file", stream, {
filename: file.originalname,
contentType: file.mimetype,
});

return await axiosPost(formData);
}

export async function deleteResource(resource: string) {
const params: any = {
user,
function: "delete_resource",
resource,
};
const formData = createFormData(params);

return await axiosPost(formData);
}
7 changes: 7 additions & 0 deletions admin/backend/src/prisma.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Module({
providers: [PrismaService],
})
export class PrismaModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,33 @@ export class SuggestionDto {
description: "Name of the recreation resource",
example: "Tamihi Creek",
})
name: string;
name: string | null;

@ApiProperty({
description: "Unique resource ID",
example: "REC12345",
})
rec_resource_id: string;
rec_resource_id: string | null;

@ApiProperty({
description:
"Type of recreation resource (e.g., Recreation site, Recreation trail, etc.)",
example: "RR",
})
recreation_resource_type: string;
recreation_resource_type: string | null;

@ApiProperty({
description: "Resource type code (e.g., RR, IF, etc.)",
example: "RR",
})
recreation_resource_type_code: string;
recreation_resource_type_code: string | null;

@ApiProperty({
description:
"Description of the district (e.g., Chilliwack, Okanagan, etc.)",
example: "Chilliwack",
})
district_description: string;
district_description: string | null;
}

/**
Expand Down
68 changes: 68 additions & 0 deletions admin/backend/src/resource-docs/dto/recreation-resource-doc.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty } from "class-validator";

/**
* Enum representing available image size options for the recreation API
*/
export enum RecreationResourceDocCode {
/** Recreation Map */
RM = "RM",
}

export class RecreationResourceDocDto {
@ApiProperty({
description: "Reference ID for the image",
example: "1000",
})
ref_id?: string;

@ApiProperty({
description: "Doc title",
example: "Campbell river site map",
})
title: string | null;

@ApiProperty({
description: "rec_resource_id",
})
rec_resource_id: string | null;

@ApiProperty({
description: "doc link",
})
url?: string;

@ApiProperty({
description: "Document code that indicates the type of document",
enum: RecreationResourceDocCode,
})
doc_code: RecreationResourceDocCode | null;

@ApiProperty({
description: "Description of the document code",
})
doc_code_description?: string;

@ApiProperty({
description: "File extension",
})
extension: string | null;
}

export class RecreationResourceDocBodyDto {
@ApiProperty({
description: "Doc title",
example: "Campbell river site map",
})
@IsNotEmpty()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoud we add length and regex constraints here?

title: string;
}

export class FileUploadDto {
@ApiProperty({
type: "string",
format: "binary",
description: "File to upload",
})
file: any;
}
Loading
Loading