feat(ghost): Implement Tag, Author and Page models; add advanced filtering, search, bulk operations, image upload, related-posts, update tests and bump dependencies
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/ghost',
|
||||
version: '1.1.0',
|
||||
version: '1.2.0',
|
||||
description: 'An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.'
|
||||
}
|
||||
|
50
ts/classes.author.ts
Normal file
50
ts/classes.author.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Ghost } from './classes.ghost.js';
|
||||
import type { IAuthor } from './classes.post.js';
|
||||
|
||||
export class Author {
|
||||
public ghostInstanceRef: Ghost;
|
||||
public authorData: IAuthor;
|
||||
|
||||
constructor(ghostInstanceRefArg: Ghost, authorData: IAuthor) {
|
||||
this.ghostInstanceRef = ghostInstanceRefArg;
|
||||
this.authorData = authorData;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this.authorData.id;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
return this.authorData.name;
|
||||
}
|
||||
|
||||
public getSlug(): string {
|
||||
return this.authorData.slug;
|
||||
}
|
||||
|
||||
public getProfileImage(): string | undefined {
|
||||
return this.authorData.profile_image;
|
||||
}
|
||||
|
||||
public getBio(): string | undefined {
|
||||
return this.authorData.bio;
|
||||
}
|
||||
|
||||
public toJson(): IAuthor {
|
||||
return this.authorData;
|
||||
}
|
||||
|
||||
public async update(authorData: Partial<IAuthor>): Promise<Author> {
|
||||
try {
|
||||
const updatedAuthorData = await this.ghostInstanceRef.adminApi.users.edit({
|
||||
...authorData,
|
||||
id: this.getId()
|
||||
});
|
||||
this.authorData = updatedAuthorData;
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error('Error updating author:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,8 @@
|
||||
import * as plugins from './ghost.plugins.js';
|
||||
import { Post, type IPostOptions } from './classes.post.js';
|
||||
import { Post, type IPost, type ITag, type IAuthor } from './classes.post.js';
|
||||
import { Author } from './classes.author.js';
|
||||
import { Tag } from './classes.tag.js';
|
||||
import { Page, type IPage } from './classes.page.js';
|
||||
|
||||
export interface IGhostConstructorOptions {
|
||||
baseUrl: string;
|
||||
@@ -28,10 +31,38 @@ export class Ghost {
|
||||
});
|
||||
}
|
||||
|
||||
public async getPosts(limit: number = 1000): Promise<Post[]> {
|
||||
public async getPosts(optionsArg?: {
|
||||
limit?: number;
|
||||
tag?: string;
|
||||
author?: string;
|
||||
featured?: boolean;
|
||||
filter?: string;
|
||||
}): Promise<Post[]> {
|
||||
try {
|
||||
const postsData = await this.contentApi.posts.browse({ limit, include: 'tags,authors' });
|
||||
return postsData.map((postData: IPostOptions) => new Post(this, postData));
|
||||
const limit = optionsArg?.limit || 1000;
|
||||
const filters: string[] = [];
|
||||
|
||||
if (optionsArg?.tag) {
|
||||
filters.push(`tag:${optionsArg.tag}`);
|
||||
}
|
||||
if (optionsArg?.author) {
|
||||
filters.push(`author:${optionsArg.author}`);
|
||||
}
|
||||
if (optionsArg?.featured !== undefined) {
|
||||
filters.push(`featured:${optionsArg.featured}`);
|
||||
}
|
||||
if (optionsArg?.filter) {
|
||||
filters.push(optionsArg.filter);
|
||||
}
|
||||
|
||||
const filterString = filters.length > 0 ? filters.join('+') : undefined;
|
||||
|
||||
const postsData = await this.contentApi.posts.browse({
|
||||
limit,
|
||||
include: 'tags,authors',
|
||||
...(filterString && { filter: filterString })
|
||||
});
|
||||
return postsData.map((postData: IPost) => new Post(this, postData));
|
||||
} catch (error) {
|
||||
console.error('Error fetching posts:', error);
|
||||
throw error;
|
||||
@@ -48,7 +79,7 @@ export class Ghost {
|
||||
}
|
||||
}
|
||||
|
||||
public async createPost(postData: IPostOptions): Promise<Post> {
|
||||
public async createPost(postData: IPost): Promise<Post> {
|
||||
try {
|
||||
const createdPostData = await this.adminApi.posts.add(postData);
|
||||
return new Post(createdPostData, this.adminApi);
|
||||
@@ -65,4 +96,213 @@ export class Ghost {
|
||||
);
|
||||
return new Post(this, postData);
|
||||
}
|
||||
|
||||
public async getTags(optionsArg?: { filter?: string; limit?: number }): Promise<ITag[]> {
|
||||
try {
|
||||
const limit = optionsArg?.limit || 1000;
|
||||
const tagsData = await this.contentApi.tags.browse({ limit });
|
||||
|
||||
if (optionsArg?.filter) {
|
||||
const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter);
|
||||
return tagsData.filter((tag: ITag) => matcher.match(tag.slug));
|
||||
}
|
||||
|
||||
return tagsData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getTagById(id: string): Promise<Tag> {
|
||||
try {
|
||||
const tagData = await this.contentApi.tags.read({ id });
|
||||
return new Tag(this, tagData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching tag with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getTagBySlug(slug: string): Promise<Tag> {
|
||||
try {
|
||||
const tagData = await this.contentApi.tags.read({ slug });
|
||||
return new Tag(this, tagData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching tag with slug ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTag(tagData: Partial<ITag>): Promise<Tag> {
|
||||
try {
|
||||
const createdTagData = await this.adminApi.tags.add(tagData);
|
||||
return new Tag(this, createdTagData);
|
||||
} catch (error) {
|
||||
console.error('Error creating tag:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAuthors(optionsArg?: { filter?: string; limit?: number }): Promise<Author[]> {
|
||||
try {
|
||||
const limit = optionsArg?.limit || 1000;
|
||||
const authorsData = await this.contentApi.authors.browse({ limit });
|
||||
|
||||
if (optionsArg?.filter) {
|
||||
const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter);
|
||||
return authorsData
|
||||
.filter((author: IAuthor) => matcher.match(author.slug))
|
||||
.map((author: IAuthor) => new Author(this, author));
|
||||
}
|
||||
|
||||
return authorsData.map((author: IAuthor) => new Author(this, author));
|
||||
} catch (error) {
|
||||
console.error('Error fetching authors:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAuthorById(id: string): Promise<Author> {
|
||||
try {
|
||||
const authorData = await this.contentApi.authors.read({ id });
|
||||
return new Author(this, authorData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching author with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAuthorBySlug(slug: string): Promise<Author> {
|
||||
try {
|
||||
const authorData = await this.contentApi.authors.read({ slug });
|
||||
return new Author(this, authorData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching author with slug ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getPages(optionsArg?: { limit?: number; filter?: string }): Promise<Page[]> {
|
||||
try {
|
||||
const limit = optionsArg?.limit || 1000;
|
||||
const pagesData = await this.contentApi.pages.browse({ limit, include: 'tags,authors' });
|
||||
|
||||
if (optionsArg?.filter) {
|
||||
const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter);
|
||||
return pagesData
|
||||
.filter((page: IPage) => matcher.match(page.slug))
|
||||
.map((page: IPage) => new Page(this, page));
|
||||
}
|
||||
|
||||
return pagesData.map((pageData: IPage) => new Page(this, pageData));
|
||||
} catch (error) {
|
||||
console.error('Error fetching pages:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getPageById(id: string): Promise<Page> {
|
||||
try {
|
||||
const pageData = await this.contentApi.pages.read({ id });
|
||||
return new Page(this, pageData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching page with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getPageBySlug(slug: string): Promise<Page> {
|
||||
try {
|
||||
const pageData = await this.contentApi.pages.read({ slug });
|
||||
return new Page(this, pageData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching page with slug ${slug}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async createPage(pageData: Partial<IPage>): Promise<Page> {
|
||||
try {
|
||||
const createdPageData = await this.adminApi.pages.add(pageData);
|
||||
return new Page(this, createdPageData);
|
||||
} catch (error) {
|
||||
console.error('Error creating page:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async searchPosts(query: string, optionsArg?: { limit?: number }): Promise<Post[]> {
|
||||
try {
|
||||
const limit = optionsArg?.limit || 100;
|
||||
const postsData = await this.contentApi.posts.browse({
|
||||
limit,
|
||||
filter: `title:~'${query}'`,
|
||||
include: 'tags,authors'
|
||||
});
|
||||
return postsData.map((postData: IPost) => new Post(this, postData));
|
||||
} catch (error) {
|
||||
console.error('Error searching posts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadImage(filePath: string): Promise<string> {
|
||||
try {
|
||||
const result = await this.adminApi.images.upload({ file: filePath });
|
||||
return result.url;
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkUpdatePosts(postIds: string[], updates: Partial<IPost>): Promise<Post[]> {
|
||||
try {
|
||||
const updatePromises = postIds.map(async (id) => {
|
||||
const post = await this.getPostById(id);
|
||||
return post.update({ ...post.postData, ...updates, id });
|
||||
});
|
||||
return await Promise.all(updatePromises);
|
||||
} catch (error) {
|
||||
console.error('Error bulk updating posts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkDeletePosts(postIds: string[]): Promise<void> {
|
||||
try {
|
||||
const deletePromises = postIds.map(async (id) => {
|
||||
const post = await this.getPostById(id);
|
||||
return post.delete();
|
||||
});
|
||||
await Promise.all(deletePromises);
|
||||
} catch (error) {
|
||||
console.error('Error bulk deleting posts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getRelatedPosts(postId: string, limit: number = 5): Promise<Post[]> {
|
||||
try {
|
||||
const post = await this.getPostById(postId);
|
||||
const tags = post.postData.tags;
|
||||
|
||||
if (!tags || !Array.isArray(tags) || tags.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tagSlugs = tags.map(tag => tag.slug).join(',');
|
||||
const postsData = await this.contentApi.posts.browse({
|
||||
limit,
|
||||
filter: `tag:[${tagSlugs}]+id:-${postId}`,
|
||||
include: 'tags,authors'
|
||||
});
|
||||
|
||||
return postsData.map((postData: IPost) => new Post(this, postData));
|
||||
} catch (error) {
|
||||
console.error('Error fetching related posts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
65
ts/classes.page.ts
Normal file
65
ts/classes.page.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Ghost } from './classes.ghost.js';
|
||||
import type { IPost, IAuthor } from './classes.post.js';
|
||||
|
||||
export interface IPage extends IPost {}
|
||||
|
||||
export class Page {
|
||||
public ghostInstanceRef: Ghost;
|
||||
public pageData: IPage;
|
||||
|
||||
constructor(ghostInstanceRefArg: Ghost, pageData: IPage) {
|
||||
this.ghostInstanceRef = ghostInstanceRefArg;
|
||||
this.pageData = pageData;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this.pageData.id;
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.pageData.title;
|
||||
}
|
||||
|
||||
public getHtml(): string {
|
||||
return this.pageData.html;
|
||||
}
|
||||
|
||||
public getSlug(): string {
|
||||
return this.pageData.slug;
|
||||
}
|
||||
|
||||
public getFeatureImage(): string | undefined {
|
||||
return this.pageData.feature_image;
|
||||
}
|
||||
|
||||
public getAuthor(): IAuthor {
|
||||
return this.pageData.primary_author;
|
||||
}
|
||||
|
||||
public toJson(): IPage {
|
||||
return this.pageData;
|
||||
}
|
||||
|
||||
public async update(pageData: Partial<IPage>): Promise<Page> {
|
||||
try {
|
||||
const updatedPageData = await this.ghostInstanceRef.adminApi.pages.edit({
|
||||
...pageData,
|
||||
id: this.getId()
|
||||
});
|
||||
this.pageData = updatedPageData;
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error('Error updating page:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
try {
|
||||
await this.ghostInstanceRef.adminApi.pages.delete({ id: this.getId() });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting page with id ${this.getId()}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
@@ -39,7 +39,7 @@ export interface ITag {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IPostOptions {
|
||||
export interface IPost {
|
||||
id: string;
|
||||
uuid: string;
|
||||
title: string;
|
||||
@@ -83,9 +83,9 @@ export interface IPostOptions {
|
||||
|
||||
export class Post {
|
||||
public ghostInstanceRef: Ghost;
|
||||
public postData: IPostOptions;
|
||||
public postData: IPost;
|
||||
|
||||
constructor(ghostInstanceRefArg: Ghost, postData: IPostOptions) {
|
||||
constructor(ghostInstanceRefArg: Ghost, postData: IPost) {
|
||||
this.ghostInstanceRef = ghostInstanceRefArg;
|
||||
this.postData = postData;
|
||||
}
|
||||
@@ -114,11 +114,11 @@ export class Post {
|
||||
return this.postData.primary_author;
|
||||
}
|
||||
|
||||
public toJson(): IPostOptions {
|
||||
public toJson(): IPost {
|
||||
return this.postData;
|
||||
}
|
||||
|
||||
public async update(postData: IPostOptions): Promise<Post> {
|
||||
public async update(postData: IPost): Promise<Post> {
|
||||
try {
|
||||
const updatedPostData = await this.ghostInstanceRef.adminApi.posts.edit(postData);
|
||||
this.postData = updatedPostData;
|
||||
|
55
ts/classes.tag.ts
Normal file
55
ts/classes.tag.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Ghost } from './classes.ghost.js';
|
||||
import type { ITag } from './classes.post.js';
|
||||
|
||||
export class Tag {
|
||||
public ghostInstanceRef: Ghost;
|
||||
public tagData: ITag;
|
||||
|
||||
constructor(ghostInstanceRefArg: Ghost, tagData: ITag) {
|
||||
this.ghostInstanceRef = ghostInstanceRefArg;
|
||||
this.tagData = tagData;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this.tagData.id;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
return this.tagData.name;
|
||||
}
|
||||
|
||||
public getSlug(): string {
|
||||
return this.tagData.slug;
|
||||
}
|
||||
|
||||
public getDescription(): string | undefined {
|
||||
return this.tagData.description;
|
||||
}
|
||||
|
||||
public toJson(): ITag {
|
||||
return this.tagData;
|
||||
}
|
||||
|
||||
public async update(tagData: Partial<ITag>): Promise<Tag> {
|
||||
try {
|
||||
const updatedTagData = await this.ghostInstanceRef.adminApi.tags.edit({
|
||||
...tagData,
|
||||
id: this.getId()
|
||||
});
|
||||
this.tagData = updatedTagData;
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error('Error updating tag:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
try {
|
||||
await this.ghostInstanceRef.adminApi.tags.delete({ id: this.getId() });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting tag with id ${this.getId()}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,9 @@
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import GhostAdminAPI from '@tryghost/admin-api';
|
||||
import * as smartmatch from '@push.rocks/smartmatch';
|
||||
|
||||
export {
|
||||
GhostContentAPI,
|
||||
GhostAdminAPI
|
||||
GhostAdminAPI,
|
||||
smartmatch
|
||||
}
|
||||
|
@@ -1,2 +1,5 @@
|
||||
export * from './classes.ghost.js';
|
||||
export * from './classes.post.js';
|
||||
export * from './classes.post.js';
|
||||
export * from './classes.author.js';
|
||||
export * from './classes.tag.js';
|
||||
export * from './classes.page.js';
|
Reference in New Issue
Block a user