supersaas-api-client
Version:
Online bookings/appointments/calendars in NodeJS using the SuperSaaS scheduling platform.
217 lines (189 loc) • 6.71 kB
JavaScript
(function() {
const http = require('http');
const https = require('https');
const querystring = require('querystring');
const Appointments = require('./api/Appointments');
const Forms = require('./api/Forms');
const Schedules = require('./api/Schedules');
const Users = require('./api/Users');
const Promotions = require('./api/Promotions');
const Groups = require('./api/Groups');
const DEFAULT_HOST = 'https://www.supersaas.com';
const Client = function Client(configuration) {
this.accountName = configuration.accountName;
this.api_key = configuration.api_key;
this.host = configuration.host || DEFAULT_HOST;
this.dryRun = configuration.dryRun;
this.verbose = configuration.verbose;
this.lastRequest = null;
this.appointments = new Appointments(this);
this.forms = new Forms(this);
this.schedules = new Schedules(this);
this.users = new Users(this);
this.groups = new Groups(this);
this.promotions = new Promotions(this);
this.q = new Array(MAX_PER_WINDOW);
};
Client.API_VERSION = '3';
Client.VERSION = '2.0.1';
Client.prototype.get = function(path, query, callback) {
return this.request('GET', path, null, query, callback);
};
Client.prototype.post = function(path, params, query, callback) {
return this.request('POST', path, params, query, callback);
};
Client.prototype.put = function(path, params, query, callback) {
return this.request('PUT', path, params, query, callback);
};
Client.prototype.delete = function(path, params, query, callback) {
return this.request('DELETE', path, params, query, callback);
};
const WINDOW_SIZE = 1000;
const MAX_PER_WINDOW = 1;
Client.prototype.throttle = async function() {
const now = Date.now();
if (!this.lastRequestTime) {
this.lastRequestTime = now;
return Promise.resolve();
}
// Calculate elapsed time and ensure full window size wait
const elapsed = now - this.lastRequestTime;
const waitTime = Math.max(0, WINDOW_SIZE - elapsed);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
// Important: Update timestamp AFTER the wait to ensure full intervals
this.lastRequestTime = Date.now();
return Promise.resolve();
};
Client.prototype.request = async function(httpMethod, path, params, query, callback) {
try {
await this.throttle();
params = params || {};
query = query || {};
if (!this.accountName) {
throw new Error('Account name not configured. Call `Client.configure`.');
}
if (!this.api_key) {
throw new Error('Account API key not configured. Call `Client.configure`.');
}
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': this._userAgent(),
'Authorization': 'Basic ' + new Buffer.from(this.accountName + ':' + this.api_key).toString('base64'),
};
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(httpMethod)) {
throw new Error('Invalid HTTP Method: ' + httpMethod + '. Only `GET`, `POST`, `PUT`, `DELETE` supported.');
}
let parsedUrl = this.host + '/api' + path + '.json';
if (query) {
const parsedQuery = this._parseParams(query);
const qs = querystring.stringify(parsedQuery);
parsedUrl += qs ? ('?' + qs) : '';
}
parsedUrl = new URL(parsedUrl);
const parsedParams = this._parseParams(params);
const requestBody = {};
requestBody.path = parsedUrl.href;
if (httpMethod) {
requestBody.method = httpMethod;
}
if (headers) {
requestBody.headers = headers;
}
if (Object.keys(parsedParams).length > 0) {
requestBody.body = JSON.stringify(parsedParams);
}
if (this.dryRun) {
this.lastRequest = requestBody;
if (callback) {
callback(null, []);
}
return {};
}
const verboseLogging = this.verbose;
if (verboseLogging) {
console.log(requestBody);
}
const response = await fetch(requestBody.path, requestBody);
if (!response.ok) {
if (verboseLogging) {
console.error(`Failed request ${response.status}, ${response.body}`);
}
throw new Error(`Request failed with status ${response.status}`);
}
const contentType = response.headers.get('Content-Type');
let body;
if (contentType.includes('application/json')) {
if (response.headers.get('Content-Length') > 0) {
body = await response.json();
}
} else if (contentType.includes('text/plain')) {
if (response.headers.get('Content-Length') > 0) {
body = await response.text();
}
} else {
console.log('Unsupported content type:', contentType);
throw new Error(`Unsupported content type: ${contentType}`);
}
if (callback) {
try {
if (response.status === 201 && httpMethod === 'POST') {
body = response.headers.get('location');
}
if (verboseLogging) {
console.log("httpMethod: ", httpMethod, ", status: ", response.status);
console.log(body);
}
callback(null, body);
} catch (error) {
callback({errors: [{title: error.message}]}, null);
}
} else {
if (response.status === 201 && httpMethod === 'POST') {
body = response.headers.get('location');
}
return body;
}
} catch (error) {
console.log(error);
if (callback) {
callback(error, null);
} else {
throw error;
}
}
};
Client.prototype._requestModule = function(url) {
return url.protocol === 'https' ? https : http;
};
Client.prototype._userAgent = function() {
return 'SSS/' + Client.VERSION + ' Node/' + process.version + ' API/' + Client.API_VERSION;
};
Client.prototype._parseParams = function(params) {
const parsed = {};
Object.keys(params).forEach(function(key) {
if (params[key] !== null && params[key] !== '') {
parsed[key] = params[key];
}
});
return parsed;
};
const config = {
accountName: process.env['SSS_API_ACCOUNT_NAME'],
api_key: process.env['SSS_API_KEY'],
host: DEFAULT_HOST,
dryRun: false,
verbose: false,
};
Client.Instance = new Client(config);
Client.configure = function(configuration) {
for (const key in config) {
if (configuration[key]) {
Client.Instance[key] = configuration[key];
}
}
};
module.exports = Client;
}).call(this);