From 08705f708af5ce6fcda6aece1a634dd5a0a9d32a Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Tue, 12 Aug 2025 16:01:42 +0000 Subject: [PATCH 01/19] add utils --- src/app/api/upload/csv/route.ts | 2 +- src/app/components/Navbar.tsx | 4 +- src/app/db/documents_schema/documents.ts | 1 + .../documents/DocumentSection.tsx | 2 + .../[propertyId]/documents/DocumentsTable.tsx | 9 ++-- .../[propertyId]/documents/UploadModal.tsx | 44 ++++++++++++++++-- src/app/utils/s3.ts | 45 +++++++++++++++++++ 7 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 src/app/utils/s3.ts diff --git a/src/app/api/upload/csv/route.ts b/src/app/api/upload/csv/route.ts index 5df4189..a996540 100644 --- a/src/app/api/upload/csv/route.ts +++ b/src/app/api/upload/csv/route.ts @@ -27,7 +27,7 @@ export async function POST(request: NextRequest) { const s3 = new S3({ signatureVersion: "v4", region: process.env.PRESIGN_AWS_REGION, - accessKeyId: process.env.PRSIGN_AWS_ACCESS_KEY, + accessKeyId: process.env.PRESIGN_AWS_ACCESS_KEY, secretAccessKey: process.env.PRESIGN_AWS_SECRET_KEY, }); diff --git a/src/app/components/Navbar.tsx b/src/app/components/Navbar.tsx index ae3e603..746bc40 100644 --- a/src/app/components/Navbar.tsx +++ b/src/app/components/Navbar.tsx @@ -42,8 +42,8 @@ function Nav({ userImage }: { userImage: string }) {
{makeLink("/home", "Home")} - {makeLink("/due-considerations", "Due Considerations")} - {makeLink("/eco-spreadsheet", "Eco Spreadsheet")} + {/* {makeLink("/due-considerations", "Due Considerations")} */} + {/* {makeLink("/eco-spreadsheet", "Eco Spreadsheet")} */} {makeLink("/help", "Help")}
diff --git a/src/app/db/documents_schema/documents.ts b/src/app/db/documents_schema/documents.ts index 11006b5..6948d09 100644 --- a/src/app/db/documents_schema/documents.ts +++ b/src/app/db/documents_schema/documents.ts @@ -9,6 +9,7 @@ export const reportType: [string, ...string[]] = [ "U_VALUE_CALCULATOR_REPORT", "OVERWRITING_U_VALUE_DECLARATION_FORM", "OSMOSIS_CONDITION_PAS_2035_REPORT", + "DOMNA_CONDITION_PAS_2035_REPORT", ]; const reportTypeEnum = pgEnum("report_type", reportType); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx index 5b52f10..08840fc 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx @@ -21,6 +21,8 @@ const descriptions: Record = { OVERWRITING_U_VALUE_DECLARATION_FORM: "Signed form for overwriting U-values", OSMOSIS_CONDITION_PAS_2035_REPORT: "Osmosis-generated PAS 2035 Condition Report", + DOMNA_CONDITION_PAS_2035_REPORT: + "Domna-generated PAS 2035 Condition Report" }; export const DocumentSection = ({ diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx index 52c40d2..403aee7 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -63,6 +63,7 @@ export const DocumentsTable: React.FC = ({ const handleUpload = () => { // Handle the upload logic here console.log("Upload button clicked"); + console.log("Junte was here"); }; // We split out the various document types. Filter all of the quidos pre-site notes @@ -70,8 +71,8 @@ export const DocumentsTable: React.FC = ({ (doc) => doc.documentType === "QUIDOS_PRESITE_NOTE" ); - const osmosisConditionReport = documents.filter( - (doc) => doc.documentType === "OSMOSIS_CONDITION_PAS_2035_REPORT" + const domnaConditionReport = documents.filter( + (doc) => doc.documentType === "DOMNA_CONDITION_PAS_2035_REPORT" ); const floors = documents.filter((doc) => doc.documentType === "FLOOR_PLAN"); @@ -97,9 +98,9 @@ export const DocumentsTable: React.FC = ({ diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx index 53ef373..9b4b515 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx @@ -24,6 +24,31 @@ const titles: Record = { QUIDOS_PRESITE_NOTE: "RdSAP Summary Report", }; +async function generatePresignedUrls({ + key, + bucket, + contentType, + expiresInSeconds, +}: { + key: string; + bucket: string; + contentType: string; + expiresInSeconds: number; +}) { + const body = JSON.stringify({ key, bucket, contentType, expiresInSeconds }); + console.log("bucket is ", bucket); + const presignedResponse = await fetch("/api/upload/s3_bucket_presigned_url", { + method: "POST", + body, + }); + + if (!presignedResponse.ok) { + throw new Error("Network response was not ok"); + } + + return presignedResponse.json(); +} + export const UploadModal = ({ open, onClose, @@ -33,6 +58,20 @@ export const UploadModal = ({ const [uploadFiles, setUploadFiles] = useState([]); const [buttonDisabled, setButtonDisabled] = useState(true); + async function handleS3Upload() { + console.log("Get Presigned url in a specific bucket location") + + const { url } = await generatePresignedUrls({ + key: "foo/test/trololol", // path in bucket + bucket: process.env.RETROFIT_ENERGY_ASSESSMENTS_BUCKET || "back up", //s3 bucket location + // bucket: "retrofit-energy-assessments-dev", //s3 bucket location + contentType: "application/pdf", + expiresInSeconds: 5 * 60, + }); + console.log("URl is ", url); + onClose(); //probably khalim call back to update the front end properl + } + function handleInputOnChange(e: React.ChangeEvent) { if (e.target.files) { const filesArray = Array.from(e.target.files); @@ -86,10 +125,7 @@ export const UploadModal = ({ Cancel
- +
From ff4bf9af6e4b5bf9d85fcda564c72e769e698fa0 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 14 Aug 2025 17:32:09 +0000 Subject: [PATCH 06/19] save! --- src/app/db/documents_schema/documents.ts | 75 ------- src/app/db/documents_schema/relations.ts | 22 -- .../insert_data_to_uploaded_files/route.ts | 59 ++++++ .../connection.ts} | 6 +- src/app/db/surveyDB/schema/documents.ts | 55 +++++ src/app/db/surveyDB/schema/surveyDB.ts | 20 ++ src/app/db/surveyDB/utils/utility.ts | 29 +++ .../documents/DocumentSection.tsx | 90 +-------- .../[propertyId]/documents/DocumentsTable.tsx | 152 ++------------ .../[propertyId]/documents/UploadModal.tsx | 191 ++++++++++-------- .../[propertyId]/documents/page.tsx | 37 ---- 11 files changed, 293 insertions(+), 443 deletions(-) delete mode 100644 src/app/db/documents_schema/documents.ts delete mode 100644 src/app/db/documents_schema/relations.ts create mode 100644 src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts rename src/app/db/{documents_db.ts => surveyDB/connection.ts} (73%) create mode 100644 src/app/db/surveyDB/schema/documents.ts create mode 100644 src/app/db/surveyDB/schema/surveyDB.ts create mode 100644 src/app/db/surveyDB/utils/utility.ts diff --git a/src/app/db/documents_schema/documents.ts b/src/app/db/documents_schema/documents.ts deleted file mode 100644 index 6948d09..0000000 --- a/src/app/db/documents_schema/documents.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; -import { pgEnum } from "drizzle-orm/pg-core"; -import { InferModel } from "drizzle-orm"; - -export const reportType: [string, ...string[]] = [ - "QUIDOS_PRESITE_NOTE", - "CHARTED_SURVEYOR_REPORT", - "ENERGY_PERFORMANCE_REPORT", - "U_VALUE_CALCULATOR_REPORT", - "OVERWRITING_U_VALUE_DECLARATION_FORM", - "OSMOSIS_CONDITION_PAS_2035_REPORT", - "DOMNA_CONDITION_PAS_2035_REPORT", -]; - -const reportTypeEnum = pgEnum("report_type", reportType); - -export const companyInfo = pgTable("companyinfo", { - id: uuid("id").primaryKey().defaultRandom(), - address: text("address").notNull(), - tradingName: text("trading_name").notNull(), - postCode: text("post_code").notNull(), - faxNumber: text("fax_number"), - relatedPartyDisclosure: text("related_party_disclosure"), -}); - -// --- assessorInfo table --- -export const assessorInfo = pgTable("assessorinfo", { - id: uuid("id").primaryKey().defaultRandom(), - accreditationNumber: text("accreditation_number").notNull(), - name: text("name").notNull(), - phoneNumber: text("phone_number"), - emailAddress: text("email_address"), - companyId: uuid("company_id").references(() => companyInfo.id), -}); - -// --- buildings table --- -export const buildings = pgTable("buildings", { - id: uuid("id").primaryKey().defaultRandom(), - address: text("address").notNull(), - postcode: text("postcode").notNull(), - uprn: text("UPRN").notNull(), - landlordId: text("landlord_id").notNull(), - domnaId: text("domna_id").notNull(), -}); - -// --- documents table --- -export const documents = pgTable("documents", { - id: uuid("id").primaryKey().defaultRandom(), - - authorId: uuid("assessor_id") - .notNull() - .references(() => assessorInfo.id), - createdAt: timestamp("created_at", { withTimezone: true }).notNull(), - documentType: reportTypeEnum("document_type").notNull(), - - buildingId: uuid("building_id") - .notNull() - .references(() => buildings.id), - targetTable: text("target_table").notNull(), - targetId: uuid("target_id").notNull(), -}); - -export type Building = InferModel; -export type Document = InferModel; -export type AssessorInfo = InferModel; - -export type DocumentWithAuthor = Document & { - author: AssessorInfo; -}; - -export type BuildingWithDocuments = Building & { - documents: DocumentWithAuthor[]; -}; - -export type ReportType = (typeof reportType)[number]; diff --git a/src/app/db/documents_schema/relations.ts b/src/app/db/documents_schema/relations.ts deleted file mode 100644 index 927be39..0000000 --- a/src/app/db/documents_schema/relations.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { pgTable, serial, text, integer } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm"; -import { - buildings, - documents, - assessorInfo, -} from "@/app/db/documents_schema/documents"; - -export const buildingsRelations = relations(buildings, ({ many }) => ({ - documents: many(documents), -})); - -export const documentsRelations = relations(documents, ({ one }) => ({ - building: one(buildings, { - fields: [documents.buildingId], - references: [buildings.id], - }), - author: one(assessorInfo, { - fields: [documents.authorId], - references: [assessorInfo.id], - }), -})); diff --git a/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts new file mode 100644 index 0000000..be51420 --- /dev/null +++ b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts @@ -0,0 +1,59 @@ +// app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts +import { NextResponse } from "next/server"; +import { z, ZodError } from "zod"; +import { insertUploadedFile } from "../../utils/utility"; // ensure path is correct +import { ReportTypeSchema } from "../../schema/documents"; + +export const runtime = "nodejs"; + +// Helper: "" or whitespace -> undefined (so optional() can drop it) +const emptyToUndefined = (v: unknown) => { + if (typeof v === "string" && v.trim() === "") return undefined; + return v; +}; + +const BodySchema = z.object({ + s3JsonUri: z.preprocess( + emptyToUndefined, + z.string().url().optional() + ), + s3FileUri: z.string().url(), + docType: ReportTypeSchema, + // Required upload timestamp (coerce from ISO string) + s3FileUploadTimestamp: z.coerce.date(), + // Optional JSON timestamp: allow "" -> undefined, then coerce to Date + s3JsonUploadTimestamp: z.preprocess( + emptyToUndefined, + z.coerce.date().optional() + ), + uprn: z.string().min(1), +}); + +export async function POST(req: Request) { + try { + const parsed = BodySchema.parse(await req.json()); + + const row = await insertUploadedFile({ + s3JsonUri: parsed.s3JsonUri, // undefined -> util converts to null + s3FileUri: parsed.s3FileUri, + docType: parsed.docType, + s3FileUploadTimestamp: parsed.s3FileUploadTimestamp, + s3JsonUploadTimestamp: parsed.s3JsonUploadTimestamp, // undefined -> util converts to null + uprn: parsed.uprn, + }); + + return NextResponse.json(row, { status: 201 }); + } catch (e) { + if (e instanceof ZodError) { + return NextResponse.json( + { error: "Invalid payload", details: e.flatten() }, + { status: 400 } + ); + } + console.error(e); + return NextResponse.json( + { error: "Failed to insert uploaded_file table in surveyDB" }, + { status: 500 } + ); + } +} diff --git a/src/app/db/documents_db.ts b/src/app/db/surveyDB/connection.ts similarity index 73% rename from src/app/db/documents_db.ts rename to src/app/db/surveyDB/connection.ts index 9a6af2c..b575361 100644 --- a/src/app/db/documents_db.ts +++ b/src/app/db/surveyDB/connection.ts @@ -1,8 +1,7 @@ // db.ts import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; -import * as documentsSchema from "@/app/db/documents_schema/documents"; -import * as relations from "@/app/db/documents_schema/relations"; +import * as documentsSchema from "@/app/db/surveyDB/schema/surveyDB"; export const pool = new Pool({ host: process.env.DOCUMENTS_DB_HOST, @@ -19,9 +18,8 @@ export const pool = new Pool({ const schema = { ...documentsSchema, - ...relations, }; -export const documentsDB = drizzle(pool, { +export const surveyDB = drizzle(pool, { schema: schema, }); diff --git a/src/app/db/surveyDB/schema/documents.ts b/src/app/db/surveyDB/schema/documents.ts new file mode 100644 index 0000000..4fbf05e --- /dev/null +++ b/src/app/db/surveyDB/schema/documents.ts @@ -0,0 +1,55 @@ +// Enum values copied from the backend (Drizzle + Python) +import { z } from "zod"; + +export const REPORT_TYPES = [ +// "quidos_presite_note", +// "charted_surveyor_report", +// "u_value_calculator_report", +// "overwriting_u_value_declaration_form", + "osmosis_condition_pas_2035_report", +// "warm_homes_condition_pas_2035_report", +// "energy_performance_report_with_data", + "energy_performance_report_summary_information", + "lodgement_xml_needed_for_lodgement_to_like_trademark", + "reduce_xml_needed_to_generate_full_sap_xml", + "full_xml_needed_for_co_ordination", +// "floor_plan", +// "occupancy_assessment", +] as const; + +export type ReportType = (typeof REPORT_TYPES)[number]; + +// Map reportType → title for UI +export const documentTypeTitles: Record = { +// quidos_presite_note: "RdSAP Summary Report", +// charted_surveyor_report: "Chartered Surveyor Report", +// u_value_calculator_report: "U-Value Calculator Report", +// overwriting_u_value_declaration_form: "Overwriting U-Value Declaration Form", + osmosis_condition_pas_2035_report: "Osmosis Condition Report (PAS 2035)", +// warm_homes_condition_pas_2035_report: "Warm Homes PAS 2035 Report", +// energy_performance_report_with_data: "EPC Report With Data", + energy_performance_report_summary_information: "EPC Summary Report", + lodgement_xml_needed_for_lodgement_to_like_trademark: "LIG XML", + reduce_xml_needed_to_generate_full_sap_xml: "RdSAP XML", + full_xml_needed_for_co_ordination: "Full SAP XML", +// floor_plan: "Floor Plan", +// occupancy_assessment: "Occupancy Assessment", +}; + +// Map reportType → accepted file extensions +export const documentTypeFileTypes: Record = { +// quidos_presite_note: ".pdf", +// charted_surveyor_report: ".pdf", +// u_value_calculator_report: ".pdf", +// overwriting_u_value_declaration_form: ".pdf", + osmosis_condition_pas_2035_report: ".pdf", +// warm_homes_condition_pas_2035_report: ".pdf", +// energy_performance_report_with_data: ".pdf", + energy_performance_report_summary_information: ".pdf", + lodgement_xml_needed_for_lodgement_to_like_trademark: ".xml", + reduce_xml_needed_to_generate_full_sap_xml: ".xml", + full_xml_needed_for_co_ordination: ".xml", +// floor_plan: ".pdf", +// occupancy_assessment: ".pdf", +}; +export const ReportTypeSchema = z.enum(REPORT_TYPES); \ No newline at end of file diff --git a/src/app/db/surveyDB/schema/surveyDB.ts b/src/app/db/surveyDB/schema/surveyDB.ts new file mode 100644 index 0000000..1e9f1f5 --- /dev/null +++ b/src/app/db/surveyDB/schema/surveyDB.ts @@ -0,0 +1,20 @@ +import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { pgEnum } from "drizzle-orm/pg-core"; + +import { REPORT_TYPES } from "./documents"; + +export const docTypeEnum = pgEnum("doc_type_enum", [...REPORT_TYPES]); + +export const uploaded_files = pgTable("uploaded_files", { + id: uuid("id").primaryKey().defaultRandom(), + + s3JsonUri: text("s3_json_uri"), + s3FileUri: text("s3_file_uri").notNull(), + + docType: docTypeEnum("doc_type").notNull(), // enum used here ✅ + + s3FileUploadTimestamp: timestamp("s3_file_upload_timestamp", { withTimezone: true }).notNull(), + s3JsonUploadTimestamp: timestamp("s3_json_upload_timestamp", { withTimezone: true }), + + uprn: text("uprn").notNull(), +}); \ No newline at end of file diff --git a/src/app/db/surveyDB/utils/utility.ts b/src/app/db/surveyDB/utils/utility.ts new file mode 100644 index 0000000..b789c9e --- /dev/null +++ b/src/app/db/surveyDB/utils/utility.ts @@ -0,0 +1,29 @@ +// insertUploadedFile.ts +import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; +import { surveyDB } from "../connection"; +import type { ReportType } from "../schema/documents"; + +export interface UploadedFileInput { + s3JsonUri?: string; // optional + s3FileUri: string; + docType: ReportType; + s3FileUploadTimestamp: Date; + s3JsonUploadTimestamp?: Date; // optional + uprn: string; +} + +export async function insertUploadedFile(data: UploadedFileInput) { + const [newFile] = await surveyDB + .insert(uploaded_files) + .values({ + s3JsonUri: data.s3JsonUri ?? null, // Pass null if missing + s3FileUri: data.s3FileUri, + docType: data.docType, + s3FileUploadTimestamp: data.s3FileUploadTimestamp, + s3JsonUploadTimestamp: data.s3JsonUploadTimestamp ?? null, // Pass null if missing + uprn: data.uprn, + }) + .returning(); + + return newFile; +} \ No newline at end of file diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx index 212b39c..3cf21d8 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx @@ -1,49 +1,20 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { TableCell, TableRow } from "@/app/shadcn_components/ui/table"; -import { - DocumentWithAuthor, - ReportType, -} from "@/app/db/documents_schema/documents"; import { BrandButton } from "@/app/components/Buttons"; -import { MenuButton } from "./MenuButton"; -import { useState } from "react"; import { UploadModal } from "./UploadModal"; -import { getPropertyMeta } from "../utils"; +import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents"; -// Descriptions based on the document types -const descriptions: Record = { - QUIDOS_PRESITE_NOTE: - "Pre-site note from Quidos, detailing surveyor's findings", - CHARTED_SURVEYOR_REPORT: "Detailed report by a chartered surveyor", - ENERGY_PERFORMANCE_REPORT: "Energy performance breakdown", - U_VALUE_CALCULATOR_REPORT: "Calculated U-values for walls, floors, and roofs", - OVERWRITING_U_VALUE_DECLARATION_FORM: "Signed form for overwriting U-values", - OSMOSIS_CONDITION_PAS_2035_REPORT: - "Osmosis-generated PAS 2035 Condition Report", - DOMNA_CONDITION_PAS_2035_REPORT: - "Domna-generated PAS 2035 Condition Report" +type Props = { + reportType: ReportType; // <- the only type selector needed + uprn: string; }; -export const DocumentSection = ({ - title, - docs, - sectionKey, - documentType, - uprn, - fileTypes, -}: { - title: string; - docs: DocumentWithAuthor[]; - sectionKey: string; - documentType: ReportType; - uprn: string; - fileTypes: ".xml,.pdf" | ".xml" | ".pdf"; -}) => { +export const DocumentSection: React.FC = ({ reportType, uprn }) => { const [showUploadModal, setShowUploadModal] = useState(false); - const [expanded, setExpanded] = useState(false); - const toggle = () => setExpanded((prev) => !prev); + + const title = documentTypeTitles[reportType]; return ( <> @@ -52,18 +23,7 @@ export const DocumentSection = ({ {title} - - {docs.length > 0 ? ( - - ) : ( - No documents available - )} - + setShowUploadModal(false)} - documentType={documentType} - fileTypes={fileTypes} + documentType={reportType} // <- strong ReportType uprn={uprn} /> - - {expanded && - docs.map((doc) => ( - - - {`Uploaded: ${doc.createdAt.toLocaleDateString("en-GB")}`} -
- {descriptions[doc.documentType] ?? ""} -
-
- - - {`Created by: ${ - doc.author.emailAddress ?? "No Author Information" - }`} - - - - { - console.log("View clicked for", doc.id); - }} - onDelete={() => { - console.log("Delete clicked for", doc.id); - }} - /> - -
- ))} ); }; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx index 7fb3e3a..9661527 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -1,147 +1,27 @@ "use client"; - import React from "react"; -import { - Table, - TableBody, - TableRow, - TableCell, -} from "@/app/shadcn_components/ui/table"; -import { DocumentWithAuthor } from "@/app/db/documents_schema/documents"; +import { Table, TableBody, TableRow, TableCell } from "@/app/shadcn_components/ui/table"; import { DocumentSection } from "./DocumentSection"; +import { type ReportType, REPORT_TYPES, documentTypeFileTypes, documentTypeTitles } from "@/app/db/surveyDB/schema/documents"; -import { useMutation } from "@tanstack/react-query"; - -import { MenuButton } from "./MenuButton"; - -type Props = { - documents: DocumentWithAuthor[]; - uprn: string, - // allowedTypes: (typeof DocumentType)[number][]; // Use the union type for allowedTypes as well -}; - -// Fetch the presigned URL from the API -async function generatePresignedUrl(fileKey: string) { - const response = await fetch("/api/energy-assessment-documents", { - method: "POST", - body: JSON.stringify({ fileKey }), - }); - - if (!response.ok) { - throw new Error("Failed to generate presigned URL"); - } - - const data = await response.json(); - return data.url; -} - -export const DocumentsTable: React.FC = ({ - documents, - uprn, - // allowedTypes, -}) => { - const [expanded, setExpanded] = React.useState(false); - - // Mutation to handle the presigned URL generation - const { mutate: fetchPresignedUrl } = useMutation( - // Use the file key as the argument to generate the URL - async (fileKey: string) => await generatePresignedUrl(fileKey), - { - onSuccess: (url) => { - window.open(url, "_blank"); // Open the file in a new tab - }, - onError: (error) => { - console.error("Error generating presigned URL:", error); - }, - } - ); - - const handleDownload = () => { - // Generate URL and open in new tab - // fetchPresignedUrl(documentLocation); - console.log("Download button clicked"); - }; - - const handleUpload = () => { - // Handle the upload logic here - console.log("Upload button clicked"); - console.log("Junte was here"); - }; - - // We split out the various document types. Filter all of the quidos pre-site notes - const quidosPreSite = documents.filter( - (doc) => doc.documentType === "QUIDOS_PRESITE_NOTE" - ); - - const osmosisConditionReport = documents.filter( - (doc) => doc.documentType === "OSMOSIS_CONDITION_PAS_2035_REPORT" - ); - - const domnaConditionReport = documents.filter( - (doc) => doc.documentType === "DOMNA_CONDITION_PAS_2035_REPORT" - ); - - const floors = documents.filter((doc) => doc.documentType === "FLOOR_PLAN"); - - const occupancy = documents.filter( - (doc) => doc.documentType === "OCCUPANCY_ASSESSMENT" - ); +type Props = { uprn: string }; +export const DocumentsTable: React.FC = ({ uprn }) => { return ( - // Quidos Pre-Site Notes Row - - - - - - - - - - - - - - - - - - - - - - + {REPORT_TYPES.map((rt) => ( + + + + + + + ))}
); -}; +}; \ No newline at end of file diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx index 82060d5..4619430 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx @@ -9,25 +9,19 @@ import { DialogFooter, } from "@/app/shadcn_components/ui/dialog"; import { Button } from "@/app/shadcn_components/ui/button"; -import { ReportType } from "@/app/db/documents_schema/documents"; import { Input } from "@/app/shadcn_components/ui/input"; -import { useParams } from 'next/navigation'; import { useState } from "react"; import { uploadFileToS3 } from "@/app/utils/s3"; -import { getPropertyMeta } from "../utils"; +import { documentTypeFileTypes, documentTypeTitles, ReportType } from "@/app/db/surveyDB/schema/documents"; type UploadModalProps = { open: boolean; onClose: () => void; - documentType: string; - uprn:string; - fileTypes: ".xml,.pdf" | ".xml" | ".pdf"; -}; - -const titles: Record = { - QUIDOS_PRESITE_NOTE: "RdSAP Summary Report", + documentType: ReportType; // <- strongly typed + uprn: string; }; +// Fetch presigned URL from API async function generatePresignedUrls({ path, contentType, @@ -37,88 +31,111 @@ async function generatePresignedUrls({ contentType: string; expiresInSeconds: number; }) { - const body = JSON.stringify({ - path: path, - expiresInSeconds: expiresInSeconds, - contentType: contentType - }); - const presignedResponse = await fetch("/api/upload/retrofit-energy-assessments", { + const body = JSON.stringify({ path, expiresInSeconds, contentType }); + const res = await fetch("/api/upload/retrofit-energy-assessments", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json" }, body, }); - - if (!presignedResponse.ok) { - throw new Error("Network response was not ok"); - } - - return presignedResponse.json(); + if (!res.ok) throw new Error("Failed to get presigned URL"); + return res.json() as Promise<{ url: string }>; } -export const UploadModal = ({ - open, - onClose, - documentType, - uprn, - fileTypes = ".xml,.pdf", -}: UploadModalProps) => { +// Decide content-type from file extension +function contentTypeFor(ext: string): string { + const e = ext.toLowerCase(); + if (e === "pdf") return "application/pdf"; + if (e === "xml") return "application/xml"; + return "application/octet-stream"; +} + +export const UploadModal = ({ open, onClose, documentType, uprn }: UploadModalProps) => { const [uploadFiles, setUploadFiles] = useState([]); - const [buttonDisabled, setButtonDisabled] = useState(true); + const [submitting, setSubmitting] = useState(false); - async function handleS3Upload() { - - const timestamp = new Date() - .toISOString() - .replace(/[-:]/g, "") - .replace("T", "_") - .split(".")[0]; // remove milliseconds - - const fileExtension = uploadFiles[0].name.split(".").pop() || "pdf"; - const s3Key = `documents/${uprn}/${documentType}/${timestamp}.${fileExtension}`; - - console.log("Get Presigned url in a specific bucket location") - const { url } = await generatePresignedUrls({ - path: s3Key, // path in bucket - contentType: "application/pdf", - expiresInSeconds: 5 * 60, - }); - - console.log("Retrievied url ", url); - console.log("uploading file..."); - await uploadFileToS3({ - presignedUrl: url, - file: uploadFiles[0], - contentType: "application/pdf" - }) - console.log("uploading Completed!!! Check aws"); - - onClose(); //probably khalim call back to update the front end properl - } + const accepted = documentTypeFileTypes[documentType]; // ".pdf" | ".xml" | ".xml,.pdf" + const title = documentTypeTitles[documentType]; function handleInputOnChange(e: React.ChangeEvent) { - if (e.target.files) { - const filesArray = Array.from(e.target.files); - const extensions = filesArray.map((file) => - file.name.split(".").pop()?.toLowerCase() - ); - // The valid extension are defined by filetypes e.g. ".xml,.pdf" so we split on the comma - const validExtensions = fileTypes - .split(",") - .map((ext) => ext.replace(".", "")); + if (!e.target.files?.length) { + setUploadFiles([]); + return; + } + const file = e.target.files[0]; - // Check if the files have valid extensions - const isValid = extensions.every((ext) => - validExtensions.includes(ext || "") - ); + // Validate by extension against accepted + const ext = (file.name.split(".").pop() || "").toLowerCase(); + const validExtensions = accepted.split(",").map((x) => x.replace(".", "")); + const isValid = validExtensions.includes(ext); - if (isValid) { - setUploadFiles(filesArray); - setButtonDisabled(false); - } else { - setButtonDisabled(true); + if (!isValid) { + setUploadFiles([]); + return; + } + setUploadFiles([file]); + } + + async function handleS3Upload() { + if (!uploadFiles.length) return; + setSubmitting(true); + + try { + // Timestamp like YYYYMMDD_HHMMSS + const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace("T", "_") + .split(".")[0]; + + const file = uploadFiles[0]; + const ext = (file.name.split(".").pop() || "").toLowerCase(); + const ct = contentTypeFor(ext); + + const s3Key = `documents/${uprn}/${documentType}/${timestamp}.${ext}`; + + // 1) Get presigned URL + const { url } = await generatePresignedUrls({ + path: s3Key, + contentType: ct, + expiresInSeconds: 5 * 60, + }); + + // 2) Upload to S3 via presigned URL + await uploadFileToS3({ + presignedUrl: url, + file, + contentType: ct, + }); + + // 3) Record in DB (store durable HTTPS URL without query params) + const presigned = new URL(url); + const s3FileUri = presigned.origin + presigned.pathname; + + const res = await fetch("/db/surveyDB/api/insert_data_to_uploaded_files", { + // If you move your route to the conventional path, change to "/api/uploaded-files" + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + s3FileUri, + docType: documentType, // ReportType value + uprn, + s3FileUploadTimestamp: new Date().toISOString(), + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + console.error("DB insert failed:", err); + throw new Error("Failed to insert uploaded file record"); } - } else { - setButtonDisabled(true); + + // Success — close the dialog and let parent refresh UI + onClose(); + } catch (err) { + console.error(err); + // You can show a toast here if you have one + } finally { + setSubmitting(false); } } @@ -128,15 +145,14 @@ export const UploadModal = ({ Upload Document - Upload an {titles[documentType]}. Once uploaded, - automated extraction can begin. + Upload a {title}. Once uploaded, automated extraction can begin.
- - diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index c8feedc..e9d61a9 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -1,42 +1,7 @@ -import { documentsDB } from "@/app/db/documents_db"; -import { - buildings, - DocumentWithAuthor, - BuildingWithDocuments, -} from "@/app/db/documents_schema/documents"; import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils"; import { eq } from "drizzle-orm"; import { DocumentsTable } from "./DocumentsTable"; -async function getDocuments( - uprn: number -): Promise { - const result = documentsDB.query.buildings.findFirst({ - where: eq(buildings.uprn, String(uprn)), - with: { - documents: { - with: { - author: true, // Include author information - there will only be one author per document - }, - }, - }, - }); - - // If we have no buildings, we return an empty object - if (!result) { - return { - id: "", - address: "", - postcode: "", - uprn: String(uprn), - landlordId: "", - domnaId: "", - documents: [] as DocumentWithAuthor[], - } as BuildingWithDocuments; - } - - return result; -} export default async function DocumentsPage( props: { @@ -51,7 +16,6 @@ export default async function DocumentsPage( } const propertyMeta = await getPropertyMeta(propertyId); - const documents = await getDocuments(propertyMeta.uprn); return ( <> @@ -61,7 +25,6 @@ export default async function DocumentsPage(
From 662a27bf385f77f8c06ac26e5780d01e8caa0b9c Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 14 Aug 2025 17:33:37 +0000 Subject: [PATCH 07/19] string type --- .../[slug]/building-passport/[propertyId]/documents/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index e9d61a9..d769870 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -25,7 +25,7 @@ export default async function DocumentsPage(
From 5b3a1afea0b348bdd22d6085aec1ff00e2ea7ce3 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Tue, 19 Aug 2025 12:05:28 +0000 Subject: [PATCH 08/19] database from python is now linked --- .../insert_data_to_uploaded_files/route.ts | 3 +-- src/app/db/surveyDB/schema/documents.ts | 20 ++++++++++++++++++- src/app/db/surveyDB/schema/surveyDB.ts | 12 ++++++++--- src/app/db/surveyDB/utils/utility.ts | 5 +++-- .../[propertyId]/documents/UploadModal.tsx | 2 +- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts index be51420..dcc2ae7 100644 --- a/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts +++ b/src/app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts @@ -2,8 +2,7 @@ import { NextResponse } from "next/server"; import { z, ZodError } from "zod"; import { insertUploadedFile } from "../../utils/utility"; // ensure path is correct -import { ReportTypeSchema } from "../../schema/documents"; - +import { ReportTypeSchema, reportTypeToDbLabel } from "../../schema/documents"; export const runtime = "nodejs"; // Helper: "" or whitespace -> undefined (so optional() can drop it) diff --git a/src/app/db/surveyDB/schema/documents.ts b/src/app/db/surveyDB/schema/documents.ts index 4fbf05e..f11bcb9 100644 --- a/src/app/db/surveyDB/schema/documents.ts +++ b/src/app/db/surveyDB/schema/documents.ts @@ -52,4 +52,22 @@ export const documentTypeFileTypes: Record DB enum NAME +export const reportTypeToDbLabel: Record = { + osmosis_condition_pas_2035_report: "ECO_CONDITION_REPORT", + energy_performance_report_summary_information: "ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION", + lodgement_xml_needed_for_lodgement_to_like_trademark: "LIG_XML", + reduce_xml_needed_to_generate_full_sap_xml: "RDSAP_XML", + full_xml_needed_for_co_ordination: "FULLSAP_XML", +}; + +// Optional reverse map (for reading from API): +export const dbLabelToReportType: Record = { + ECO_CONDITION_REPORT: "osmosis_condition_pas_2035_report", + ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION: "energy_performance_report_summary_information", + LIG_XML: "lodgement_xml_needed_for_lodgement_to_like_trademark", + RDSAP_XML: "reduce_xml_needed_to_generate_full_sap_xml", + FULLSAP_XML: "full_xml_needed_for_co_ordination", +}; \ No newline at end of file diff --git a/src/app/db/surveyDB/schema/surveyDB.ts b/src/app/db/surveyDB/schema/surveyDB.ts index 1e9f1f5..1c16a7a 100644 --- a/src/app/db/surveyDB/schema/surveyDB.ts +++ b/src/app/db/surveyDB/schema/surveyDB.ts @@ -1,9 +1,15 @@ import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; import { pgEnum } from "drizzle-orm/pg-core"; + +export const DB_REPORT_TYPES = [ + "ECO_CONDITION_REPORT", + "ENERGY_PERFORMANCE_REPORT_SUMMARY_INFORMATION", + "LIG_XML", + "RDSAP_XML", + "FULLSAP_XML", +] as const; -import { REPORT_TYPES } from "./documents"; - -export const docTypeEnum = pgEnum("doc_type_enum", [...REPORT_TYPES]); +export const docTypeEnum = pgEnum("reporttype", DB_REPORT_TYPES); export const uploaded_files = pgTable("uploaded_files", { id: uuid("id").primaryKey().defaultRandom(), diff --git a/src/app/db/surveyDB/utils/utility.ts b/src/app/db/surveyDB/utils/utility.ts index b789c9e..5fbd0c8 100644 --- a/src/app/db/surveyDB/utils/utility.ts +++ b/src/app/db/surveyDB/utils/utility.ts @@ -1,7 +1,8 @@ // insertUploadedFile.ts import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; import { surveyDB } from "../connection"; -import type { ReportType } from "../schema/documents"; +import type { ReportType, ReportTypeSchema} from "../schema/documents"; +import { reportTypeToDbLabel } from "../schema/documents"; export interface UploadedFileInput { s3JsonUri?: string; // optional @@ -18,7 +19,7 @@ export async function insertUploadedFile(data: UploadedFileInput) { .values({ s3JsonUri: data.s3JsonUri ?? null, // Pass null if missing s3FileUri: data.s3FileUri, - docType: data.docType, + docType: reportTypeToDbLabel[data.docType], // map UI value -> DB enum NAME s3FileUploadTimestamp: data.s3FileUploadTimestamp, s3JsonUploadTimestamp: data.s3JsonUploadTimestamp ?? null, // Pass null if missing uprn: data.uprn, diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx index 4619430..c44e654 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx @@ -128,7 +128,7 @@ export const UploadModal = ({ open, onClose, documentType, uprn }: UploadModalPr console.error("DB insert failed:", err); throw new Error("Failed to insert uploaded file record"); } - + console.log("returned",res.json()); // Success — close the dialog and let parent refresh UI onClose(); } catch (err) { From cf0cc5e79a4e89e83d873a243b15a40d41779a39 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Tue, 19 Aug 2025 13:10:49 +0000 Subject: [PATCH 09/19] added sqs utils --- package-lock.json | 1295 ++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/app/utils/sqs.ts | 74 +++ 3 files changed, 1370 insertions(+) create mode 100644 src/app/utils/sqs.ts diff --git a/package-lock.json b/package-lock.json index 1309502..1e485d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "assessment-model", "version": "0.1.0", "dependencies": { + "@aws-sdk/client-sqs": "^3.864.0", "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.9.1", @@ -79,6 +80,648 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.864.0.tgz", + "integrity": "sha512-SxEdQW/g2hb7/O4juAQL0kOD86/QBUSNkdJ5rN3Nd04rJmYTCxe38iCJBT637n+hiedxThLuj8H9ZmY1/OSg7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/credential-provider-node": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-sdk-sqs": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", + "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", + "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", + "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", + "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", + "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", + "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-ini": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", + "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", + "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.864.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/token-providers": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", + "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", + "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", + "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", + "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.862.0.tgz", + "integrity": "sha512-DBX+xTAd3uhMYUFI3wIoSQYBmVFmq918Ah2t/NhTtkNmiuHAFxCy4fSzSklt9qS0i1WzccJEqOZNmqxGEFtolA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", + "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", + "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", + "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", + "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", + "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", + "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", + "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", + "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -3600,6 +4243,616 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", + "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", + "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", + "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", + "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", + "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", + "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -4011,6 +5264,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -5167,6 +6426,12 @@ "dev": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", + "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -7403,6 +8668,24 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -11825,6 +13108,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", diff --git a/package.json b/package.json index 44ff6c8..3112b7b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "create_user": "tsx src/app/db/create_user.ts" }, "dependencies": { + "@aws-sdk/client-sqs": "^3.864.0", "@headlessui/react": "^2.2.7", "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.9.1", diff --git a/src/app/utils/sqs.ts b/src/app/utils/sqs.ts new file mode 100644 index 0000000..c2fa0b0 --- /dev/null +++ b/src/app/utils/sqs.ts @@ -0,0 +1,74 @@ +// utils/sqs.ts +import { + SQSClient, + SendMessageCommand, + GetQueueUrlCommand, + ListQueuesCommand, + SendMessageCommandOutput, +} from "@aws-sdk/client-sqs"; +// If you prefer explicit creds via env, keep your current config; +// otherwise, this ctor will use the default credential chain (env vars, shared profile, role, etc.) +const sqsClient = new SQSClient({ + region: process.env.AWS_REGION, +}); + +let cachedQueueUrl: string | null = null; + +// Export if you want to reuse elsewhere +export async function getQueueUrl(queueName: string): Promise { + if (cachedQueueUrl) return cachedQueueUrl; + + const resp = await sqsClient.send(new GetQueueUrlCommand({ QueueName: queueName })); + if (!resp.QueueUrl) throw new Error(`Could not resolve SQS URL for queue: ${queueName}`); + cachedQueueUrl = resp.QueueUrl; + return cachedQueueUrl; +} + +type SendOptions = { + queueName?: string; // defaults to env + groupId?: string; // for FIFO queues only + deduplicationId?: string; // for FIFO queues only + delaySeconds?: number; // 0-900 +}; + +/** + * Send a message to SQS. Handles both standard and FIFO queues. + */ +export async function sendToQueue( + messageBody: unknown, + opts: SendOptions = {} +): Promise { + const queueName = opts.queueName ?? (process.env.AWS_SQS_QUEUE_NAME as string); + if (!queueName) throw new Error("Missing AWS_SQS_QUEUE_NAME or sendToQueue opts.queueName"); + + const queueUrl = await getQueueUrl(queueName); + + const params: any = { + QueueUrl: queueUrl, + MessageBody: JSON.stringify(messageBody), + }; + + // If it's a FIFO queue (ends with .fifo), include group/dedupe if provided + const isFifo = queueUrl.endsWith(".fifo"); + if (isFifo) { + params.MessageGroupId = opts.groupId ?? "default-group"; + if (opts.deduplicationId) params.MessageDeduplicationId = opts.deduplicationId; + } + + if (typeof opts.delaySeconds === "number") { + params.DelaySeconds = opts.delaySeconds; + } + + return sqsClient.send(new SendMessageCommand(params)); +} + +/** + * List queues in the configured region. + * Optionally filter by name prefix. + */ +export async function listQueues(prefix?: string): Promise { + const resp = await sqsClient.send(new ListQueuesCommand( + prefix ? { QueueNamePrefix: prefix } : {} + )); + return resp.QueueUrls ?? []; +} From 4f0badf1689721d521a4768fe7d16e26ae55cb1b Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Tue, 19 Aug 2025 21:43:54 +0100 Subject: [PATCH 10/19] sqs save --- .../trigger_sqs_to_create_json_body/route.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/app/db/surveyDB/api/trigger_sqs_to_create_json_body/route.ts diff --git a/src/app/db/surveyDB/api/trigger_sqs_to_create_json_body/route.ts b/src/app/db/surveyDB/api/trigger_sqs_to_create_json_body/route.ts new file mode 100644 index 0000000..dcc2ae7 --- /dev/null +++ b/src/app/db/surveyDB/api/trigger_sqs_to_create_json_body/route.ts @@ -0,0 +1,58 @@ +// app/db/surveyDB/api/insert_data_to_uploaded_files/route.ts +import { NextResponse } from "next/server"; +import { z, ZodError } from "zod"; +import { insertUploadedFile } from "../../utils/utility"; // ensure path is correct +import { ReportTypeSchema, reportTypeToDbLabel } from "../../schema/documents"; +export const runtime = "nodejs"; + +// Helper: "" or whitespace -> undefined (so optional() can drop it) +const emptyToUndefined = (v: unknown) => { + if (typeof v === "string" && v.trim() === "") return undefined; + return v; +}; + +const BodySchema = z.object({ + s3JsonUri: z.preprocess( + emptyToUndefined, + z.string().url().optional() + ), + s3FileUri: z.string().url(), + docType: ReportTypeSchema, + // Required upload timestamp (coerce from ISO string) + s3FileUploadTimestamp: z.coerce.date(), + // Optional JSON timestamp: allow "" -> undefined, then coerce to Date + s3JsonUploadTimestamp: z.preprocess( + emptyToUndefined, + z.coerce.date().optional() + ), + uprn: z.string().min(1), +}); + +export async function POST(req: Request) { + try { + const parsed = BodySchema.parse(await req.json()); + + const row = await insertUploadedFile({ + s3JsonUri: parsed.s3JsonUri, // undefined -> util converts to null + s3FileUri: parsed.s3FileUri, + docType: parsed.docType, + s3FileUploadTimestamp: parsed.s3FileUploadTimestamp, + s3JsonUploadTimestamp: parsed.s3JsonUploadTimestamp, // undefined -> util converts to null + uprn: parsed.uprn, + }); + + return NextResponse.json(row, { status: 201 }); + } catch (e) { + if (e instanceof ZodError) { + return NextResponse.json( + { error: "Invalid payload", details: e.flatten() }, + { status: 400 } + ); + } + console.error(e); + return NextResponse.json( + { error: "Failed to insert uploaded_file table in surveyDB" }, + { status: 500 } + ); + } +} From 0a835eb025648cb3c086f11c38ae4198f3740ea5 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Wed, 20 Aug 2025 11:56:52 +0000 Subject: [PATCH 11/19] upload --- .../[propertyId]/documents/UploadModal.tsx | 25 ++++++++++++++++++- src/app/utils/sqs.ts | 6 ++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx index c44e654..e7dd19a 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/UploadModal.tsx @@ -41,6 +41,18 @@ async function generatePresignedUrls({ return res.json() as Promise<{ url: string }>; } +// fetch sqs quess and show it in logs for testing purposes +export async function fetchQueuesAndLog() { + try { + const res = await fetch("/db/surveyDB/api/show_all_sqs_available"); + if (!res.ok) throw new Error("Failed to fetch queues"); + const data = await res.json(); + console.log("✅ Available SQS queues:", data.queues); + } catch (err) { + console.error("❌ Error fetching queues:", err); + } +} + // Decide content-type from file extension function contentTypeFor(ext: string): string { const e = ext.toLowerCase(); @@ -128,7 +140,18 @@ export const UploadModal = ({ open, onClose, documentType, uprn }: UploadModalPr console.error("DB insert failed:", err); throw new Error("Failed to insert uploaded file record"); } - console.log("returned",res.json()); + const { id: db_id } = await res.json() + console.log("db_id is ", db_id); + // SQS list + console.log("Sending request to sqs") + + // enqueue only the id + await fetch("/db/surveyDB/api/send_to_extractor_loader", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: db_id }), // 👈 only id + }); + console.log(`sent request with ${db_id} check with aws sqs queue`); // Success — close the dialog and let parent refresh UI onClose(); } catch (err) { diff --git a/src/app/utils/sqs.ts b/src/app/utils/sqs.ts index c2fa0b0..dff14ae 100644 --- a/src/app/utils/sqs.ts +++ b/src/app/utils/sqs.ts @@ -9,7 +9,11 @@ import { // If you prefer explicit creds via env, keep your current config; // otherwise, this ctor will use the default credential chain (env vars, shared profile, role, etc.) const sqsClient = new SQSClient({ - region: process.env.AWS_REGION, + region: process.env.SQS_AWS_REGION, + credentials: { + accessKeyId: process.env.SQS_AWS_ACCESS_KEY_ID as string, + secretAccessKey: process.env.SQS_AWS_SECRET_ACCESS_KEY as string, + }, }); let cachedQueueUrl: string | null = null; From c27addcc21276a018457622443683c7da3347d79 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Wed, 20 Aug 2025 12:20:41 +0000 Subject: [PATCH 12/19] save --- .../api/send_to_extractor_loader/route.ts | 22 +++++++++++++++++++ .../api/show_all_sqs_available/route.ts | 13 +++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/app/db/surveyDB/api/send_to_extractor_loader/route.ts create mode 100644 src/app/db/surveyDB/api/show_all_sqs_available/route.ts diff --git a/src/app/db/surveyDB/api/send_to_extractor_loader/route.ts b/src/app/db/surveyDB/api/send_to_extractor_loader/route.ts new file mode 100644 index 0000000..37e7c33 --- /dev/null +++ b/src/app/db/surveyDB/api/send_to_extractor_loader/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sendToQueue } from "@/app/utils/sqs"; + +export async function POST(req: NextRequest) { + try { + const { id } = await req.json(); + + if (!id) { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + + const resp = await sendToQueue({ id }, { queueName: "extractor-loader-queue" }); + + return NextResponse.json( + { ok: true, messageId: resp.MessageId }, + { status: 200 } + ); + } catch (err: any) { + console.error("SQS enqueue failed:", err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/db/surveyDB/api/show_all_sqs_available/route.ts b/src/app/db/surveyDB/api/show_all_sqs_available/route.ts new file mode 100644 index 0000000..c4ba9c3 --- /dev/null +++ b/src/app/db/surveyDB/api/show_all_sqs_available/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { listQueues } from "@/app/utils/sqs"; + +// Handle GET requests +export async function GET() { + try { + const queues = await listQueues(); // optionally pass a prefix + return NextResponse.json({ queues }, { status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} From a30d98d153d79d506cd8be3e4583acf5fe7d5a7a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 Aug 2025 13:25:26 +0100 Subject: [PATCH 13/19] push --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e720d02..89a2707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -## Getting Started +### Getting Started When first getting set up you'll firstly want to install the existing dependencies. To do this, simply run From 47135502762de300acac89345d71af2b24d04c21 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 Aug 2025 13:26:21 +0100 Subject: [PATCH 14/19] deploy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89a2707..e720d02 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -### Getting Started +## Getting Started When first getting set up you'll firstly want to install the existing dependencies. To do this, simply run From 2b02e3714bed14941de5548bd6df2e9ea0c50651 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 21 Aug 2025 11:30:23 +0100 Subject: [PATCH 15/19] save --- cypress/support/component.ts | 2 +- drizzle.config.ts | 1 + src/app/components/Navbar.tsx | 2 +- .../components/portfolio/summary/SelectComparisonModal.tsx | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cypress/support/component.ts b/cypress/support/component.ts index 37f59ed..bd8ff01 100644 --- a/cypress/support/component.ts +++ b/cypress/support/component.ts @@ -19,7 +19,7 @@ import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') -import { mount } from 'cypress/react18' +import { mount } from 'cypress/react' // Augment the Cypress namespace to include type definitions for // your custom command. diff --git a/drizzle.config.ts b/drizzle.config.ts index d0f4098..dfba60b 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -3,4 +3,5 @@ import type { Config } from "drizzle-kit"; export default { schema: "./src/app/db/schema/*", out: "./src/app/db/migrations", + dialect: "postgresql", } satisfies Config; diff --git a/src/app/components/Navbar.tsx b/src/app/components/Navbar.tsx index 746bc40..e01038f 100644 --- a/src/app/components/Navbar.tsx +++ b/src/app/components/Navbar.tsx @@ -108,7 +108,7 @@ function Nav({ userImage }: { userImage: string }) { > {(ref) => (
-
+
} className="px-2 pt-2 pb-3 space-y-1 sm:px-3"> - + {/* This element is to trick the browser into centering the modal contents. */} From f8db2da9e65f04b481e3d22ce6f2c5d8896a4d2e Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 21 Aug 2025 11:30:51 +0000 Subject: [PATCH 16/19] addded changes to let build work --- run_build.sh | 1 + src/app/db/schema/solar.ts | 2 +- src/app/db/surveyDB/utils/utility.ts | 5 ++-- .../components/RemoteAssessmentDropdowns.tsx | 2 +- .../components/RemoteAssessmentModal.tsx | 28 ++++++++++++------- .../[slug]/components/UploadCsvModal.tsx | 24 ++++++++++------ src/app/shadcn_components/ui/dialog.tsx | 13 +++++++-- 7 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 run_build.sh diff --git a/run_build.sh b/run_build.sh new file mode 100644 index 0000000..d6cb288 --- /dev/null +++ b/run_build.sh @@ -0,0 +1 @@ +npm run build diff --git a/src/app/db/schema/solar.ts b/src/app/db/schema/solar.ts index ea7ca22..94088a4 100644 --- a/src/app/db/schema/solar.ts +++ b/src/app/db/schema/solar.ts @@ -10,7 +10,7 @@ import { boolean, } from "drizzle-orm/pg-core"; import { InferModel } from "drizzle-orm"; -import { C } from "drizzle-orm/db.d-cf0abe10"; +// import { C } from "drizzle-orm/db.d-cf0abe10"; export const solar = pgTable("solar", { id: bigserial("id", { mode: "bigint" }).primaryKey(), diff --git a/src/app/db/surveyDB/utils/utility.ts b/src/app/db/surveyDB/utils/utility.ts index 5fbd0c8..adeeaf0 100644 --- a/src/app/db/surveyDB/utils/utility.ts +++ b/src/app/db/surveyDB/utils/utility.ts @@ -1,8 +1,9 @@ // insertUploadedFile.ts -import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; +import { uploaded_files, docTypeEnum } from "@/app/db/surveyDB/schema/surveyDB"; import { surveyDB } from "../connection"; import type { ReportType, ReportTypeSchema} from "../schema/documents"; import { reportTypeToDbLabel } from "../schema/documents"; +type DbDocType = (typeof docTypeEnum.enumValues)[number]; export interface UploadedFileInput { s3JsonUri?: string; // optional @@ -19,7 +20,7 @@ export async function insertUploadedFile(data: UploadedFileInput) { .values({ s3JsonUri: data.s3JsonUri ?? null, // Pass null if missing s3FileUri: data.s3FileUri, - docType: reportTypeToDbLabel[data.docType], // map UI value -> DB enum NAME + docType: reportTypeToDbLabel[data.docType] as DbDocType, // map UI value -> DB enum NAME s3FileUploadTimestamp: data.s3FileUploadTimestamp, s3JsonUploadTimestamp: data.s3JsonUploadTimestamp ?? null, // Pass null if missing uprn: data.uprn, diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentDropdowns.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentDropdowns.tsx index 1da5392..706e272 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentDropdowns.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentDropdowns.tsx @@ -2,7 +2,7 @@ import { Menu, Transition } from "@headlessui/react"; import { Fragment } from "react"; import { Button } from "@/app/shadcn_components/ui/button"; import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; -import { Float } from "@headlessui-float/react"; +// import { Float } from "@headlessui-float/react"; export type Option = { label: string; value: string; disabled?: boolean }; diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index e838e08..b3ea2a9 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -1,6 +1,14 @@ "use client"; -import { Dialog, Transition } from "@headlessui/react"; +import { + Dialog, + DialogBackdrop, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from "@headlessui/react"; + import { Fragment, useMemo } from "react"; import { Input } from "@/app/shadcn_components/ui/input"; import { Button } from "@/app/shadcn_components/ui/button"; @@ -542,7 +550,7 @@ export default function RemoteAssessmentModal({ onClose={() => setIsOpen(false)} >
- - - + + {/* Spacer for centering */} - -
- + + Remote Assessment Details - +
@@ -929,8 +937,8 @@ export default function RemoteAssessmentModal({ )}
-
-
+ +
diff --git a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx index 5033b3c..f209312 100644 --- a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx +++ b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx @@ -1,6 +1,14 @@ "use client"; -import { Dialog, Transition } from "@headlessui/react"; +import { + Dialog, + DialogBackdrop, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from "@headlessui/react"; + import { Fragment, useMemo, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { InputFile } from "@/app/portfolio/[slug]/components/InputFile"; @@ -427,7 +435,7 @@ export default function UploadCsvModal({ onClose={() => setIsOpen(false)} >
- - - + + -
- + Upload Property Data - +
-
+
diff --git a/src/app/shadcn_components/ui/dialog.tsx b/src/app/shadcn_components/ui/dialog.tsx index 7ac0d44..abaf256 100644 --- a/src/app/shadcn_components/ui/dialog.tsx +++ b/src/app/shadcn_components/ui/dialog.tsx @@ -14,9 +14,16 @@ const DialogPortal = ({ className, children, ...props -}: DialogPrimitive.DialogPortalProps) => ( - -
+}: React.ComponentPropsWithoutRef & { + className?: string +}) => ( + +
{children}
From 0393299936b33fcc12712270741f4fd3d2ba0821 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 21 Aug 2025 14:35:27 +0000 Subject: [PATCH 17/19] uploade files show visually in frontend --- src/app/db/surveyDB/schema/surveyDB.ts | 7 +- .../documents/DocumentSection.tsx | 65 ++++++++++++++++--- .../[propertyId]/documents/DocumentsTable.tsx | 64 +++++++++++++----- .../[propertyId]/documents/page.tsx | 22 +++++++ 4 files changed, 133 insertions(+), 25 deletions(-) diff --git a/src/app/db/surveyDB/schema/surveyDB.ts b/src/app/db/surveyDB/schema/surveyDB.ts index 1c16a7a..f8b89da 100644 --- a/src/app/db/surveyDB/schema/surveyDB.ts +++ b/src/app/db/surveyDB/schema/surveyDB.ts @@ -1,5 +1,6 @@ import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; import { pgEnum } from "drizzle-orm/pg-core"; + export const DB_REPORT_TYPES = [ "ECO_CONDITION_REPORT", @@ -23,4 +24,8 @@ export const uploaded_files = pgTable("uploaded_files", { s3JsonUploadTimestamp: timestamp("s3_json_upload_timestamp", { withTimezone: true }), uprn: text("uprn").notNull(), -}); \ No newline at end of file +}); + +export type getUploadedFile = typeof uploaded_files.$inferSelect + +export type getUploadedFiles = getUploadedFile[]; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx index 3cf21d8..7d8a1e4 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentSection.tsx @@ -1,20 +1,42 @@ "use client"; - -import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import React from "react"; import { TableCell, TableRow } from "@/app/shadcn_components/ui/table"; import { BrandButton } from "@/app/components/Buttons"; import { UploadModal } from "./UploadModal"; import { documentTypeTitles, type ReportType } from "@/app/db/surveyDB/schema/documents"; +import type { getUploadedFiles, getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB"; type Props = { - reportType: ReportType; // <- the only type selector needed + reportType: ReportType; uprn: string; + files: getUploadedFiles; }; -export const DocumentSection: React.FC = ({ reportType, uprn }) => { - const [showUploadModal, setShowUploadModal] = useState(false); +export const DocumentSection: React.FC = ({ reportType, uprn, files }) => { + const [showUploadModal, setShowUploadModal] = React.useState(false); + const router = useRouter(); + + const latestFile = React.useMemo(() => { + if (!files?.length) return null; + return files.reduce((acc, cur) => { + const accTime = new Date(acc.s3FileUploadTimestamp as any).getTime(); + const curTime = new Date(cur.s3FileUploadTimestamp as any).getTime(); + return curTime > accTime ? cur : acc; + }, files[0]); + }, [files]); + + const formatWhen = (d: string | Date) => + new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "short", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(d)); const title = documentTypeTitles[reportType]; + const count = files.length; return ( <> @@ -23,7 +45,30 @@ export const DocumentSection: React.FC = ({ reportType, uprn }) => { {title} - + + {latestFile ? ( +
+
+ + View latest file + + + uploaded {formatWhen(latestFile.s3FileUploadTimestamp)} + +
+ + {count} file{count !== 1 && "s"} on record + +
+ ) : ( + No files uploaded yet + )} + = ({ reportType, uprn }) => { onClick={() => setShowUploadModal(true)} backgroundColor="brandblue" /> - setShowUploadModal(false)} - documentType={reportType} // <- strong ReportType + onClose={() => { + setShowUploadModal(false); + router.refresh(); + }} + documentType={reportType} uprn={uprn} /> diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx index 9661527..d81acab 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/DocumentsTable.tsx @@ -2,26 +2,60 @@ import React from "react"; import { Table, TableBody, TableRow, TableCell } from "@/app/shadcn_components/ui/table"; import { DocumentSection } from "./DocumentSection"; -import { type ReportType, REPORT_TYPES, documentTypeFileTypes, documentTypeTitles } from "@/app/db/surveyDB/schema/documents"; +import { + type ReportType, + REPORT_TYPES, + dbLabelToReportType, // <-- import the map +} from "@/app/db/surveyDB/schema/documents"; +import type { getUploadedFile } from "@/app/db/surveyDB/schema/surveyDB"; -type Props = { uprn: string }; +type Props = { + uprn: string; + uploadedFilesData: getUploadedFile[]; +}; + +export const DocumentsTable: React.FC = ({ uprn, uploadedFilesData }) => { + const filesByType = React.useMemo(() => { + const map: Partial> = {}; + + for (const file of uploadedFilesData ?? []) { + const uiKey = dbLabelToReportType[file.docType]; // map DB → UI + if (!uiKey) continue; // unknown/legacy type? skip safely + + (map[uiKey] ??= []).push(file); + } + + // newest first within each group + Object.values(map).forEach(arr => + arr!.sort( + (a, b) => + new Date(b.s3FileUploadTimestamp as any).getTime() - + new Date(a.s3FileUploadTimestamp as any).getTime() + ) + ); + + return map; + }, [uploadedFilesData]); -export const DocumentsTable: React.FC = ({ uprn }) => { return ( - {REPORT_TYPES.map((rt) => ( - - - - - - - ))} + {REPORT_TYPES.map((reportType) => { + const filesForType = filesByType[reportType] ?? []; + return ( + + + + + + + ); + })}
); -}; \ No newline at end of file +}; diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index d769870..df910e0 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -1,8 +1,28 @@ import { getPropertyMeta } from "@/app/portfolio/[slug]/building-passport/[propertyId]/utils"; import { eq } from "drizzle-orm"; import { DocumentsTable } from "./DocumentsTable"; +import { surveyDB } from "@/app/db/surveyDB/connection"; +import { uploaded_files } from "@/app/db/surveyDB/schema/surveyDB"; +import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; +import { EmptyObject } from "react-hook-form"; +export async function fetchDocuments(uprn: number): Promise { + return surveyDB.query.uploaded_files.findMany({ + where: eq(uploaded_files.uprn, String(uprn)), + }); +} + +async function getDocuments( + uprn: number +): Promise< getUploadedFiles> { + const result = surveyDB.query.uploaded_files.findMany({ + where: eq(uploaded_files.uprn, String(uprn)), + }); + + return result; +} + export default async function DocumentsPage( props: { params: Promise<{ slug: string; propertyId: string }>; @@ -16,6 +36,7 @@ export default async function DocumentsPage( } const propertyMeta = await getPropertyMeta(propertyId); + const uploadedFiles = await getDocuments(propertyMeta.uprn); return ( <> @@ -26,6 +47,7 @@ export default async function DocumentsPage(
From 0a93f0b743a0065dd5d897f5b41bf67304a96977 Mon Sep 17 00:00:00 2001 From: Jun-te kim Date: Thu, 21 Aug 2025 14:57:25 +0000 Subject: [PATCH 18/19] addded page.tsx --- .../building-passport/[propertyId]/documents/page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx index df910e0..f860078 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/documents/page.tsx @@ -7,12 +7,6 @@ import { type getUploadedFiles } from "@/app/db/surveyDB/schema/surveyDB"; import { EmptyObject } from "react-hook-form"; -export async function fetchDocuments(uprn: number): Promise { - return surveyDB.query.uploaded_files.findMany({ - where: eq(uploaded_files.uprn, String(uprn)), - }); -} - async function getDocuments( uprn: number ): Promise< getUploadedFiles> { From 0941942888c4e7c6ef0eab14f0544d68ee86d862 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 21 Aug 2025 15:21:31 +0000 Subject: [PATCH 19/19] force deploy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e720d02..89a2707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -## Getting Started +### Getting Started When first getting set up you'll firstly want to install the existing dependencies. To do this, simply run