Clients/GroupClient.ts

import { IAuth } from "../auth/IAuth";
import { sendlix } from "../proto/group";

const {
  CheckEmailInGroupRequest,
  InsertEmailToGroupRequest,
  RemoveEmailFromGroupRequest,
  GroupClient: gRpcGroupClient,
  GroupEntry,
  FailureHandler,
} = sendlix.api.v1;

import { EmailData as EmailDataProto } from "../proto/EmailData";

import { Client } from "./Client";

/**
 * Failure handling strategy for group insert operations
 *
 * @typedef {("ABORT"|string)} FailHandling
 * Keys of FailureHandler enum, e.g. "ABORT".
 */
type FailHandling = keyof typeof FailureHandler;

/**
 * Recipient email value
 *
 * @typedef {Object|string} EmailData
 * @property {string} email - The email address (when using object form)
 * @property {string} [name] - Optional display name (object form only)
 */
type EmailData =
  | {
      email: string;
      name?: string;
    }
  | string;

/**
 * Group entry record to insert
 *
 * @typedef {Object} EmailRecord
 * @property {EmailData} email - Recipient email as string or object with optional name
 * @property {Object.<string,string>} [substitutions] - Optional key-value substitutions available in templates
 */
type EmailRecord = {
  email: EmailData;
  substitutions?: Record<string, string>;
};

/**
 * GroupClient is a client for managing group operations via gRPC.
 * It provides methods for inserting and deleting emails from groups.
 */
export class GroupClient extends Client<typeof gRpcGroupClient> {
  /**
   * Constructs a new GroupClient instance.
   * @param auth - Authentication object implementing IAuth or a raw token string.
   */
  constructor(auth: IAuth | string) {
    super(auth, gRpcGroupClient);
  }

  /**
   * Inserts one or multiple email records into a group.
   *
   * @param groupId - The unique identifier of the group.
   * @param email - A single EmailRecord or an array of EmailRecord objects.
   * @param failHandling - Strategy on individual insert failure. One of the keys of FailureHandler (e.g. "ABORT").
   * @returns Promise that resolves to true on success, otherwise rejects with an Error.
   *
   * @example
   * const client = new GroupClient("your_api_key");
   * // Single email as simple string
   * await client.insertEmailIntoGroup("groupId", { email: "info@example.com" });
   *
   * // With display name and substitutions
   * await client.insertEmailIntoGroup("groupId", {
   *   email: { email: "john@example.com", name: "John Doe" },
   *   substitutions: { firstName: "John" }
   * });
   *
   * // Multiple records
   * await client.insertEmailIntoGroup("groupId", [
   *   { email: "a@example.com" },
   *   { email: { email: "b@example.com", name: "User B" }, substitutions: { plan: "pro" } },
   * ]);
   */
  public async insertEmailIntoGroup(
    groupId: string,
    email: EmailRecord | EmailRecord[],
    failHandling: FailHandling = "ABORT"
  ): Promise<boolean> {
    return new Promise((resolve, reject) => {
      // Build the request with group id and failure handling strategy
      const request = new InsertEmailToGroupRequest({
        groupId: groupId,
        onFailure: FailureHandler[failHandling],
      });

      // Normalize to array to simplify processing logic
      if (!Array.isArray(email)) {
        email = [email];
      }

      for (const item of email) {
        // Convert substitutions object into a map as required by the gRPC schema
        const substitutionsMap = new Map<string, string>();
        if (item.substitutions) {
          for (const [key, value] of Object.entries(item.substitutions)) {
            substitutionsMap.set(key, value);
          }
        }

        // Build the EmailData protobuf message from either string or object input
        const email =
          typeof item.email === "string"
            ? new EmailDataProto({ email: item.email })
            : new EmailDataProto({
                email: item.email.email,
                name: item.email.name,
              });

        // Add the entry to the request payload
        request.entries.push(
          new GroupEntry({
            substitutions: substitutionsMap,
            email,
          })
        );
      }

      // Execute the RPC and map response to boolean result
      this.client.InsertEmailToGroup(request, (err, response) => {
        if (err) {
          reject(err);
        } else {
          if (!response?.success) {
            reject(new Error(response?.message));
          } else resolve(response.success);
        }
      });
    });
  }

  /**
   * Deletes an email from a group.
   *
   * Important: Emails can only be removed if they were added at least 30 minutes ago.
   *
   * @param groupId - The unique identifier of the group.
   * @param email - The email address to remove from the group.
   * @returns Promise that resolves to true on success, otherwise rejects with an Error.
   * @example
   * const client = new GroupClient("your_api_key");
   * await client.deleteEmailFromGroup("groupId", "info@example.com");
   */
  public async deleteEmailFromGroup(
    groupId: string,
    email: string
  ): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const request = new RemoveEmailFromGroupRequest({
        email,
        groupId,
      });

      this.client.RemoveEmailFromGroup(request, (err, response) => {
        if (err) {
          reject(err);
        } else {
          if (!response?.success) {
            reject(new Error(response?.message));
          } else resolve(response.success);
        }
      });
    });
  }

  /**
   * Checks whether an email exists in a group.
   *
   * @param groupId - The unique identifier of the group.
   * @param email - The email address to check.
   * @returns Promise that resolves to true if the email exists in the group; false otherwise.
   */
  public async containsEmailInGroup(
    groupId: string,
    email: string
  ): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => {
      const request = new CheckEmailInGroupRequest({
        groupId,
        email,
      });

      this.client.CheckEmailInGroup(request, (err, response) => {
        if (err) {
          reject(err);
        } else {
          resolve(response?.exists == true);
        }
      });
    });
  }
}