feat(apiclient): Add native Admin & Content API clients, JWT generator, and tag visibility features; remove external @tryghost deps and update docs
This commit is contained in:
191
ts/apiclient/ghost.contentapi.ts
Normal file
191
ts/apiclient/ghost.contentapi.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Ghost Content API Client
|
||||
* Read-only API for published content using native fetch
|
||||
*/
|
||||
|
||||
import type { IBrowseOptions, IReadOptions, IGhostAPIResponse, IGhostErrorResponse } from './ghost.types.js';
|
||||
|
||||
export interface IGhostContentAPIOptions {
|
||||
url: string;
|
||||
key: string;
|
||||
version?: string;
|
||||
ghostPath?: string;
|
||||
}
|
||||
|
||||
export class GhostContentAPI {
|
||||
private url: string;
|
||||
private key: string;
|
||||
private version: string;
|
||||
private ghostPath: string;
|
||||
|
||||
constructor(options: IGhostContentAPIOptions) {
|
||||
this.url = options.url.replace(/\/$/, ''); // Remove trailing slash
|
||||
this.key = options.key;
|
||||
this.version = options.version || 'v3';
|
||||
this.ghostPath = options.ghostPath || 'ghost';
|
||||
|
||||
if (!this.url) {
|
||||
throw new Error('GhostContentAPI: url is required');
|
||||
}
|
||||
if (!this.key) {
|
||||
throw new Error('GhostContentAPI: key is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API prefix based on version
|
||||
*/
|
||||
private getAPIPrefix(): string {
|
||||
// v5+ doesn't need version prefix
|
||||
if (this.version === 'v5' || this.version === 'v6' || this.version.match(/^v[5-9]\.\d+/)) {
|
||||
return `/content/`;
|
||||
}
|
||||
// v2-v4 and canary need version prefix
|
||||
return `/${this.version}/content/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full API URL
|
||||
*/
|
||||
private buildUrl(resource: string, identifier?: string, params?: Record<string, any>): string {
|
||||
const apiPrefix = this.getAPIPrefix();
|
||||
let endpoint = `${this.url}/${this.ghostPath}/api${apiPrefix}${resource}/`;
|
||||
|
||||
if (identifier) {
|
||||
endpoint += `${identifier}/`;
|
||||
}
|
||||
|
||||
// Add key to params
|
||||
const queryParams = {
|
||||
key: this.key,
|
||||
...params
|
||||
};
|
||||
|
||||
// Build query string
|
||||
const queryString = Object.keys(queryParams)
|
||||
.filter(key => queryParams[key] !== undefined && queryParams[key] !== null)
|
||||
.map(key => {
|
||||
const value = Array.isArray(queryParams[key])
|
||||
? queryParams[key].join(',')
|
||||
: queryParams[key];
|
||||
return `${key}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
return queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
*/
|
||||
private async makeRequest<T>(
|
||||
resource: string,
|
||||
identifier?: string,
|
||||
params?: Record<string, any>
|
||||
): Promise<T | T[]> {
|
||||
const url = this.buildUrl(resource, identifier, params);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Version': this.version.match(/^\d+\.\d+$/) ? `v${this.version}` : this.version,
|
||||
'User-Agent': 'GhostContentAPI/2.0'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData: IGhostErrorResponse = await response.json().catch(() => ({
|
||||
errors: [{ type: 'UnknownError', message: response.statusText }]
|
||||
}));
|
||||
|
||||
const error = errorData.errors?.[0];
|
||||
const err = new Error(error?.message || response.statusText);
|
||||
Object.assign(err, {
|
||||
name: error?.type || 'GhostContentAPIError',
|
||||
statusCode: response.status,
|
||||
...error
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data: IGhostAPIResponse<T> = await response.json();
|
||||
|
||||
// Extract the resource data
|
||||
const resourceData = data[resource];
|
||||
if (!resourceData) {
|
||||
throw new Error(`Response missing ${resource} property`);
|
||||
}
|
||||
|
||||
// If it's an array and has meta, attach meta to the array
|
||||
if (Array.isArray(resourceData) && data.meta) {
|
||||
return Object.assign(resourceData, { meta: data.meta });
|
||||
}
|
||||
|
||||
// If it's an array with single item and no meta, return the item
|
||||
if (Array.isArray(resourceData) && resourceData.length === 1 && !data.meta) {
|
||||
return resourceData[0];
|
||||
}
|
||||
|
||||
return resourceData as T | T[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create resource API methods
|
||||
*/
|
||||
public posts = {
|
||||
browse: (options?: IBrowseOptions) => this.makeRequest('posts', undefined, options),
|
||||
read: (options: IReadOptions) => {
|
||||
if (options.slug) {
|
||||
return this.makeRequest('posts', `slug/${options.slug}`, options);
|
||||
}
|
||||
if (options.id) {
|
||||
return this.makeRequest('posts', options.id, options);
|
||||
}
|
||||
throw new Error('Must provide id or slug');
|
||||
}
|
||||
};
|
||||
|
||||
public pages = {
|
||||
browse: (options?: IBrowseOptions) => this.makeRequest('pages', undefined, options),
|
||||
read: (options: IReadOptions) => {
|
||||
if (options.slug) {
|
||||
return this.makeRequest('pages', `slug/${options.slug}`, options);
|
||||
}
|
||||
if (options.id) {
|
||||
return this.makeRequest('pages', options.id, options);
|
||||
}
|
||||
throw new Error('Must provide id or slug');
|
||||
}
|
||||
};
|
||||
|
||||
public tags = {
|
||||
browse: (options?: IBrowseOptions) => this.makeRequest('tags', undefined, options),
|
||||
read: (options: IReadOptions) => {
|
||||
if (options.slug) {
|
||||
return this.makeRequest('tags', `slug/${options.slug}`, options);
|
||||
}
|
||||
if (options.id) {
|
||||
return this.makeRequest('tags', options.id, options);
|
||||
}
|
||||
throw new Error('Must provide id or slug');
|
||||
}
|
||||
};
|
||||
|
||||
public authors = {
|
||||
browse: (options?: IBrowseOptions) => this.makeRequest('authors', undefined, options),
|
||||
read: (options: IReadOptions) => {
|
||||
if (options.slug) {
|
||||
return this.makeRequest('authors', `slug/${options.slug}`, options);
|
||||
}
|
||||
if (options.id) {
|
||||
return this.makeRequest('authors', options.id, options);
|
||||
}
|
||||
throw new Error('Must provide id or slug');
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user