@romanzubenko_afternoon/quickbooks
Version:
QuickBooks JavaScript Client
233 lines (232 loc) • 8.96 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QuickBooks = void 0;
const node_querystring_1 = __importDefault(require("node:querystring"));
const schemas_1 = require("./schemas");
const zod_1 = require("zod");
const crypto_1 = __importDefault(require("crypto"));
class QuickBooks {
// Properties
clientId;
clientSecret;
redirectUri;
responseType;
authorizeEndpoint;
tokenEndpoint;
revokeEndpoint;
userEndpoint;
apiBaseUrl;
scopes;
accessTokenLatency;
refreshTokenLatency;
minorversion;
// Constructor
constructor(config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.redirectUri = config.redirectUri;
this.responseType = 'code';
this.authorizeEndpoint = 'https://appcenter.intuit.com/connect/oauth2';
this.tokenEndpoint = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
this.revokeEndpoint = 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke';
this.userEndpoint =
config.environment === 'production'
? 'https://accounts.platform.intuit.com/v1/openid_connect/userinfo'
: 'https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo';
this.apiBaseUrl =
config.environment === 'production'
? 'https://quickbooks.api.intuit.com'
: 'https://sandbox-quickbooks.api.intuit.com';
this.scopes = {
accounting: 'com.intuit.quickbooks.accounting',
// Payment: 'com.intuit.quickbooks.payment',
// Payroll: 'com.intuit.quickbooks.payroll',
// TimeTracking: 'com.intuit.quickbooks.payroll.timetracking',
// Benefits: 'com.intuit.quickbooks.payroll.benefits',
profile: 'profile',
email: 'email',
phone: 'phone',
address: 'address',
openid: 'openid',
// IntuitName: 'intuit_name',
};
this.minorversion = '65';
this.accessTokenLatency = 60; // 60 second buffer for access token refresh
this.refreshTokenLatency = 60 * 60 * 24 * 3; // 3 day buffer for refresh token refresh
}
getUnixTimestamp() {
return Math.floor(Date.now() / 1000);
}
// Generate a random string that is 32 characters long (two characters per byte)
createStateString() {
return crypto_1.default.randomBytes(16).toString('hex');
}
// Methods
getAuthUrl() {
const query = node_querystring_1.default.encode({
client_id: this.clientId,
response_type: this.responseType,
state: this.createStateString(),
scope: [
this.scopes.accounting,
this.scopes.openid,
this.scopes.profile,
this.scopes.email,
this.scopes.phone,
this.scopes.address,
].join(' '),
redirect_uri: this.redirectUri,
});
return `${this.authorizeEndpoint}?${query}`;
}
isAccessTokenValid(token) {
return token.expires_at > this.getUnixTimestamp();
}
isRefreshTokenValid(token) {
return token.x_refresh_token_expires_at > this.getUnixTimestamp();
}
parseToken(token, realm_id) {
return {
...token,
realm_id,
expires_at: this.getUnixTimestamp() + token.expires_in - this.accessTokenLatency,
x_refresh_token_expires_at: this.getUnixTimestamp() + token.x_refresh_token_expires_in - this.refreshTokenLatency,
};
}
getAuthHeader() {
return Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
}
async getTokenFromGrant(grant) {
schemas_1.grantSchema.parse(grant);
const body = {
grant_type: 'authorization_code',
code: grant.code,
redirect_uri: this.redirectUri,
};
const res = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
Authorization: `Basic ${this.getAuthHeader()}`,
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: node_querystring_1.default.encode(body),
});
const data = await res.json();
// Parse the response as JSON and return it
// Manually add the created_at property
// Subtracting 10 seconds of latency to the created_at property
// to ensure the token is detected as expired before it actually is
const token = this.parseToken(data, grant.realmId);
return token;
}
async getRefreshedToken(token) {
const body = { grant_type: 'refresh_token', refresh_token: token.refresh_token };
const res = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
Authorization: `Basic ${this.getAuthHeader()}`,
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: node_querystring_1.default.encode(body),
});
const data = await res.json();
const refreshedToken = this.parseToken(data, token.realm_id);
return refreshedToken;
}
async getValidToken(token) {
// Parse the token
token = schemas_1.tokenSchema.parse(token);
if (this.isAccessTokenValid(token)) {
return token;
}
if (!this.isRefreshTokenValid(token)) {
throw new Error('Refresh token is expired');
}
return this.getRefreshedToken(token);
}
async getUserInfo(token) {
const res = await fetch(this.userEndpoint, {
headers: {
Authorization: `Bearer ${token.access_token}`,
Accept: 'application/json',
},
});
const data = await res.json();
return data;
}
async getCompanyInfo(token) {
// Build the url
const url = `${this.apiBaseUrl}/v3/company/${token.realm_id}/companyinfo/${token.realm_id}?minorversion=${this.minorversion}`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token.access_token}`,
Accept: 'application/json',
},
});
const data = await res.json();
// Check if data has a property called CompanyInfo
const parsed = zod_1.z.object({ CompanyInfo: zod_1.z.any() }).parse(data);
return parsed.CompanyInfo;
}
async query({ token, query }) {
token = await this.getValidToken(token);
// Build the url
const url = `${this.apiBaseUrl}/v3/company/${token.realm_id}/query?query=${query}&minorverion=${this.minorversion}`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token.access_token}`,
Accept: 'application/json',
},
});
const data = await res.json();
return data;
}
// Do the same sort of work as above to see what else can be generalized
async post({ token, endpoint, body }) {
token = await this.getValidToken(token);
const url = `${this.apiBaseUrl}/v3/company/${token.realm_id}${endpoint}`;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${token.access_token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await res.json();
return data;
}
async get({ token, endpoint, params }) {
token = await this.getValidToken(token);
const url = `${this.apiBaseUrl}/v3/company/${token.realm_id}${endpoint}?${node_querystring_1.default.encode(params)}`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token.access_token}`,
Accept: 'application/json',
},
});
const data = await res.json();
return data;
}
async revokeAccess(token) {
const body = { token: token.access_token };
const res = await fetch(this.revokeEndpoint, {
method: 'POST',
headers: {
Authorization: `Basic ${this.getAuthHeader()}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(body),
});
const data = await res.json();
return data;
}
}
exports.QuickBooks = QuickBooks;