import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { z } from 'zod';
// Zod schemas for type validation
const ProjectSchema = z.object({
id: z.string(),
name: z.string(),
isPublic: z.boolean().optional(),
allowAutoPublish: z.boolean().optional(),
requireApproval: z.boolean().optional(),
defaultTags: z.array(z.string()).optional(),
createdAt: z.string(),
updatedAt: z.string().optional(),
// Additional fields from API
entryCount: z.number().optional(),
latestEntry: z.object({
id: z.string(),
version: z.string().optional(),
createdAt: z.string(),
}).optional(),
});
const ChangelogEntrySchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
version: z.string().optional(),
publishedAt: z.string().nullable().optional(),
createdAt: z.string(),
updatedAt: z.string(),
changelogId: z.string(),
tags: z.array(z.object({
id: z.string(),
name: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
})).optional(),
});
const TagSchema = z.object({
id: z.string(),
name: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
// Type definitions
export type Project = z.infer<typeof ProjectSchema>;
export type ChangelogEntry = z.infer<typeof ChangelogEntrySchema>;
export type Tag = z.infer<typeof TagSchema>;
export interface ChangerawrClientConfig {
baseUrl: string;
apiKey: string;
timeout?: number;
}
export interface CreateProjectData {
name: string;
isPublic?: boolean | undefined;
allowAutoPublish?: boolean | undefined;
requireApproval?: boolean | undefined;
defaultTags?: string[] | undefined;
}
export interface CreateChangelogEntryData {
title: string;
content: string;
version?: string | undefined;
tags?: string[] | undefined;
publishedAt?: string | undefined;
}
export interface UpdateChangelogEntryData {
title?: string | undefined;
content?: string | undefined;
version?: string | undefined;
tags?: string[] | undefined;
}
export class ChangerawrClient {
public axios: AxiosInstance;
constructor(config: ChangerawrClientConfig) {
this.axios = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 30000,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
});
// Add response interceptor for error handling
this.axios.interceptors.response.use(
(response) => response,
(error) => {
console.error('Axios error:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
url: error.config?.url,
method: error.config?.method
});
if (error.response?.status === 401) {
throw new Error('Authentication failed. Check your API key.');
}
if (error.response?.status === 403) {
throw new Error('Access denied. Check your permissions.');
}
if (error.response?.status === 404) {
throw new Error(`Resource not found: ${error.config?.url}`);
}
if (error.response?.data?.error) {
throw new Error(`API Error: ${error.response.data.error}`);
}
if (error.response?.data?.message) {
throw new Error(`API Error: ${error.response.data.message}`);
}
// Network or other errors
if (error.code === 'ECONNREFUSED') {
throw new Error('Cannot connect to Changerawr server. Is it running?');
}
throw new Error(`HTTP ${error.response?.status || 'Unknown'}: ${error.message}`);
}
);
}
// Test connection to Changerawr
async testConnection(): Promise<boolean> {
try {
console.error('Testing connection to Changerawr...');
const response = await this.axios.get('/api/projects');
console.error(`Connection test successful. Status: ${response.status}`);
return response.status === 200;
} catch (error) {
console.error('Connection test failed:', error);
if (error instanceof Error) {
throw new Error(`Connection test failed: ${error.message}`);
}
throw error;
}
}
// Project operations
async listProjects(): Promise<Project[]> {
const response = await this.axios.get('/api/projects');
// API returns array directly, not wrapped in object
return z.array(ProjectSchema).parse(response.data);
}
async getProject(projectId: string): Promise<Project> {
const response = await this.axios.get(`/api/projects/${projectId}`);
return ProjectSchema.parse(response.data);
}
async createProject(data: CreateProjectData): Promise<Project> {
const response = await this.axios.post('/api/projects', data);
return ProjectSchema.parse(response.data);
}
async updateProject(projectId: string, data: Partial<CreateProjectData>): Promise<Project> {
const response = await this.axios.patch(`/api/projects/${projectId}`, data);
return ProjectSchema.parse(response.data);
}
async deleteProject(projectId: string): Promise<void> {
await this.axios.delete(`/api/projects/${projectId}`);
}
// Changelog entry operations
async listChangelogEntries(projectId: string, options?: {
published?: boolean;
limit?: number;
cursor?: string;
}): Promise<{ entries: ChangelogEntry[]; nextCursor?: string }> {
try {
console.error(`Fetching changelog entries for project: ${projectId}`, options);
const params = new URLSearchParams();
if (options?.published !== undefined) {
params.append('published', options.published.toString());
}
if (options?.limit) {
params.append('limit', options.limit.toString());
}
if (options?.cursor) {
params.append('cursor', options.cursor);
}
const response = await this.axios.get(
`/api/projects/${projectId}/changelog?${params.toString()}`
);
// API returns { entries: [...], pagination: {...} }
const entries = response.data.entries || response.data;
if (!Array.isArray(entries)) {
throw new Error('API returned unexpected format for changelog entries');
}
return {
entries: z.array(ChangelogEntrySchema).parse(entries),
nextCursor: response.data.pagination?.nextCursor,
};
} catch (error) {
console.error('Error fetching changelog entries:', error);
throw error;
}
}
async getChangelogEntry(projectId: string, entryId: string): Promise<ChangelogEntry> {
const response = await this.axios.get(`/api/projects/${projectId}/changelog/${entryId}`);
return ChangelogEntrySchema.parse(response.data);
}
async createChangelogEntry(
projectId: string,
data: CreateChangelogEntryData
): Promise<ChangelogEntry> {
const response = await this.axios.post(`/api/projects/${projectId}/changelog`, data);
return ChangelogEntrySchema.parse(response.data);
}
async updateChangelogEntry(
projectId: string,
entryId: string,
data: UpdateChangelogEntryData
): Promise<ChangelogEntry> {
const response = await this.axios.put(
`/api/projects/${projectId}/changelog/${entryId}`,
data
);
return ChangelogEntrySchema.parse(response.data);
}
async publishChangelogEntry(projectId: string, entryId: string): Promise<ChangelogEntry> {
const response = await this.axios.patch(
`/api/projects/${projectId}/changelog/${entryId}`,
{ action: 'publish' }
);
return ChangelogEntrySchema.parse(response.data);
}
async unpublishChangelogEntry(projectId: string, entryId: string): Promise<ChangelogEntry> {
const response = await this.axios.patch(
`/api/projects/${projectId}/changelog/${entryId}`,
{ action: 'unpublish' }
);
return ChangelogEntrySchema.parse(response.data);
}
async deleteChangelogEntry(projectId: string, entryId: string): Promise<void> {
await this.axios.delete(`/api/projects/${projectId}/changelog/${entryId}`);
}
// Tag operations
async listTags(): Promise<Tag[]> {
// Tags are accessed per project, not globally
const response = await this.axios.get('/api/projects'); // Get projects first
return []; // Return empty for now, would need projectId to get actual tags
}
async createTag(name: string): Promise<Tag> {
throw new Error('Creating tags requires a project ID. Use project-specific tag creation.');
}
async deleteTag(tagId: string): Promise<void> {
throw new Error('Deleting tags requires a project ID. Use project-specific tag deletion.');
}
// Project-specific tag operations
async listProjectTags(projectId: string): Promise<Tag[]> {
try {
console.error(`Fetching tags for project: ${projectId}`);
const response = await this.axios.get(`/api/projects/${projectId}/changelog/tags`);
// The API might return { tags: [...] } or just [...]
const tags = response.data.tags || response.data;
if (!Array.isArray(tags)) {
throw new Error('API returned unexpected format for tags');
}
return z.array(TagSchema).parse(tags);
} catch (error) {
console.error('Error fetching project tags:', error);
throw error;
}
}
async createProjectTag(projectId: string, name: string): Promise<Tag> {
try {
console.error(`Creating tag "${name}" for project: ${projectId}`);
const response = await this.axios.post(`/api/projects/${projectId}/changelog/tags`, { name });
return TagSchema.parse(response.data);
} catch (error) {
console.error('Error creating project tag:', error);
throw error;
}
}
// Analytics and stats
async getProjectStats(projectId: string): Promise<any> {
// Use dashboard stats endpoint since project-specific stats don't exist
const response = await this.axios.get('/api/dashboard/stats');
return response.data;
}
// Widget operations
async getPublicChangelog(projectId: string): Promise<ChangelogEntry[]> {
const response = await this.axios.get(`/api/changelog/${projectId}/entries`);
return z.array(ChangelogEntrySchema).parse(response.data.items || response.data);
}
}