Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 81 additions & 67 deletions dist/core.cjs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions dist/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ declare class TwitterAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
fetchViewedTweetsByUsername(twitterUserName: string, page?: number, limit?: number): Promise<object>;
/**
* Private method to validate username, page, and limit parameters.
* @param {string} twitterUserName - The Twitter username.
* @param {number} page - The page number.
* @param {number} limit - The limit number.
* @returns {object} - Validated parameters.
* @throws {ValidationError} - Throws an error if validation fails.
*/
private _validateUsernamePageLimit;
/**
* Private method to fetch data with authorization header.
* @param {string} url - The URL to fetch.
Expand Down
9 changes: 9 additions & 0 deletions dist/core.esm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ declare class TwitterAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
fetchViewedTweetsByUsername(twitterUserName: string, page?: number, limit?: number): Promise<object>;
/**
* Private method to validate username, page, and limit parameters.
* @param {string} twitterUserName - The Twitter username.
* @param {number} page - The page number.
* @param {number} limit - The limit number.
* @returns {object} - Validated parameters.
* @throws {ValidationError} - Throws an error if validation fails.
*/
private _validateUsernamePageLimit;
/**
* Private method to fetch data with authorization header.
* @param {string} url - The URL to fetch.
Expand Down
148 changes: 81 additions & 67 deletions dist/core.esm.js

Large diffs are not rendered by default.

57 changes: 46 additions & 11 deletions src/core/spotify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fetchData, buildURL, baseSpotifyURL as baseURL } from "../utils";
import { APIError } from "../errors";
import { APIError, ValidationError } from "../errors";

interface SpotifyAPIOptions {
apiKey: string;
Expand All @@ -20,7 +20,10 @@ class SpotifyAPI {
* @throws {Error} - Throws an error if the API key is not provided.
*/
constructor(options: SpotifyAPIOptions) {
this.apiKey = options.apiKey;
if (!options || !options.apiKey || typeof options.apiKey !== 'string' || options.apiKey.trim() === '') {
throw new ValidationError("API key is required and must be a non-empty string");
}
this.apiKey = options.apiKey.trim();
}

/**
Expand All @@ -30,8 +33,11 @@ class SpotifyAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchSavedTracksById(spotifyId: string): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/save-tracks`, {
spotifyId,
spotifyId: spotifyId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -43,8 +49,11 @@ class SpotifyAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchPlayedTracksById(spotifyId: string): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/played-tracks`, {
spotifyId,
spotifyId: spotifyId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -56,8 +65,11 @@ class SpotifyAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchSavedAlbumsById(spotifyId: string): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/saved-albums`, {
spotifyId,
spotifyId: spotifyId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -69,8 +81,11 @@ class SpotifyAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchSavedPlaylistsById(spotifyId: string): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/saved-playlists`, {
spotifyId,
spotifyId: spotifyId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -86,9 +101,15 @@ class SpotifyAPI {
spotifyId: string,
albumId: string
): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
if (!albumId || typeof albumId !== 'string' || albumId.trim() === '') {
throw new ValidationError("Album ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/album/tracks`, {
spotifyId,
albumId,
spotifyId: spotifyId.trim(),
albumId: albumId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -104,9 +125,15 @@ class SpotifyAPI {
spotifyId: string,
playlistId: string
): Promise<object> {
if (!spotifyId || typeof spotifyId !== 'string' || spotifyId.trim() === '') {
throw new ValidationError("Spotify ID is required and must be a non-empty string");
}
if (!playlistId || typeof playlistId !== 'string' || playlistId.trim() === '') {
throw new ValidationError("Playlist ID is required and must be a non-empty string");
}
const url = buildURL(`${baseURL}/playlist/tracks`, {
spotifyId,
playlistId,
spotifyId: spotifyId.trim(),
playlistId: playlistId.trim(),
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -118,7 +145,15 @@ class SpotifyAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchUserByWalletAddress(walletAddress: string): Promise<object> {
const url = buildURL(`${baseURL}/wallet-spotify-data`, { walletAddress });
if (!walletAddress || typeof walletAddress !== 'string' || walletAddress.trim() === '') {
throw new ValidationError("Wallet address is required and must be a non-empty string");
}
// Basic evm address format validation
const trimmedAddress = walletAddress.trim();
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmedAddress)) {
throw new ValidationError("Invalid wallet address format");
}
const url = buildURL(`${baseURL}/wallet-spotify-data`, { walletAddress: trimmedAddress });
return this._fetchDataWithAuth(url);
}

Expand Down
135 changes: 108 additions & 27 deletions src/core/twitter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fetchData, buildURL, baseTwitterURL as baseURL } from "../utils";
import { APIError } from "../errors";
import { APIError, ValidationError } from "../errors";

/**
* The TwitterAPI class.
Expand All @@ -14,7 +14,10 @@ class TwitterAPI {
* @param {string} options.apiKey - The API key. (Needed for data fetching)
*/
constructor({ apiKey }: { apiKey: string }) {
this.apiKey = apiKey;
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') {
throw new ValidationError("API key is required and must be a non-empty string");
}
this.apiKey = apiKey.trim();
}

/**
Expand All @@ -24,7 +27,15 @@ class TwitterAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchUserByUsername(twitterUserName: string): Promise<object> {
const url = buildURL(`${baseURL}/user`, { twitterUserName });
if (!twitterUserName || typeof twitterUserName !== 'string' || twitterUserName.trim() === '') {
throw new ValidationError("Twitter username is required and must be a non-empty string");
}
const trimmedUsername = twitterUserName.trim();
// Basic Twitter(X) username validation (no @ symbol, alphanumeric + underscore)
if (!/^[a-zA-Z0-9_]{1,15}$/.test(trimmedUsername)) {
throw new ValidationError("Invalid Twitter username format");
}
const url = buildURL(`${baseURL}/user`, { twitterUserName: trimmedUsername });
return this._fetchDataWithAuth(url);
}

Expand All @@ -41,8 +52,21 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
if (!twitterUserName || typeof twitterUserName !== 'string' || twitterUserName.trim() === '') {
throw new ValidationError("Twitter username is required and must be a non-empty string");
}
const trimmedUsername = twitterUserName.trim();
if (!/^[a-zA-Z0-9_]{1,15}$/.test(trimmedUsername)) {
throw new ValidationError("Invalid Twitter username format");
}
if (!Number.isInteger(page) || page < 1) {
throw new ValidationError("Page must be a positive integer");
}
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new ValidationError("Limit must be a positive integer between 1 and 100");
}
const url = buildURL(`${baseURL}/tweets`, {
twitterUserName,
twitterUserName: trimmedUsername,
page,
limit,
});
Expand All @@ -62,10 +86,11 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/followers`, {
twitterUserName,
page,
limit,
twitterUserName: validatedUsername,
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -83,10 +108,11 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/following`, {
twitterUserName,
page,
limit,
twitterUserName: validatedUsername,
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -98,7 +124,15 @@ class TwitterAPI {
* @throws {APIError} - Throws an error if the request fails.
*/
async fetchTweetById(tweetId: string): Promise<object> {
const url = buildURL(`${baseURL}/getTweetById`, { tweetId });
if (!tweetId || typeof tweetId !== 'string' || tweetId.trim() === '') {
throw new ValidationError("Tweet ID is required and must be a non-empty string");
}
const trimmedTweetId = tweetId.trim();
// Basic tweet ID validation (should be numeric)
if (!/^\d+$/.test(trimmedTweetId)) {
throw new ValidationError("Invalid tweet ID format");
}
const url = buildURL(`${baseURL}/getTweetById`, { tweetId: trimmedTweetId });
return this._fetchDataWithAuth(url);
}

Expand All @@ -115,8 +149,21 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
if (!walletAddress || typeof walletAddress !== 'string' || walletAddress.trim() === '') {
throw new ValidationError("Wallet address is required and must be a non-empty string");
}
const trimmedAddress = walletAddress.trim();
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmedAddress)) {
throw new ValidationError("Invalid wallet address format");
}
if (!Number.isInteger(page) || page < 1) {
throw new ValidationError("Page must be a positive integer");
}
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new ValidationError("Limit must be a positive integer between 1 and 100");
}
const url = buildURL(`${baseURL}/wallet-twitter-data`, {
walletAddress,
walletAddress: trimmedAddress,
page,
limit,
});
Expand All @@ -136,10 +183,11 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/reposted`, {
twitterUserName,
page,
limit,
twitterUserName: validatedUsername,
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -157,10 +205,11 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/replies`, {
twitterUserName,
page,
limit,
twitterUserName: validatedUsername,
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -178,9 +227,10 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const url = buildURL(`${baseURL}/event/likes/${twitterUserName}`, {
page,
limit,
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/event/likes/${validatedUsername}`, {
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -198,9 +248,10 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const url = buildURL(`${baseURL}/event/follows/${twitterUserName}`, {
page,
limit,
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/event/follows/${validatedUsername}`, {
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}
Expand All @@ -218,13 +269,43 @@ class TwitterAPI {
page: number = 1,
limit: number = 10
): Promise<object> {
const url = buildURL(`${baseURL}/event/viewed-tweets/${twitterUserName}`, {
page,
limit,
const { validatedUsername, validatedPage, validatedLimit } = this._validateUsernamePageLimit(twitterUserName, page, limit);
const url = buildURL(`${baseURL}/event/viewed-tweets/${validatedUsername}`, {
page: validatedPage,
limit: validatedLimit,
});
return this._fetchDataWithAuth(url);
}

/**
* Private method to validate username, page, and limit parameters.
* @param {string} twitterUserName - The Twitter username.
* @param {number} page - The page number.
* @param {number} limit - The limit number.
* @returns {object} - Validated parameters.
* @throws {ValidationError} - Throws an error if validation fails.
*/
private _validateUsernamePageLimit(twitterUserName: string, page: number, limit: number) {
if (!twitterUserName || typeof twitterUserName !== 'string' || twitterUserName.trim() === '') {
throw new ValidationError("Twitter username is required and must be a non-empty string");
}
const trimmedUsername = twitterUserName.trim();
if (!/^[a-zA-Z0-9_]{1,15}$/.test(trimmedUsername)) {
throw new ValidationError("Invalid Twitter username format");
}
if (!Number.isInteger(page) || page < 1) {
throw new ValidationError("Page must be a positive integer");
}
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new ValidationError("Limit must be a positive integer between 1 and 100");
}
return {
validatedUsername: trimmedUsername,
validatedPage: page,
validatedLimit: limit
};
}

/**
* Private method to fetch data with authorization header.
* @param {string} url - The URL to fetch.
Expand Down
2 changes: 1 addition & 1 deletion test/spotify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('SpotifyAPI', () => {
let spotifyAPI;
const apiKey = 'test-api-key';
const spotifyId = 'test-spotify-id';
const walletAddress = 'test-wallet-address';
const walletAddress = '0x1234567890abcdef1234567890abcdef12345678';

beforeEach(() => {
spotifyAPI = new SpotifyAPI({ apiKey });
Expand Down
Loading