made submit pqq answer for host api
This commit is contained in:
166
src/modules/host/handlers/submitPqqAns.ts
Normal file
166
src/modules/host/handlers/submitPqqAns.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import config from '@/config/config';
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
|
||||
import AWS from 'aws-sdk';
|
||||
import Busboy from 'busboy';
|
||||
import crypto from 'crypto';
|
||||
import { PrismaService } from '../../../common/database/prisma.service';
|
||||
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { HostService } from '../services/host.service';
|
||||
|
||||
const prisma = new PrismaService();
|
||||
const pqqService = new HostService(prisma);
|
||||
|
||||
const s3 = new AWS.S3({ region: config.aws.region });
|
||||
|
||||
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string): Promise<string> {
|
||||
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
||||
const s3Key = `${prefix}/${uniqueKey}`;
|
||||
|
||||
await s3.upload({
|
||||
Bucket: config.aws.bucketName,
|
||||
Key: s3Key,
|
||||
Body: buffer,
|
||||
ContentType: mimeType,
|
||||
ACL: 'private'
|
||||
}).promise();
|
||||
|
||||
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
// 1) Auth
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) throw new ApiError(401, 'Missing token.');
|
||||
const user = await verifyHostToken(token);
|
||||
|
||||
// 2) Content-Type check
|
||||
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
|
||||
if (!contentType?.startsWith("multipart/form-data"))
|
||||
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
||||
|
||||
if (!event.isBase64Encoded)
|
||||
throw new ApiError(400, "Body must be base64 encoded");
|
||||
|
||||
const bodyBuffer = Buffer.from(event.body!, "base64");
|
||||
|
||||
const fields: any = {};
|
||||
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
||||
|
||||
// 3) Parse multipart data
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const bb = Busboy({ headers: { 'content-type': contentType } });
|
||||
|
||||
bb.on('file', (fieldname, file, info) => {
|
||||
const { filename, mimeType } = info;
|
||||
const chunks: Buffer[] = [];
|
||||
let totalSize = 0;
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
file.on('data', (chunk) => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > MAX_SIZE) {
|
||||
file.resume();
|
||||
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
file.on('end', () => {
|
||||
files.push({
|
||||
buffer: Buffer.concat(chunks),
|
||||
mimeType,
|
||||
fileName: filename,
|
||||
fieldName: fieldname,
|
||||
});
|
||||
});
|
||||
|
||||
file.on('error', (err) => {
|
||||
reject(new ApiError(400, `File upload error: ${err.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
bb.on('field', (fieldname, val) => {
|
||||
try {
|
||||
fields[fieldname] = JSON.parse(val);
|
||||
} catch {
|
||||
fields[fieldname] = val;
|
||||
}
|
||||
});
|
||||
|
||||
bb.on('close', () => {
|
||||
console.log("✅ Busboy parsing completed");
|
||||
resolve();
|
||||
});
|
||||
|
||||
bb.on('error', (err) => {
|
||||
console.error("❌ Busboy error:", err);
|
||||
reject(new ApiError(400, `Multipart parsing error: ${err.message}`));
|
||||
});
|
||||
|
||||
bb.end(bodyBuffer);
|
||||
});
|
||||
|
||||
console.log("📌 Parsed Files:", files);
|
||||
|
||||
// 4) Extract required fields
|
||||
const activityXid = Number(fields.activityXid);
|
||||
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
||||
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
||||
const comments = fields.comments || null;
|
||||
|
||||
if (!activityXid) throw new ApiError(400, "activityXid is required");
|
||||
if (!pqqQuestionXid) throw new ApiError(400, "pqqQuestionXid is required");
|
||||
if (!pqqAnswerXid) throw new ApiError(400, "pqqAnswerXid is required");
|
||||
|
||||
// 5) Create or update header
|
||||
const header = await pqqService.createOrUpdateHeader(
|
||||
activityXid,
|
||||
pqqQuestionXid,
|
||||
pqqAnswerXid,
|
||||
comments
|
||||
);
|
||||
|
||||
// 6) Upload files
|
||||
const uploadedFiles: any[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const url = await uploadToS3(
|
||||
file.buffer,
|
||||
file.mimeType,
|
||||
file.fileName,
|
||||
`ActivityOnboarding/Activity_${activityXid}/supportings`
|
||||
);
|
||||
|
||||
const supporting = await pqqService.addSupportingFile(
|
||||
header.id,
|
||||
file.mimeType,
|
||||
url
|
||||
);
|
||||
|
||||
uploadedFiles.push(supporting);
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "PQQ answer submitted successfully",
|
||||
data: {
|
||||
headerId: header.id,
|
||||
uploadedFiles
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error in submitPqqAnswer:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -450,4 +450,59 @@ export class HostService {
|
||||
return `HOSTREFNO-${String(nextId).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
async createOrUpdateHeader(
|
||||
activityXid: number,
|
||||
pqqQuestionXid: number,
|
||||
pqqAnswerXid: number,
|
||||
comments: string | null
|
||||
) {
|
||||
// find existing header
|
||||
const existing = await this.prisma.activityPQQheader.findFirst({
|
||||
where: { activityXid, pqqQuestionXid, deletedAt: null }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return await this.prisma.activityPQQheader.create({
|
||||
data: {
|
||||
activityXid,
|
||||
pqqQuestionXid,
|
||||
pqqAnswerXid,
|
||||
comments
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// mark old supportings deleted
|
||||
await this.prisma.activityPQQSupportings.updateMany({
|
||||
where: { activityPqqHeaderXid: existing.id },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// update header
|
||||
return await this.prisma.activityPQQheader.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
pqqAnswerXid,
|
||||
comments
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async addSupportingFile(
|
||||
headerId: number,
|
||||
mimeType: string,
|
||||
fileUrl: string
|
||||
) {
|
||||
return await this.prisma.activityPQQSupportings.create({
|
||||
data: {
|
||||
activityPqqHeaderXid: headerId,
|
||||
mediaType: mimeType,
|
||||
mediaFileName: fileUrl
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user