fix: edit profile in user and admin page

This commit is contained in:
unknown 2025-05-18 02:03:40 +07:00
parent 8e92a58e49
commit 6d87d8d0a1
502 changed files with 65275 additions and 15953 deletions

View File

@ -8,5 +8,6 @@ EMAIL_HOST=
EMAIL_PORT=
EMAIL_USER=
EMAIL_PASS=
SENDGRID_SENDER_NAME=
SENDGRID_API_KEY=
EMAIL_FROM=

View File

@ -3,6 +3,8 @@ const argon2 = require('argon2');
const randomstring = require('randomstring');
const {User} = require('../models'); // Pastikan sesuai dengan struktur project
require('dotenv').config();
const nodemailer = require('nodemailer');
const sgMail = require('@sendgrid/mail');
// Fungsi untuk membuat token JWT
const generateToken = (user) => {
@ -78,30 +80,198 @@ exports.login = async (req, res) => {
}
};
exports.forgotPassword = async (req, res) => {
// Buat transporter Nodemailer dengan Gmail
const createGmailTransporter = () => {
return nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_FROM, // sibayam52@gmail.com dari .env yang sudah ada
pass: process.env.EMAIL_PASS, // Gunakan App Password, bukan password biasa!
},
// Pool koneksi untuk menghindari rate limiting
pool: true,
maxConnections: 3,
maxMessages: 50,
rateDelta: 1500,
rateLimit: 3
});
};
exports.sendResetCodeWithGmail = async (req, res) => {
const { email } = req.body;
try {
const { email, password } = req.body; // Gunakan 'password' sebagai field yang dikirim
// 🔹 Validasi input
if (!email || !password) {
return res.status(400).json({ message: "Email dan password baru harus diisi" });
}
// 🔹 Cek apakah user dengan email tersebut ada
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(404).json({ message: "User tidak ditemukan" });
}
// 🔹 Hash password baru dengan Argon2
const hashedPassword = await argon2.hash(password);
// 🔹 Update password user di database
await user.update({ password: hashedPassword });
res.status(200).json({ message: "Password berhasil diperbarui" });
// Validasi email
if (!email || !email.includes('@')) {
return res.status(400).json({ message: 'Email tidak valid' });
}
const user = await User.findOne({ where: { email } });
if (!user) return res.status(404).json({ message: 'User tidak ditemukan' });
// Generate 6 digit random code
const code = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
await user.update({
resetToken: code,
resetTokenExpiry: expiresAt,
});
// Nama aplikasi yang konsisten
const appName = process.env.SENDGRID_SENDER_NAME || 'SistemPakar SIBAYAM';
// Coba buat transporter setiap kali untuk menghindari koneksi mati
const transporter = createGmailTransporter();
// Email sangat sederhana tetapi efektif
const mailOptions = {
from: `"${appName}" <${process.env.EMAIL_FROM}>`, // Menggunakan EMAIL_FROM dari .env
to: email,
subject: `[${code}] Kode Verifikasi ${appName}`, // Tanda kode di subject membantu visibilitas
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 5px;">
<h2 style="color: #333; margin-bottom: 20px;">Reset Password</h2>
<p>Halo ${user.name || 'Pengguna'},</p>
<p style="color: #666; margin-bottom: 15px;">Anda telah meminta untuk mereset password akun SIBAYAM Anda.</p>
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="font-size: 16px; margin: 0;">Kode verifikasi Anda:</p>
<h1 style="color: #4CAF50; margin: 10px 0; letter-spacing: 5px;">${code}</h1>
</div>
<p style="color: #666; margin-bottom: 15px;">Kode ini akan kadaluarsa dalam 10 menit.</p>
<p style="color: #999; font-size: 12px; margin-top: 30px;">Jika Anda tidak meminta reset password, abaikan email ini.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;"/>
<p style="color: #999; font-size: 12px;">Email ini dikirim oleh sistem SIBAYAM. Mohon jangan membalas email ini.</p>
</div>
`
};
// Tambahkan debugging
console.log('Mengirim email ke:', email);
console.log('Menggunakan akun:', process.env.EMAIL_FROM);
console.log('Dengan transporter:', transporter ? 'Berhasil dibuat' : 'Gagal dibuat');
// Kirim email dengan try-catch terpisah untuk debugging
try {
const info = await transporter.sendMail(mailOptions);
console.log('Email reset password berhasil dikirim via Gmail:', info.messageId);
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
return res.json({
message: 'Kode verifikasi telah dikirim ke email Anda.',
expiresIn: '10 menit',
});
} catch (emailError) {
console.error('Error saat mengirim email:', emailError);
return res.status(500).json({
message: 'Gagal mengirim kode verifikasi via email',
error: process.env.NODE_ENV === 'development' ? emailError.toString() : undefined,
});
}
} catch (error) {
console.error("Error pada forgotPassword:", error);
res.status(500).json({ message: "Terjadi kesalahan", error: error.message });
console.error('Error in sendResetCodeWithGmail:', error);
return res.status(500).json({
message: 'Gagal memproses permintaan reset password',
error: process.env.NODE_ENV === 'development' ? error.toString() : undefined,
});
}
};
};
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
exports.resetPasswordWithCode = async (req, res) => {
const { code, password } = req.body;
try {
// Validasi input
if (!code || !password) {
return res.status(400).json({
message: "Kode dan password baru wajib diisi"
});
}
// Validasi password yang lebih ketat
if (password.length < 8) {
return res.status(400).json({
message: "Password harus minimal 8 karakter"
});
}
const user = await User.findOne({
where: {
resetToken: code
}
});
if (!user) {
return res.status(400).json({
message: "Kode verifikasi salah"
});
}
// Check if code is expired
const now = new Date();
if (now > user.resetTokenExpiry) {
return res.status(400).json({
message: "Kode verifikasi sudah kadaluarsa. Silakan minta kode baru."
});
}
const hashedPassword = await argon2.hash(password);
await user.update({
password: hashedPassword,
resetToken: null,
resetTokenExpiry: null
});
// Kirim email konfirmasi menggunakan Gmail
try {
const appName = process.env.SENDGRID_SENDER_NAME || 'SistemPakar SIBAYAM';
const transporter = createGmailTransporter();
const mailOptions = {
from: `"${appName}" <${process.env.EMAIL_FROM}>`,
to: user.email,
subject: `Password Berhasil Diubah - ${appName}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 5px;">
<h2 style="color: #333; margin-bottom: 20px;">Password Berhasil Diubah</h2>
<p>Halo ${user.name || 'Pengguna'},</p>
<p style="color: #666; margin-bottom: 15px;">Password akun SIBAYAM Anda telah berhasil diubah.</p>
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="font-size: 16px; margin: 0; color: #4CAF50;"> Password telah diperbarui</p>
</div>
<p style="color: #666; margin-bottom: 15px;">Jika Anda tidak melakukan perubahan ini, segera hubungi tim dukungan kami.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;"/>
<p style="color: #999; font-size: 12px;">Email ini dikirim oleh sistem SIBAYAM. Mohon jangan membalas email ini.</p>
</div>
`
};
// Tambahkan debugging
console.log('Mengirim email konfirmasi ke:', user.email);
console.log('Menggunakan akun:', process.env.EMAIL_FROM);
const info = await transporter.sendMail(mailOptions);
console.log('Email konfirmasi berhasil dikirim via Gmail:', info.messageId);
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
} catch (emailError) {
console.error('Error saat mengirim email konfirmasi:', emailError);
// Tidak menghentikan proses meski email konfirmasi gagal
}
return res.json({
message: "Password berhasil direset",
success: true
});
} catch (error) {
console.error("Error in resetPasswordWithCode:", error);
return res.status(500).json({
message: "Terjadi kesalahan pada server",
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};

View File

@ -1,9 +1,80 @@
const { Rule_penyakit, Rule_hama, Gejala, Penyakit, Hama, Histori } = require('../models');
const moment = require('moment');
// Helper function to calculate Bayes probability
function calculateBayesProbability(rules, entityType) {
if (!rules || rules.length === 0) return null;
const entityData = rules[0][entityType];
const entityName = entityData.nama;
const entityId = entityType === 'penyakit' ? rules[0].id_penyakit : rules[0].id_hama;
// LANGKAH 1: Mencari nilai semesta P(E|Hi) untuk setiap gejala
let nilai_semesta = 0;
const gejalaValues = {};
for (const rule of rules) {
gejalaValues[rule.id_gejala] = rule.nilai_pakar;
nilai_semesta += rule.nilai_pakar;
}
// LANGKAH 2: Mencari hasil bobot P(Hi) untuk setiap gejala
const bobotGejala = {};
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
bobotGejala[idGejala] = nilai / nilai_semesta;
}
// LANGKAH 3: Hitung probabilitas H tanpa memandang Evidence P(E|Hi) × P(Hi)
const probTanpaEvidence = {};
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
probTanpaEvidence[idGejala] = nilai * bobotGejala[idGejala];
}
// Hitung total untuk digunakan di langkah 4
let totalProbTanpaEvidence = 0;
for (const nilai of Object.values(probTanpaEvidence)) {
totalProbTanpaEvidence += nilai;
}
// LANGKAH 4: Hitung probabilitas H dengan memandang Evidence P(Hi|E)
const probDenganEvidence = {};
for (const [idGejala, nilai] of Object.entries(probTanpaEvidence)) {
probDenganEvidence[idGejala] = nilai / totalProbTanpaEvidence;
}
// LANGKAH 5: Hitung Nilai Bayes ∑bayes = ∑(P(E|Hi) × P(Hi|E))
let nilaiBayes = 0;
const detailBayes = [];
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
const bayes = nilai * probDenganEvidence[idGejala];
nilaiBayes += bayes;
detailBayes.push({
id_gejala: parseInt(idGejala),
P_E_given_Hi: nilai,
P_Hi: bobotGejala[idGejala],
P_E_Hi_x_P_Hi: probTanpaEvidence[idGejala],
P_Hi_given_E: probDenganEvidence[idGejala],
bayes_value: bayes
});
}
// Hasil akhir
const idField = entityType === 'penyakit' ? 'id_penyakit' : 'id_hama';
return {
[idField]: entityId,
nama: entityName,
nilai_semesta: nilai_semesta,
detail_perhitungan: detailBayes,
nilai_bayes: nilaiBayes,
probabilitas_persen: nilaiBayes * 100
};
}
exports.diagnosa = async (req, res) => {
const { gejala } = req.body; // array of id_gejala
const userId = req.user?.id; // Use optional chaining to avoid errors if req.user is undefined
const { gejala } = req.body;
const userId = req.user?.id;
const tanggal_diagnosa = moment().format('YYYY-MM-DD');
if (!gejala || !Array.isArray(gejala)) {
@ -16,269 +87,112 @@ exports.diagnosa = async (req, res) => {
where: { id: gejala }
});
// ========== HITUNG TOTAL P(E) UNTUK SEMUA GEJALA ==========
// P(E) seharusnya sama untuk semua penyakit dan hama yang memiliki gejala yang sama
// Object untuk menyimpan P(E) untuk setiap gejala
const evidenceProbabilities = {};
// Hitung P(E) untuk PENYAKIT
for (const idGejala of gejala) {
// Dapatkan semua rule untuk gejala ini di semua penyakit
const penyakitRulesForGejala = await Rule_penyakit.findAll({
where: { id_gejala: idGejala },
include: [{ model: Penyakit, as: 'penyakit' }]
});
let evidenceProbForGejala = 0;
// Hitung P(E) = Σ [P(E|Hi) * P(Hi)] untuk penyakit
for (const rule of penyakitRulesForGejala) {
const pHi = rule.penyakit.nilai_pakar; // P(Hi)
const pEgivenHi = rule.nilai_pakar; // P(E|Hi)
evidenceProbForGejala += pEgivenHi * pHi;
}
// Dapatkan semua rule untuk gejala ini di semua hama
const hamaRulesForGejala = await Rule_hama.findAll({
where: { id_gejala: idGejala },
include: [{ model: Hama, as: 'hama' }]
});
// Hitung P(E) = Σ [P(E|Hi) * P(Hi)] untuk hama
for (const rule of hamaRulesForGejala) {
const pHi = rule.hama.nilai_pakar; // P(Hi)
const pEgivenHi = rule.nilai_pakar; // P(E|Hi)
evidenceProbForGejala += pEgivenHi * pHi;
}
// Simpan P(E) untuk gejala ini
evidenceProbabilities[idGejala] = evidenceProbForGejala;
}
// Hitung total P(E) untuk semua gejala yang diinput
let totalEvidenceProbability = 0;
for (const idGejala of gejala) {
totalEvidenceProbability += evidenceProbabilities[idGejala] || 0;
}
// Pastikan total P(E) tidak nol untuk menghindari division by zero
if (totalEvidenceProbability === 0) {
totalEvidenceProbability = 1.0;
}
// ========== PENYAKIT ==========
const allPenyakitRules = await Rule_penyakit.findAll({
where: { id_gejala: gejala },
include: [{ model: Penyakit, as: 'penyakit' }]
});
// Mendapatkan semua penyakit unik yang memiliki gejala yang dipilih
const uniquePenyakitIds = [...new Set(allPenyakitRules.map(rule => rule.id_penyakit))];
// Hasil perhitungan untuk setiap penyakit
const hasilPenyakit = [];
// Hitung untuk setiap penyakit
for (const idPenyakit of uniquePenyakitIds) {
// Filter rules yang berhubungan dengan penyakit ini
const penyakitRules = allPenyakitRules.filter(rule => rule.id_penyakit === idPenyakit);
if (penyakitRules.length > 0) {
const dataPenyakit = penyakitRules[0].penyakit;
const namaPenyakit = dataPenyakit.nama;
const priorProbability = dataPenyakit.nilai_pakar; // P(H) - prior probability penyakit
// Menghitung P(E|H) untuk setiap gejala
const evidenceGivenHypothesis = {};
for (const rule of penyakitRules) {
evidenceGivenHypothesis[rule.id_gejala] = rule.nilai_pakar; // P(E|H) untuk setiap gejala
}
// Menghitung P(H|E) = [P(E|H) * P(H)] / P(E) untuk semua gejala
let posteriorNumerator = priorProbability; // Inisialisasi dengan P(H)
const evidencesUsed = [];
// Mengalikan dengan nilai P(E|H) untuk setiap gejala yang ada
for (const idGejala of gejala) {
if (evidenceGivenHypothesis[idGejala]) {
posteriorNumerator *= evidenceGivenHypothesis[idGejala];
evidencesUsed.push({
id_gejala: parseInt(idGejala),
P_E_given_H: evidenceGivenHypothesis[idGejala],
nilai_P_E: evidenceProbabilities[idGejala] || 0
});
}
}
// Posterior probability adalah P(H|E)
const posteriorProbability = posteriorNumerator / totalEvidenceProbability;
hasilPenyakit.push({
id_penyakit: idPenyakit,
nama: namaPenyakit,
P_H: priorProbability,
P_E: totalEvidenceProbability,
evidences: evidencesUsed,
posterior_numerator: posteriorNumerator,
posterior_probability: posteriorProbability,
probabilitas: posteriorProbability
});
const hasil = calculateBayesProbability(penyakitRules, 'penyakit');
if (hasil) {
hasilPenyakit.push(hasil);
}
}
// ========== HAMA ==========
const allHamaRules = await Rule_hama.findAll({
where: { id_gejala: gejala },
include: [{ model: Hama, as: 'hama' }]
});
// Mendapatkan semua hama unik yang memiliki gejala yang dipilih
const uniqueHamaIds = [...new Set(allHamaRules.map(rule => rule.id_hama))];
// Hasil perhitungan untuk setiap hama
const hasilHama = [];
// Hitung untuk setiap hama
for (const idHama of uniqueHamaIds) {
// Filter rules yang berhubungan dengan hama ini
const hamaRules = allHamaRules.filter(rule => rule.id_hama === idHama);
if (hamaRules.length > 0) {
const dataHama = hamaRules[0].hama;
const namaHama = dataHama.nama;
const priorProbability = dataHama.nilai_pakar; // P(H) - prior probability hama
// Menghitung P(E|H) untuk setiap gejala
const evidenceGivenHypothesis = {};
for (const rule of hamaRules) {
evidenceGivenHypothesis[rule.id_gejala] = rule.nilai_pakar; // P(E|H) untuk setiap gejala
}
// Menghitung P(H|E) = [P(E|H) * P(H)] / P(E) untuk semua gejala
let posteriorNumerator = priorProbability; // Inisialisasi dengan P(H)
const evidencesUsed = [];
// Mengalikan dengan nilai P(E|H) untuk setiap gejala yang ada
for (const idGejala of gejala) {
if (evidenceGivenHypothesis[idGejala]) {
posteriorNumerator *= evidenceGivenHypothesis[idGejala];
evidencesUsed.push({
id_gejala: parseInt(idGejala),
P_E_given_H: evidenceGivenHypothesis[idGejala],
nilai_P_E: evidenceProbabilities[idGejala] || 0
});
}
}
// Posterior probability adalah P(H|E)
const posteriorProbability = posteriorNumerator / totalEvidenceProbability;
hasilHama.push({
id_hama: idHama,
nama: namaHama,
P_H: priorProbability,
P_E: totalEvidenceProbability,
evidences: evidencesUsed,
posterior_numerator: posteriorNumerator,
posterior_probability: posteriorProbability,
probabilitas: posteriorProbability
});
const hasil = calculateBayesProbability(hamaRules, 'hama');
if (hasil) {
hasilHama.push(hasil);
}
}
// Urutkan hasil berdasarkan probabilitas
const sortedPenyakit = hasilPenyakit.sort((a, b) => b.probabilitas - a.probabilitas);
const sortedHama = hasilHama.sort((a, b) => b.probabilitas - a.probabilitas);
// Buat ringkasan gejala yang dimasukkan
const gejalaSummary = await Gejala.findAll({
where: { id: gejala },
attributes: ['id', 'kode', 'nama']
});
const sortedPenyakit = hasilPenyakit.sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
const sortedHama = hasilHama.sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
// Gabung hasil dan ambil yang tertinggi (bisa penyakit atau hama)
const allResults = [
...sortedPenyakit.map(p => ({ type: 'penyakit', ...p })),
...sortedHama.map(h => ({ type: 'hama', ...h }))
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
// Simpan histori diagnosa jika ada user yang login dan ada hasil diagnosa
if (!userId) {
console.error('ID user tidak ditemukan pada request. Histori tidak dapat disimpan.');
console.error('ID user tidak ditemukan. Histori tidak dapat disimpan.');
} else {
const idGejalaDipilih = gejala;
const semuaHasil = [...hasilPenyakit, ...hasilHama];
if (semuaHasil.length > 0) {
const hasilTerbesar = semuaHasil.reduce((max, current) => {
return current.probabilitas > max.probabilitas ? current : max;
return current.probabilitas_persen > max.probabilitas_persen ? current : max;
});
// Base histori data without id_gejala
const baseHistoriData = {
userId: userId,
tanggal_diagnosa: tanggal_diagnosa,
hasil: hasilTerbesar.probabilitas
userId: userId, // harus ada
tanggal_diagnosa: tanggal_diagnosa, // harus ada
hasil: hasilTerbesar.nilai_bayes, // harus ada, harus tipe FLOAT
};
// Tambahkan id_penyakit / id_hama jika ada
if (hasilTerbesar.id_penyakit) {
baseHistoriData.id_penyakit = hasilTerbesar.id_penyakit;
} else if (hasilTerbesar.id_hama) {
baseHistoriData.id_hama = hasilTerbesar.id_hama;
}
}
try {
// Option 1: Store only the first gejala ID if we're limited to one record
// if (idGejalaDipilih.length > 0) {
// await Histori.create({
// ...baseHistoriData,
// id_gejala: parseInt(idGejalaDipilih[0]) // Store as integer
// }, { timestamps: false });
// console.log('Histori berhasil disimpan dengan gejala utama');
// }
// Option 2 (Uncomment if you want to create multiple records):
// Create multiple records - one for each gejala
const historiPromises = idGejalaDipilih.map(gejalaId => {
const historiPromises = gejala.map(gejalaId => {
return Histori.create({
...baseHistoriData,
id_gejala: parseInt(gejalaId) // Store as integer
}, { timestamps: false });
id_gejala: parseInt(gejalaId)
});
});
await Promise.all(historiPromises);
console.log(`Histori berhasil disimpan untuk ${idGejalaDipilih.length} gejala`);
console.log(`Histori berhasil disimpan untuk ${gejala.length} gejala.`);
} catch (error) {
console.error('Gagal menyimpan histori:', error.message);
}
} else {
console.log('Tidak ada hasil untuk disimpan ke histori.');
console.log('Tidak ada hasil diagnosa untuk disimpan.');
}
}
// Kirim hasil perhitungan sebagai respons
res.json({
input_gejala: gejalaSummary,
total_evidence_probability: totalEvidenceProbability,
evidence_per_gejala: evidenceProbabilities,
penyakit: sortedPenyakit,
hama: sortedHama,
detail_perhitungan: {
keterangan: "Menggunakan teorema Bayes: P(H|E) = [P(E|H) * P(H)] / P(E)",
formula: {
P_H: "Prior probability (nilai pakar untuk penyakit/hama)",
P_E_given_H: "Likelihood (nilai pakar untuk gejala terhadap penyakit/hama)",
P_E: "Evidence probability = Σ [P(E|Hi) * P(Hi)] untuk semua hipotesis",
P_H_given_E: "Posterior probability (hasil akhir)"
}
return res.status(200).json({
success: true,
message: 'Berhasil melakukan diagnosa',
data: {
penyakit: sortedPenyakit,
hama: sortedHama,
gejala_input: gejala.map(id => parseInt(id)),
hasil_tertinggi: allResults.length > 0 ? allResults[0] : null
}
});
} catch (error) {
console.error('Error dalam perhitungan Bayes:', error);
res.status(500).json({
message: 'Terjadi kesalahan dalam proses diagnosa',
error: error.message
console.error('Error diagnosa:', error);
return res.status(500).json({
success: false,
message: 'Gagal melakukan diagnosa',
error: error.message
});
}
};

View File

@ -66,39 +66,70 @@ exports.updateUser = async (req, res) => {
}
};
// Mengupdate berdasarkan email
// Mengupdate berdasarkan ID
exports.updateUserEmail = async (req, res) => {
try {
const { email, name, alamat, nomorTelepon, newPassword } = req.body;
if (!email) {
return res.status(400).json({ message: "Email harus disertakan" });
}
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(404).json({ message: "User tidak ditemukan" });
}
let hashedPassword = user.password;
if (newPassword) {
hashedPassword = await argon2.hash(newPassword);
}
await user.update({
name: name || user.name,
alamat: alamat || user.alamat,
nomorTelepon: nomorTelepon || user.nomorTelepon,
password: hashedPassword,
});
res.status(200).json({ message: "User berhasil diperbarui", user });
} catch (error) {
res.status(500).json({ message: "Terjadi kesalahan pada server", error });
try {
const { id } = req.params;
const { name, email, alamat, nomorTelepon, password } = req.body;
if (!id) {
return res.status(400).json({ message: "ID harus disertakan" });
}
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ message: "User tidak ditemukan" });
}
};
// Check email uniqueness
if (email && email !== user.email) {
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(400).json({ message: "Email sudah digunakan" });
}
}
// Hash password if provided
let hashedPassword = user.password;
if (password) {
hashedPassword = await argon2.hash(password);
}
await user.update({
name: name || user.name,
email: email || user.email,
alamat: alamat || user.alamat,
nomorTelepon: nomorTelepon || user.nomorTelepon,
password: hashedPassword,
});
// Updated response object
const userResponse = {
id: user.id,
name: user.name,
email: user.email,
alamat: user.alamat,
nomorTelepon: user.nomorTelepon,
role: user.role,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
passwordUpdated: Boolean(password) // Menggunakan Boolean untuk mengkonversi ke true/false
};
res.status(200).json({
message: "User berhasil diperbarui",
user: userResponse
});
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({
message: "Terjadi kesalahan pada server",
error: error.message
});
}
};
// Menghapus user berdasarkan ID (soft delete)
exports.deleteUser = async (req, res) => {

View File

@ -0,0 +1,24 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('Users', 'resetToken', {
type: Sequelize.STRING,
allowNull: true,
after: 'password'
});
await queryInterface.addColumn('Users', 'resetTokenExpiry', {
type: Sequelize.DATE,
allowNull: true,
after: 'resetToken'
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('Users', 'resetToken');
await queryInterface.removeColumn('Users', 'resetTokenExpiry');
}
};

View File

@ -40,6 +40,14 @@ module.exports = (sequelize) => {
type: DataTypes.STRING,
allowNull: false,
},
resetToken: {
type: DataTypes.STRING,
allowNull: true,
},
resetTokenExpiry: {
type: DataTypes.DATE,
allowNull: true,
},
},
{
sequelize,

16
backend/node_modules/.bin/mkdirp generated vendored
View File

@ -1,16 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
else
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
fi

17
backend/node_modules/.bin/mkdirp.cmd generated vendored
View File

@ -1,17 +0,0 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*

28
backend/node_modules/.bin/mkdirp.ps1 generated vendored
View File

@ -1,28 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

View File

@ -106,6 +106,44 @@
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@sendgrid/client": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz",
"integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==",
"license": "MIT",
"dependencies": {
"@sendgrid/helpers": "^8.0.0",
"axios": "^1.8.2"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/@sendgrid/helpers": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
"integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/@sendgrid/mail": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz",
"integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==",
"license": "MIT",
"dependencies": {
"@sendgrid/client": "^8.1.5",
"@sendgrid/helpers": "^8.0.0"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -323,6 +361,12 @@
"version": "1.1.1",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -342,6 +386,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -561,6 +616,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
@ -665,6 +732,24 @@
"ms": "2.0.0"
}
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -808,6 +893,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
@ -993,6 +1093,26 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@ -1010,6 +1130,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"license": "MIT",
@ -1169,6 +1304,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"license": "MIT",
@ -1721,6 +1871,15 @@
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
@ -1874,6 +2033,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.13.0",
"license": "BSD-3-Clause",

View File

@ -1,45 +1,6 @@
MIT License
# isarray
`Array#isArray` for older browsers.
[![build status](https://secure.travis-ci.org/juliangruber/isarray.svg)](http://travis-ci.org/juliangruber/isarray)
[![downloads](https://img.shields.io/npm/dm/isarray.svg)](https://www.npmjs.org/package/isarray)
[![browser support](https://ci.testling.com/juliangruber/isarray.png)
](https://ci.testling.com/juliangruber/isarray)
## Usage
```js
var isArray = require('isarray');
console.log(isArray([])); // => true
console.log(isArray({})); // => false
```
## Installation
With [npm](http://npmjs.org) do
```bash
$ npm install isarray
```
Then bundle for the browser with
[browserify](https://github.com/substack/browserify).
With [component](http://component.io) do
```bash
$ component install juliangruber/isarray
```
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Copyright (C) 2025, Twilio SendGrid, Inc. <help@twilio.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

123
backend/node_modules/@sendgrid/client/README.md generated vendored Normal file
View File

@ -0,0 +1,123 @@
[![BuildStatus](https://travis-ci.com/sendgrid/sendgrid-nodejs.svg?branch=main)](https://travis-ci.com/sendgrid/sendgrid-nodejs)
[![npm version](https://badge.fury.io/js/%40sendgrid%2Fclient.svg)](https://www.npmjs.com/org/sendgrid)
**This package is part of a monorepo, please see [this README](../../README.md) for details.**
# Client for the SendGrid v3 Web API
This client library is used by the other [Twilio SendGrid service packages](https://www.npmjs.com/org/sendgrid) to make requests to the [Twilio SendGrid v3 Web API](https://sendgrid.com/docs/api-reference/). You can also use it independently to make custom requests to the Twilio SendGrid v3 Web API and other HTTP APIs.
# Installation
## Prerequisites
- Node.js version 6, 8 or >=10
- A Twilio SendGrid account, [sign up for free](https://sendgrid.com/free?source=sendgrid-nodejs) to send up to 40,000 emails for the first 30 days or check out [our pricing](https://sendgrid.com/pricing?source=sendgrid-nodejs).
## Obtain an API Key
Grab your API Key from the [Twilio SendGrid UI](https://app.sendgrid.com/settings/api_keys).
## Setup Environment Variables
Do not hardcode your [Twilio SendGrid API Key](https://app.sendgrid.com/settings/api_keys) into your code. Instead, use an environment variable or some other secure means of protecting your Twilio SendGrid API Key. Following is an example of using an environment variable.
Update the development environment with your [SENDGRID_API_KEY](https://app.sendgrid.com/settings/api_keys), for example:
```bash
echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env
echo "sendgrid.env" >> .gitignore
source ./sendgrid.env
```
## Install Package
The following recommended installation requires [npm](https://npmjs.org/). If you are unfamiliar with npm, see the [npm docs](https://npmjs.org/doc/). Npm comes installed with Node.js since node version 0.8.x, therefore, you likely already have it.
```sh
npm install --save @sendgrid/client
```
You may also use [yarn](https://yarnpkg.com/en/) to install.
```sh
yarn add @sendgrid/client
```
<a name="general"></a>
## General v3 Web API Usage Example
Please see [USAGE.md](USAGE.md) for all endpoint examples for the [Twilio SendGrid v3 Web API](https://sendgrid.com/docs/API_Reference/api_v3.html).
```js
const client = require('@sendgrid/client');
client.setApiKey(process.env.SENDGRID_API_KEY);
const request = {
method: 'GET',
url: '/v3/api_keys'
};
client.request(request)
.then(([response, body]) => {
console.log(response.statusCode);
console.log(body);
})
```
## Add a Custom Default Header
```js
client.setDefaultHeader('User-Agent', 'Some user agent string');
// or
client.setDefaultHeader({'User-Agent': 'Some user agent string'});
```
## Change Request Defaults
```js
client.setDefaultRequest('baseUrl', 'https://api.sendgrid.com/');
// or
client.setDefaultRequest({baseUrl: 'https://api.sendgrid.com/'});
```
## Overwrite Promise Implementation
You can overwrite the promise implementation you want the client to use. Defaults to the ES6 `Promise`:
```js
global.Promise = require('bluebird');
```
## Instantiate Client Instances Manually
```js
const {Client} = require('@sendgrid/client');
const sgClient1 = new Client();
const sgClient2 = new Client();
sgClient1.setApiKey('KEY1');
sgClient2.setApiKey('KEY2');
```
<a name="announcements"></a>
# Announcements
All updates to this library are documented in our [CHANGELOG](../../CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-nodejs/releases).
<a name="contribute"></a>
# How to Contribute
We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](../../CONTRIBUTING.md) guide for details.
* [Feature Request](../../CONTRIBUTING.md#feature-request)
* [Bug Reports](../../CONTRIBUTING.md#submit-a-bug-report)
* [Improvements to the Codebase](../../CONTRIBUTING.md#improvements-to-the-codebase)
<a name="troubleshooting"></a>
# Troubleshooting
Please see our [troubleshooting guide](https://github.com/sendgrid/sendgrid-nodejs/blob/main/TROUBLESHOOTING.md) for common library issues.
<a name="about"></a>
# About
@sendgrid/client is maintained and funded by Twilio SendGrid, Inc. The names and logos for @sendgrid/client are trademarks of Twilio SendGrid, Inc.
If you need help installing or using the library, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com).
If you've instead found a bug in the library or would like new features added, go ahead and open issues or pull requests against this repo!
![Twilio SendGrid Logo](https://github.com/sendgrid/sendgrid-nodejs/blob/main/twilio_sendgrid_logo.png?raw=true)

6437
backend/node_modules/@sendgrid/client/USAGE.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

29
backend/node_modules/@sendgrid/client/USE_CASES.md generated vendored Normal file
View File

@ -0,0 +1,29 @@
This document provides examples for specific Twilio SendGrid v3 API non-mail/send use cases. Please [open an issue](https://github.com/sendgrid/sendgrid-nodejs/issues) or make a pull request for any use cases you would like us to document here. Thank you!
# Table of Contents
- [How to Setup a Domain Authentication](#domain-authentication)
- [How to View Email Statistics](#how-to-view-email-statistics)
- [How to use the Email Activity Feed](#how-to-use-the-email-activity-feed)
<a name="domain-authentication"></a>
# How to Setup a Domain Authentication
You can find documentation for how to setup a domain authentication via the UI [here](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication/) and via API [here](USAGE.md#sender-authentication).
Find more information about all of Twilio SendGrid's authentication related documentation [here](https://sendgrid.com/docs/ui/account-and-settings/).
<a name="email-stats"></a>
# How to View Email Statistics
You can find documentation for how to view your email statistics via the UI [here](https://app.sendgrid.com/statistics) and via API [here](USAGE.md#stats).
Alternatively, we can post events to a URL of your choice via our [Event Webhook](https://sendgrid.com/docs/API_Reference/Webhooks/event.html) about events that occur as Twilio SendGrid processes your email.
<a name="email-activity-feed">
# How to use the Email Activity Feed
You can find documentation for how to use the Email Activity Feed via the UI [here](https://sendgrid.com/docs/ui/analytics-and-reporting/email-activity-feed/) and via API [here](USAGE.md#messages).
Find more information about getting started with the Email Activity Feed API [here](https://sendgrid.com/docs/API_Reference/Web_API_v3/Tutorials/getting_started_email_activity_api.html).

3
backend/node_modules/@sendgrid/client/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,3 @@
import Client = require("@sendgrid/client/src/client");
export = Client;

7
backend/node_modules/@sendgrid/client/index.js generated vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const client = require('./src/client');
const Client = require('./src/classes/client');
module.exports = client;
module.exports.Client = Client;

48
backend/node_modules/@sendgrid/client/package.json generated vendored Normal file
View File

@ -0,0 +1,48 @@
{
"name": "@sendgrid/client",
"description": "Twilio SendGrid NodeJS API client",
"version": "8.1.5",
"author": "Twilio SendGrid <help@twilio.com> (sendgrid.com)",
"contributors": [
"Kyle Partridge <kyle.partridge@sendgrid.com>",
"David Tomberlin <david.tomberlin@sendgrid.com>",
"Swift <swift@sendgrid.com>",
"Brandon West <brandon.west@sendgrid.com>",
"Scott Motte <scott.motte@sendgrid.com>",
"Robert Acosta <robert.acosta@sendgrid.com>",
"Elmer Thomas <ethomas@twilio.com>",
"Adam Reis <adam@reis.nz>"
],
"license": "MIT",
"homepage": "https://sendgrid.com",
"repository": {
"type": "git",
"url": "git://github.com/sendgrid/sendgrid-nodejs.git"
},
"publishConfig": {
"access": "public"
},
"main": "index.js",
"engines": {
"node": ">=12.*"
},
"dependencies": {
"@sendgrid/helpers": "^8.0.0",
"axios": "^1.8.2"
},
"devDependencies": {
"chai": "4.2.0",
"nock": "^10.0.6"
},
"resolutions": {
"chai": "4.2.0"
},
"tags": [
"http",
"rest",
"api",
"mail",
"sendgrid"
],
"gitHead": "2bac86884f71be3fb19f96a10c02a1fb616b81fc"
}

View File

@ -0,0 +1,189 @@
'use strict';
const axios = require('axios');
const pkg = require('../../package.json');
const {
helpers: {
mergeData,
},
classes: {
Response,
ResponseError,
},
} = require('@sendgrid/helpers');
const API_KEY_PREFIX = 'SG.';
const SENDGRID_BASE_URL = 'https://api.sendgrid.com/';
const TWILIO_BASE_URL = 'https://email.twilio.com/';
const SENDGRID_REGION = 'global';
// Initialize the allowed regions and their corresponding hosts
const REGION_HOST_MAP = {
eu: 'https://api.eu.sendgrid.com/',
global: 'https://api.sendgrid.com/',
};
class Client {
constructor() {
this.auth = '';
this.impersonateSubuser = '';
this.sendgrid_region = SENDGRID_REGION;
this.defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'sendgrid/' + pkg.version + ';nodejs',
};
this.defaultRequest = {
baseUrl: SENDGRID_BASE_URL,
url: '',
method: 'GET',
headers: {},
maxContentLength: Infinity, // Don't limit the content length.
maxBodyLength: Infinity,
};
}
setApiKey(apiKey) {
this.auth = 'Bearer ' + apiKey;
this.setDefaultRequest('baseUrl', REGION_HOST_MAP[this.sendgrid_region]);
if (!this.isValidApiKey(apiKey)) {
console.warn(`API key does not start with "${API_KEY_PREFIX}".`);
}
}
setTwilioEmailAuth(username, password) {
const b64Auth = Buffer.from(username + ':' + password).toString('base64');
this.auth = 'Basic ' + b64Auth;
this.setDefaultRequest('baseUrl', TWILIO_BASE_URL);
if (!this.isValidTwilioAuth(username, password)) {
console.warn('Twilio Email credentials must be non-empty strings.');
}
}
isValidApiKey(apiKey) {
return this.isString(apiKey) && apiKey.trim().startsWith(API_KEY_PREFIX);
}
isValidTwilioAuth(username, password) {
return this.isString(username) && username
&& this.isString(password) && password;
}
isString(value) {
return typeof value === 'string' || value instanceof String;
}
setImpersonateSubuser(subuser) {
this.impersonateSubuser = subuser;
}
setDefaultHeader(key, value) {
if (key !== null && typeof key === 'object') {
// key is an object
Object.assign(this.defaultHeaders, key);
return this;
}
this.defaultHeaders[key] = value;
return this;
}
setDefaultRequest(key, value) {
if (key !== null && typeof key === 'object') {
// key is an object
Object.assign(this.defaultRequest, key);
return this;
}
this.defaultRequest[key] = value;
return this;
}
/**
* Global is the default residency (or region)
* Global region means the message will be sent through https://api.sendgrid.com
* EU region means the message will be sent through https://api.eu.sendgrid.com
**/
setDataResidency(region) {
if (!REGION_HOST_MAP.hasOwnProperty(region)) {
console.warn('Region can only be "global" or "eu".');
} else {
this.sendgrid_region = region;
this.setDefaultRequest('baseUrl', REGION_HOST_MAP[region]);
}
return this;
}
createHeaders(data) {
// Merge data with default headers.
const headers = mergeData(this.defaultHeaders, data);
// Add auth, but don't overwrite if header already set.
if (typeof headers.Authorization === 'undefined' && this.auth) {
headers.Authorization = this.auth;
}
if (this.impersonateSubuser) {
headers['On-Behalf-Of'] = this.impersonateSubuser;
}
return headers;
}
createRequest(data) {
let options = {
url: data.uri || data.url,
baseUrl: data.baseUrl,
method: data.method,
data: data.body,
params: data.qs,
headers: data.headers,
};
// Merge data with default request.
options = mergeData(this.defaultRequest, options);
options.headers = this.createHeaders(options.headers);
options.baseURL = options.baseUrl;
delete options.baseUrl;
return options;
}
request(data, cb) {
data = this.createRequest(data);
const promise = new Promise((resolve, reject) => {
axios(data)
.then(response => {
return resolve([
new Response(response.status, response.data, response.headers),
response.data,
]);
})
.catch(error => {
if (error.response) {
if (error.response.status >= 400) {
return reject(new ResponseError(error.response));
}
}
return reject(error);
});
});
// Throw an error in case a callback function was not passed.
if (cb && typeof cb !== 'function') {
throw new Error('Callback passed is not a function.');
}
if (cb) {
return promise
.then(result => cb(null, result))
.catch(error => cb(error, null));
}
return promise;
}
}
module.exports = Client;

58
backend/node_modules/@sendgrid/client/src/client.d.ts generated vendored Normal file
View File

@ -0,0 +1,58 @@
import {ResponseError} from "@sendgrid/helpers/classes";
import {ClientRequest} from "@sendgrid/client/src/request";
import {ClientResponse} from "@sendgrid/client/src/response";
declare class Client {
constructor();
/**
* Set the SendGrid API key.
*/
setApiKey(apiKey: string): void;
/**
* Set the Twilio Email credentials.
*/
setTwilioEmailAuth(username: string, password: string): void;
/**
* Set client requests to impersonate a subuser
*/
setImpersonateSubuser(subuser: string): void;
/**
* Set default header
*/
setDefaultHeader(key: string | { [s: string]: string }, value ?: string): this;
/**
* Set default request
*/
setDefaultRequest<K extends keyof ClientRequest>(key: K | ClientRequest, value ?: ClientRequest[K]): this;
/**
* Sets the data residency as per region provided
*/
setDataResidency(region: string): this;
/**
* Create headers for request
*/
createHeaders(data: { [key: string]: string }): { [key: string]: string };
/**
* Create request
*/
createRequest(data: ClientRequest): ClientRequest;
/**
* Do a request
*/
request(data: ClientRequest, cb?: (err: ResponseError, response: [ClientResponse, any]) => void): Promise<[ClientResponse, any]>;
}
declare const client: Client;
// @ts-ignore
export = client
export {Client};

9
backend/node_modules/@sendgrid/client/src/client.js generated vendored Normal file
View File

@ -0,0 +1,9 @@
'use strict';
/**
* Dependencies
*/
const Client = require('./classes/client');
//Export singleton instance
module.exports = new Client();

3146
backend/node_modules/@sendgrid/client/src/client.spec.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
import RequestOptions from "@sendgrid/helpers/classes/request";
export type ClientRequest = RequestOptions;

View File

@ -0,0 +1,3 @@
import Response from "@sendgrid/helpers/classes/response";
export type ClientResponse = Response;

21
backend/node_modules/@sendgrid/helpers/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (C) 2023, Twilio SendGrid, Inc. <help@twilio.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
backend/node_modules/@sendgrid/helpers/README.md generated vendored Normal file
View File

@ -0,0 +1,45 @@
[![BuildStatus](https://travis-ci.com/sendgrid/sendgrid-nodejs.svg?branch=main)](https://travis-ci.com/sendgrid/sendgrid-nodejs)
[![npm version](https://badge.fury.io/js/%40sendgrid%2Fclient.svg)](https://www.npmjs.com/org/sendgrid)
**This package is part of a monorepo, please see [this README](../../README.md) for details.**
# Support classes and helpers for the SendGrid NodeJS libraries
This is a collection of classes and helpers used internally by the
[SendGrid NodeJS libraries](https://www.npmjs.com/org/sendgrid).
Note that not all objects represented in the SendGrid API have helper classes assigned to them because it is not expected that developers will use these classes themselves. They are primarily for internal use and developers are expected to use the publicly exposed API in the [various endpoint services](https://www.npmjs.com/org/sendgrid).
## Mail class
Used to compose a `Mail` object that converts itself to proper JSON for use with the [SendGrid v3 API](https://sendgrid.com/docs/api-reference/). This class supports a slightly different API to make sending emails easier in many cases by not having to deal with personalization arrays, instead offering a more straightforward interface for composing emails.
## Attachment class
Used by the inbound mail parser to compose `Attachment` objects.
## Personalization class
Used by the Mail class to compose `Personalization` objects.
## Email address
`Helper` class to represent an email address with name/email. Used by both the `Mail` and `Personalization` classes to deal with email addresses of various formats.
## Helpers
Internal helpers that mostly speak for themselves.
<a name="contribute"></a>
# How to Contribute
We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/sendgrid/sendgrid-nodejs/blob/HEAD/CONTRIBUTING.md) guide for details.
* [Feature Request](../../CONTRIBUTING.md#feature-request)
* [Bug Reports](../../CONTRIBUTING.md#submit-a-bug-report)
* [Improvements to the Codebase](../../CONTRIBUTING.md#improvements-to-the-codebase)
<a name="about"></a>
# About
@sendgrid/helpers are maintained and funded by Twilio SendGrid, Inc. The names and logos for @sendgrid/helpers are trademarks of Twilio SendGrid, Inc.
If you need help installing or using the library, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com).
If you've instead found a bug in the library or would like new features added, go ahead and open issues or pull requests against this repo!
![Twilio SendGrid Logo](https://github.com/sendgrid/sendgrid-nodejs/blob/main/twilio_sendgrid_logo.png?raw=true)

View File

@ -0,0 +1 @@
Just a little file for testing attachments.

View File

@ -0,0 +1,33 @@
export interface AttachmentData {
content: string;
filename: string;
type?: string;
disposition?: string;
contentId?: string;
}
export interface AttachmentJSON {
content: string;
filename: string;
type?: string;
disposition?: string;
content_id?: string;
}
export default class Attachment implements AttachmentData {
content: string;
filename: string;
type?: string;
disposition?: string;
contentId?: string;
constructor(data?: AttachmentData);
fromData(data: AttachmentData): void;
setContent(content: string): void;
setFilename(filename: string): void;
setType(type: string): void;
setDisposition(disposition: string): void;
setContentId(contentId: string): void;
toJSON(): AttachmentJSON;
}

View File

@ -0,0 +1,187 @@
'use strict';
/**
* Dependencies
*/
const toCamelCase = require('../helpers/to-camel-case');
const toSnakeCase = require('../helpers/to-snake-case');
const deepClone = require('../helpers/deep-clone');
const fs = require('fs');
const path = require('path');
/**
* Attachment class
*/
class Attachment {
/**
* Constructor
*/
constructor(data) {
//Create from data
if (data) {
this.fromData(data);
}
}
/**
* From data
*/
fromData(data) {
//Expecting object
if (typeof data !== 'object') {
throw new Error('Expecting object for Mail data');
}
//Convert to camel case to make it workable, making a copy to prevent
//changes to the original objects
data = deepClone(data);
data = toCamelCase(data);
//Extract properties from data
const {
content,
filename,
type,
disposition,
contentId,
filePath,
} = data;
if ((typeof content !== 'undefined') && (typeof filePath !== 'undefined')) {
throw new Error(
"The props 'content' and 'filePath' cannot be used together."
);
}
//Set data
this.setFilename(filename);
this.setType(type);
this.setDisposition(disposition);
this.setContentId(contentId);
this.setContent(filePath ? this.readFile(filePath) : content);
}
/**
* Read a file and return its content as base64
*/
readFile(filePath) {
return fs.readFileSync(path.resolve(filePath));
}
/**
* Set content
*/
setContent(content) {
//Duck type check toString on content if it's a Buffer as that's the method that will be called.
if (typeof content === 'string') {
this.content = content;
return;
} else if (content instanceof Buffer && content.toString !== undefined) {
this.content = content.toString();
if (this.disposition === 'attachment') {
this.content = content.toString('base64');
}
return;
}
throw new Error('`content` expected to be either Buffer or string');
}
/**
* Set content
*/
setFileContent(content) {
if (content instanceof Buffer && content.toString !== undefined) {
this.content = content.toString('base64');
return;
}
throw new Error('`content` expected to be Buffer');
}
/**
* Set filename
*/
setFilename(filename) {
if (typeof filename === 'undefined') {
return;
}
if (filename && typeof filename !== 'string') {
throw new Error('String expected for `filename`');
}
this.filename = filename;
}
/**
* Set type
*/
setType(type) {
if (typeof type === 'undefined') {
return;
}
if (typeof type !== 'string') {
throw new Error('String expected for `type`');
}
this.type = type;
}
/**
* Set disposition
*/
setDisposition(disposition) {
if (typeof disposition === 'undefined') {
return;
}
if (typeof disposition !== 'string') {
throw new Error('String expected for `disposition`');
}
this.disposition = disposition;
}
/**
* Set content ID
*/
setContentId(contentId) {
if (typeof contentId === 'undefined') {
return;
}
if (typeof contentId !== 'string') {
throw new Error('String expected for `contentId`');
}
this.contentId = contentId;
}
/**
* To JSON
*/
toJSON() {
//Extract properties from self
const {content, filename, type, disposition, contentId} = this;
//Initialize with mandatory properties
const json = {content, filename};
//Add whatever else we have
if (typeof type !== 'undefined') {
json.type = type;
}
if (typeof disposition !== 'undefined') {
json.disposition = disposition;
}
if (typeof contentId !== 'undefined') {
json.contentId = contentId;
}
//Return
return toSnakeCase(json);
}
}
//Export class
module.exports = Attachment;

View File

@ -0,0 +1,95 @@
'use strict';
const fs = require('fs');
const path = require('path');
/**
* Dependencies
*/
const Attachment = require('./attachment');
/**
* Tests
*/
describe('Attachment', function() {
let attachment;
beforeEach(function() {
attachment = new Attachment();
});
//Set content as string
describe('setContent(), string', function() {
it('should set string as content', function() {
attachment.setContent('Just a string.');
expect(attachment.content).to.equal('Just a string.');
});
});
//Set content as stream
describe('setContent(), stream', function() {
it('should convert stream to string and set as content', function() {
const fileData = fs.readFileSync('./packages/helpers/attachment.txt');
attachment.setContent(fileData);
expect(attachment.content).to.equal('Just a little file for testing attachments.');
});
});
//Set content as wrong type
describe('setContent(), wrong type', function() {
it('should not allow setting content of wrong type', function() {
expect(() => attachment.setContent(null)).to.throw('`content` expected to be either Buffer or string');
});
});
//Constructor
describe('constructor(data)', function() {
it('should not accept both content and filePath', function() {
expect(function() {
attachment = new Attachment({
filename: 'attachment.txt',
type: 'plain/text',
disposition: 'attachment',
contentId: 'myattachment',
content: '',
filePath: '',
});
}).to.throw(Error);
});
});
});
//Set content
describe('setContent()', function() {
let attachment;
beforeEach(function() {
attachment = new Attachment({
filename: 'attachment.txt',
type: 'plain/text',
disposition: 'attachment',
contentId: 'myattachment',
content: 'SGVsbG8gV29ybGQK',
});
});
it('should set the given value', function() {
expect(attachment.content).to.equal('SGVsbG8gV29ybGQK');
});
it('should accept a buffer', function() {
attachment.setContent(new Buffer('Hello World\n'));
expect(attachment.content).to.equal('SGVsbG8gV29ybGQK');
});
it('should accept a raw file', function() {
attachment = new Attachment({
filename: 'attachment.txt',
type: 'plain/text',
disposition: 'attachment',
contentId: 'myattachment',
filePath: path.join(__dirname, '/attachment.js'),
});
expect(attachment.content).to.be.a('string');
});
});

View File

@ -0,0 +1,24 @@
export type EmailData = string|{ name?: string; email: string; }
export type EmailJSON = { name?: string; email: string }
export default class EmailAddress {
constructor(data?: EmailData);
/**
* From data
*/
fromData(data: EmailData): void;
/**
* Set name
*/
setName(name: string): void;
/**
* Set email (mandatory)
*/
setEmail(email: string): void;
toJSON(): EmailJSON;
}

View File

@ -0,0 +1,121 @@
'use strict';
/**
* Dependencies
*/
const splitNameEmail = require('../helpers/split-name-email');
/**
* Email address class
*/
class EmailAddress {
/**
* Constructor
*/
constructor(data) {
//Construct from data
if (data) {
this.fromData(data);
}
}
/**
* From data
*/
fromData(data) {
//String given
if (typeof data === 'string') {
const [name, email] = splitNameEmail(data);
data = {name, email};
}
//Expecting object
if (typeof data !== 'object') {
throw new Error('Expecting object or string for EmailAddress data');
}
//Extract name and email
const {name, email} = data;
//Set
this.setEmail(email);
this.setName(name);
}
/**
* Set name
*/
setName(name) {
if (typeof name === 'undefined') {
return;
}
if (typeof name !== 'string') {
throw new Error('String expected for `name`');
}
this.name = name;
}
/**
* Set email (mandatory)
*/
setEmail(email) {
if (typeof email === 'undefined') {
throw new Error('Must provide `email`');
}
if (typeof email !== 'string') {
throw new Error('String expected for `email`');
}
this.email = email;
}
/**
* To JSON
*/
toJSON() {
//Get properties
const {email, name} = this;
//Initialize with mandatory properties
const json = {email};
//Add name if present
if (name !== '') {
json.name = name;
}
//Return
return json;
}
/**************************************************************************
* Static helpers
***/
/**
* Create an EmailAddress instance from given data
*/
static create(data) {
//Array?
if (Array.isArray(data)) {
return data
.filter(item => !!item)
.map(item => this.create(item));
}
//Already instance of EmailAddress class?
if (data instanceof EmailAddress) {
return data;
}
//Create instance
return new EmailAddress(data);
}
}
//Export class
module.exports = EmailAddress;

View File

@ -0,0 +1,175 @@
'use strict';
/**
* Dependencies
*/
const EmailAddress = require('./email-address');
/**
* Tests
*/
describe('EmailAddress', function() {
//Test data
const data = [
'test@example.org',
'Test <test@example.org>',
{name: 'Test', email: 'test@example.org'},
];
//Set email
describe('setEmail()', function() {
let email;
beforeEach(function() {
email = new EmailAddress();
});
it('should set the email address', function() {
email.setEmail('test@example.org');
expect(email.email).to.equal('test@example.org');
});
it('should throw an error for invalid input', function() {
expect(function() {
email.setEmail(5);
}).to.throw(Error);
expect(function() {
email.setEmail(null);
}).to.throw(Error);
});
it('should throw an error for no input', function() {
expect(function() {
email.setEmail();
}).to.throw(Error);
});
});
//Set name
describe('setName()', function() {
let email;
beforeEach(function() {
email = new EmailAddress();
});
it('should set the name', function() {
email.setName('Test');
expect(email.name).to.equal('Test');
});
it('should not wrap name in quotes if a comma is present', function() {
email.setName('Doe, John');
expect(email.name).to.equal('Doe, John');
});
it('should not double wrap in quotes', function() {
email.setName('\"Doe, John\"');
expect(email.name).to.equal('\"Doe, John\"');
});
it('should throw an error for invalid input', function() {
expect(function() {
email.setName(5);
}).to.throw(Error);
expect(function() {
email.setName(null);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
email.setName();
}).not.to.throw(Error);
});
});
//To JSON
describe('toJSON()', function() {
let emails;
beforeEach(function() {
emails = data.map(email => EmailAddress.create(email));
});
it('should always have the email field', function() {
emails.forEach(email => {
const json = email.toJSON();
expect(json).to.have.property('email');
expect(json.email).to.equal(email.email);
});
});
it('should have the name field if given', function() {
emails.filter(email => email.name !== '').forEach(email => {
const json = email.toJSON();
expect(json).to.have.property('name');
expect(json.name).to.equal(email.name);
});
});
it('should not have the name field if not given', function() {
emails.filter(email => email.name === '').forEach(email => {
const json = email.toJSON();
expect(json).not.to.have.property('name');
});
});
});
//From data
describe('fromData()', function() {
let email;
beforeEach(function() {
email = new EmailAddress();
});
it('should handle email address strings', function() {
email.fromData(data[0]);
expect(email.email).to.equal('test@example.org');
expect(email.name).to.equal('');
});
it('should handle name and email address strings', function() {
email.fromData(data[1]);
expect(email.email).to.equal('test@example.org');
expect(email.name).to.equal('Test');
});
it('should handle objects', function() {
email.fromData(data[2]);
expect(email.email).to.equal('test@example.org');
expect(email.name).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
email.fromData(5);
}).to.throw(Error);
});
});
//Static create method
describe('create()', function() {
let emails;
beforeEach(function() {
emails = data.map(email => EmailAddress.create(email));
});
it('should create email address instances from given data', function() {
emails.forEach(email => {
expect(email).to.be.an.instanceof(EmailAddress);
});
});
it('should have the expected properties for each email', function() {
emails.forEach(email => {
expect(email).to.have.property('email');
expect(email).to.have.property('name');
});
});
it('should handle arrays', function() {
const emailsArr = EmailAddress.create(data);
expect(emailsArr).to.be.an.instanceof(Array);
expect(emailsArr).to.have.lengthOf(3);
emailsArr.forEach(email => {
expect(email).to.be.an.instanceof(EmailAddress);
expect(email).to.have.property('email');
expect(email).to.have.property('name');
});
});
it('should handle instances of EmailAddress', function() {
const email1 = new EmailAddress({email: 'test@example.org'});
const email2 = EmailAddress.create(email1);
expect(email2).to.be.an.instanceof(EmailAddress);
expect(email2.email).to.equal(email1.email);
});
});
});

View File

@ -0,0 +1,15 @@
import Attachment from "@sendgrid/helpers/classes/attachment";
import EmailAddress from "@sendgrid/helpers/classes/email-address";
import Mail from "@sendgrid/helpers/classes/mail"
import Personalization from "@sendgrid/helpers/classes/personalization";
import Response from "@sendgrid/helpers/classes/response";
import ResponseError from "@sendgrid/helpers/classes/response-error";
export {
Attachment,
EmailAddress,
Mail,
Personalization,
Response,
ResponseError,
}

View File

@ -0,0 +1,25 @@
'use strict';
/**
* Expose classes
*/
const Attachment = require('./attachment');
const EmailAddress = require('./email-address');
const Mail = require('./mail');
const Personalization = require('./personalization');
const Response = require('./response');
const ResponseError = require('./response-error');
const Statistics = require('./statistics');
/**
* Export
*/
module.exports = {
Attachment,
EmailAddress,
Mail,
Personalization,
Response,
ResponseError,
Statistics,
};

View File

@ -0,0 +1,373 @@
import {AttachmentData, AttachmentJSON} from "./attachment";
import {EmailData, EmailJSON} from "./email-address";
import Personalization from "./personalization";
import {PersonalizationData, PersonalizationJSON} from "./personalization";
export interface MailContent {
type: string;
value: string;
}
export interface ASMOptions {
groupId: number;
groupsToDisplay?: number[];
}
export interface ASMOptionsJSON {
group_id: number;
groups_to_display?: number[];
}
export interface MailSettings {
bcc?: {
enable?: boolean;
email?: string;
};
bypassListManagement?: {
enable?: boolean;
};
bypassSpamManagement?: {
enable?: boolean;
};
bypassBounceManagement?: {
enable?: boolean;
};
bypassUnsubscribeManagement?: {
enable?: boolean;
};
footer?: {
enable?: boolean;
text?: string;
html?: string;
};
sandboxMode?: {
enable?: boolean;
};
spamCheck?: {
enable?: boolean;
threshold?: number;
postToUrl?: string;
};
}
export interface MailSettingsJSON {
bcc?: {
enable?: boolean;
email?: string;
};
bypass_list_management?: {
enable?: boolean;
};
footer?: {
enable?: boolean;
text?: string;
html?: string;
};
sandbox_mode?: {
enable?: boolean;
};
spam_check?: {
enable?: boolean;
threshold?: number;
post_to_url?: string;
};
}
export interface TrackingSettings {
clickTracking?: {
enable?: boolean;
enableText?: boolean;
};
openTracking?: {
enable?: boolean;
substitutionTag?: string;
};
subscriptionTracking?: {
enable?: boolean;
text?: string;
html?: string;
substitutionTag?: string;
};
ganalytics?: {
enable?: boolean;
utmSource?: string;
utmMedium?: string;
utmTerm?: string;
utmContent?: string;
utmCampaign?: string;
};
}
export interface TrackingSettingsJSON {
click_tracking?: {
enable?: boolean;
enable_text?: boolean;
};
open_tracking?: {
enable?: boolean;
substitution_tag?: string;
};
subscription_tracking?: {
enable?: boolean;
text?: string;
html?: string;
substitution_tag?: string;
};
ganalytics?: {
enable?: boolean;
utm_source?: string;
utm_medium?: string;
utm_term?: string;
utm_content?: string;
utm_campaign?: string;
};
}
export interface MailData {
to?: EmailData|EmailData[],
cc?: EmailData|EmailData[],
bcc?: EmailData|EmailData[],
from: EmailData,
replyTo?: EmailData,
sendAt?: number,
subject?: string,
text?: string,
html?: string,
content?: MailContent[],
templateId?: string,
personalizations?: PersonalizationData[],
attachments?: AttachmentData[],
ipPoolName?: string,
batchId?: string,
sections?: { [key: string]: string },
headers?: { [key: string]: string },
categories?: string[],
category?: string,
customArgs?: { [key: string]: any },
asm?: ASMOptions,
mailSettings?: MailSettings,
trackingSettings?: TrackingSettings,
substitutions?: { [key: string]: string },
substitutionWrappers?: string[],
isMultiple?: boolean,
dynamicTemplateData?: { [key: string]: any },
hideWarnings?: boolean,
replyToList?: EmailJSON | EmailJSON[],
}
export type MailDataRequired = MailData & (
{ text: string } | { html: string } | { templateId: string } | { content: MailContent[] & { 0: MailContent } });
export interface MailJSON {
from: EmailJSON;
subject: string;
content: MailContent[];
personalizations: PersonalizationJSON[];
attachments?: AttachmentJSON[];
categories?: string[];
headers?: { [key: string]: string };
mail_settings?: MailSettingsJSON;
tracking_settings?: TrackingSettingsJSON;
custom_args?: { [key: string]: string };
sections?: { [key: string]: string };
asm?: ASMOptionsJSON;
reply_to?: EmailJSON;
send_at?: number;
batch_id?: string;
template_id?: string;
ip_pool_name?: string;
reply_to_list?: EmailJSON[];
}
export default class Mail {
constructor(data?: MailData);
/**
* Build from data
*/
fromData(data: MailData): void;
/**
* Set from email
*/
setFrom(from: EmailData): void;
/**
* Set reply to
*/
setReplyTo(replyTo: EmailData): void;
/**
* Set subject
*/
setSubject(subject: string): void;
/**
* Set send at
*/
setSendAt(sendAt: number): void;
/**
* Set template ID
*/
setTemplateId(templateId: string): void;
/**
* Set batch ID
*/
setBatchId(batchId: string): void;
/**
* Set IP pool name
*/
setIpPoolName(ipPoolName: string): void;
/**
* Set ASM
*/
setAsm(asm: ASMOptions): void;
/**
* Set personalizations
*/
setPersonalizations(personalizations: PersonalizationData[]): void;
/**
* Add personalization
*/
addPersonalization(personalization: PersonalizationData): void;
/**
* Convenience method for quickly creating personalizations
*/
addTo(to: EmailData|EmailData[], cc: EmailData|EmailData[], bcc: EmailData|EmailData[]): void;
/**
* Set substitutions
*/
setSubstitutions(substitutions: { [key: string]: string }): void;
/**
* Set substitution wrappers
*/
setSubstitutionWrappers(wrappers: string[]): void;
/**
* Helper which applies globally set substitutions to personalizations
*/
applySubstitutions(personalization: Personalization): void;
/**
* Set content
*/
setContent(content: MailContent[]): void;
/**
* Add content
*/
addContent(content: MailContent): void;
/**
* Add text content
*/
addTextContent(text: string): void;
/**
* Add HTML content
*/
addHtmlContent(html: string): void;
/**
* Set attachments
*/
setAttachments(attachments: AttachmentData[]): void;
/**
* Add attachment
*/
addAttachment(attachment: AttachmentData): void;
/**
* Set categories
*/
setCategories(categories: string[]): void;
/**
* Add category
*/
addCategory(category: string): void;
/**
* Set headers
*/
setHeaders(headers: { [key: string]: string }): void;
/**
* Add a header
*/
addHeader(key: string, value: string): void;
/**
* Set sections
*/
setSections(sections: { [key: string]: string }): void;
/**
* Set custom args
*/
setCustomArgs(customArgs: { [key: string]: string }): void;
/**
* Set tracking settings
*/
setTrackingSettings(settings: TrackingSettings): void;
/**
* Set mail settings
*/
setMailSettings(settings: MailSettings): void;
/**
* Set hide warnings
*/
setHideWarnings(hide: boolean): void;
/**
* To JSON
*/
toJSON(): MailJSON;
/**
* Create a Mail instance from given data
*/
static create(data: MailData): Mail;
/**
* Create a Mail instance from given data
*/
static create(data: Mail): Mail;
/**
* Create a Mail instance from given data
*/
static create(data: MailData[]): Mail[];
/**
* Set reply_to_list header from given data
*/
setReplyToList(replyToList: EmailJSON[]): void;
}

689
backend/node_modules/@sendgrid/helpers/classes/mail.js generated vendored Normal file
View File

@ -0,0 +1,689 @@
'use strict';
/**
* Dependencies
*/
const EmailAddress = require('./email-address');
const Personalization = require('./personalization');
const toCamelCase = require('../helpers/to-camel-case');
const toSnakeCase = require('../helpers/to-snake-case');
const deepClone = require('../helpers/deep-clone');
const arrayToJSON = require('../helpers/array-to-json');
const { DYNAMIC_TEMPLATE_CHAR_WARNING } = require('../constants');
const {validateMailSettings, validateTrackingSettings} = require('../helpers/validate-settings');
/**
* Mail class
*/
class Mail {
/**
* Constructor
*/
constructor(data) {
//Initialize array and object properties
this.isDynamic = false;
this.hideWarnings = false;
this.personalizations = [];
this.attachments = [];
this.content = [];
this.categories = [];
this.headers = {};
this.sections = {};
this.customArgs = {};
this.trackingSettings = {};
this.mailSettings = {};
this.asm = {};
//Helper properties
this.substitutions = null;
this.substitutionWrappers = null;
this.dynamicTemplateData = null;
//Process data if given
if (data) {
this.fromData(data);
}
}
/**
* Build from data
*/
fromData(data) {
//Expecting object
if (typeof data !== 'object') {
throw new Error('Expecting object for Mail data');
}
//Convert to camel case to make it workable, making a copy to prevent
//changes to the original objects
data = deepClone(data);
data = toCamelCase(data, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers', 'sections']);
//Extract properties from data
const {
to, from, replyTo, cc, bcc, sendAt, subject, text, html, content,
templateId, personalizations, attachments, ipPoolName, batchId,
sections, headers, categories, category, customArgs, asm, mailSettings,
trackingSettings, substitutions, substitutionWrappers, dynamicTemplateData, isMultiple,
hideWarnings, replyToList,
} = data;
//Set data
this.setFrom(from);
this.setReplyTo(replyTo);
this.setSubject(subject);
this.setSendAt(sendAt);
this.setTemplateId(templateId);
this.setBatchId(batchId);
this.setIpPoolName(ipPoolName);
this.setAttachments(attachments);
this.setContent(content);
this.setSections(sections);
this.setHeaders(headers);
this.setCategories(category);
this.setCategories(categories);
this.setCustomArgs(customArgs);
this.setAsm(asm);
this.setMailSettings(mailSettings);
this.setTrackingSettings(trackingSettings);
this.setHideWarnings(hideWarnings);
this.setReplyToList(replyToList);
if (this.isDynamic) {
this.setDynamicTemplateData(dynamicTemplateData);
} else {
this.setSubstitutions(substitutions);
this.setSubstitutionWrappers(substitutionWrappers);
}
//Add contents from text/html properties
this.addTextContent(text);
this.addHtmlContent(html);
//Using "to" property for personalizations
if (personalizations) {
this.setPersonalizations(personalizations);
} else if (isMultiple && Array.isArray(to)) {
//Multiple individual emails
to.forEach(to => this.addTo(to, cc, bcc));
} else {
//Single email (possibly with multiple recipients in the to field)
this.addTo(to, cc, bcc);
}
}
/**
* Set from email
*/
setFrom(from) {
if (this._checkProperty('from', from, [this._checkUndefined])) {
if (typeof from !== 'string' && typeof from.email !== 'string') {
throw new Error('String or address object expected for `from`');
}
this.from = EmailAddress.create(from);
}
}
/**
* Set reply to
*/
setReplyTo(replyTo) {
if (this._checkProperty('replyTo', replyTo, [this._checkUndefined])) {
if (typeof replyTo !== 'string' && typeof replyTo.email !== 'string') {
throw new Error('String or address object expected for `replyTo`');
}
this.replyTo = EmailAddress.create(replyTo);
}
}
/**
* Set subject
*/
setSubject(subject) {
this._setProperty('subject', subject, 'string');
}
/**
* Set send at
*/
setSendAt(sendAt) {
if (this._checkProperty('sendAt', sendAt, [this._checkUndefined, this._createCheckThatThrows(Number.isInteger, 'Integer expected for `sendAt`')])) {
this.sendAt = sendAt;
}
}
/**
* Set template ID, also checks if the template is dynamic or legacy
*/
setTemplateId(templateId) {
if (this._setProperty('templateId', templateId, 'string')) {
if (templateId.indexOf('d-') === 0) {
this.isDynamic = true;
}
}
}
/**
* Set batch ID
*/
setBatchId(batchId) {
this._setProperty('batchId', batchId, 'string');
}
/**
* Set IP pool name
*/
setIpPoolName(ipPoolName) {
this._setProperty('ipPoolName', ipPoolName, 'string');
}
/**
* Set ASM
*/
setAsm(asm) {
if (this._checkProperty('asm', asm, [this._checkUndefined, this._createTypeCheck('object')])) {
if (typeof asm.groupId !== 'number') {
throw new Error('Expected `asm` to include an integer in its `groupId` field');
}
if (asm.groupsToDisplay &&
(!Array.isArray(asm.groupsToDisplay) || !asm.groupsToDisplay.every(group => typeof group === 'number'))) {
throw new Error('Array of integers expected for `asm.groupsToDisplay`');
}
this.asm = asm;
}
}
/**
* Set personalizations
*/
setPersonalizations(personalizations) {
if (!this._doArrayCheck('personalizations', personalizations)) {
return;
}
if (!personalizations.every(personalization => typeof personalization === 'object')) {
throw new Error('Array of objects expected for `personalizations`');
}
//Clear and use add helper to add one by one
this.personalizations = [];
personalizations
.forEach(personalization => this.addPersonalization(personalization));
}
/**
* Add personalization
*/
addPersonalization(personalization) {
//We should either send substitutions or dynamicTemplateData
//depending on the templateId
if (this.isDynamic && personalization.substitutions) {
delete personalization.substitutions;
} else if (!this.isDynamic && personalization.dynamicTemplateData) {
delete personalization.dynamicTemplateData;
}
//Convert to class if needed
if (!(personalization instanceof Personalization)) {
personalization = new Personalization(personalization);
}
//If this is dynamic, set dynamicTemplateData, or set substitutions
if (this.isDynamic) {
this.applyDynamicTemplateData(personalization);
} else {
this.applySubstitutions(personalization);
}
//Push personalization to array
this.personalizations.push(personalization);
}
/**
* Convenience method for quickly creating personalizations
*/
addTo(to, cc, bcc) {
if (
typeof to === 'undefined' &&
typeof cc === 'undefined' &&
typeof bcc === 'undefined'
) {
throw new Error('Provide at least one of to, cc or bcc');
}
this.addPersonalization(new Personalization({to, cc, bcc}));
}
/**
* Set substitutions
*/
setSubstitutions(substitutions) {
this._setProperty('substitutions', substitutions, 'object');
}
/**
* Set substitution wrappers
*/
setSubstitutionWrappers(substitutionWrappers) {
let lengthCheck = (propertyName, value) => {
if (!Array.isArray(value) || value.length !== 2) {
throw new Error('Array expected with two elements for `' + propertyName + '`');
}
};
if (this._checkProperty('substitutionWrappers', substitutionWrappers, [this._checkUndefined, lengthCheck])) {
this.substitutionWrappers = substitutionWrappers;
}
}
/**
* Helper which applies globally set substitutions to personalizations
*/
applySubstitutions(personalization) {
if (personalization instanceof Personalization) {
personalization.reverseMergeSubstitutions(this.substitutions);
personalization.setSubstitutionWrappers(this.substitutionWrappers);
}
}
/**
* Helper which applies globally set dynamic_template_data to personalizations
*/
applyDynamicTemplateData(personalization) {
if (personalization instanceof Personalization) {
personalization.deepMergeDynamicTemplateData(this.dynamicTemplateData);
}
}
/**
* Set dynamicTemplateData
*/
setDynamicTemplateData(dynamicTemplateData) {
if (typeof dynamicTemplateData === 'undefined') {
return;
}
if (typeof dynamicTemplateData !== 'object') {
throw new Error('Object expected for `dynamicTemplateData`');
}
// Check dynamic template for non-escaped characters and warn if found
if (!this.hideWarnings) {
Object.values(dynamicTemplateData).forEach(value => {
if (/['"&]/.test(value)) {
console.warn(DYNAMIC_TEMPLATE_CHAR_WARNING);
}
});
}
this.dynamicTemplateData = dynamicTemplateData;
}
/**
* Set content
*/
setContent(content) {
if (this._doArrayCheck('content', content)) {
if (!content.every(contentField => typeof contentField === 'object')) {
throw new Error('Expected each entry in `content` to be an object');
}
if (!content.every(contentField => typeof contentField.type === 'string')) {
throw new Error('Expected each `content` entry to contain a `type` string');
}
if (!content.every(contentField => typeof contentField.value === 'string')) {
throw new Error('Expected each `content` entry to contain a `value` string');
}
this.content = content;
}
}
/**
* Add content
*/
addContent(content) {
if (this._checkProperty('content', content, [this._createTypeCheck('object')])) {
this.content.push(content);
}
}
/**
* Add text content
*/
addTextContent(text) {
if (this._checkProperty('text', text, [this._checkUndefined, this._createTypeCheck('string')])) {
this.addContent({
value: text,
type: 'text/plain',
});
}
}
/**
* Add HTML content
*/
addHtmlContent(html) {
if (this._checkProperty('html', html, [this._checkUndefined, this._createTypeCheck('string')])) {
this.addContent({
value: html,
type: 'text/html',
});
}
}
/**
* Set attachments
*/
setAttachments(attachments) {
if (this._doArrayCheck('attachments', attachments)) {
if (!attachments.every(attachment => typeof attachment.content === 'string')) {
throw new Error('Expected each attachment to contain a `content` string');
}
if (!attachments.every(attachment => typeof attachment.filename === 'string')) {
throw new Error('Expected each attachment to contain a `filename` string');
}
if (!attachments.every(attachment => !attachment.type || typeof attachment.type === 'string')) {
throw new Error('Expected the attachment\'s `type` field to be a string');
}
if (!attachments.every(attachment => !attachment.disposition || typeof attachment.disposition === 'string')) {
throw new Error('Expected the attachment\'s `disposition` field to be a string');
}
this.attachments = attachments;
}
}
/**
* Add attachment
*/
addAttachment(attachment) {
if (this._checkProperty('attachment', attachment, [this._checkUndefined, this._createTypeCheck('object')])) {
this.attachments.push(attachment);
}
}
/**
* Set categories
*/
setCategories(categories) {
let allElementsAreStrings = (propertyName, value) => {
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
throw new Error('Array of strings expected for `' + propertyName + '`');
}
};
if (typeof categories === 'string') {
categories = [categories];
}
if (this._checkProperty('categories', categories, [this._checkUndefined, allElementsAreStrings])) {
this.categories = categories;
}
}
/**
* Add category
*/
addCategory(category) {
if (this._checkProperty('category', category, [this._createTypeCheck('string')])) {
this.categories.push(category);
}
}
/**
* Set headers
*/
setHeaders(headers) {
this._setProperty('headers', headers, 'object');
}
/**
* Add a header
*/
addHeader(key, value) {
if (this._checkProperty('key', key, [this._createTypeCheck('string')])
&& this._checkProperty('value', value, [this._createTypeCheck('string')])) {
this.headers[key] = value;
}
}
/**
* Set sections
*/
setSections(sections) {
this._setProperty('sections', sections, 'object');
}
/**
* Set custom args
*/
setCustomArgs(customArgs) {
this._setProperty('customArgs', customArgs, 'object');
}
/**
* Set tracking settings
*/
setTrackingSettings(settings) {
if (typeof settings === 'undefined') {
return;
}
validateTrackingSettings(settings);
this.trackingSettings = settings;
}
/**
* Set mail settings
*/
setMailSettings(settings) {
if (typeof settings === 'undefined') {
return;
}
validateMailSettings(settings);
this.mailSettings = settings;
}
/**
* Set hide warnings
*/
setHideWarnings(hide) {
if (typeof hide === 'undefined') {
return;
}
if (typeof hide !== 'boolean') {
throw new Error('Boolean expected for `hideWarnings`');
}
this.hideWarnings = hide;
}
/**
* To JSON
*/
toJSON() {
//Extract properties from self
const {
from, replyTo, sendAt, subject, content, templateId,
personalizations, attachments, ipPoolName, batchId, asm,
sections, headers, categories, customArgs, mailSettings,
trackingSettings, replyToList,
} = this;
//Initialize with mandatory values
const json = {
from, subject,
personalizations: arrayToJSON(personalizations),
};
//Array properties
if (Array.isArray(attachments) && attachments.length > 0) {
json.attachments = arrayToJSON(attachments);
}
if (Array.isArray(categories) && categories.length > 0) {
json.categories = categories.filter(cat => cat !== '');
}
if (Array.isArray(content) && content.length > 0) {
json.content = arrayToJSON(content);
}
//Object properties
if (Object.keys(headers).length > 0) {
json.headers = headers;
}
if (Object.keys(mailSettings).length > 0) {
json.mailSettings = mailSettings;
}
if (Object.keys(trackingSettings).length > 0) {
json.trackingSettings = trackingSettings;
}
if (Object.keys(customArgs).length > 0) {
json.customArgs = customArgs;
}
if (Object.keys(sections).length > 0) {
json.sections = sections;
}
if (Object.keys(asm).length > 0) {
json.asm = asm;
}
//Simple properties
if (typeof replyTo !== 'undefined') {
json.replyTo = replyTo;
}
if (typeof sendAt !== 'undefined') {
json.sendAt = sendAt;
}
if (typeof batchId !== 'undefined') {
json.batchId = batchId;
}
if (typeof templateId !== 'undefined') {
json.templateId = templateId;
}
if (typeof ipPoolName !== 'undefined') {
json.ipPoolName = ipPoolName;
}
if(typeof replyToList !== 'undefined') {
json.replyToList = replyToList;
}
//Return as snake cased object
return toSnakeCase(json, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers', 'sections']);
}
/**************************************************************************
* Static helpers
***/
/**
* Create a Mail instance from given data
*/
static create(data) {
//Array?
if (Array.isArray(data)) {
return data
.filter(item => !!item)
.map(item => this.create(item));
}
//Already instance of Mail class?
if (data instanceof Mail) {
return data;
}
//Create instance
return new Mail(data);
}
/**************************************************************************
* helpers for property-setting checks
***/
/**
* Perform a set of checks on the new property value. Returns true if all
* checks complete successfully without throwing errors or returning true.
*/
_checkProperty(propertyName, value, checks) {
return !checks.some((e) => e(propertyName, value));
}
/**
* Set a property with normal undefined and type-checks
*/
_setProperty(propertyName, value, propertyType) {
let propertyChecksPassed = this._checkProperty(
propertyName,
value,
[this._checkUndefined, this._createTypeCheck(propertyType)]);
if (propertyChecksPassed) {
this[propertyName] = value;
}
return propertyChecksPassed;
}
/**
* Fail if the value is undefined.
*/
_checkUndefined(propertyName, value) {
return typeof value === 'undefined';
}
/**
* Create and return a function that checks for a given type
*/
_createTypeCheck(propertyType) {
return (propertyName, value) => {
if (typeof value !== propertyType) {
throw new Error(propertyType + ' expected for `' + propertyName + '`');
}
};
}
/**
* Create a check out of a callback. If the callback
* returns false, the check will throw an error.
*/
_createCheckThatThrows(check, errorString) {
return (propertyName, value) => {
if (!check(value)) {
throw new Error(errorString);
}
};
}
/**
* Set an array property after checking that the new value is an
* array.
*/
_setArrayProperty(propertyName, value) {
if (this._doArrayCheck(propertyName, value)) {
this[propertyName] = value;
}
}
/**
* Check that a value isn't undefined and is an array.
*/
_doArrayCheck(propertyName, value) {
return this._checkProperty(
propertyName,
value,
[this._checkUndefined, this._createCheckThatThrows(Array.isArray, 'Array expected for`' + propertyName + '`')]);
}
/**
* Set the replyToList from email body
*/
setReplyToList(replyToList) {
if (this._doArrayCheck('replyToList', replyToList) && replyToList.length) {
if (!replyToList.every(replyTo => replyTo && typeof replyTo.email === 'string')) {
throw new Error('Expected each replyTo to contain an `email` string');
}
this.replyToList = replyToList;
}
}
}
//Export class
module.exports = Mail;

View File

@ -0,0 +1,307 @@
'use strict';
/**
* Dependencies
*/
const Mail = require('./mail');
const { DYNAMIC_TEMPLATE_CHAR_WARNING } = require('../constants');
/**
* Tests
*/
describe('Mail', function() {
describe('construct', function() {
it('shouldn\'t convert the headers to camel/snake case', function() {
const mail = new Mail({
personalizations: [{
to: 'test@example.com',
headers: {
'test-header': 'test',
},
}],
from: {
email: 'test@example.com',
},
subject: 'test',
content: [{
type: 'text/plain',
value: 'test',
}],
category: 'test',
headers: {
'List-Unsubscribe': '<mailto:test@test.com>',
},
});
expect(mail.headers['List-Unsubscribe']).to
.equal('<mailto:test@test.com>');
expect(mail.toJSON().headers['List-Unsubscribe']).to
.equal('<mailto:test@test.com>');
});
it('should detect dynamic template id', function() {
const mail = new Mail({
personalizations: [{
to: 'test@example.com',
headers: {
'test-header': 'test',
},
}],
from: {
email: 'test@example.com',
},
templateId: 'd-df80613cccc6441ea5cd7c95377bc1ef',
subject: 'test',
content: [{
type: 'text/plain',
value: 'test',
}],
});
expect(mail.isDynamic).to.equal(true);
});
it('should detect legacy template id', function() {
const mail = new Mail({
personalizations: [{
to: 'test@example.com',
headers: {
'test-header': 'test',
},
}],
from: {
email: 'test@example.com',
},
templateId: 'df80613cccc6441ea5cd7c95377bc1ef',
subject: 'test',
content: [{
type: 'text/plain',
value: 'test',
}],
});
expect(mail.isDynamic).to.equal(false);
});
it('should ignore substitutions if templateId is dynamic', function() {
const mail = new Mail({
personalizations: [{
to: 'test@example.com',
headers: {
'test-header': 'test',
},
substitutions: {
test2: 'Test2',
},
dynamicTemplateData: {
test2: 'Testy 2',
test3: 'Testy 3',
},
}],
dynamicTemplateData: {
test1: 'Test 1',
test2: 'Test 2',
},
substitutions: {
test1: 'Test1',
},
from: {
email: 'test@example.com',
},
templateId: 'd-df80613cccc6441ea5cd7c95377bc1ef',
subject: 'test',
content: [{
type: 'text/plain',
value: 'test',
}],
});
expect(mail.substitutions).to.equal(null);
expect(mail.personalizations[0].substitutions).to.deep.equal({});
expect(mail.dynamicTemplateData).to.deep.equal({ test1: 'Test 1', test2: 'Test 2' });
expect(mail.personalizations[0].dynamicTemplateData).to.deep.equal({ test1: 'Test 1', test2: 'Testy 2', test3: 'Testy 3' });
expect(mail.toJSON()).to.deep.equal({
content: [
{
type: 'text/plain',
value: 'test',
},
],
from: {
email: 'test@example.com',
},
personalizations: [
{
dynamic_template_data: {
test1: 'Test 1',
test2: 'Testy 2',
test3: 'Testy 3',
},
headers: {
'test-header': 'test',
},
to: [
{
email: 'test@example.com',
name: '',
},
],
},
],
subject: 'test',
template_id: 'd-df80613cccc6441ea5cd7c95377bc1ef',
});
});
describe('attachments', () => {
it('handles multiple attachments', () => {
const mail = new Mail({
to: 'recipient@example.org',
attachments: [{
content: 'test-content',
filename: 'name-that-file',
type: 'file-type',
}, {
content: 'other-content',
filename: 'name-this-file',
disposition: 'inline',
}],
});
expect(mail.toJSON()['attachments']).to.have.a.lengthOf(2);
});
it('requires content', () => {
expect(() => new Mail({
attachments: [{
filename: 'missing content',
}],
})).to.throw('content');
});
it('requires filename', () => {
expect(() => new Mail({
attachments: [{
content: 'missing filename',
}],
})).to.throw('filename');
});
});
});
describe('dynamic template handlebars substitutions', () => {
let logSpy; let data;
beforeEach(() => {
logSpy = sinon.spy(console, 'warn');
data = {
to: 'recipient@example.org',
from: 'sender@example.org',
subject: 'Hello world',
text: 'Hello plain world!',
html: '<p>Hello HTML world!</p>',
templateId: 'd-df80613cccc6441ea5cd7c95377bc1ef',
};
});
afterEach(() => {
console.warn.restore();
});
it('should log an error if template subject line contains improperly escaped "\'" character', () => {
data = Object.assign(data, {
dynamicTemplateData: {
subject: 'Testing Templates and \'Stuff\'',
},
});
const mail = new Mail(data);
expect(logSpy.calledOnce).to.equal(true);
expect(logSpy.calledWith(DYNAMIC_TEMPLATE_CHAR_WARNING)).to.equal(true);
});
it('should log an error if template subject line contains improperly escaped """ character', () => {
data = Object.assign(data, {
dynamicTemplateData: {
subject: '"Testing Templates" and Stuff',
},
});
const mail = new Mail(data);
expect(logSpy.calledOnce).to.equal(true);
expect(logSpy.calledWith(DYNAMIC_TEMPLATE_CHAR_WARNING)).to.equal(true);
});
it('should log an error if template subject line contains improperly escaped "&" character', () => {
data = Object.assign(data, {
dynamicTemplateData: {
subject: 'Testing Templates & Stuff',
},
});
const mail = new Mail(data);
expect(logSpy.calledOnce).to.equal(true);
expect(logSpy.calledWith(DYNAMIC_TEMPLATE_CHAR_WARNING)).to.equal(true);
});
});
describe('set replyToList to set multiple reply-to', () => {
let data;
this.beforeEach(() => {
data = {
to: 'send-to@example.org',
from: 'sender@example.org',
subject: 'test replyToList',
category: 'test',
text: 'Testing replyToList settings',
html: '<p>Testing replyToList settings</p>',
};
});
it('should set the replyToList', () => {
let replyToList = [
{
'name': 'Test User1',
'email': 'test_user1@example.org'
},
{
'email': 'test_user2@example.org'
}
];
data.replyToList = replyToList;
const mail = new Mail(data);
expect(mail.replyToList)
.to.be.deep.equal(replyToList);
});
it('should throw error for incorrect replyToList format', () => {
let replyToList = [
{
'name': 'Test User1'
},
{
'email_data': 'test_user2@example.org'
}
];
data.replyToList = replyToList;
expect(() => new Mail(data))
.to.throw('Expected each replyTo to contain an `email` string');
});
it('should throw error for as replyToList is not an array', () => {
let replyToList = {
'name': 'Test User1',
'email': 'test_user1@example.org'
};
data.replyToList = replyToList;
expect(() => new Mail(data))
.to.throw('Array expected for`replyToList`');
});
});
});

View File

@ -0,0 +1,128 @@
import { EmailData, EmailJSON } from "./email-address";
export interface PersonalizationData {
to: EmailData | EmailData[],
from?: EmailData,
cc?: EmailData | EmailData[],
bcc?: EmailData | EmailData[],
subject?: string;
headers?: { [key: string]: string };
substitutions?: { [key: string]: string };
dynamicTemplateData?: { [key: string]: any; };
customArgs?: { [key: string]: string };
sendAt?: number;
}
export interface PersonalizationJSON {
to: EmailJSON | EmailJSON[];
from?: EmailJSON;
cc?: EmailJSON[];
bcc?: EmailJSON[];
headers?: { [key: string]: string; };
substitutions?: { [key: string]: string; };
dynamic_template_data?: { [key: string]: string; };
custom_args?: { [key: string]: string; };
subject?: string;
send_at?: number;
}
export default class Personalization {
constructor(data?: PersonalizationData);
fromData(data: PersonalizationData): void;
/**
* Set subject
*/
setSubject(subject: string): void;
/**
* Set send at
*/
setSendAt(sendAt: number): void;
/**
* Set to
*/
setTo(to: EmailData | EmailData[]): void;
/**
* Set from
*/
setFrom(from: EmailData): void;
/**
* Add a single to
*/
addTo(to: EmailData): void;
/**
* Set cc
*/
setCc(cc: EmailData | EmailData[]): void;
/**
* Add a single cc
*/
addCc(cc: EmailData): void;
/**
* Set bcc
*/
setBcc(bcc: EmailData | EmailData[]): void;
/**
* Add a single bcc
*/
addBcc(bcc: EmailData): void;
/**
* Set headers
*/
setHeaders(headers: { [key: string]: string }): void;
/**
* Add a header
*/
addHeader(key: string, value: string): void;
/**
* Set custom args
*/
setCustomArgs(customArgs: { [key: string]: string }): void;
/**
* Add a custom arg
*/
addCustomArg(key: string, value: string): void;
/**
* Set substitutions
*/
setSubstitutions(substitutions: { [key: string]: string }): void;
/**
* Add a substitution
*/
addSubstitution(key: string, value: string): void;
/**
* Reverse merge substitutions, preserving existing ones
*/
reverseMergeSubstitutions(substitutions: { [key: string]: string }): void;
/**
* Set substitution wrappers
*/
setSubstitutionWrappers(wrappers: string[]): void;
/**
* Set dynamic template data
*/
setDynamicTemplateData(dynamicTemplateData: { [key: string]: any }): void;
/**
* To JSON
*/
toJSON(): PersonalizationJSON;
}

View File

@ -0,0 +1,371 @@
'use strict';
/**
* Dependencies
*/
const EmailAddress = require('./email-address');
const toCamelCase = require('../helpers/to-camel-case');
const toSnakeCase = require('../helpers/to-snake-case');
const deepClone = require('../helpers/deep-clone');
const deepMerge = require('deepmerge');
const wrapSubstitutions = require('../helpers/wrap-substitutions');
/**
* Personalization class
*/
class Personalization {
/**
* Constructor
*/
constructor(data) {
//Init array and object placeholders
this.to = [];
this.cc = [];
this.bcc = [];
this.headers = {};
this.customArgs = {};
this.substitutions = {};
this.substitutionWrappers = ['{{', '}}'];
this.dynamicTemplateData = {};
//Build from data if given
if (data) {
this.fromData(data);
}
}
/**
* From data
*/
fromData(data) {
//Expecting object
if (typeof data !== 'object') {
throw new Error('Expecting object for Mail data');
}
//Convert to camel case to make it workable, making a copy to prevent
//changes to the original objects
data = deepClone(data);
data = toCamelCase(data, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers']);
//Extract properties from data
const {
to, from, cc, bcc, subject, headers, customArgs, sendAt,
substitutions, substitutionWrappers, dynamicTemplateData,
} = data;
//Set data
this.setTo(to);
this.setFrom(from);
this.setCc(cc);
this.setBcc(bcc);
this.setSubject(subject);
this.setHeaders(headers);
this.setSubstitutions(substitutions);
this.setSubstitutionWrappers(substitutionWrappers);
this.setCustomArgs(customArgs);
this.setDynamicTemplateData(dynamicTemplateData);
this.setSendAt(sendAt);
}
/**
* Set subject
*/
setSubject(subject) {
if (typeof subject === 'undefined') {
return;
}
if (typeof subject !== 'string') {
throw new Error('String expected for `subject`');
}
this.subject = subject;
}
/**
* Set send at
*/
setSendAt(sendAt) {
if (typeof sendAt === 'undefined') {
return;
}
if (!Number.isInteger(sendAt)) {
throw new Error('Integer expected for `sendAt`');
}
this.sendAt = sendAt;
}
/**
* Set to
*/
setTo(to) {
if (typeof to === 'undefined') {
return;
}
if (!Array.isArray(to)) {
to = [to];
}
this.to = EmailAddress.create(to);
}
/**
* Set from
* */
setFrom(from) {
if (typeof from === 'undefined') {
return;
}
this.from = EmailAddress.create(from);
}
/**
* Add a single to
*/
addTo(to) {
if (typeof to === 'undefined') {
return;
}
this.to.push(EmailAddress.create(to));
}
/**
* Set cc
*/
setCc(cc) {
if (typeof cc === 'undefined') {
return;
}
if (!Array.isArray(cc)) {
cc = [cc];
}
this.cc = EmailAddress.create(cc);
}
/**
* Add a single cc
*/
addCc(cc) {
if (typeof cc === 'undefined') {
return;
}
this.cc.push(EmailAddress.create(cc));
}
/**
* Set bcc
*/
setBcc(bcc) {
if (typeof bcc === 'undefined') {
return;
}
if (!Array.isArray(bcc)) {
bcc = [bcc];
}
this.bcc = EmailAddress.create(bcc);
}
/**
* Add a single bcc
*/
addBcc(bcc) {
if (typeof bcc === 'undefined') {
return;
}
this.bcc.push(EmailAddress.create(bcc));
}
/**
* Set headers
*/
setHeaders(headers) {
if (typeof headers === 'undefined') {
return;
}
if (typeof headers !== 'object' || headers === null) {
throw new Error('Object expected for `headers`');
}
this.headers = headers;
}
/**
* Add a header
*/
addHeader(key, value) {
if (typeof key !== 'string') {
throw new Error('String expected for header key');
}
if (typeof value !== 'string') {
throw new Error('String expected for header value');
}
this.headers[key] = value;
}
/**
* Set custom args
*/
setCustomArgs(customArgs) {
if (typeof customArgs === 'undefined') {
return;
}
if (typeof customArgs !== 'object' || customArgs === null) {
throw new Error('Object expected for `customArgs`');
}
this.customArgs = customArgs;
}
/**
* Add a custom arg
*/
addCustomArg(key, value) {
if (typeof key !== 'string') {
throw new Error('String expected for custom arg key');
}
if (typeof value !== 'string') {
throw new Error('String expected for custom arg value');
}
this.customArgs[key] = value;
}
/**
* Set substitutions
*/
setSubstitutions(substitutions) {
if (typeof substitutions === 'undefined') {
return;
}
if (typeof substitutions !== 'object') {
throw new Error('Object expected for `substitutions`');
}
this.substitutions = substitutions;
}
/**
* Add a substitution
*/
addSubstitution(key, value) {
if (typeof key !== 'string') {
throw new Error('String expected for substitution key');
}
if (typeof value !== 'string' && typeof value !== 'number') {
throw new Error('String or Number expected for substitution value');
}
this.substitutions[key] = value;
}
/**
* Reverse merge substitutions, preserving existing ones
*/
reverseMergeSubstitutions(substitutions) {
if (typeof substitutions === 'undefined' || substitutions === null) {
return;
}
if (typeof substitutions !== 'object') {
throw new Error(
'Object expected for `substitutions` in reverseMergeSubstitutions'
);
}
this.substitutions = Object.assign({}, substitutions, this.substitutions);
}
/**
* Set substitution wrappers
*/
setSubstitutionWrappers(wrappers) {
if (typeof wrappers === 'undefined' || wrappers === null) {
return;
}
if (!Array.isArray(wrappers) || wrappers.length !== 2) {
throw new Error(
'Array expected with two elements for `substitutionWrappers`'
);
}
this.substitutionWrappers = wrappers;
}
/**
* Reverse merge dynamic template data, preserving existing ones
*/
deepMergeDynamicTemplateData(dynamicTemplateData) {
if (typeof dynamicTemplateData === 'undefined' || dynamicTemplateData === null) {
return;
}
if (typeof dynamicTemplateData !== 'object') {
throw new Error(
'Object expected for `dynamicTemplateData` in deepMergeDynamicTemplateData'
);
}
this.dynamicTemplateData = deepMerge(dynamicTemplateData, this.dynamicTemplateData);
}
/**
* Set dynamic template data
*/
setDynamicTemplateData(dynamicTemplateData) {
if (typeof dynamicTemplateData === 'undefined') {
return;
}
if (typeof dynamicTemplateData !== 'object') {
throw new Error('Object expected for `dynamicTemplateData`');
}
this.dynamicTemplateData = dynamicTemplateData;
}
/**
* To JSON
*/
toJSON() {
//Get data from self
const {
to, from, cc, bcc, subject, headers, customArgs, sendAt,
substitutions, substitutionWrappers, dynamicTemplateData,
} = this;
//Initialize with mandatory values
const json = {to};
//Arrays
if (Array.isArray(cc) && cc.length > 0) {
json.cc = cc;
}
if (Array.isArray(bcc) && bcc.length > 0) {
json.bcc = bcc;
}
//Objects
if (Object.keys(headers).length > 0) {
json.headers = headers;
}
if (substitutions && Object.keys(substitutions).length > 0) {
const [left, right] = substitutionWrappers;
json.substitutions = wrapSubstitutions(substitutions, left, right);
}
if (Object.keys(customArgs).length > 0) {
json.customArgs = customArgs;
}
if (dynamicTemplateData && Object.keys(dynamicTemplateData).length > 0) {
json.dynamicTemplateData = dynamicTemplateData;
}
//Simple properties
if (typeof subject !== 'undefined') {
json.subject = subject;
}
if (typeof sendAt !== 'undefined') {
json.sendAt = sendAt;
}
if (typeof from !== 'undefined') {
json.from = from;
}
//Return as snake cased object
return toSnakeCase(json, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers']);
}
}
//Export class
module.exports = Personalization;

View File

@ -0,0 +1,36 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
// camel case headers test
describe('#527', function() {
it('shouldn\'t convert the headers to camel/snake case', function() {
const p = new Personalization({
to: 'test@example.com',
headers: {
'List-Unsubscribe': '<mailto:test@test.com>',
},
});
expect(p.headers['List-Unsubscribe']).to.equal('<mailto:test@test.com>');
expect(p.toJSON().headers['List-Unsubscribe']).to
.equal('<mailto:test@test.com>');
});
});
});

View File

@ -0,0 +1,23 @@
#### Personalization helper specs
- setSubject() - set-subject.spec.js
- setSendAt() - set-send-at.spec.js
- setTo() - set-to.spec.js
- setFrom() - set-from.spec.js
- addTo() - add-to.spec.js
- setCc() - set-cc.spec.js
- addCc() - add-cc.spec.js
- setBcc() - set-bcc.spec.js
- addBcc() - add-bcc.spec.js
- setHeaders() - set-headers.spec.js
- addHeader() - add-headers.spec.js
- setCustomArgs() - set-custom-args.spec.js
- addCustomArg() - add-custom-args.spec.js
- setSubstitutions() - set-substitutions.spec.js
- addSubstitution() - add-substitutions.spec.js
- reverseMergeSubstitutions() - reverse-merge-substitutions.spec.js
- setSubstitutionWrappers() - set-substitutions-wrappers.spec.js
- deepMergeDynamicTemplateData() - reverse-merge-dynamic_template_data.spec.js
- toJSON() - to-json.spec.js
- fromData() - from-data.spec.js
- #527 - 527-camel-case-headers.spec.js

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add bcc
describe('addBcc()', function() {
it('should add the item', function() {
p.addBcc('test@example.org');
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(1);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.addBcc('test1@example.org');
p.addBcc('test2@example.org');
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(2);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test1@example.org');
expect(p.bcc[1]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.addBcc();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.addBcc('test@example.org');
p.addBcc();
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(1);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add cc
describe('addCc()', function() {
it('should add the item', function() {
p.addCc('test@example.org');
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(1);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.addCc('test1@example.org');
p.addCc('test2@example.org');
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(2);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test1@example.org');
expect(p.cc[1]).to.be.an.instanceof(EmailAddress);
expect(p.cc[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.addCc();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.addCc('test@example.org');
p.addCc();
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(1);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,48 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add custom arg
describe('addCustomArg()', function() {
it('should set the given value', function() {
p.addCustomArg('test', 'Test');
expect(p.customArgs).to.be.an.instanceof(Object);
expect(p.customArgs).to.have.a.property('test');
expect(p.customArgs.test).to.equal('Test');
});
it('should add multiple values', function() {
p.addCustomArg('test1', 'Test1');
p.addCustomArg('test2', 'Test2');
expect(p.customArgs).to.have.a.property('test1');
expect(p.customArgs).to.have.a.property('test2');
expect(p.customArgs.test1).to.equal('Test1');
expect(p.customArgs.test2).to.equal('Test2');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.addCustomArg('test');
}).to.throw(Error);
expect(function() {
p.addCustomArg(null, 'test');
}).to.throw(Error);
expect(function() {
p.addCustomArg(3, 5);
}).to.throw(Error);
});
});
});

View File

@ -0,0 +1,48 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add header
describe('addHeader()', function() {
it('should set the given value', function() {
p.addHeader('test', 'Test');
expect(p.headers).to.be.an.instanceof(Object);
expect(p.headers).to.have.a.property('test');
expect(p.headers.test).to.equal('Test');
});
it('should add multiple values', function() {
p.addHeader('test1', 'Test1');
p.addHeader('test2', 'Test2');
expect(p.headers).to.have.a.property('test1');
expect(p.headers).to.have.a.property('test2');
expect(p.headers.test1).to.equal('Test1');
expect(p.headers.test2).to.equal('Test2');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.addHeader('test');
}).to.throw(Error);
expect(function() {
p.addHeader(null, 'test');
}).to.throw(Error);
expect(function() {
p.addHeader(3, 5);
}).to.throw(Error);
});
});
});

View File

@ -0,0 +1,48 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add substitution
describe('addSubstitution()', function() {
it('should set the given value', function() {
p.addSubstitution('test', 'Test');
expect(p.substitutions).to.be.an.instanceof(Object);
expect(p.substitutions).to.have.a.property('test');
expect(p.substitutions.test).to.equal('Test');
});
it('should add multiple values', function() {
p.addSubstitution('test1', 'Test1');
p.addSubstitution('test2', 2);
expect(p.substitutions).to.have.a.property('test1');
expect(p.substitutions).to.have.a.property('test2');
expect(p.substitutions.test1).to.equal('Test1');
expect(p.substitutions.test2).to.equal(2);
});
it('should throw an error for invalid input', function() {
expect(function() {
p.addSubstitution('test');
}).to.throw(Error);
expect(function() {
p.addSubstitution(null, 'test');
}).to.throw(Error);
expect(function() {
p.addSubstitution(3, false);
}).to.throw(Error);
});
});
});

View File

@ -0,0 +1,89 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//From data
describe('fromData()', function() {
//Data
const data = {
to: 'to@example.org',
from: 'from@example.org',
cc: ['cc1@example.org', 'cc2@example.org'],
bcc: ['bcc1@example.org', 'bcc2@example.org'],
subject: 'Test',
sendAt: 1000,
headers: {test: 'Test'},
customArgs: {snake_case: 'Test', T_EST: 'Test', camelCase: 'Test'},
substitutions: {snake_case: 'Test', T_EST: 'Test', camelCase: 'Test'},
substitutionWrappers: ['[', ']'],
};
//Tests
it('should call fromData() from the constructor', () => {
p = new Personalization(data);
expect(p.to[0].email).to.equal('to@example.org');
expect(p.subject).to.equal('Test');
});
it('should throw an error for invalid input', () => {
expect(function() {
p.fromData(5);
}).to.throw(Error);
});
it('should have set all properties', () => {
p.fromData(data);
expect(p.to[0].email).to.equal('to@example.org');
expect(p.from.email).to.equal('from@example.org');
expect(p.cc[0].email).to.equal('cc1@example.org');
expect(p.cc[1].email).to.equal('cc2@example.org');
expect(p.bcc[0].email).to.equal('bcc1@example.org');
expect(p.bcc[1].email).to.equal('bcc2@example.org');
expect(p.subject).to.equal('Test');
expect(p.sendAt).to.equal(1000);
expect(p.headers.test).to.equal('Test');
expect(p.customArgs.snake_case).to.equal('Test');
expect(p.substitutions.snake_case).to.equal('Test');
expect(p.substitutionWrappers).to.have.members(['[', ']']);
});
it('should not modify the keys of substitutions and custom args', () => {
p.fromData(data);
expect(p.substitutions.T_EST).to.equal('Test');
expect(p.substitutions.camelCase).to.equal('Test');
expect(p.substitutions.snake_case).to.equal('Test');
expect(p.customArgs.T_EST).to.equal('Test');
expect(p.customArgs.camelCase).to.equal('Test');
expect(p.customArgs.snake_case).to.equal('Test');
});
});
describe('#527', function() {
it('shouldn\'t convert the headers to camel/snake case', function() {
const p = new Personalization({
to: 'test@example.com',
headers: {
'List-Unsubscribe': '<mailto:test@test.com>',
},
});
expect(p.headers['List-Unsubscribe']).to.equal('<mailto:test@test.com>');
expect(p.toJSON().headers['List-Unsubscribe']).to
.equal('<mailto:test@test.com>');
});
});
});

View File

@ -0,0 +1,54 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Reverse merge substitutions
describe('deepMergeDynamicTemplateData()', function() {
it('should reverse merge dynamicTemplateData', function() {
p.setDynamicTemplateData({ test1: 'Test1' });
p.deepMergeDynamicTemplateData({ test2: 'Test2' });
expect(p.dynamicTemplateData).to.have.a.property('test1');
expect(p.dynamicTemplateData).to.have.a.property('test2');
expect(p.dynamicTemplateData.test1).to.equal('Test1');
expect(p.dynamicTemplateData.test2).to.equal('Test2');
});
it('should not overwrite existing keys', function() {
p.setDynamicTemplateData({ test1: 'Test1' });
p.deepMergeDynamicTemplateData({ test1: 'Test3', test2: 'Test2' });
expect(p.dynamicTemplateData).to.have.a.property('test1');
expect(p.dynamicTemplateData).to.have.a.property('test2');
expect(p.dynamicTemplateData.test1).to.equal('Test1');
expect(p.dynamicTemplateData.test2).to.equal('Test2');
});
it('should work without prior dynamicTemplateData', function() {
p.deepMergeDynamicTemplateData({ test2: 'Test2' });
expect(p.dynamicTemplateData).to.have.a.property('test2');
expect(p.dynamicTemplateData.test2).to.equal('Test2');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.deepMergeDynamicTemplateData(3);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.deepMergeDynamicTemplateData();
}).not.to.throw(Error);
});
});
});

View File

@ -0,0 +1,54 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Reverse merge substitutions
describe('reverseMergeSubstitutions()', function() {
it('should reverse merge substitutions', function() {
p.setSubstitutions({test1: 'Test1'});
p.reverseMergeSubstitutions({test2: 'Test2'});
expect(p.substitutions).to.have.a.property('test1');
expect(p.substitutions).to.have.a.property('test2');
expect(p.substitutions.test1).to.equal('Test1');
expect(p.substitutions.test2).to.equal('Test2');
});
it('should not overwrite existing keys', function() {
p.setSubstitutions({test1: 'Test1'});
p.reverseMergeSubstitutions({test1: 'Test3', test2: 'Test2'});
expect(p.substitutions).to.have.a.property('test1');
expect(p.substitutions).to.have.a.property('test2');
expect(p.substitutions.test1).to.equal('Test1');
expect(p.substitutions.test2).to.equal('Test2');
});
it('should work without prior substitutions', function() {
p.reverseMergeSubstitutions({test2: 'Test2'});
expect(p.substitutions).to.have.a.property('test2');
expect(p.substitutions.test2).to.equal('Test2');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.reverseMergeSubstitutions(3);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.reverseMergeSubstitutions();
}).not.to.throw(Error);
});
});
});

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Add to
describe('addTo()', function() {
it('should add the item', function() {
p.addTo('test@example.org');
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(1);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.addTo('test1@example.org');
p.addTo('test2@example.org');
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(2);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test1@example.org');
expect(p.to[1]).to.be.an.instanceof(EmailAddress);
expect(p.to[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.addTo();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.addTo('test@example.org');
p.addTo();
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(1);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set bcc
describe('setBcc()', function() {
it('should handle array values', function() {
p.setBcc(['test@example.org']);
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(1);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test@example.org');
});
it('should handle string values', function() {
p.setBcc('test@example.org');
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(1);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.setBcc(['test1@example.org', 'test2@example.org']);
expect(p.bcc).to.be.an.instanceof(Array);
expect(p.bcc).to.have.a.lengthOf(2);
expect(p.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[0].email).to.equal('test1@example.org');
expect(p.bcc[1]).to.be.an.instanceof(EmailAddress);
expect(p.bcc[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.setBcc();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setBcc('test@example.org');
p.setBcc();
expect(p.bcc[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set cc
describe('setCc()', function() {
it('should handle array values', function() {
p.setCc(['test@example.org']);
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(1);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test@example.org');
});
it('should handle string values', function() {
p.setCc('test@example.org');
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(1);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.setCc(['test1@example.org', 'test2@example.org']);
expect(p.cc).to.be.an.instanceof(Array);
expect(p.cc).to.have.a.lengthOf(2);
expect(p.cc[0]).to.be.an.instanceof(EmailAddress);
expect(p.cc[0].email).to.equal('test1@example.org');
expect(p.cc[1]).to.be.an.instanceof(EmailAddress);
expect(p.cc[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.setCc();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setCc('test@example.org');
p.setCc();
expect(p.cc[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,47 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set custom args
describe('setCustomArgs()', function() {
it('should set the given value', function() {
p.setCustomArgs({test: 'Test'});
expect(p.customArgs).to.be.an.instanceof(Object);
expect(p.customArgs).to.have.a.property('test');
expect(p.customArgs.test).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setCustomArgs('Invalid');
}).to.throw(Error);
expect(function() {
p.setCustomArgs(null);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setCustomArgs();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setCustomArgs({test: 'Test'});
p.setCustomArgs();
expect(p.customArgs.test).to.equal('Test');
});
});
});

View File

@ -0,0 +1,45 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set from
describe('setFrom()', function() {
it('should accept string values', function() {
p.setFrom('test@example.org');
expect(p.from).to.be.an.instanceof(EmailAddress);
expect(p.from.email).to.equal('test@example.org');
});
it('should properly update from value', function() {
p.setFrom('test1@example.com');
p.setFrom('test2@example.com');
p.setFrom('test3@example.com');
p.setFrom('test4@example.com');
expect(p.from.email).to.equal('test4@example.com');
});
it('should accept no input', function() {
expect(function() {
p.setFrom();
}).not.to.throw(Error);
});
it('should not overwrite value with no input', function() {
p.setFrom('test@example.org');
p.setFrom();
expect(p.from.email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,47 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set headers
describe('setHeaders()', function() {
it('should set the given value', function() {
p.setHeaders({test: 'Test'});
expect(p.headers).to.be.an.instanceof(Object);
expect(p.headers).to.have.a.property('test');
expect(p.headers.test).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setHeaders('Invalid');
}).to.throw(Error);
expect(function() {
p.setHeaders(null);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setHeaders();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setHeaders({test: 'Test'});
p.setHeaders();
expect(p.headers.test).to.equal('Test');
});
});
});

View File

@ -0,0 +1,45 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set send at
describe('setSendAt()', function() {
it('should set the given value', function() {
p.setSendAt(1500077141);
expect(p.sendAt).to.equal(1500077141);
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setSendAt('Invalid');
}).to.throw(Error);
expect(function() {
p.setSendAt(null);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setSendAt();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setSendAt(1500077141);
p.setSendAt();
expect(p.sendAt).to.equal(1500077141);
});
});
});

View File

@ -0,0 +1,45 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set subject
describe('setSubject()', function() {
it('should set the given value', function() {
p.setSubject('Test');
expect(p.subject).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setSubject(5);
}).to.throw(Error);
expect(function() {
p.setSubject(null);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setSubject();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setSubject('Test');
p.setSubject();
expect(p.subject).to.equal('Test');
});
});
});

View File

@ -0,0 +1,47 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set substitutions
describe('setSubstitutions()', function() {
it('should set the given value', function() {
p.setSubstitutions({test: 'Test'});
expect(p.substitutions).to.be.an.instanceof(Object);
expect(p.substitutions).to.have.a.property('test');
expect(p.substitutions.test).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setSubstitutions('Invalid');
}).to.throw(Error);
expect(function() {
p.setSubstitutions(3);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setSubstitutions();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setSubstitutions({test: 'Test'});
p.setSubstitutions();
expect(p.substitutions.test).to.equal('Test');
});
});
});

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set to
describe('setTo()', function() {
it('should handle array values', function() {
p.setTo(['test@example.org']);
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(1);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test@example.org');
});
it('should handle string values', function() {
p.setTo('test@example.org');
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(1);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test@example.org');
});
it('should handle multiple values', function() {
p.setTo(['test1@example.org', 'test2@example.org']);
expect(p.to).to.be.an.instanceof(Array);
expect(p.to).to.have.a.lengthOf(2);
expect(p.to[0]).to.be.an.instanceof(EmailAddress);
expect(p.to[0].email).to.equal('test1@example.org');
expect(p.to[1]).to.be.an.instanceof(EmailAddress);
expect(p.to[1].email).to.equal('test2@example.org');
});
it('should accept no input', function() {
expect(function() {
p.setTo();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setTo('test@example.org');
p.setTo();
expect(p.to[0].email).to.equal('test@example.org');
});
});
});

View File

@ -0,0 +1,47 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set substitutions
describe('setDynamicTemplateData()', function() {
it('should set the given value', function() {
p.setDynamicTemplateData({ test: 'Test' });
expect(p.dynamicTemplateData).to.be.an.instanceof(Object);
expect(p.dynamicTemplateData).to.have.a.property('test');
expect(p.dynamicTemplateData.test).to.equal('Test');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setDynamicTemplateData('Invalid');
}).to.throw(Error);
expect(function() {
p.setDynamicTemplateData(3);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setDynamicTemplateData();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setDynamicTemplateData({ test: 'Test' });
p.setDynamicTemplateData();
expect(p.dynamicTemplateData.test).to.equal('Test');
});
});
});

View File

@ -0,0 +1,58 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//Set substitutions wrappers
describe('setSubstitutionWrappers()', function() {
it('should have defaults', function() {
expect(p.substitutionWrappers).to.be.an.instanceof(Array);
expect(p.substitutionWrappers).to.have.a.lengthOf(2);
expect(p.substitutionWrappers[0]).to.equal('{{');
expect(p.substitutionWrappers[1]).to.equal('}}');
});
it('should set the given value', function() {
p.setSubstitutionWrappers(['a', 'b']);
expect(p.substitutionWrappers).to.be.an.instanceof(Array);
expect(p.substitutionWrappers).to.have.a.lengthOf(2);
expect(p.substitutionWrappers[0]).to.equal('a');
expect(p.substitutionWrappers[1]).to.equal('b');
});
it('should throw an error for invalid input', function() {
expect(function() {
p.setSubstitutionWrappers('Invalid');
}).to.throw(Error);
expect(function() {
p.setSubstitutionWrappers(['a']);
}).to.throw(Error);
expect(function() {
p.setSubstitutionWrappers(['a', 'b', 'c']);
}).to.throw(Error);
});
it('should accept no input', function() {
expect(function() {
p.setSubstitutionWrappers();
}).not.to.throw(Error);
});
it('should not overwrite with no input', function() {
p.setSubstitutionWrappers(['a', 'b']);
p.setSubstitutionWrappers();
expect(p.substitutionWrappers[0]).to.equal('a');
expect(p.substitutionWrappers[1]).to.equal('b');
});
});
});

View File

@ -0,0 +1,144 @@
'use strict';
/**
* Dependencies
*/
const Personalization = require('../personalization');
const EmailAddress = require('../email-address');
/**
* Tests
*/
describe('Personalization', function() {
//Create new personalization before each test
let p;
beforeEach(function() {
p = new Personalization();
});
//JSON conversion
describe('toJSON()', function() {
beforeEach(function() {
p.setTo('test@example.org');
});
it('should always have the to field', function() {
const json = p.toJSON();
expect(json).to.have.property('to');
expect(json.to).to.be.an.instanceof(Array);
expect(json.to).to.have.a.lengthOf(1);
expect(json.to[0]).to.be.an.instanceof(EmailAddress);
expect(json.to[0].email).to.equal('test@example.org');
});
it('should set the from field', function() {
p.setFrom('testfrom@example.org');
const json = p.toJSON();
expect(json).to.have.property('from');
expect(json.from).to.be.an.instanceof(EmailAddress);
expect(json.from.email).to.equal('testfrom@example.org');
});
it('should set the cc field', function() {
p.setCc('testcc@example.org');
const json = p.toJSON();
expect(json).to.have.property('cc');
expect(json.cc).to.be.an.instanceof(Array);
expect(json.cc).to.have.a.lengthOf(1);
expect(json.cc[0]).to.be.an.instanceof(EmailAddress);
expect(json.cc[0].email).to.equal('testcc@example.org');
});
it('should set the bcc field', function() {
p.setBcc('testbcc@example.org');
const json = p.toJSON();
expect(json).to.have.property('bcc');
expect(json.bcc).to.be.an.instanceof(Array);
expect(json.bcc).to.have.a.lengthOf(1);
expect(json.bcc[0]).to.be.an.instanceof(EmailAddress);
expect(json.bcc[0].email).to.equal('testbcc@example.org');
});
it('should set the headers field', function() {
p.setHeaders({ test: 'Test' });
const json = p.toJSON();
expect(json).to.have.property('headers');
expect(json.headers).to.be.an.instanceof(Object);
expect(json.headers.test).to.equal('Test');
});
it('should set the custom_args field', function() {
p.setCustomArgs({ test: 'Test' });
const json = p.toJSON();
expect(json).to.have.property('custom_args');
expect(json.custom_args).to.be.an.instanceof(Object);
expect(json.custom_args.test).to.equal('Test');
});
it('should set the substitutions field', function() {
p.setSubstitutions({ test: 'Test' });
const json = p.toJSON();
expect(json).to.have.property('substitutions');
expect(json.substitutions).to.be.an.instanceof(Object);
});
it('should apply wrappers to the substitutions', function() {
p.setSubstitutions({ test: 'Test', otherTest2: 'Test2' });
p.setSubstitutionWrappers(['{{', '}}']);
const json = p.toJSON();
expect(json.substitutions).to.have.property('{{test}}');
expect(json.substitutions).to.have.property('{{otherTest2}}');
expect(json.substitutions['{{test}}']).to.equal('Test');
expect(json.substitutions['{{otherTest2}}']).to.equal('Test2');
expect(json.substitutions).not.to.have.property('test');
expect(json.substitutions).not.to.have.property('otherTest2');
});
it('should set the dynamicTemplateData field', function() {
p.setDynamicTemplateData({ test: 'Test' });
const json = p.toJSON();
expect(json).to.have.property('dynamic_template_data');
expect(json.dynamic_template_data).to.be.an.instanceof(Object);
});
it('should set the subject field', function() {
p.setSubject('Test');
const json = p.toJSON();
expect(json).to.have.property('subject');
expect(json.subject).to.equal('Test');
});
it('should set the send_at field', function() {
p.setSendAt(555);
const json = p.toJSON();
expect(json).to.have.property('send_at');
expect(json.send_at).to.equal(555);
});
it('should not modify the keys of substitutions and custom args', () => {
const data = {
to: 'to@example.org',
customArgs: { snake_case: 'Test', T_EST: 'Test', camelCase: 'Test' },
substitutions: { snake_case: 'Test', T_EST: 'Test', camelCase: 'Test' },
};
p.fromData(data);
const json = p.toJSON();
expect(json.substitutions).to.have.property('{{T_EST}}');
expect(json.substitutions).to.have.property('{{camelCase}}');
expect(json.substitutions).to.have.property('{{snake_case}}');
expect(json.substitutions['{{T_EST}}']).to.equal('Test');
expect(json.substitutions['{{camelCase}}']).to.equal('Test');
expect(json.substitutions['{{snake_case}}']).to.equal('Test');
expect(json.custom_args).to.have.property('T_EST');
expect(json.custom_args).to.have.property('camelCase');
expect(json.custom_args).to.have.property('snake_case');
expect(json.custom_args.T_EST).to.equal('Test');
expect(json.custom_args.camelCase).to.equal('Test');
expect(json.custom_args.snake_case).to.equal('Test');
});
it('should not modify the keys of dynamic template data', () => {
const data = {
to: 'to@example.org',
dynamicTemplateData: { snake_case: 'Test', T_EST: 'Test', camelCase: 'Test' },
};
p.fromData(data);
const json = p.toJSON();
expect(json.dynamic_template_data).to.have.property('T_EST');
expect(json.dynamic_template_data).to.have.property('camelCase');
expect(json.dynamic_template_data).to.have.property('snake_case');
expect(json.dynamic_template_data.T_EST).to.equal('Test');
expect(json.dynamic_template_data.camelCase).to.equal('Test');
expect(json.dynamic_template_data.snake_case).to.equal('Test');
});
});
});

View File

@ -0,0 +1,13 @@
import * as https from 'https';
type HttpMethod = 'get'|'GET'|'post'|'POST'|'put'|'PUT'|'patch'|'PATCH'|'delete'|'DELETE';
export default interface RequestOptions<TData = any, TParams = object> {
url: string;
method?: HttpMethod;
baseUrl?: string;
qs?: TParams;
body?: TData;
headers?: object;
httpsAgent?: https.Agent;
}

View File

@ -0,0 +1,8 @@
export default class ResponseError extends Error {
code: number;
message: string;
response: {
headers: { [key: string]: string; };
body: string;
};
}

View File

@ -0,0 +1,61 @@
'use strict';
/**
* Response error class
*/
class ResponseError extends Error {
/**
* Constructor
*/
constructor(response) {
//Super
super();
//Extract data from response
const { headers, status, statusText, data } = response;
//Set data
this.code = status;
this.message = statusText;
this.response = { headers, body: data };
//Capture stack trace
if (!this.stack) {
Error.captureStackTrace(this, this.constructor);
}
//Clean up stack trace
const regex = new RegExp(process.cwd() + '/', 'gi');
this.stack = this.stack.replace(regex, '');
}
/**
* Convert to string
*/
toString() {
const { body } = this.response;
let err = `${this.message} (${this.code})`;
if (body && Array.isArray(body.errors)) {
body.errors.forEach(error => {
const message = error.message;
const field = error.field;
const help = error.help;
err += `\n ${message}\n ${field}\n ${help}`;
});
}
return err;
}
/**
* Convert to simple object for JSON responses
*/
toJSON() {
const { message, code, response } = this;
return { message, code, response };
}
}
//Export
module.exports = ResponseError;

View File

@ -0,0 +1,7 @@
export default class Response<TPayload = object> {
statusCode: number;
body: TPayload;
headers: any;
constructor(statusCode: number, body: TPayload, headers?: any);
toString(): string;
}

View File

@ -0,0 +1,15 @@
'use strict';
class Response {
constructor(statusCode, body, headers) {
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
}
toString() {
return 'HTTP ' + this.statusCode + ' ' + this.body;
}
}
module.exports = Response;

View File

@ -0,0 +1,56 @@
export class Stats {
startDate: Date;
endDate?: Date;
aggregatedBy?: string;
}
export default class Statistics {
constructor(data?: Stats);
fromData(data: Stats): void;
/**
* To JSON
*/
toJSON(): Stats;
/**
* Get Advanced Statistics
*/
getAdvanced();
/**
* Get Category Statistics
*/
getCategory();
/**
* Get Global Statistics
*/
getGlobal();
/**
* Get Parse Statistics
*/
getParse();
/**
* Get Subuser Statistics
*/
getSubuser();
/**
* Set StartDate
*/
setStartDate(startDate: Date): void;
/**
* Set EndDate
*/
setEndDate(endDate: Date): void;
/**
* Set AggregatedBy
*/
setAggregatedBy(aggregatedBy: string): void;
}

View File

@ -0,0 +1,283 @@
'use strict';
/**
* Dependencies
*/
const toCamelCase = require('../helpers/to-camel-case');
const deepClone = require('../helpers/deep-clone');
/**
* Options
*/
const AggregatedByOptions = ['day', 'week', 'month'];
const CountryOptions = ['us', 'ca'];
const SortByDirection = ['desc', 'asc'];
/**
* Statistics class
*/
class Statistics {
constructor(data) {
this.startDate = null;
this.endDate = null;
this.aggregatedBy = null;
if (data) {
this.fromData(data);
}
}
/**
* Build from data
*/
fromData(data) {
//Expecting object
if (typeof data !== 'object') {
throw new Error('Expecting object for Statistics data');
}
//Convert to camel case to make it workable, making a copy to prevent
//changes to the original objects
data = deepClone(data);
data = toCamelCase(data, ['substitutions', 'customArgs']);
const { startDate,
endDate,
aggregatedBy,
} = data;
this.setStartDate(startDate);
this.setEndDate(endDate);
this.setAggregatedBy(aggregatedBy);
}
/**
* Set startDate
*/
setStartDate(startDate) {
if (typeof startDate === 'undefined') {
throw new Error('Date expected for `startDate`');
}
if ((new Date(startDate) === 'Invalid Date') ||
isNaN(new Date(startDate))) {
throw new Error('Date expected for `startDate`');
}
console.log(startDate);
this.startDate = new Date(startDate).toISOString().slice(0, 10);
}
/**
* Set endDate
*/
setEndDate(endDate) {
if (typeof endDate === 'undefined') {
this.endDate = new Date().toISOString().slice(0, 10);
return;
}
if (new Date(endDate) === 'Invalid Date' || isNaN(new Date(endDate))) {
throw new Error('Date expected for `endDate`');
}
this.endDate = new Date(endDate).toISOString().slice(0, 10);
}
/**
* Set aggregatedBy
*/
setAggregatedBy(aggregatedBy) {
if (typeof aggregatedBy === 'undefined') {
return;
}
if (typeof aggregatedBy === 'string' &&
AggregatedByOptions.includes(aggregatedBy.toLowerCase())) {
this.aggregatedBy = aggregatedBy;
} else {
throw new Error('Incorrect value for `aggregatedBy`');
}
}
/**
* Get Global
*/
getGlobal() {
const { startDate, endDate, aggregatedBy } = this;
return { startDate, endDate, aggregatedBy };
}
/**
* Get Advanced
*/
getAdvanced(country) {
const json = this.getGlobal();
if (typeof country === 'undefined') {
return json;
}
if (typeof country === 'string' &&
CountryOptions.includes(country.toLowerCase())) {
json.country = country;
}
return json;
}
/**
* Get Advanced Mailbox Providers
*/
getAdvancedMailboxProviders(mailBoxProviders) {
const json = this.getGlobal();
if (typeof mailBoxProviders === 'undefined') {
return json;
}
if (Array.isArray(mailBoxProviders) &&
mailBoxProviders.some(x => typeof x !== 'string')) {
throw new Error('Array of strings expected for `mailboxProviders`');
}
json.mailBoxProviders = mailBoxProviders;
return json;
}
/**
* Get Advanced Browsers
*/
getAdvancedBrowsers(browsers) {
const json = this.getGlobal();
if (typeof browsers === 'undefined') {
return json;
}
if (Array.isArray(browsers) && browsers.some(x => typeof x !== 'string')) {
throw new Error('Array of strings expected for `browsers`');
}
json.browsers = browsers;
return json;
}
/**
* Get Categories
*/
getCategories(categories) {
if (typeof categories === 'undefined') {
throw new Error('Array of strings expected for `categories`');
}
if (!this._isValidArrayOfStrings(categories)) {
throw new Error('Array of strings expected for `categories`');
}
const json = this.getGlobal();
json.categories = categories;
return json;
}
/**
* Get Subuser
*/
getSubuser(subusers) {
if (typeof subusers === 'undefined') {
throw new Error('Array of strings expected for `subusers`');
}
if (!this._isValidArrayOfStrings(subusers)) {
throw new Error('Array of strings expected for `subusers`');
}
const json = this.getGlobal();
json.subusers = subusers;
return json;
}
/**
* Get Subuser Sum
*/
getSubuserSum(sortByMetric = 'delivered',
sortByDirection = SortByDirection[0], limit = 5, offset = 0) {
if (typeof sortByMetric !== 'string') {
throw new Error('string expected for `sortByMetric`');
}
if (!SortByDirection.includes(sortByDirection.toLowerCase())) {
throw new Error('desc or asc expected for `sortByDirection`');
}
if (typeof limit !== 'number') {
throw new Error('number expected for `limit`');
}
if (typeof offset !== 'number') {
throw new Error('number expected for `offset`');
}
const json = this.getGlobal();
json.sortByMetric = sortByMetric;
json.sortByDirection = sortByDirection;
json.limit = limit;
json.offset = offset;
return json;
}
/**
* Get Subuser Monthly
*/
getSubuserMonthly(sortByMetric = 'delivered',
sortByDirection = SortByDirection[0], limit = 5, offset = 0) {
if (typeof sortByMetric !== 'string') {
throw new Error('string expected for `sortByMetric`');
}
if (!SortByDirection.includes(sortByDirection.toLowerCase())) {
throw new Error('desc or asc expected for `sortByDirection`');
}
if (typeof limit !== 'number') {
throw new Error('number expected for `limit`');
}
if (typeof offset !== 'number') {
throw new Error('number expected for `offset`');
}
const json = this.getGlobal();
json.sortByMetric = sortByMetric;
json.sortByDirection = sortByDirection;
json.limit = limit;
json.offset = offset;
return json;
}
_isValidArrayOfStrings(arr) {
if (!Array.isArray(arr)) {
return false;
}
if (arr.length < 1 || arr.some(x => typeof x !== 'string')) {
return false;
}
return true;
}
}
//Export class
module.exports = Statistics;

View File

@ -0,0 +1,192 @@
'use strict';
/**
* Dependencies
*/
const Statistics = require('./statistics');
/**
* Tests
*/
describe('Statistics', function() {
//Test data
const data = {
startDate: new Date(),
endDate: new Date('2017-10-21'),
aggregatedBy: 'week',
};
describe('setStartDate()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should set the startDate', function() {
stats.setStartDate(new Date('2017-10-22'));
expect(stats.startDate).equal('2017-10-22');
});
it('should throw error for invalid input', function() {
expect(function() {
stats.setStartDate('');
}).to.throw(Error);
expect(function() {
stats.setStartDate({});
}).to.throw(Error);
});
it('should throw error for no input', function() {
expect(function() {
stats.setStartDate();
}).to.throw(Error);
});
});
describe('endDate()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should set endDate', function() {
stats.setEndDate(new Date('2017-10-22'));
expect(stats.endDate).equal('2017-10-22');
});
it('should throw error for invalid input', function() {
expect(function() {
stats.setEndDate('');
}).to.throw(Error);
});
});
describe('setAggregatedBy()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should set aggregatedBy', function() {
stats.setAggregatedBy('week');
expect(stats.aggregatedBy).equal('week');
});
it('should throw error for invalid input', function() {
expect(function() {
stats.setAggregatedBy('');
}).to.throw(Error);
expect(function() {
stats.setAggregatedBy([1]);
}).to.throw(Error);
});
});
describe('getAdvanced()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get advanced', function() {
const advanced = stats.getAdvanced('US');
expect(advanced.country).equal('US');
});
});
describe('getAdvancedMailboxProviders()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get advanced mailbox providers', function() {
const arr = ['something'];
const advanced = stats.getAdvancedMailboxProviders(arr);
expect(advanced.mailBoxProviders).equal(arr);
});
});
describe('getAdvancedBrowsers()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get advanced browsers', function() {
const arr = ['something'];
const advanced = stats.getAdvancedBrowsers(arr);
expect(advanced.browsers).equal(arr);
});
});
describe('getCategories()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get categories', function() {
const arr = ['something'];
const categories = stats.getCategories(arr);
expect(categories.categories).equal(arr);
});
});
describe('getSubuser()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get subusers', function() {
const arr = ['something'];
const advanced = stats.getSubuser(arr);
expect(advanced.subusers).equal(arr);
});
});
describe('getSubuserSum()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get subusersum', function() {
const subuser = stats.getSubuserSum('delivered', 'asc', 10, 0);
expect(subuser.sortByMetric).equal('delivered');
expect(subuser.sortByDirection).equal('asc');
expect(subuser.limit).equal(10);
expect(subuser.offset).equal(0);
});
});
describe('getSubuserMonthly()', function() {
let stats;
beforeEach(function() {
stats = new Statistics(data);
});
it('should get subusersmonthly', function() {
const subuser = stats.getSubuserMonthly('delivered', 'asc', 10, 0);
expect(subuser.sortByMetric).equal('delivered');
expect(subuser.sortByDirection).equal('asc');
expect(subuser.limit).equal(10);
expect(subuser.offset).equal(0);
});
});
});

View File

@ -0,0 +1,8 @@
const DYNAMIC_TEMPLATE_CHAR_WARNING = `
Content with characters ', " or & may need to be escaped with three brackets
{{{ content }}}
See https://sendgrid.com/docs/for-developers/sending-email/using-handlebars/ for more information.`;
module.exports = {
DYNAMIC_TEMPLATE_CHAR_WARNING,
};

View File

@ -0,0 +1,6 @@
/**
* Helper to convert an array of objects to JSON
*/
declare function arrayToJSON(arr: any[]): any[];
export = arrayToJSON;

View File

@ -0,0 +1,13 @@
'use strict';
/**
* Helper to convert an array of objects to JSON
*/
module.exports = function arrayToJSON(arr) {
return arr.map(item => {
if (typeof item === 'object' && item !== null && typeof item.toJSON === 'function') {
return item.toJSON();
}
return item;
});
};

View File

@ -0,0 +1,41 @@
'use strict';
/**
* Dependencies
*/
const arrayToJSON = require('./array-to-json');
/**
* Tests
*/
describe('arrayToJSON', function() {
//Test object with toJSON function
const obj1 = {
toJSON() {
return {a: 1, b: 2};
},
};
//Test plain object
const obj2 = {c: 3, d: 4};
//Create mixed array
const test = [obj1, obj2, null, obj2, obj1, 2, 'test'];
const json = arrayToJSON(test);
//Tests
it('should leave non object values as is', function() {
expect(json[2]).to.be.null();
expect(json[5]).to.equal(2);
expect(json[6]).to.equal('test');
});
it('should leave plain objects as they are', function() {
expect(json[1]).to.have.property('c');
expect(json[3]).to.have.property('d');
});
it('should use the toJSON() handler if specified', function() {
expect(json[0]).to.have.property('a');
expect(json[4]).to.have.property('b');
});
});

View File

@ -0,0 +1,6 @@
/**
* Helper to convert an object's keys
*/
declare function convertKeys<T extends {}, S extends {}>(obj: T, converter: (key: string) => string, ignored?: string[]): S;
export = convertKeys;

View File

@ -0,0 +1,49 @@
'use strict';
/**
* Helper to convert an object's keys
*/
module.exports = function convertKeys(obj, converter, ignored) {
//Validate
if (typeof obj !== 'object' || obj === null) {
throw new Error('Non object passed to convertKeys: ' + obj);
}
//Ignore arrays
if (Array.isArray(obj)) {
return obj;
}
//Ensure array for ignored values
if (!Array.isArray(ignored)) {
ignored = [];
}
//Process all properties
for (const key in obj) {
//istanbul ignore else
if (obj.hasOwnProperty(key)) {
//Convert key to snake case
const converted = converter(key);
//Recursive for child objects, unless ignored
//The ignored check checks both variants of the key
if (typeof obj[key] === 'object' && obj[key] !== null) {
if (!ignored.includes(key) && !ignored.includes(converted)) {
obj[key] = convertKeys(obj[key], converter, ignored);
}
}
//Convert key to snake case and set if needed
if (converted !== key) {
obj[converted] = obj[key];
delete obj[key];
}
}
}
//Return object
return obj;
};

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Dependencies
*/
const convertKeys = require('./convert-keys');
const deepClone = require('./deep-clone');
const strToCamelCase = require('./str-to-camel-case');
/**
* Tests
*/
describe('convertKeys', function() {
//Test object
const obj = {
a: 1,
snake_case: 2,
camelCase: 3,
nested_snake_case: {
a: 1,
snake_case: 2,
camelCase: 3,
},
nestedCamelCase: {
a: 1,
snake_case: 2,
camelCase: 3,
},
arr: ['a', 'b'],
};
//Create copy of the object
const objCopy = deepClone(obj);
//Convert keys
convertKeys(obj, strToCamelCase);
//Tests
it('should convert top level keys properly', function() {
expect(obj).to.have.property('a');
expect(obj).to.have.property('snakeCase');
expect(obj).to.have.property('camelCase');
expect(obj).to.have.property('nestedSnakeCase');
expect(obj).to.have.property('nestedCamelCase');
expect(obj).not.to.have.property('snake_case');
expect(obj).not.to.have.property('nested_snake_case');
});
it('should convert nested keys properly', function() {
expect(obj.nestedSnakeCase).to.have.property('a');
expect(obj.nestedSnakeCase).to.have.property('snakeCase');
expect(obj.nestedSnakeCase).to.have.property('camelCase');
expect(obj.nestedSnakeCase).not.to.have.property('snake_case');
expect(obj.nestedCamelCase).to.have.property('a');
expect(obj.nestedCamelCase).to.have.property('snakeCase');
expect(obj.nestedCamelCase).to.have.property('camelCase');
expect(obj.nestedCamelCase).not.to.have.property('snake_case');
});
it('should handle arrays properly', function() {
expect(obj.arr).to.be.an.instanceof(Array);
expect(obj.arr).to.have.lengthOf(2);
expect(obj.arr).to.have.members(['a', 'b']);
});
it('should not converted nested objects if ignored', function() {
convertKeys(objCopy, strToCamelCase, ['nestedSnakeCase']);
expect(objCopy.nestedCamelCase).to.have.property('a');
expect(objCopy.nestedCamelCase).to.have.property('snakeCase');
expect(objCopy.nestedCamelCase).to.have.property('camelCase');
expect(objCopy.nestedCamelCase).not.to.have.property('snake_case');
expect(objCopy.nestedSnakeCase).to.have.property('a');
expect(objCopy.nestedSnakeCase).to.have.property('camelCase');
expect(objCopy.nestedSnakeCase).to.have.property('snake_case');
expect(objCopy.nestedSnakeCase).not.to.have.property('snakeCase');
});
it('should throw an error for non object input', function() {
expect(function() {
convertKeys(null);
}).to.throw(Error);
expect(function() {
convertKeys(5);
}).to.throw(Error);
});
});

View File

@ -0,0 +1,6 @@
/**
* Deep cloning helper for objects
*/
declare function deepClone<T>(obj: T): T;
export = deepClone;

View File

@ -0,0 +1,8 @@
'use strict';
/**
* Deep cloning helper for objects
*/
module.exports = function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
};

View File

@ -0,0 +1,43 @@
'use strict';
/**
* Dependencies
*/
const deepClone = require('./deep-clone');
/**
* Tests
*/
describe('deepClone', function() {
//Test object
const obj = {
nested: {a: 1, b: 2, c: {d: 4}},
e: 5,
arr: ['a', 'b'],
};
//Create clone
const clone = deepClone(obj);
//Tests
it('should equal the objects to themselves', function() {
expect(obj).to.equal(obj);
expect(clone).to.equal(clone);
});
it('should make a copy of the object', function() {
expect(obj).to.not.equal(clone);
expect(obj).to.deep.equal(clone);
});
it('should make a copy of nested objects', function() {
expect(obj.nested).to.not.equal(clone.nested);
expect(obj.nested).to.deep.equal(clone.nested);
expect(obj.nested.c).to.not.equal(clone.nested.c);
expect(obj.nested.c).to.deep.equal(clone.nested.c);
});
it('should handle arrays properly', function() {
expect(clone.arr).to.be.an.instanceof(Array);
expect(clone.arr).to.have.lengthOf(2);
expect(clone.arr).to.have.members(['a', 'b']);
});
});

View File

@ -0,0 +1,10 @@
'use strict';
/**
* Helper to convert an HTML string to a plain text string
*/
module.exports = function convertHTML2PlainString(html) {
let text = html.replace(/(<([^>]+)>)/g, '');
text = text.replace(/\s+/g, ' ');
return text;
};

View File

@ -0,0 +1,32 @@
'use strict';
/**
* Dependencies
*/
const convertHTML2PlainString = require('./html-to-plain-text');
/**
* Tests
*/
describe('convertHTML2PlainString', function() {
//Test string with one html tag
const html1 = '<p>Hello world</p>';
//Test string with nested html tags
const html2 = '<div><p>Hello <b>World!</b></p></div>';
//Test string with html tag with attributes
const html3 = '<div class="test-class">Hello World!</div>';
//Tests
it('should strip out html tags', function() {
expect(convertHTML2PlainString(html1)).to.be.equal('Hello world');
});
it('should strip out nested html tags', function() {
expect(convertHTML2PlainString(html2)).to.be.equal('Hello World!');
});
it('should strip out html tags with attributes', function() {
expect(convertHTML2PlainString(html3)).to.be.equal('Hello World!');
});
});

View File

@ -0,0 +1,19 @@
import arrayToJSON = require("@sendgrid/helpers/helpers/array-to-json");
import convertKeys = require("@sendgrid/helpers/helpers/convert-keys");
import deepClone = require("@sendgrid/helpers/helpers/deep-clone");
import mergeData = require("@sendgrid/helpers/helpers/merge-data");
import splitNameEmail = require("@sendgrid/helpers/helpers/split-name-email");
import toCamelCase = require("@sendgrid/helpers/helpers/to-camel-case");
import toSnakeCase = require("@sendgrid/helpers/helpers/to-snake-case");
import wrapSubstitutions = require("@sendgrid/helpers/helpers/wrap-substitutions");
export {
arrayToJSON,
convertKeys,
deepClone,
mergeData,
splitNameEmail,
toCamelCase,
toSnakeCase,
wrapSubstitutions,
}

View File

@ -0,0 +1,27 @@
'use strict';
/**
* Expose helpers
*/
const arrayToJSON = require('./array-to-json');
const convertKeys = require('./convert-keys');
const deepClone = require('./deep-clone');
const mergeData = require('./merge-data');
const splitNameEmail = require('./split-name-email');
const toCamelCase = require('./to-camel-case');
const toSnakeCase = require('./to-snake-case');
const wrapSubstitutions = require('./wrap-substitutions');
/**
* Export
*/
module.exports = {
arrayToJSON,
convertKeys,
deepClone,
mergeData,
splitNameEmail,
toCamelCase,
toSnakeCase,
wrapSubstitutions,
};

View File

@ -0,0 +1,6 @@
/**
* Merge data deep helper
*/
declare function mergeDataDeep<T, S>(base: T, data: S): T & S;
export = mergeDataDeep;

View File

@ -0,0 +1,34 @@
'use strict';
/**
* Merge data deep helper
*/
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
module.exports = function mergeDeep(base, data) {
//Validate data
if (typeof base !== 'object' || base === null) {
throw new Error('Not an object provided for base');
}
if (typeof data !== 'object' || data === null) {
throw new Error('Not an object provided for data');
}
let output = Object.assign({}, base);
if (isObject(base) && isObject(data)) {
Object.keys(data).forEach(key => {
if (isObject(data[key])) {
if (!(key in base)) {
Object.assign(output, { [key]: data[key] });
} else {
output[key] = mergeDeep(base[key], data[key]);
}
} else {
Object.assign(output, { [key]: data[key] });
}
});
}
return output;
};

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Dependencies
*/
const mergeDataDeep = require('./merge-data-deep');
/**
* Tests
*/
describe('mergeDataDeep', function() {
//Test objects
const obj1 = {
a: 1,
b: 2,
e: { g: 9 },
arr: ['a', 'b'],
};
const obj2 = {
a: 3,
c: 3,
d: 4,
e: { f: 6 },
arr: ['c'],
};
//Merge
const merged = mergeDataDeep(obj1, obj2);
//Tests
it('should merge the two objects', function() {
expect(merged).to.have.property('a');
expect(merged).to.have.property('b');
expect(merged).to.have.property('c');
expect(merged).to.have.property('d');
expect(merged).to.have.property('e');
expect(merged.a).to.equal(3);
expect(merged.e).to.have.property('f');
expect(merged.e).to.have.property('g');
});
it('should throw on invalid input', function() {
expect(function() {
mergeDataDeep(null, obj2);
}).to.throw(Error);
expect(function() {
mergeDataDeep(obj1, 4);
}).to.throw(Error);
});
it('should overwrite arrays', function() {
expect(merged).to.have.property('arr');
expect(merged.arr).to.be.an.instanceof(Array);
expect(merged.arr).to.have.lengthOf(1);
expect(merged.arr[0]).to.equal('c');
});
});

View File

@ -0,0 +1,6 @@
/**
* Merge data helper
*/
declare function mergeData<T, S>(base: T, data: S): T&S;
export = mergeData;

View File

@ -0,0 +1,35 @@
'use strict';
/**
* Merge data helper
*/
module.exports = function mergeData(base, data) {
//Validate data
if (typeof base !== 'object' || base === null) {
throw new Error('Not an object provided for base');
}
if (typeof data !== 'object' || data === null) {
throw new Error('Not an object provided for data');
}
//Copy base
const merged = Object.assign({}, base);
//Add data
for (const key in data) {
//istanbul ignore else
if (data.hasOwnProperty(key)) {
if (data[key] && Array.isArray(data[key])) {
merged[key] = data[key];
} else if (data[key] && typeof data[key] === 'object') {
merged[key] = Object.assign({}, data[key]);
} else if (data[key]) {
merged[key] = data[key];
}
}
}
//Return
return merged;
};

View File

@ -0,0 +1,52 @@
'use strict';
/**
* Dependencies
*/
const mergeData = require('./merge-data');
/**
* Tests
*/
describe('mergeData', function() {
//Test objects
const obj1 = {
a: 1,
b: 2,
arr: ['a', 'b'],
};
const obj2 = {
c: 3,
d: 4,
e: {f: 6},
arr: ['c'],
};
//Merge
const merged = mergeData(obj1, obj2);
//Tests
it('should merge the two objects', function() {
expect(merged).to.have.property('a');
expect(merged).to.have.property('b');
expect(merged).to.have.property('c');
expect(merged).to.have.property('d');
expect(merged).to.have.property('e');
expect(merged.e).to.have.property('f');
});
it('should throw on invalid input', function() {
expect(function() {
mergeData(null, obj2);
}).to.throw(Error);
expect(function() {
mergeData(obj1, 4);
}).to.throw(Error);
});
it('should overwrite arrays', function() {
expect(merged).to.have.property('arr');
expect(merged.arr).to.be.an.instanceof(Array);
expect(merged.arr).to.have.lengthOf(1);
expect(merged.arr[0]).to.equal('c');
});
});

View File

@ -0,0 +1,6 @@
/**
* Split name and email address from string
*/
declare function splitNameEmail(str: string): string[];
export = splitNameEmail;

View File

@ -0,0 +1,22 @@
'use strict';
/**
* Split name and email address from string
*/
module.exports = function splitNameEmail(str) {
//If no email bracket present, return as is
if (str.indexOf('<') === -1) {
return ['', str];
}
//Split into name and email
let [name, email] = str.split('<');
//Trim and fix up
name = name.trim();
email = email.replace('>', '').trim();
//Return as array
return [name, email];
};

View File

@ -0,0 +1,22 @@
'use strict';
/**
* Dependencies
*/
const splitNameEmail = require('./split-name-email');
/**
* Tests
*/
describe('splitNameEmail', function() {
it('should not split strings without < symbol', function() {
const [name, email] = splitNameEmail('test@test.com');
expect(name).to.equal('');
expect(email).to.equal('test@test.com');
});
it('should split strings with < symbol', function() {
const [name, email] = splitNameEmail('Tester <test@test.com>');
expect(name).to.equal('Tester');
expect(email).to.equal('test@test.com');
});
});

View File

@ -0,0 +1,19 @@
'use strict';
/**
* Internal conversion helper
*/
module.exports = function strToCamelCase(str) {
if (typeof str !== 'string') {
throw new Error('String expected for conversion to snake case');
}
return str
.trim()
.replace(/_+|\-+/g, ' ')
.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) {
if (Number(match) === 0) {
return '';
}
return (index === 0) ? match.toLowerCase() : match.toUpperCase();
});
};

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Dependencies
*/
const toCamelCase = require('./str-to-camel-case');
/**
* Tests
*/
describe('toCamelCase', function() {
it('should camel case an already camel cased string', function() {
expect(toCamelCase('camelCase')).to.equal('camelCase');
});
it('should camel case a snake cased string', function() {
expect(toCamelCase('camel_case')).to.equal('camelCase');
});
it('should camel case a dasherized string', function() {
expect(toCamelCase('camel-case')).to.equal('camelCase');
});
it('should camel case a string with spaces', function() {
expect(toCamelCase('camel case')).to.equal('camelCase');
});
it('should camel case a string with multiple spaces', function() {
expect(toCamelCase('camel case')).to.equal('camelCase');
expect(toCamelCase('camel ca se')).to.equal('camelCaSe');
});
it('should camel case a mixed string', function() {
expect(toCamelCase('CamelCase With snake_case _and dash-erized -andCamel'))
.to.equal('camelCaseWithSnakeCaseAndDashErizedAndCamel');
expect(toCamelCase('camel_case With vari-ety andCamel'))
.to.equal('camelCaseWithVariEtyAndCamel');
});
it('should lowercase single letters', function() {
expect(toCamelCase('A')).to.equal('a');
expect(toCamelCase('F')).to.equal('f');
expect(toCamelCase('Z')).to.equal('z');
});
it('should trim and camel case properly with leading/trailing spaces', function() {
expect(toCamelCase(' test_me ')).to.equal('testMe');
expect(toCamelCase(' test_me')).to.equal('testMe');
expect(toCamelCase('test_me ')).to.equal('testMe');
expect(toCamelCase(' test_me ')).to.equal('testMe');
});
it('should throw an error for non string input', function() {
expect(function() {
toCamelCase(2);
}).to.throw(Error);
expect(function() {
toCamelCase(null);
}).to.throw(Error);
});
});

View File

@ -0,0 +1,14 @@
'use strict';
/**
* Internal conversion helper
*/
module.exports = function strToSnakeCase(str) {
if (typeof str !== 'string') {
throw new Error('String expected for conversion to snake case');
}
return str.trim().replace(/(\s*\-*\b\w|[A-Z])/g, function($1) {
$1 = $1.trim().toLowerCase().replace('-', '');
return ($1[0] === '_' ? '' : '_') + $1;
}).slice(1);
};

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Dependencies
*/
const toSnakeCase = require('./str-to-snake-case');
/**
* Tests
*/
describe('toSnakeCase', function() {
it('should snake case an already snake cased string', function() {
expect(toSnakeCase('snake_case')).to.equal('snake_case');
});
it('should snake case a camel cased string', function() {
expect(toSnakeCase('snakeCase')).to.equal('snake_case');
expect(toSnakeCase('SnakeCase')).to.equal('snake_case');
expect(toSnakeCase('SnAkeCASe')).to.equal('sn_ake_c_a_se');
});
it('should snake case a dasherized string', function() {
expect(toSnakeCase('snake-case')).to.equal('snake_case');
expect(toSnakeCase('Snake-Case')).to.equal('snake_case');
});
it('should snake case a string with spaces', function() {
expect(toSnakeCase('Snake Case')).to.equal('snake_case');
});
it('should snake case a string with multiple spaces', function() {
expect(toSnakeCase('Snake Case')).to.equal('snake_case');
expect(toSnakeCase('Snake Ca se')).to.equal('snake_ca_se');
});
it('should snake case a mixed string', function() {
expect(toSnakeCase('Snake-Case mixEd Stri_ng te-st'))
.to.equal('snake_case_mix_ed_stri_ng_te_st');
expect(toSnakeCase('CamelCase With snake_case _and dash-erized -andCamel'))
.to.equal('camel_case_with_snake_case_and_dash_erized_and_camel');
});
it('should lowercase single letters', function() {
expect(toSnakeCase('A')).to.equal('a');
expect(toSnakeCase('F')).to.equal('f');
expect(toSnakeCase('Z')).to.equal('z');
});
it('should trim and snake case properly with leading/trailing spaces', function() {
expect(toSnakeCase(' TestMe ')).to.equal('test_me');
expect(toSnakeCase(' TestMe')).to.equal('test_me');
expect(toSnakeCase('TestMe ')).to.equal('test_me');
expect(toSnakeCase(' TestMe ')).to.equal('test_me');
});
it('should throw an error for non string input', function() {
expect(function() {
toSnakeCase(2);
}).to.throw(Error);
expect(function() {
toSnakeCase(null);
}).to.throw(Error);
});
});

View File

@ -0,0 +1,6 @@
/**
* Convert object keys to camel case
*/
declare function toCamelCase<T extends {}, S extends {}>(obj: T, ignored?: string[]): S;
export = toCamelCase;

View File

@ -0,0 +1,14 @@
'use strict';
/**
* Dependencies
*/
const convertKeys = require('./convert-keys');
const strToCamelCase = require('./str-to-camel-case');
/**
* Convert object keys to camel case
*/
module.exports = function toCamelCase(obj, ignored) {
return convertKeys(obj, strToCamelCase, ignored);
};

View File

@ -0,0 +1,6 @@
/**
* Convert object keys to snake case
*/
declare function toSnakeCase<T extends {}, S extends {}>(obj: T, ignored?: string[]): S;
export = toSnakeCase;

Some files were not shown because too many files have changed in this diff Show More