condition check lambda
This commit is contained in:
89
lambdas/shared/db/connection.js
Normal file
89
lambdas/shared/db/connection.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { Pool } = require("pg");
|
||||
|
||||
let pool = null;
|
||||
|
||||
/**
|
||||
* Get or create a PostgreSQL connection pool.
|
||||
* Uses connection pooling optimized for Lambda:
|
||||
* - Reuses connections across invocations (when container is warm)
|
||||
* - Small pool size to avoid exhausting database connections
|
||||
*
|
||||
* Expects DATABASE_URL environment variable in format:
|
||||
* postgresql://user:password@host:port/database
|
||||
*/
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error("DATABASE_URL environment variable is required");
|
||||
}
|
||||
|
||||
pool = new Pool({
|
||||
connectionString,
|
||||
// Lambda-optimized settings
|
||||
max: 1, // Single connection per Lambda instance
|
||||
idleTimeoutMillis: 120000, // 2 minutes - keep connection warm
|
||||
connectionTimeoutMillis: 5000, // 5 seconds to connect
|
||||
});
|
||||
|
||||
// Handle pool errors
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database pool error:", err);
|
||||
pool = null; // Reset pool on error
|
||||
});
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query with automatic connection management.
|
||||
* @param {string} text - SQL query text
|
||||
* @param {Array} params - Query parameters
|
||||
* @returns {Promise<object>} Query result
|
||||
*/
|
||||
async function query(text, params) {
|
||||
const pool = getPool();
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const result = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
level: "debug",
|
||||
message: "Executed query",
|
||||
query: text.substring(0, 100),
|
||||
duration,
|
||||
rows: result.rowCount,
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
level: "error",
|
||||
message: "Query failed",
|
||||
query: text.substring(0, 100),
|
||||
error: error.message,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection pool (for cleanup).
|
||||
* Call this at the end of Lambda execution if needed.
|
||||
*/
|
||||
async function closePool() {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPool,
|
||||
query,
|
||||
closePool,
|
||||
};
|
||||
109
lambdas/shared/db/queries.js
Normal file
109
lambdas/shared/db/queries.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const { query } = require("./connection");
|
||||
|
||||
/**
|
||||
* Get rental with all related details (owner, renter, item).
|
||||
* @param {string} rentalId - UUID of the rental
|
||||
* @returns {Promise<object|null>} Rental with relations or null if not found
|
||||
*/
|
||||
async function getRentalWithDetails(rentalId) {
|
||||
const result = await query(
|
||||
`SELECT
|
||||
r.id,
|
||||
r."itemId",
|
||||
r."renterId",
|
||||
r."ownerId",
|
||||
r."startDateTime",
|
||||
r."endDateTime",
|
||||
r."totalAmount",
|
||||
r.status,
|
||||
r."createdAt",
|
||||
r."updatedAt",
|
||||
-- Owner fields
|
||||
owner.id AS "owner_id",
|
||||
owner.email AS "owner_email",
|
||||
owner."firstName" AS "owner_firstName",
|
||||
owner."lastName" AS "owner_lastName",
|
||||
-- Renter fields
|
||||
renter.id AS "renter_id",
|
||||
renter.email AS "renter_email",
|
||||
renter."firstName" AS "renter_firstName",
|
||||
renter."lastName" AS "renter_lastName",
|
||||
-- Item fields
|
||||
item.id AS "item_id",
|
||||
item.name AS "item_name",
|
||||
item.description AS "item_description"
|
||||
FROM "Rentals" r
|
||||
INNER JOIN "Users" owner ON r."ownerId" = owner.id
|
||||
INNER JOIN "Users" renter ON r."renterId" = renter.id
|
||||
INNER JOIN "Items" item ON r."itemId" = item.id
|
||||
WHERE r.id = $1`,
|
||||
[rentalId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
|
||||
// Transform flat result into nested structure
|
||||
return {
|
||||
id: row.id,
|
||||
itemId: row.itemId,
|
||||
renterId: row.renterId,
|
||||
ownerId: row.ownerId,
|
||||
startDateTime: row.startDateTime,
|
||||
endDateTime: row.endDateTime,
|
||||
totalAmount: row.totalAmount,
|
||||
status: row.status,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
owner: {
|
||||
id: row.owner_id,
|
||||
email: row.owner_email,
|
||||
firstName: row.owner_firstName,
|
||||
lastName: row.owner_lastName,
|
||||
},
|
||||
renter: {
|
||||
id: row.renter_id,
|
||||
email: row.renter_email,
|
||||
firstName: row.renter_firstName,
|
||||
lastName: row.renter_lastName,
|
||||
},
|
||||
item: {
|
||||
id: row.item_id,
|
||||
name: row.item_name,
|
||||
description: row.item_description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID.
|
||||
* @param {string} userId - UUID of the user
|
||||
* @returns {Promise<object|null>} User or null if not found
|
||||
*/
|
||||
async function getUserById(userId) {
|
||||
const result = await query(
|
||||
`SELECT
|
||||
id,
|
||||
email,
|
||||
"firstName",
|
||||
"lastName",
|
||||
phone
|
||||
FROM "Users"
|
||||
WHERE id = $1`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRentalWithDetails,
|
||||
getUserById,
|
||||
};
|
||||
196
lambdas/shared/email/client.js
Normal file
196
lambdas/shared/email/client.js
Normal file
@@ -0,0 +1,196 @@
|
||||
const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses");
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
let sesClient = null;
|
||||
|
||||
/**
|
||||
* Get or create an SES client.
|
||||
* Reuses client across Lambda invocations for better performance.
|
||||
*/
|
||||
function getSESClient() {
|
||||
if (!sesClient) {
|
||||
sesClient = new SESClient({
|
||||
region: process.env.AWS_REGION || "us-east-1",
|
||||
});
|
||||
}
|
||||
return sesClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML to plain text for email fallback.
|
||||
* @param {string} html - HTML content to convert
|
||||
* @returns {string} Plain text version
|
||||
*/
|
||||
function htmlToPlainText(html) {
|
||||
return html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/p>/gi, "\n\n")
|
||||
.replace(/<\/div>/gi, "\n")
|
||||
.replace(/<\/li>/gi, "\n")
|
||||
.replace(/<\/h[1-6]>/gi, "\n\n")
|
||||
.replace(/<li>/gi, "- ")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\n\s*\n\s*\n/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template by replacing {{variable}} placeholders with values.
|
||||
* @param {string} template - Template string with {{placeholders}}
|
||||
* @param {Object} variables - Key-value pairs to substitute
|
||||
* @returns {string} Rendered template
|
||||
*/
|
||||
function renderTemplate(template, variables = {}) {
|
||||
let rendered = template;
|
||||
|
||||
Object.keys(variables).forEach((key) => {
|
||||
const regex = new RegExp(`{{${key}}}`, "g");
|
||||
rendered = rendered.replace(regex, variables[key] ?? "");
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a template file from disk.
|
||||
* @param {string} templatePath - Absolute path to the template file
|
||||
* @returns {Promise<string>} Template content
|
||||
*/
|
||||
async function loadTemplate(templatePath) {
|
||||
try {
|
||||
return await fs.readFile(templatePath, "utf-8");
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
level: "error",
|
||||
message: "Failed to load email template",
|
||||
templatePath,
|
||||
error: error.message,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email using AWS SES.
|
||||
* @param {string|string[]} to - Recipient email address(es)
|
||||
* @param {string} subject - Email subject line
|
||||
* @param {string} htmlBody - HTML content of the email
|
||||
* @param {string|null} textBody - Plain text content (auto-generated if not provided)
|
||||
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||
*/
|
||||
async function sendEmail(to, subject, htmlBody, textBody = null) {
|
||||
// Check if email sending is enabled
|
||||
if (process.env.EMAIL_ENABLED !== "true") {
|
||||
console.log(JSON.stringify({
|
||||
level: "info",
|
||||
message: "Email sending disabled, skipping",
|
||||
to,
|
||||
subject,
|
||||
}));
|
||||
return { success: true, messageId: "disabled" };
|
||||
}
|
||||
|
||||
const client = getSESClient();
|
||||
|
||||
// Auto-generate plain text from HTML if not provided
|
||||
const plainText = textBody || htmlToPlainText(htmlBody);
|
||||
|
||||
// Build sender address with friendly name
|
||||
const fromName = process.env.SES_FROM_NAME || "Village Share";
|
||||
const fromEmail = process.env.SES_FROM_EMAIL;
|
||||
|
||||
if (!fromEmail) {
|
||||
throw new Error("SES_FROM_EMAIL environment variable is required");
|
||||
}
|
||||
|
||||
const source = `${fromName} <${fromEmail}>`;
|
||||
|
||||
const params = {
|
||||
Source: source,
|
||||
Destination: {
|
||||
ToAddresses: Array.isArray(to) ? to : [to],
|
||||
},
|
||||
Message: {
|
||||
Subject: {
|
||||
Data: subject,
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
Body: {
|
||||
Html: {
|
||||
Data: htmlBody,
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
Text: {
|
||||
Data: plainText,
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Add reply-to if configured
|
||||
if (process.env.SES_REPLY_TO_EMAIL) {
|
||||
params.ReplyToAddresses = [process.env.SES_REPLY_TO_EMAIL];
|
||||
}
|
||||
|
||||
try {
|
||||
const command = new SendEmailCommand(params);
|
||||
const result = await client.send(command);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
level: "info",
|
||||
message: "Email sent successfully",
|
||||
to,
|
||||
subject,
|
||||
messageId: result.MessageId,
|
||||
}));
|
||||
|
||||
return { success: true, messageId: result.MessageId };
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
level: "error",
|
||||
message: "Failed to send email",
|
||||
to,
|
||||
subject,
|
||||
error: error.message,
|
||||
}));
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for email display.
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
function formatEmailDate(date) {
|
||||
const dateObj = typeof date === "string" ? new Date(date) : date;
|
||||
return dateObj.toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendEmail,
|
||||
loadTemplate,
|
||||
renderTemplate,
|
||||
htmlToPlainText,
|
||||
formatEmailDate,
|
||||
};
|
||||
15
lambdas/shared/index.js
Normal file
15
lambdas/shared/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Shared utilities for Rentall Lambda functions.
|
||||
*/
|
||||
|
||||
const db = require("./db/connection");
|
||||
const queries = require("./db/queries");
|
||||
const email = require("./email/client");
|
||||
const logger = require("./utils/logger");
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
queries,
|
||||
email,
|
||||
logger,
|
||||
};
|
||||
5891
lambdas/shared/package-lock.json
generated
Normal file
5891
lambdas/shared/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
lambdas/shared/package.json
Normal file
17
lambdas/shared/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@rentall/lambda-shared",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared utilities for Rentall Lambda functions",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-ses": "^3.896.0",
|
||||
"@aws-sdk/client-scheduler": "^3.896.0",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
}
|
||||
}
|
||||
61
lambdas/shared/utils/logger.js
Normal file
61
lambdas/shared/utils/logger.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* CloudWatch-compatible structured logger for Lambda functions.
|
||||
* Outputs JSON logs that CloudWatch Logs Insights can parse and query.
|
||||
*/
|
||||
|
||||
const LOG_LEVELS = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
// Default to 'info' in production, 'debug' in development
|
||||
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
|
||||
|
||||
/**
|
||||
* Create a log entry in CloudWatch-compatible JSON format.
|
||||
* @param {string} level - Log level (debug, info, warn, error)
|
||||
* @param {string} message - Log message
|
||||
* @param {Object} meta - Additional metadata to include
|
||||
*/
|
||||
function log(level, message, meta = {}) {
|
||||
if (LOG_LEVELS[level] < currentLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: level.toUpperCase(),
|
||||
message,
|
||||
...meta,
|
||||
};
|
||||
|
||||
// Add Lambda context if available
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
||||
entry.function = process.env.AWS_LAMBDA_FUNCTION_NAME;
|
||||
}
|
||||
if (process.env.AWS_LAMBDA_LOG_STREAM_NAME) {
|
||||
entry.logStream = process.env.AWS_LAMBDA_LOG_STREAM_NAME;
|
||||
}
|
||||
|
||||
const output = JSON.stringify(entry);
|
||||
|
||||
switch (level) {
|
||||
case "error":
|
||||
console.error(output);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(output);
|
||||
break;
|
||||
default:
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
debug: (message, meta) => log("debug", message, meta),
|
||||
info: (message, meta) => log("info", message, meta),
|
||||
warn: (message, meta) => log("warn", message, meta),
|
||||
error: (message, meta) => log("error", message, meta),
|
||||
};
|
||||
Reference in New Issue
Block a user