Source

services/ScenidCloudService.class.js

/**
 * @typedef {Object} ServiceResponse
 * @property {number} status - HTTP status code returned by the service.
 * @property {*} result - Parsed JSON body of the response, or `undefined` when the body is empty.
 */

import debug from 'debug'

export const METHODS = {
  GET: 'GET',
  PUT: 'PUT',
  POST: 'POST',
  DELETE: 'DELETE'
}

const dbg = debug('scenid:sdk')

/**
 * Base class for all Scenid service clients.
 *
 * Handles token acquisition, request building, and response parsing.
 * All service-specific classes extend this class and call `this.get`,
 * `this.post`, `this.put`, and `this.delete` to communicate with their
 * respective backend service.
 */
class ScenidCloudService {
  /**
   * @param {string|function(): Promise.<string>} tokenSource
   *   Either a static token string or an async function that resolves to a
   *   Bearer token. The function form is used in production — the SDK calls
   *   it before every request so tokens are always fresh.
   * @param {string} serviceUrl - Base URL of the target service (e.g. `https://crm-service.example.com`).
   */
  constructor (tokenSource, serviceUrl) {
    this._tokenSource = tokenSource
    this.serviceUrl = serviceUrl
  }

  /**
   * Resolves the Bearer token for the current request.
   * Accepts both a static string and an async factory function.
   *
   * @returns {Promise<string>}
   * @private
   */
  async _getToken () {
    if (typeof this._tokenSource === 'string') return this._tokenSource
    return this._tokenSource()
  }

  /**
   * Makes an authenticated HTTP request to the service and returns the parsed response.
   *
   * Acquires a token, constructs the full endpoint URL, serialises the body as
   * JSON when provided, and parses the JSON response. Throws a typed error on
   * network failure or non-JSON responses.
   *
   * @param {string} method - HTTP method (`GET`, `POST`, `PUT`, or `DELETE`).
   * @param {string} call - Path to call relative to `serviceUrl` (leading `/` optional).
   * @param {*} [body] - Request body. Serialised to JSON unless already a string.
   * @returns {Promise<ServiceResponse>}
   * @throws {Error} `sdk/fetch-failed` – network-level failure.
   * @throws {Error} `sdk/endpoint-invalid` – service returned 404 with non-JSON body.
   * @throws {Error} `sdk/non-json-response` – response body could not be parsed.
   */
  async makeRequest (method, call, body) {
    const finalCall = call.startsWith('/') ? call : `/${call}`
    const endpoint = `${this.serviceUrl}${finalCall}`

    let token
    try {
      token = await this._getToken()
    } catch (e) {
      dbg('token-failed %s', e.message)
      throw e
    }

    const request = {
      method,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      }
    }

    if (body !== undefined) {
      request.body = typeof body !== 'string' ? JSON.stringify(body) : body
    }

    dbg('→ %s %s', method, endpoint)
    if (body !== undefined) dbg('  body: %s', request.body)

    let res, text, result
    const t0 = Date.now()

    try {
      res = await fetch(endpoint, request)
    } catch (e) {
      dbg('fetch-failed %s', e.message)
      throw new Error('sdk/fetch-failed')
    }

    try {
      text = await res.text()
      result = text ? JSON.parse(text) : undefined
    } catch (e) {
      if (res.status === 404) throw new Error('sdk/endpoint-invalid')
      dbg('non-json-response status=%d body=%s', res.status, text)
      throw new Error('sdk/non-json-response')
    }

    dbg('← %d (%dms) %s', res.status, Date.now() - t0, result !== undefined ? JSON.stringify(result) : '(empty)')

    return { status: res.status, result }
  }

  /**
   * Sends an authenticated GET request.
   * @param {string} call - Path relative to `serviceUrl`.
   * @returns {Promise<ServiceResponse>}
   */
  get (call) {
    return this.makeRequest(METHODS.GET, call)
  }

  /**
   * Sends an authenticated PUT request.
   * @param {string} call - Path relative to `serviceUrl`.
   * @param {*} body - Request body, serialised to JSON.
   * @returns {Promise<ServiceResponse>}
   */
  put (call, body) {
    return this.makeRequest(METHODS.PUT, call, body)
  }

  /**
   * Sends an authenticated POST request.
   * @param {string} call - Path relative to `serviceUrl`.
   * @param {*} [body] - Request body, serialised to JSON.
   * @returns {Promise<ServiceResponse>}
   */
  post (call, body) {
    return this.makeRequest(METHODS.POST, call, body)
  }

  /**
   * Sends an authenticated DELETE request.
   * @param {string} call - Path relative to `serviceUrl`.
   * @param {*} [body] - Optional request body, serialised to JSON.
   * @returns {Promise<ServiceResponse>}
   */
  delete (call, body) {
    return this.makeRequest(METHODS.DELETE, call, body)
  }
}

export default ScenidCloudService