diff --git a/lib/goodreads/goodreads.ts b/lib/goodreads/goodreads.ts deleted file mode 100644 index 1f613e4..0000000 --- a/lib/goodreads/goodreads.ts +++ /dev/null @@ -1,75 +0,0 @@ -// TODO: to be moved to a separate package -import * as parser from "xml2js"; -import { - isRawGoodreadsFeed, - isRawGoodreadsFeedChannel, - isRawGoodreadsFeedRSS, - isRawGoodreadsItem, - isValidClientSideFeed, -} from "./typeguards"; -import { GoodreadsItem, RawGoodreadsItem } from "./types"; - -const CORS_PROXY = "https://api.allorigins.win/get?url="; -const isBrowser = typeof document !== "undefined"; - -// Utils -const transformRawGoodreadsItem = (item: RawGoodreadsItem): GoodreadsItem => { - return { - title: item.title[0], - link: item.link[0], - book_image_url: item["book_image_url"][0], - author_name: item["author_name"][0], - book_description: item["book_description"][0], - }; -}; - -const parseXML = async ( - xml = "", - /* eslint-disable @typescript-eslint/no-explicit-any */ - callback?: (err: Error | null, result: any) => void, -) => { - if (!callback) return parser.parseStringPromise(xml); - parser.parseString(xml, callback); -}; - -// Internal API - -const getRawXMLGoodreadsFeed = async (feedUrl: string) => { - try { - const path = isBrowser - ? `${CORS_PROXY}${encodeURIComponent(feedUrl)}` - : feedUrl; - const promise = await fetch(path); - if (promise.ok) return isBrowser ? promise.json() : promise.text(); - } catch (error) { - throw new Error("Error occurred fetching Feed from Goodreads", error); - } -}; - -// Goodreads Public API -export const getGoodreadsFeed = async ( - feedUrl: string, - callback?: (err: Error | null, result: unknown) => void, -): Promise => { - const rawXML = await getRawXMLGoodreadsFeed(feedUrl); - // NOTE: server side call - if (!isBrowser) { - return parseXML(rawXML, callback); - } - // NOTE: client side call - if (!isValidClientSideFeed(rawXML)) - throw new Error("Error occurred fetching Feed from Substack"); - await parseXML(rawXML.contents, callback); -}; -export const getGoodreadsFeedItems = (rawFeed: unknown): GoodreadsItem[] => { - if (!isRawGoodreadsFeed(rawFeed)) - throw new Error("Goodreads feed is not in the correct format"); - if (!isRawGoodreadsFeedRSS(rawFeed.rss)) - throw new Error("Goodreads RSS feed is not in the correct format"); - const channels = rawFeed.rss.channel.filter(isRawGoodreadsFeedChannel); - if (channels.length === 0) - throw new Error("Goodreads feed does not contain any channels"); - const channel = channels[0]; - if (!Array.isArray(channel.item)) return []; - return channel.item.filter(isRawGoodreadsItem).map(transformRawGoodreadsItem); -}; diff --git a/lib/goodreads/typeguards.ts b/lib/goodreads/typeguards.ts deleted file mode 100644 index df8d4b6..0000000 --- a/lib/goodreads/typeguards.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Goodreads Feed Typeguards -// TODO: to be moved to a separate package -import { - RawGoodreadsFeed, - RawGoodreadsFeedChannel, - RawGoodreadsFeedRSS, - RawGoodreadsItem, -} from "./types"; - -export const isRawGoodreadsFeed = (data: unknown): data is RawGoodreadsFeed => { - return ( - data !== null && typeof data === "object" && data.hasOwnProperty("rss") - ); -}; - -export const isRawGoodreadsFeedRSS = ( - data: unknown, -): data is RawGoodreadsFeedRSS => { - return ( - typeof data === "object" && data !== null && data.hasOwnProperty("channel") - ); -}; - -export const isRawGoodreadsFeedChannel = ( - channel: unknown, -): channel is RawGoodreadsFeedChannel => { - return ( - typeof channel === "object" && - channel !== null && - channel.hasOwnProperty("title") && - channel.hasOwnProperty("item") - ); -}; -export const isRawGoodreadsItem = (item: unknown): item is RawGoodreadsItem => { - return ( - typeof item === "object" && - item !== null && - item.hasOwnProperty("title") && - item.hasOwnProperty("link") && - item.hasOwnProperty("book_image_url") && - item.hasOwnProperty("author_name") && - item.hasOwnProperty("book_description") - ); -}; - -export const isValidClientSideFeed = (data: unknown): boolean => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("rss") && - data.hasOwnProperty("status") - ); -}; diff --git a/lib/goodreads/types.ts b/lib/goodreads/types.ts deleted file mode 100644 index b4e4932..0000000 --- a/lib/goodreads/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Goodreads Feed Types -// TODO: to be moved to a separate package -// -export type RawGoodreadsFeed = { - rss: RawGoodreadsFeedRSS; -}; - -export type RawGoodreadsFeedRSS = { - channel: RawGoodreadsFeedChannel[]; - [key: string]: unknown; -}; - -export type RawGoodreadsFeedChannel = { - title: string[]; - item: RawGoodreadsItem[]; - [key: string]: unknown; -}; - -export type RawGoodreadsItem = { - title: string[]; - link: string[]; - book_image_url: string[]; - author_name: string[]; - book_description: string[]; - [key: string]: unknown; -}; - -export type GoodreadsFeedChannel = { - title: string; - item: GoodreadsItem[]; - [key: string]: unknown; -}; - -export type GoodreadsItem = { - title: string; - link: string; - book_image_url: string; - author_name: string; - book_description: string; - [key: string]: unknown; -}; diff --git a/lib/typeguards.ts b/lib/typeguards.ts deleted file mode 100644 index 7e549cf..0000000 --- a/lib/typeguards.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - AtomLink, - Enclosure, - Guid, - ItunesOwner, - RawFeed, - RawFeedChannel, - RawGoodreadsFeed, - RawGoodreadsFeedChannel, - RawGoodreadsFeedRSS, - RawGoodreadsItem, - RawImage, - RawItem, -} from "./types"; - -export const isRawFeed = (data: any): data is RawFeed => { - return ( - data !== null && - typeof data === "object" && - data.hasOwnProperty("rss") && - typeof data.rss === "object" && - data.rss !== null && - data.rss.hasOwnProperty("channel") && - Array.isArray(data.rss.channel) - ); -}; - -export const isRawFeedChannel = (data: any): data is RawFeedChannel => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("title") && - Array.isArray(data.title) && - data.hasOwnProperty("description") && - Array.isArray(data.description) && - data.hasOwnProperty("link") && - Array.isArray(data.link) && - data.hasOwnProperty("image") && - Array.isArray(data.image) && - data.image.every(isRawImage) && - data.hasOwnProperty("generator") && - Array.isArray(data.generator) && - data.hasOwnProperty("lastBuildDate") && - Array.isArray(data.lastBuildDate) && - data.hasOwnProperty("atom:link") && - Array.isArray(data["atom:link"]) && - data["atom:link"].every(isAtomLink) && - data.hasOwnProperty("copyright") && - Array.isArray(data.copyright) && - data.hasOwnProperty("language") && - Array.isArray(data.language) && - data.hasOwnProperty("webMaster") && - Array.isArray(data.webMaster) && - data.hasOwnProperty("itunes:owner") && - Array.isArray(data["itunes:owner"]) && - data["itunes:owner"].every(isItunesOwner) && - data.hasOwnProperty("itunes:author") && - Array.isArray(data["itunes:author"]) && - data.hasOwnProperty("googleplay:owner") && - Array.isArray(data["googleplay:owner"]) && - data.hasOwnProperty("googleplay:email") && - Array.isArray(data["googleplay:email"]) && - data.hasOwnProperty("googleplay:author") && - Array.isArray(data["googleplay:author"]) && - data.hasOwnProperty("item") && - Array.isArray(data.item) && - data.item.every(isRawItem) - ); -}; - -export const isRawImage = (data: any): data is RawImage => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("url") && - Array.isArray(data.url) && - data.hasOwnProperty("title") && - Array.isArray(data.title) && - data.hasOwnProperty("link") && - Array.isArray(data.link) - ); -}; - -export const isAtomLink = (data: any): data is AtomLink => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("$") && - typeof data.$ === "object" && - data.$ !== null && - data.$.hasOwnProperty("href") && - typeof data.$.href === "string" && - data.$.hasOwnProperty("rel") && - typeof data.$.rel === "string" && - data.$.hasOwnProperty("type") && - typeof data.$.type === "string" - ); -}; - -export const isItunesOwner = (data: any): data is ItunesOwner => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("itunes:email") && - Array.isArray(data["itunes:email"]) && - data.hasOwnProperty("itunes:name") && - Array.isArray(data["itunes:name"]) - ); -}; - -export const isRawItem = (data: any): data is RawItem => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("title") && - Array.isArray(data.title) && - data.hasOwnProperty("description") && - Array.isArray(data.description) && - data.hasOwnProperty("link") && - Array.isArray(data.link) && - data.hasOwnProperty("guid") && - Array.isArray(data.guid) && - data.guid.every(isGuid) && - data.hasOwnProperty("dc:creator") && - Array.isArray(data["dc:creator"]) && - data.hasOwnProperty("pubDate") && - Array.isArray(data.pubDate) && - data.hasOwnProperty("enclosure") && - Array.isArray(data.enclosure) && - data.enclosure.every(isEnclosure) && - data.hasOwnProperty("content:encoded") && - Array.isArray(data["content:encoded"]) - ); -}; - -export const isGuid = (data: any): data is Guid => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("_") && - typeof data._ === "string" && - data.hasOwnProperty("$") && - typeof data.$ === "object" && - data.$ !== null && - data.$.hasOwnProperty("isPermaLink") && - typeof data.$.isPermaLink === "string" - ); -}; - -export const isEnclosure = (data: any): data is Enclosure => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("$") && - typeof data.$ === "object" && - data.$ !== null && - data.$.hasOwnProperty("url") && - typeof data.$.url === "string" && - data.$.hasOwnProperty("length") && - typeof data.$.length === "string" && - data.$.hasOwnProperty("type") && - typeof data.$.type === "string" - ); -}; - -export const isValidClientSideFeed = (data: any): boolean => { - return data && data.contents && data.status.http_code == 200; -}; - -// Goodreads Feed Typeguards -// TODO: to be moved to a separate package - -export const isRawGoodreadsFeed = (data: unknown): data is RawGoodreadsFeed => { - return ( - data !== null && typeof data === "object" && data.hasOwnProperty("rss") - ); -}; - -export const isRawGoodreadsFeedRSS = ( - data: unknown, -): data is RawGoodreadsFeedRSS => { - return ( - typeof data === "object" && data !== null && data.hasOwnProperty("channel") - ); -}; - -export const isRawGoodreadsFeedChannel = ( - channel: unknown, -): channel is RawGoodreadsFeedChannel => { - return ( - typeof channel === "object" && - channel !== null && - channel.hasOwnProperty("title") && - channel.hasOwnProperty("item") - ); -}; -export const isRawGoodreadsItem = (item: unknown): item is RawGoodreadsItem => { - return ( - typeof item === "object" && - item !== null && - item.hasOwnProperty("title") && - item.hasOwnProperty("link") && - item.hasOwnProperty("book_image_url") && - item.hasOwnProperty("book_large_image_url") && - item.hasOwnProperty("author_name") && - item.hasOwnProperty("book_description") - ); -}; - -export const isValidGoodreadsClientSideFeed = (data: unknown): boolean => { - return ( - typeof data === "object" && - data !== null && - data.hasOwnProperty("contents") && - data.hasOwnProperty("status") - ); -}; diff --git a/lib/types.ts b/lib/types.ts deleted file mode 100644 index b06c240..0000000 --- a/lib/types.ts +++ /dev/null @@ -1,127 +0,0 @@ -export type RawFeed = { - rss: { - channel: RawFeedChannel[]; - [key: string]: unknown; - }; -}; - -export interface RawFeedChannel { - title: string[]; - description: string[]; - link: string[]; - image: RawImage[]; - generator: string[]; - lastBuildDate: string[]; - "atom:link": AtomLink[]; - copyright: string[]; - language: string[]; - webMaster: string[]; - "itunes:owner": ItunesOwner[]; - "itunes:author": string[]; - "googleplay:owner": string[]; - "googleplay:email": string[]; - "googleplay:author": string[]; - item: RawItem[]; -} - -export interface RawImage { - url: string[]; - title: string[]; - link: string[]; -} - -export interface AtomLink { - $: GeneratedType; -} - -export interface GeneratedType { - href: string; - rel: string; - type: string; -} - -export interface ItunesOwner { - "itunes:email": string[]; - "itunes:name": string[]; -} - -export interface RawItem { - title: string[]; - description: string[]; - link: string[]; - guid: Guid[]; - "dc:creator": string[]; - pubDate: string[]; - enclosure: Enclosure[]; - "content:encoded": string[]; -} - -export interface Guid { - _: string; - $: GeneratedType2; -} - -export interface GeneratedType2 { - isPermaLink: string; -} - -export interface Enclosure { - $: GeneratedType3; -} - -export interface GeneratedType3 { - url: string; - length: string; - type: string; -} - -export type SubstackItem = { - title: string; - description: string; - link: string; - pubDate: string; - content: string; -}; - -// Goodreads Feed Types -// TODO: to be moved to a separate package - -export type RawGoodreadsFeed = { - rss: RawGoodreadsFeedRSS; -}; - -export type RawGoodreadsFeedRSS = { - channel: RawGoodreadsFeedChannel[]; - [key: string]: unknown; -}; - -export type RawGoodreadsFeedChannel = { - title: string[]; - item: RawGoodreadsItem[]; - [key: string]: unknown; -}; - -export type RawGoodreadsItem = { - title: string[]; - link: string[]; - book_image_url: string[]; - book_large_image_url: string[]; - author_name: string[]; - book_description: string[]; - [key: string]: unknown; -}; - -export type GoodreadsFeedChannel = { - title: string; - item: GoodreadsItem[]; - [key: string]: unknown; -}; - -export type GoodreadsItem = { - title: string; - link: string; - book_image_url: string; - author_name: string; - book_description: string; - [key: string]: unknown; -}; diff --git a/package-lock.json b/package-lock.json index df7524b..94f8910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "substack-feed-api", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "substack-feed-api", - "version": "2.0.0", + "version": "2.0.1", "dependencies": { "cheerio": "^1.2.0", "node-fetch": "^3.3.2", @@ -59,6 +59,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", diff --git a/package.json b/package.json index c7f71a7..876ac01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "substack-feed-api", - "version": "2.0.0", + "version": "2.0.1", "type": "module", "files": [ "dist", diff --git a/vite.config.ts b/vite.config.ts index 6010570..4b8fa79 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -51,7 +51,7 @@ export default defineConfig(({ mode }) => { }, build: { lib: { - entry: "./lib/index.ts", + entry: ["./lib/index.ts", "./lib/goodreads.ts", "./lib/substack.ts"], name: "SubstackFeedAPI", fileName: "substackFeedApi", },