diff options
author | vnugent <public@vaughnnugent.com> | 2023-07-12 01:28:23 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-07-12 01:28:23 -0400 |
commit | f64955c69d91e578e580b409ba31ac4b3477da96 (patch) | |
tree | 16f01392ddf1abfea13d7d1ede3bfb0459fe8f0d /lib/client/src |
Initial commit
Diffstat (limited to 'lib/client/src')
-rw-r--r-- | lib/client/src/channels.ts | 104 | ||||
-rw-r--r-- | lib/client/src/content.ts | 120 | ||||
-rw-r--r-- | lib/client/src/index.ts | 19 | ||||
-rw-r--r-- | lib/client/src/posts.ts | 169 | ||||
-rw-r--r-- | lib/client/src/types.ts | 172 |
5 files changed, 584 insertions, 0 deletions
diff --git a/lib/client/src/channels.ts b/lib/client/src/channels.ts new file mode 100644 index 0000000..b5fb817 --- /dev/null +++ b/lib/client/src/channels.ts @@ -0,0 +1,104 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import { find, isEqual } from 'lodash'; +import { CMNextApi, CMNextIndex, ChannelMeta } from './types' + +export interface ChannelApi extends CMNextApi<ChannelMeta> { + /** + * Gets the endpoint url for the channel + */ + readonly channelFile: string; +} + +/** + * Gets the channel api for the given channel json file url + * @param endpoint The url of the channel json file + */ +export const createChannelApi = (channelFile: string): ChannelApi => { + + const getIndex = async () : Promise<CMNextIndex<ChannelMeta>> => { + const res = await fetch(channelFile) + return await res.json() + } + + return { channelFile, getIndex } +} + +export interface ScopedChannelApi extends ChannelApi { + /** + * Gets the post index path for the currently selected channel + */ + getPostIndexPath(): Promise<string | undefined>; + /** + * Gets the content index path for the currently selected channel + */ + getContentIndexPath(): Promise<string | undefined>; + /** + * Gets the base dir for the currently selected channel + */ + getBaseDir(): Promise<string | undefined>; + /** + * Gets the content dir for the currently selected channel + */ + getContentDir(): Promise<string | undefined>; +} + +export const createScopedChannelApi = (channelFile: string, channelId: string): ScopedChannelApi => { + + const channelApi = createChannelApi(channelFile); + + const getSelectedChannel = async (): Promise<ChannelMeta | undefined> => { + const index = await channelApi.getIndex() + //Get the selected channel from the channels + return find(index.records, i => isEqual(i.id, channelId)) + } + + //begin getting the selected channel + const index = getSelectedChannel(); + + const getPostIndexPath = async (): Promise<string | undefined> => { + //Await the selected channel index + const channel = await index + return channel ? `${channel.path}/${channel.index}` : undefined; + } + + const getContentDir = async (): Promise<string | undefined> => { + //Await the selected channel index + const channel = await index + return channel ? channel.content : undefined; + } + + const getBaseDir = async (): Promise<string | undefined> => { + //Await the selected channel index + const channel = await index + return channel ? channel.path : undefined; + } + + const getContentIndexPath = async (): Promise<string | undefined> => { + //Await the selected channel index + const channel = await index + //Get the post index from the channel + return channel ? `${channel.path}/${channel.content}` : undefined; + } + + return{ + ...channelApi, + getBaseDir, + getContentDir, + getPostIndexPath, + getContentIndexPath, + } +}
\ No newline at end of file diff --git a/lib/client/src/content.ts b/lib/client/src/content.ts new file mode 100644 index 0000000..f801432 --- /dev/null +++ b/lib/client/src/content.ts @@ -0,0 +1,120 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import { startsWith } from "lodash"; +import { CMNextApi, CMNextAutoConfig, CMNextEntity, CMNextIndex, ContentMeta } from "./types"; +import { createScopedChannelApi } from "./channels"; + +export interface ContentApi extends CMNextApi<ContentMeta> { + + /** + * Gets the public url for the content item to load + * in the page + * @param item + */ + getContentUrl(item: ContentMeta | CMNextEntity): Promise<string>; + + /** + * Fetches the raw string content for the given item and returns a string + * of the content + * @param item The content item to fetch the content for + */ + getStringContent(item: ContentMeta | CMNextEntity): Promise<string>; +} + +export interface ContentApiManualConfig { + /** + * The root directory path of the desired channel + */ + readonly channelRootDir: string; + /** + * The relative path to the channel's content directory + */ + readonly contentDir: string; + /** + * The relative path to the channel's content index file + */ + readonly contentIndexPath: string; +} + +/** + * Creates a manual content api for the given manual content config, that + * uses the known endpoint constants to avoid extra netowrk requests when + * getting content. + * @param param0 The CMNext configuration for the manual content api + * @returns The manual content api + */ +export const createManualContentApi = ({ channelRootDir, contentIndexPath, contentDir }: ContentApiManualConfig): ContentApi => { + + //Make sure the content dir begins with a slash + contentDir = startsWith(contentDir, '/') ? contentDir : `/${contentDir}` + + const getIndex = async () : Promise<CMNextIndex<ContentMeta>> => { + const res = await fetch(`${channelRootDir}/${contentIndexPath}`) + return await res.json() + } + + const getContentUrl = async (item: ContentMeta): Promise<string> => { + //Content resides in the content dir within the channel dir + return `${channelRootDir}${contentDir}/${item.path}` + } + + const getStringContent = async (item: ContentMeta): Promise<string> => { + const url = await getContentUrl(item); + const res = await fetch(url) + return await res.text() + } + + return { getIndex, getContentUrl, getStringContent } +} + +/** + * Creates an automatic content api for the given auto content config, that + * uses the known global CMS catalog and the id of a channel to get content for + * then discovers the required config from the channel. This api will cause multple + * network requests to the CMS to discover the CMS configuration automatically. + * @param param0 The CMNext configuration for the auto content api + * @returns The automatic discovery content api + */ +export const createAutoContentApi = ({ cmsChannelIndexPath, channelId }: CMNextAutoConfig): ContentApi => { + + const channelApi = createScopedChannelApi(cmsChannelIndexPath, channelId); + + const getIndex = async () : Promise<CMNextIndex<ContentMeta>> => { + //Get the content index path from the channel api + const contentIndexPath = await channelApi.getContentIndexPath() + if(!contentIndexPath){ + return { version : '0.0.0', records: [], date: 0 } + } + //Fetch the content index + const res = await fetch(contentIndexPath) + return await res.json() + } + + const getContentUrl = async (item: ContentMeta): Promise<string> => { + //Content resides in the content dir within the channel dir + const contentDir = await channelApi.getContentDir(); + return contentDir ? `${contentDir}/${item.path}` : '' + } + + const getStringContent = async (item: ContentMeta): Promise<string> => { + //Get the url of the item then fetch it + const url = await getContentUrl(item); + const res = await fetch(url) + return await res.text() + } + + return { getIndex, getContentUrl, getStringContent } +}
\ No newline at end of file diff --git a/lib/client/src/index.ts b/lib/client/src/index.ts new file mode 100644 index 0000000..6a21ea7 --- /dev/null +++ b/lib/client/src/index.ts @@ -0,0 +1,19 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +export * from './types' +export * from './channels' +export * from './posts' +export * from './content' diff --git a/lib/client/src/posts.ts b/lib/client/src/posts.ts new file mode 100644 index 0000000..f673b21 --- /dev/null +++ b/lib/client/src/posts.ts @@ -0,0 +1,169 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import { defaultTo, startsWith } from "lodash"; +import { ChannelApi, createScopedChannelApi } from "./channels"; +import { CMNextApi, CMNextAutoConfig, CMNextIndex, PostMeta } from "./types"; + +export interface PostApi extends CMNextApi<PostMeta> { + /** + * Gets the content for a post by its Id + * @param postId The id of the post to fetch content for + * @returns A promise that resolves to the post content as a string + */ + getPostContent: (post: string | PostMeta, extension?:string) => Promise<string>; + /** + * Gets the index file path for the channel + */ + getIndexFilePath(): Promise<string> +} + +/** + * A post api created from an automatic configuration + */ +export interface AutoPostApi extends PostApi, CMNextAutoConfig { + /** + * The channel api pass-thru + */ + readonly channelApi: ChannelApi; +} + +/** + * A post api created from a manual configuration + */ +export interface ManualPostApi extends PostApi, PostApiManualConfig { +} + +export interface PostApiManualConfig { + /** + * The root directory path of the desired channel + */ + readonly channelRootDir: string; + /** + * The relative path to the channel's post index file + */ + readonly postIndexPath: string; + /** + * The relative path to the channel's content directory + */ + readonly contentDir: string; +} + + +/** + * Creates a post api around the channel index file and the desired channel id + * to get posts from. This method requires an additional fetch to get the channel + * information before it can fetch posts, so it will be slower, but its + * discovery is automatic. + * @param channelUrl The url to the channel index file + * @param channelId The id of the channel to get posts from + * @returns A post api that can be used to get posts from the channel + */ +export const createAutoPostApi = ({ cmsChannelIndexPath, channelId }: CMNextAutoConfig): AutoPostApi => { + //Use scoped channel api + const channelApi = createScopedChannelApi(cmsChannelIndexPath, channelId); + + const getIndex = async (): Promise<CMNextIndex<PostMeta>> => { + //Await the selected channel index + const indexUrl = await channelApi.getPostIndexPath(); + + if (!indexUrl){ + //Return empty index + return { date: 0, records: [], version: "0.0.0" } + } + + //Fetch the index file + const res = await fetch(indexUrl) + return await res.json() + } + + const getPostContent = async (post: PostMeta | string, extension = '.html'): Promise<string> => { + //Get the selected channel + const contentDir = await channelApi.getContentDir(); + const baseDir = await channelApi.getBaseDir(); + + if (!contentDir || !baseDir){ + //Return empty content + return "" + } + + const itemId = defaultTo(post.id, post) + + //Fetch the content as text because it is html + const res = await fetch(`${baseDir}${contentDir}/${itemId}${extension}`) + return await res.text() + } + + const getIndexFilePath = async () : Promise<string> => { + const indexUrl = await channelApi.getPostIndexPath(); + return indexUrl || "" + } + + return{ + channelApi, + getPostContent, + channelId, + getIndexFilePath, + getIndex, + cmsChannelIndexPath, + } + +} + +/** + * Creates a post api around known channel information to avoid additional fetch + * requests to get the channel information. This method is faster, but requires + * the channel information to be known and remain constant. + * @param baseUrl The base url of the desired channel + * @param indexFilePath The path to the index file within the channel + * @param contentDir The path to the content directory within the channel + * @returns A post api that can be used to get posts from the channel + */ +export const createManualPostApi = ({ channelRootDir, contentDir, postIndexPath }: PostApiManualConfig): ManualPostApi => { + + //Make sure inedx file has a leading slash + postIndexPath = startsWith(postIndexPath, '/') ? postIndexPath : `/${postIndexPath}` + contentDir = startsWith(contentDir, '/') ? contentDir : `/${contentDir}` + + const getItemPath = (path: string) => `${channelRootDir}${path}` + + const getIndex = async (): Promise<CMNextIndex<PostMeta>> => { + //Fetch the index file + const res = await fetch(getItemPath(postIndexPath)) + return await res.json() + } + + const getPostContent = async (post: PostMeta | string, extension = '.html'): Promise<string> => { + //Get the content url + const contentUrl = `${getItemPath(contentDir)}/${defaultTo(post.id, post)}${extension}` + + //Fetch the content as text because it is html + const res = await fetch(contentUrl) + return await res.text() + } + + const getIndexFilePath = async () : Promise<string> => { + return getItemPath(postIndexPath) + } + + return{ + getIndex, + channelRootDir, + contentDir, + postIndexPath, + getPostContent, + getIndexFilePath + } +}
\ No newline at end of file diff --git a/lib/client/src/types.ts b/lib/client/src/types.ts new file mode 100644 index 0000000..aa158e9 --- /dev/null +++ b/lib/client/src/types.ts @@ -0,0 +1,172 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +export interface CMNextAutoConfig{ + /** + * The url to the global channel catalog index + */ + readonly cmsChannelIndexPath: string; + /** + * The id of the channel to load + */ + readonly channelId: string; +} + + +/** + * Represents a 1:1 mapping of a blog index to + * the CMS representation of the index + */ +export interface CMNextIndex<T>{ + /** + * The collection of records in the index + */ + readonly records: T[]; + /** + * The date the index was last modified in unix seconds + */ + readonly date: number; + /** + * The version of the index + */ + readonly version: string; +} + +/** + * A base blog entity that has a globally unique id and a date + */ +export interface CMNextEntity { + /** + * The globally unique id of the entity + */ + readonly id: string; + /** + * The date the entity was last modified in unix seconds + */ + readonly date: number; +} + +/** + * A uniform api for the CMNext cms + */ +export interface CMNextApi<T> { + /** + * Gets the index file from the configured endpoint + */ + getIndex(): Promise<CMNextIndex<T>>; +} + + +/** + * A channel configuration entity + */ +export interface ChannelMeta extends CMNextEntity { + /** + * The base path of the channel + */ + readonly path: string; + /** + * The realtive path of the channel's index file + */ + readonly index: string; + /** + * The realtive directory within the channel to the channel's content + */ + readonly content: string; + /** + * Optiona channel feed configuration + */ + readonly feed?: ChannelFeed; +} + +/** + * A channel's feed configuration + */ +export interface ChannelFeed { + /** + * The public url the feed points to, aka the public url to this channel + */ + readonly url: string; + /** + * The realtive path to the channel's rss feed xml file within the channel + */ + readonly path: string; + /** + * The url to the image for this channel + */ + readonly image?: string; + /** + * The description of the channel + */ + readonly description?: string; + /** + * The author of the channel + */ + readonly author?: string; + /** + * The webmaster contact email for the channel + */ + readonly contact?: string; +} + +/** + * A blog post entity and its metadata + */ +export interface PostMeta extends CMNextEntity { + /** + * The title of the post + */ + readonly title?: string; + /** + * The summary of the post + */ + readonly summary?: string; + /** + * The author of the post + */ + readonly author?: string; + /** + * The date the post was created in unix seconds + */ + readonly created?: number; + /** + * The post tags for categorization + */ + readonly tags: string[]; + /** + * The post's image, assumed to be an absolute url + */ + readonly image?: string; +} + +export interface ContentMeta extends CMNextEntity { + /** + * The relative path to the content file within the + * content directory of the channel + */ + readonly path: string; + /** + * The name of the content file + */ + readonly name: string; + /** + * The content type of the content file + */ + readonly content_type: string; + /** + * The length of the content file in bytes + */ + readonly length: number; +} |