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:
318
ts/apiclient/ghost.adminapi.ts
Normal file
318
ts/apiclient/ghost.adminapi.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Ghost Admin API Client
|
||||
* Full CRUD operations for Ghost content using native fetch
|
||||
*/
|
||||
|
||||
import { generateToken } from './ghost.jwt.js';
|
||||
import type { THttpMethod, IBrowseOptions, IGhostAPIResponse, IGhostErrorResponse } from './ghost.types.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface IGhostAdminAPIOptions {
|
||||
url: string;
|
||||
key: string;
|
||||
version?: string;
|
||||
ghostPath?: string;
|
||||
}
|
||||
|
||||
export class GhostAdminAPI {
|
||||
private url: string;
|
||||
private key: string;
|
||||
private version: string;
|
||||
private ghostPath: string;
|
||||
private acceptVersionHeader: string;
|
||||
|
||||
constructor(options: IGhostAdminAPIOptions) {
|
||||
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('GhostAdminAPI: url is required');
|
||||
}
|
||||
if (!this.key) {
|
||||
throw new Error('GhostAdminAPI: key is required');
|
||||
}
|
||||
|
||||
// Set accept version header
|
||||
if (this.version.match(/^v\d+$/)) {
|
||||
this.acceptVersionHeader = `${this.version}.0`;
|
||||
} else if (this.version.match(/^v\d+\.\d+$/)) {
|
||||
this.acceptVersionHeader = this.version;
|
||||
} else {
|
||||
this.acceptVersionHeader = 'v6.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `/admin/`;
|
||||
}
|
||||
// v2-v4 and canary need version prefix
|
||||
return `/${this.version}/admin/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}/`;
|
||||
}
|
||||
|
||||
// Build query string if params exist
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const queryString = Object.keys(params)
|
||||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||||
.map(key => {
|
||||
const value = Array.isArray(params[key])
|
||||
? params[key].join(',')
|
||||
: params[key];
|
||||
return `${key}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
if (queryString) {
|
||||
endpoint += `?${queryString}`;
|
||||
}
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization token
|
||||
*/
|
||||
private async getAuthToken(): Promise<string> {
|
||||
const audience = this.getAPIPrefix();
|
||||
return await generateToken(this.key, audience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request
|
||||
*/
|
||||
private async makeRequest<T>(
|
||||
resource: string,
|
||||
method: THttpMethod = 'GET',
|
||||
identifier?: string,
|
||||
body?: any,
|
||||
queryParams?: Record<string, any>
|
||||
): Promise<T | T[] | void> {
|
||||
const url = this.buildUrl(resource, identifier, queryParams);
|
||||
const token = await this.getAuthToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Ghost ${token}`,
|
||||
'Accept-Version': this.acceptVersionHeader,
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'GhostAdminAPI/2.0'
|
||||
};
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers
|
||||
};
|
||||
|
||||
// Add body for POST/PUT
|
||||
if (body && (method === 'POST' || method === 'PUT')) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
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 || 'GhostAdminAPIError',
|
||||
statusCode: response.status,
|
||||
...error
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
// DELETE returns empty response
|
||||
if (method === 'DELETE') {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: IGhostAPIResponse<T> = await response.json();
|
||||
|
||||
// Extract the resource data
|
||||
const resourceData = data[resource];
|
||||
if (!resourceData) {
|
||||
return data as any; // For some special endpoints that don't follow standard format
|
||||
}
|
||||
|
||||
// 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 factory
|
||||
*/
|
||||
private createResourceAPI(resourceType: string) {
|
||||
return {
|
||||
browse: (options?: IBrowseOptions) => {
|
||||
return this.makeRequest(resourceType, 'GET', undefined, undefined, options);
|
||||
},
|
||||
|
||||
read: (data: { id?: string; slug?: string; email?: string }, queryParams?: Record<string, any>) => {
|
||||
if (data.slug) {
|
||||
return this.makeRequest(resourceType, 'GET', `slug/${data.slug}`, undefined, queryParams);
|
||||
}
|
||||
if (data.email) {
|
||||
return this.makeRequest(resourceType, 'GET', `email/${data.email}`, undefined, queryParams);
|
||||
}
|
||||
if (data.id) {
|
||||
return this.makeRequest(resourceType, 'GET', data.id, undefined, queryParams);
|
||||
}
|
||||
throw new Error('Must provide id, slug, or email');
|
||||
},
|
||||
|
||||
add: (data: any, queryParams?: Record<string, any>) => {
|
||||
if (!data || !Object.keys(data).length) {
|
||||
return Promise.reject(new Error('Missing data'));
|
||||
}
|
||||
const body: any = {};
|
||||
body[resourceType] = [data];
|
||||
return this.makeRequest(resourceType, 'POST', undefined, body, queryParams);
|
||||
},
|
||||
|
||||
edit: (data: any, queryParams?: Record<string, any>) => {
|
||||
if (!data) {
|
||||
return Promise.reject(new Error('Missing data'));
|
||||
}
|
||||
if (!data.id) {
|
||||
return Promise.reject(new Error('Must include data.id'));
|
||||
}
|
||||
|
||||
const id = data.id;
|
||||
const updateData = { ...data };
|
||||
delete updateData.id;
|
||||
|
||||
const body: any = {};
|
||||
body[resourceType] = [updateData];
|
||||
|
||||
return this.makeRequest(resourceType, 'PUT', id, body, queryParams);
|
||||
},
|
||||
|
||||
delete: (data: { id?: string; email?: string }, queryParams?: Record<string, any>) => {
|
||||
if (!data) {
|
||||
return Promise.reject(new Error('Missing data'));
|
||||
}
|
||||
if (!data.id && !data.email) {
|
||||
return Promise.reject(new Error('Must include either data.id or data.email'));
|
||||
}
|
||||
|
||||
const identifier = data.email ? `email/${data.email}` : data.id;
|
||||
return this.makeRequest(resourceType, 'DELETE', identifier, undefined, queryParams);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Resource APIs
|
||||
public posts = this.createResourceAPI('posts');
|
||||
public pages = this.createResourceAPI('pages');
|
||||
public tags = this.createResourceAPI('tags');
|
||||
public members = this.createResourceAPI('members');
|
||||
public users = this.createResourceAPI('users');
|
||||
|
||||
// Webhooks (limited operations)
|
||||
public webhooks = {
|
||||
add: (data: any, queryParams?: Record<string, any>) => {
|
||||
const body: any = { webhooks: [data] };
|
||||
return this.makeRequest('webhooks', 'POST', undefined, body, queryParams);
|
||||
},
|
||||
|
||||
edit: (data: any, queryParams?: Record<string, any>) => {
|
||||
if (!data.id) {
|
||||
return Promise.reject(new Error('Must include data.id'));
|
||||
}
|
||||
const id = data.id;
|
||||
const updateData = { ...data };
|
||||
delete updateData.id;
|
||||
const body: any = { webhooks: [updateData] };
|
||||
return this.makeRequest('webhooks', 'PUT', id, body, queryParams);
|
||||
},
|
||||
|
||||
delete: (data: { id: string }, queryParams?: Record<string, any>) => {
|
||||
if (!data.id) {
|
||||
return Promise.reject(new Error('Must include data.id'));
|
||||
}
|
||||
return this.makeRequest('webhooks', 'DELETE', data.id, undefined, queryParams);
|
||||
}
|
||||
};
|
||||
|
||||
// Image upload
|
||||
public images = {
|
||||
upload: async (data: { file: string }) => {
|
||||
if (!data || !data.file) {
|
||||
throw new Error('Must provide file path');
|
||||
}
|
||||
|
||||
const url = this.buildUrl('images', 'upload');
|
||||
const token = await this.getAuthToken();
|
||||
|
||||
// Read file
|
||||
const fileBuffer = fs.readFileSync(data.file);
|
||||
const fileName = path.basename(data.file);
|
||||
|
||||
// Create FormData
|
||||
const formData = new FormData();
|
||||
// Convert Buffer to ArrayBuffer for Blob
|
||||
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength) as ArrayBuffer;
|
||||
const blob = new Blob([arrayBuffer], { type: 'image/*' });
|
||||
formData.append('file', blob, fileName);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Ghost ${token}`,
|
||||
'Accept-Version': this.acceptVersionHeader,
|
||||
'User-Agent': 'GhostAdminAPI/2.0'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
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);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.images?.[0] || result;
|
||||
}
|
||||
};
|
||||
}
|
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');
|
||||
}
|
||||
};
|
||||
}
|
116
ts/apiclient/ghost.jwt.ts
Normal file
116
ts/apiclient/ghost.jwt.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* JWT token generator for Ghost Admin API
|
||||
* Implements HS256 signing compatible with Ghost's authentication
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base64 URL encode (without padding)
|
||||
*/
|
||||
function base64UrlEncode(data: Uint8Array): string {
|
||||
const base64 = typeof Buffer !== 'undefined'
|
||||
? Buffer.from(data).toString('base64')
|
||||
: btoa(String.fromCharCode(...data));
|
||||
|
||||
return base64
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string to Uint8Array
|
||||
*/
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT token for Ghost Admin API
|
||||
* @param key - Admin API key in format {id}:{secret}
|
||||
* @param audience - Token audience (API prefix like '/admin/')
|
||||
* @returns JWT token string
|
||||
*/
|
||||
export async function generateToken(key: string, audience: string): Promise<string> {
|
||||
// Parse the admin key
|
||||
const [keyId, secret] = key.split(':');
|
||||
|
||||
if (!keyId || !secret) {
|
||||
throw new Error('Invalid admin API key format. Expected {id}:{secret}');
|
||||
}
|
||||
|
||||
if (keyId.length !== 24 || secret.length !== 64) {
|
||||
throw new Error('Invalid admin API key format. Expected 24 hex chars for id and 64 for secret');
|
||||
}
|
||||
|
||||
// Create JWT header
|
||||
const header = {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT',
|
||||
kid: keyId
|
||||
};
|
||||
|
||||
// Create JWT payload
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = {
|
||||
iat: now,
|
||||
exp: now + 300, // 5 minutes
|
||||
aud: audience
|
||||
};
|
||||
|
||||
// Encode header and payload
|
||||
const headerEncoded = base64UrlEncode(
|
||||
new TextEncoder().encode(JSON.stringify(header))
|
||||
);
|
||||
const payloadEncoded = base64UrlEncode(
|
||||
new TextEncoder().encode(JSON.stringify(payload))
|
||||
);
|
||||
|
||||
// Create signature data
|
||||
const signatureData = `${headerEncoded}.${payloadEncoded}`;
|
||||
|
||||
// Convert secret from hex to bytes
|
||||
const secretBytes = hexToBytes(secret);
|
||||
|
||||
// Import key for HMAC
|
||||
let cryptoKey: CryptoKey;
|
||||
|
||||
// Try to use Web Crypto API (works in Node 15+ and all modern browsers)
|
||||
try {
|
||||
const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
|
||||
// Convert to proper BufferSource type
|
||||
const secretBuffer = secretBytes.buffer.slice(secretBytes.byteOffset, secretBytes.byteOffset + secretBytes.byteLength) as ArrayBuffer;
|
||||
cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
secretBuffer,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
// Sign the data
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(signatureData)
|
||||
);
|
||||
|
||||
// Encode signature
|
||||
const signatureEncoded = base64UrlEncode(new Uint8Array(signature));
|
||||
|
||||
// Return complete JWT
|
||||
return `${signatureData}.${signatureEncoded}`;
|
||||
} catch (error) {
|
||||
// Fallback for older Node versions using crypto module directly
|
||||
const crypto = await import('crypto');
|
||||
const hmac = crypto.createHmac('sha256', secretBytes);
|
||||
hmac.update(signatureData);
|
||||
const signature = hmac.digest();
|
||||
const signatureEncoded = base64UrlEncode(signature);
|
||||
|
||||
return `${signatureData}.${signatureEncoded}`;
|
||||
}
|
||||
}
|
66
ts/apiclient/ghost.types.ts
Normal file
66
ts/apiclient/ghost.types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shared types for Ghost API clients
|
||||
*/
|
||||
|
||||
export interface IGhostAPIResponse<T> {
|
||||
[key: string]: T | T[] | IGhostMeta;
|
||||
meta?: IGhostMeta;
|
||||
}
|
||||
|
||||
export interface IGhostMeta {
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
pages: number;
|
||||
total: number;
|
||||
next: number | null;
|
||||
prev: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGhostError {
|
||||
type: string;
|
||||
message: string;
|
||||
context?: string;
|
||||
property?: string;
|
||||
help?: string;
|
||||
code?: string;
|
||||
id?: string;
|
||||
ghostErrorCode?: string;
|
||||
}
|
||||
|
||||
export interface IGhostErrorResponse {
|
||||
errors: IGhostError[];
|
||||
}
|
||||
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
|
||||
export interface IRequestOptions {
|
||||
method?: THttpMethod;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query parameters for browse operations
|
||||
*/
|
||||
export interface IBrowseOptions {
|
||||
limit?: number;
|
||||
page?: number;
|
||||
filter?: string;
|
||||
include?: string;
|
||||
fields?: string;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for read operations (by ID, slug, or email)
|
||||
*/
|
||||
export interface IReadOptions {
|
||||
id?: string;
|
||||
slug?: string;
|
||||
email?: string;
|
||||
include?: string;
|
||||
fields?: string;
|
||||
}
|
Reference in New Issue
Block a user