/**
* @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
Source