mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 02:04:40 +10:00
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { parse } from 'csv-parse/sync';
|
|
import { z } from 'zod';
|
|
|
|
import { zEmail } from '../../utils/zod';
|
|
|
|
const ZRecipientRowSchema = z.object({
|
|
name: z.string().optional(),
|
|
email: z.union([
|
|
zEmail('Value must be a valid email or empty string'),
|
|
z.string().max(0, { message: 'Value must be a valid email or empty string' }),
|
|
]),
|
|
});
|
|
|
|
export type TBulkSendCsvRow = Record<string, string | undefined>;
|
|
|
|
export type TBulkSendCsvRowError = {
|
|
/**
|
|
* The 1-indexed row number the error occurred on, excluding the header row.
|
|
*/
|
|
row: number;
|
|
|
|
/**
|
|
* The column the error occurred in, such as `recipient_1_email`.
|
|
*/
|
|
column: string;
|
|
|
|
message: string;
|
|
};
|
|
|
|
export type TBulkSendCsvError =
|
|
| { type: 'PARSE_ERROR' }
|
|
| { type: 'EMPTY' }
|
|
| { type: 'ROW_LIMIT_EXCEEDED'; rowCount: number; maxRows: number }
|
|
| { type: 'MISSING_COLUMNS'; missingColumns: string[] }
|
|
| { type: 'INVALID_RECIPIENTS'; rowErrors: TBulkSendCsvRowError[] };
|
|
|
|
export type TValidateBulkSendCsvResult =
|
|
| { success: true; data: TBulkSendCsvRow[] }
|
|
| { success: false; error: TBulkSendCsvError };
|
|
|
|
export type ValidateBulkSendCsvOptions = {
|
|
csvContent: string;
|
|
|
|
/**
|
|
* The number of recipients configured on the template, used to derive the
|
|
* required `recipient_N_email` columns.
|
|
*/
|
|
recipientCount: number;
|
|
|
|
maxRows?: number;
|
|
};
|
|
|
|
/**
|
|
* Validate the CSV provided for a template bulk send.
|
|
*
|
|
* Returns a discriminated union so callers can surface structured error
|
|
* details, such as which rows contain invalid recipients.
|
|
*/
|
|
export const validateBulkSendCsv = ({
|
|
csvContent,
|
|
recipientCount,
|
|
maxRows = 100,
|
|
}: ValidateBulkSendCsvOptions): TValidateBulkSendCsvResult => {
|
|
let rows: TBulkSendCsvRow[];
|
|
|
|
try {
|
|
rows = parse(csvContent, { columns: true, skip_empty_lines: true });
|
|
} catch {
|
|
return { success: false, error: { type: 'PARSE_ERROR' } };
|
|
}
|
|
|
|
if (rows.length === 0) {
|
|
return { success: false, error: { type: 'EMPTY' } };
|
|
}
|
|
|
|
if (rows.length > maxRows) {
|
|
return { success: false, error: { type: 'ROW_LIMIT_EXCEEDED', rowCount: rows.length, maxRows } };
|
|
}
|
|
|
|
const csvHeaders = Object.keys(rows[0]);
|
|
|
|
const requiredHeaders = Array.from({ length: recipientCount }, (_, index) => `recipient_${index + 1}_email`);
|
|
|
|
const missingColumns = requiredHeaders.filter((header) => !csvHeaders.includes(header));
|
|
|
|
if (missingColumns.length > 0) {
|
|
return { success: false, error: { type: 'MISSING_COLUMNS', missingColumns } };
|
|
}
|
|
|
|
const rowErrors: TBulkSendCsvRowError[] = [];
|
|
|
|
for (const [rowIndex, row] of rows.entries()) {
|
|
for (let recipientIndex = 0; recipientIndex < recipientCount; recipientIndex += 1) {
|
|
const nameKey = `recipient_${recipientIndex + 1}_name`;
|
|
const emailKey = `recipient_${recipientIndex + 1}_email`;
|
|
|
|
const parsed = ZRecipientRowSchema.safeParse({
|
|
name: row[nameKey],
|
|
email: row[emailKey],
|
|
});
|
|
|
|
if (!parsed.success) {
|
|
rowErrors.push({
|
|
row: rowIndex + 1,
|
|
column: emailKey,
|
|
message: parsed.error.issues?.[0]?.message ?? 'Invalid value',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rowErrors.length > 0) {
|
|
return { success: false, error: { type: 'INVALID_RECIPIENTS', rowErrors } };
|
|
}
|
|
|
|
return { success: true, data: rows };
|
|
};
|