feat: enhance contact addition with bulk email lookup and subscription options
This commit is contained in:
@@ -273,6 +273,30 @@ export class Contacts {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /contacts/lookup
|
||||
* Bulk-check which emails already exist in the project (max 500)
|
||||
*/
|
||||
@Post('lookup')
|
||||
@Middleware([requireAuth, requireEmailVerified])
|
||||
@CatchAsync
|
||||
public async lookup(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth;
|
||||
const {emails} = req.body as {emails: string[]};
|
||||
|
||||
if (!Array.isArray(emails) || emails.length === 0) {
|
||||
return res.status(400).json({error: 'emails must be a non-empty array'});
|
||||
}
|
||||
|
||||
if (emails.length > 500) {
|
||||
return res.status(400).json({error: 'Maximum 500 emails per lookup'});
|
||||
}
|
||||
|
||||
const result = await ContactService.lookup(auth.projectId!, emails);
|
||||
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /contacts/import
|
||||
* Import contacts from CSV file
|
||||
|
||||
@@ -155,7 +155,7 @@ export class Segments {
|
||||
public async addMembers(req: Request, res: Response, _next: NextFunction) {
|
||||
const auth = res.locals.auth;
|
||||
const segmentId = req.params.id;
|
||||
const {emails} = req.body;
|
||||
const {emails, createMissing, subscribed} = req.body as {emails: string[]; createMissing?: boolean; subscribed?: boolean};
|
||||
|
||||
if (!segmentId) {
|
||||
return res.status(400).json({error: 'Segment ID is required'});
|
||||
@@ -165,7 +165,7 @@ export class Segments {
|
||||
return res.status(400).json({error: 'emails must be a non-empty array'});
|
||||
}
|
||||
|
||||
const result = await SegmentService.addContacts(auth.projectId!, segmentId, emails);
|
||||
const result = await SegmentService.addContacts(auth.projectId!, segmentId, emails, createMissing ?? false, subscribed ?? true);
|
||||
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,23 @@ export class ContactService {
|
||||
return contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-check which emails exist in the project — single query, safe for up to 500 addresses.
|
||||
*/
|
||||
public static async lookup(
|
||||
projectId: string,
|
||||
emails: string[],
|
||||
): Promise<{found: string[]; notFound: string[]}> {
|
||||
const rows = await prisma.contact.findMany({
|
||||
where: {projectId, email: {in: emails, mode: 'insensitive'}},
|
||||
select: {email: true},
|
||||
});
|
||||
const foundSet = new Set(rows.map(r => r.email.toLowerCase()));
|
||||
const found = emails.filter(e => foundSet.has(e.toLowerCase()));
|
||||
const notFound = emails.filter(e => !foundSet.has(e.toLowerCase()));
|
||||
return {found, notFound};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a contact by email (returns null if not found)
|
||||
*/
|
||||
|
||||
@@ -328,7 +328,9 @@ export class SegmentService {
|
||||
projectId: string,
|
||||
segmentId: string,
|
||||
emails: string[],
|
||||
): Promise<{added: number; notFound: string[]}> {
|
||||
createMissing = false,
|
||||
subscribed = true,
|
||||
): Promise<{added: number; created: number; notFound: string[]}> {
|
||||
const segment = await this.get(projectId, segmentId);
|
||||
|
||||
if (segment.type !== 'STATIC') {
|
||||
@@ -336,7 +338,7 @@ export class SegmentService {
|
||||
}
|
||||
|
||||
// Look up contacts by email (case-insensitive)
|
||||
const contacts = await prisma.contact.findMany({
|
||||
let contacts = await prisma.contact.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
email: {in: emails, mode: 'insensitive'},
|
||||
@@ -344,7 +346,24 @@ export class SegmentService {
|
||||
select: {id: true, email: true},
|
||||
});
|
||||
|
||||
const foundEmails = new Set(contacts.map(c => c.email.toLowerCase()));
|
||||
let foundEmails = new Set(contacts.map(c => c.email.toLowerCase()));
|
||||
const missing = emails.filter(e => !foundEmails.has(e.toLowerCase()));
|
||||
|
||||
let created = 0;
|
||||
if (createMissing && missing.length > 0) {
|
||||
await prisma.contact.createMany({
|
||||
data: missing.map(email => ({projectId, email, subscribed})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
created = missing.length;
|
||||
// Re-fetch to include newly created contacts
|
||||
contacts = await prisma.contact.findMany({
|
||||
where: {projectId, email: {in: emails, mode: 'insensitive'}},
|
||||
select: {id: true, email: true},
|
||||
});
|
||||
foundEmails = new Set(contacts.map(c => c.email.toLowerCase()));
|
||||
}
|
||||
|
||||
const notFound = emails.filter(e => !foundEmails.has(e.toLowerCase()));
|
||||
|
||||
if (contacts.length > 0) {
|
||||
@@ -377,7 +396,7 @@ export class SegmentService {
|
||||
await prisma.segment.update({where: {id: segmentId}, data: {memberCount}});
|
||||
}
|
||||
|
||||
return {added: contacts.length, notFound};
|
||||
return {added: contacts.length, created, notFound};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user