All files / src/services/db address.db.ts

46.19% Statements 91/197
83.33% Branches 10/12
57.14% Functions 8/14
49.39% Lines 81/164

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 1652x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 34x 2x 2x 2x 2x 2x 2x 2x 2x 4x 4x 2x 2x 2x 3x 2x   1x   3x                                   2x 13x 13x 13x 13x 2x 2x 13x 1x 1x       48x       13x 48x 47x     13x                 13x 10x   3x           2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x                                                 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x                                     2x 2x 2x 2x 2x 2x 2x 2x 2x 2x  
// src/services/db/address.db.ts
import type { Pool, PoolClient } from 'pg';
import { getPool } from './connection.db';
import type { Logger } from 'pino';
import { NotFoundError, handleDbError } from './errors.db';
import { Address } from '../../types';
 
export class AddressRepository {
  // The repository only needs an object with a `query` method, matching the Pool/PoolClient interface.
  // Using `Pick` makes this dependency explicit and simplifies testing by reducing the mock surface.
  private db: Pick<Pool | PoolClient, 'query'>;
 
  constructor(db: Pick<Pool | PoolClient, 'query'> = getPool()) {
    this.db = db;
  }
 
  /**
   * Retrieves a single address by its ID.
   * @param addressId The ID of the address to retrieve.
   * @returns A promise that resolves to the Address object or undefined.
   */
  async getAddressById(addressId: number, logger: Logger): Promise<Address> {
    try {
      const res = await this.db.query<Address>(
        'SELECT * FROM public.addresses WHERE address_id = $1',
        [addressId],
      );
      if (res.rowCount === 0) {
        throw new NotFoundError(`Address with ID ${addressId} not found.`);
      }
      return res.rows[0];
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in getAddressById',
        { addressId },
        {
          defaultMessage: 'Failed to retrieve address.',
        },
      );
    }
  }

  /**
   * Creates or updates an address and returns its ID.
   * This function uses an "upsert" pattern.
   * @param address The address data.
   * @returns The ID of the created or updated address.
   */
  async upsertAddress(address: Partial<Address>, logger: Logger): Promise<number> {
    try {
      const { address_id, ...addressData } = address;
      const columns = Object.keys(addressData);
      const values = Object.values(addressData);
 
      // If an address_id is provided, include it for the ON CONFLICT clause.
      if (address_id) {
        columns.unshift('address_id');
        values.unshift(address_id);
      }

      // Dynamically build the parameter placeholders ($1, $2, etc.)
      const valuePlaceholders = columns.map((_, i) => `$${i + 1}`).join(', ');

      // Dynamically build the SET clause for the UPDATE part.
      // EXCLUDED refers to the values from the failed INSERT attempt.
      const updateSetClauses = columns
        .filter((col) => col !== 'address_id') // Don't update the primary key
        .map((col) => `${col} = EXCLUDED.${col}`)
        .join(', ');

      const query = `
        INSERT INTO public.addresses (${columns.join(', ')})
        VALUES (${valuePlaceholders})
        ON CONFLICT (address_id) DO UPDATE
        SET ${updateSetClauses},
            updated_at = now()
        RETURNING address_id;
      `;

      const res = await this.db.query<{ address_id: number }>(query, values);
      return res.rows[0].address_id;
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in upsertAddress',
        { address },
        {
          uniqueMessage: 'An identical address already exists.',
          defaultMessage: 'Failed to upsert address.',
        },
      );
    }
  }
 
  /**
   * Searches for addresses by text (matches against address_line_1, city, or postal_code).
   * @param query Search query
   * @param logger Logger instance
   * @param limit Maximum number of results (default: 10)
   * @returns Array of matching Address objects
   */
  async searchAddressesByText(
    query: string,
    logger: Logger,
    limit: number = 10,
  ): Promise<Address[]> {
    try {
      const sql = `
        SELECT * FROM public.addresses
        WHERE
          address_line_1 ILIKE $1 OR
          city ILIKE $1 OR
          postal_code ILIKE $1
        ORDER BY city ASC, address_line_1 ASC
        LIMIT $2
      `;
      const result = await this.db.query<Address>(sql, [`%${query}%`, limit]);
      return result.rows;
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in searchAddressesByText',
        { query, limit },
        {
          defaultMessage: 'Failed to search addresses.',
        },
      );
    }
  }
 
  /**
   * Retrieves all addresses associated with a given store.
   * @param storeId The store ID
   * @param logger Logger instance
   * @returns Array of Address objects
   */
  async getAddressesByStoreId(storeId: number, logger: Logger): Promise<Address[]> {
    try {
      const query = `
        SELECT a.*
        FROM public.addresses a
        INNER JOIN public.store_locations sl ON a.address_id = sl.address_id
        WHERE sl.store_id = $1
        ORDER BY sl.created_at ASC
      `;
      const result = await this.db.query<Address>(query, [storeId]);
      return result.rows;
    } catch (error) {
      handleDbError(
        error,
        logger,
        'Database error in getAddressesByStoreId',
        { storeId },
        {
          defaultMessage: 'Failed to retrieve addresses for store.',
        },
      );
    }
  }
}