RustyCMS: File-based headless CMS with REST API, admin UI, multilingual support
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
222
admin-ui/src/lib/api.ts
Normal file
222
admin-ui/src/lib/api.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* RustyCMS API client. Base URL from NEXT_PUBLIC_RUSTYCMS_API_URL.
|
||||
* Optional RUSTYCMS_API_KEY for write operations (sent as X-API-Key).
|
||||
*/
|
||||
|
||||
const getBaseUrl = () =>
|
||||
process.env.NEXT_PUBLIC_RUSTYCMS_API_URL || "http://127.0.0.1:3000";
|
||||
|
||||
const getHeaders = (): HeadersInit => {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
const key =
|
||||
typeof window !== "undefined"
|
||||
? process.env.NEXT_PUBLIC_RUSTYCMS_API_KEY ?? null
|
||||
: process.env.RUSTYCMS_API_KEY ?? process.env.NEXT_PUBLIC_RUSTYCMS_API_KEY ?? null;
|
||||
if (key) headers["X-API-Key"] = key;
|
||||
return headers;
|
||||
};
|
||||
|
||||
export type CollectionMeta = {
|
||||
name: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
category?: string;
|
||||
field_count?: number;
|
||||
extends?: string | string[];
|
||||
strict?: boolean;
|
||||
};
|
||||
|
||||
export type CollectionsResponse = {
|
||||
collections: CollectionMeta[];
|
||||
};
|
||||
|
||||
export type FieldDefinition = {
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
collection?: string;
|
||||
collections?: string[];
|
||||
enum?: unknown[];
|
||||
default?: unknown;
|
||||
items?: FieldDefinition;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type SchemaDefinition = {
|
||||
name: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
category?: string;
|
||||
fields: Record<string, FieldDefinition>;
|
||||
extends?: string | string[];
|
||||
reusable?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ListResponse<T = Record<string, unknown>> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
};
|
||||
|
||||
export async function fetchCollections(): Promise<CollectionsResponse> {
|
||||
const res = await fetch(`${getBaseUrl()}/api/collections`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Collections: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchSchema(
|
||||
collection: string
|
||||
): Promise<SchemaDefinition> {
|
||||
const res = await fetch(
|
||||
`${getBaseUrl()}/api/collections/${encodeURIComponent(collection)}`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
if (!res.ok) throw new Error(`Schema ${collection}: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createSchema(
|
||||
schema: SchemaDefinition
|
||||
): Promise<SchemaDefinition> {
|
||||
const res = await fetch(`${getBaseUrl()}/api/schemas`, {
|
||||
method: "POST",
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(schema),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
const msg = (err as { error?: string }).error ?? (err as { errors?: string[] }).errors?.join(", ") ?? `Schema: ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export type SlugCheckResponse = {
|
||||
valid: boolean;
|
||||
normalized: string;
|
||||
available: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function checkSlug(
|
||||
collection: string,
|
||||
slug: string,
|
||||
params: { exclude?: string; _locale?: string } = {}
|
||||
): Promise<SlugCheckResponse> {
|
||||
const sp = new URLSearchParams({ slug: slug.trim() });
|
||||
if (params.exclude) sp.set("exclude", params.exclude);
|
||||
if (params._locale) sp.set("_locale", params._locale);
|
||||
const res = await fetch(
|
||||
`${getBaseUrl()}/api/collections/${encodeURIComponent(collection)}/slug-check?${sp}`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
if (!res.ok) throw new Error(`Slug-Check: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export type ContentListParams = {
|
||||
_page?: number;
|
||||
_per_page?: number;
|
||||
_sort?: string;
|
||||
_order?: "asc" | "desc";
|
||||
_resolve?: string;
|
||||
_locale?: string;
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
|
||||
export async function fetchContentList<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
params: ContentListParams = {}
|
||||
): Promise<ListResponse<T>> {
|
||||
const sp = new URLSearchParams();
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== "") sp.set(k, String(v));
|
||||
});
|
||||
const qs = sp.toString();
|
||||
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}${qs ? `?${qs}` : ""}`;
|
||||
const res = await fetch(url, { headers: getHeaders() });
|
||||
if (!res.ok) throw new Error(`List ${collection}: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchEntry<T = Record<string, unknown>>(
|
||||
collection: string,
|
||||
slug: string,
|
||||
params: { _resolve?: string; _locale?: string } = {}
|
||||
): Promise<T> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params._resolve) sp.set("_resolve", params._resolve);
|
||||
if (params._locale) sp.set("_locale", params._locale);
|
||||
const qs = sp.toString();
|
||||
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}${qs ? `?${qs}` : ""}`;
|
||||
const res = await fetch(url, { headers: getHeaders() });
|
||||
if (!res.ok) throw new Error(`Entry ${collection}/${slug}: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createEntry(
|
||||
collection: string,
|
||||
data: Record<string, unknown>,
|
||||
params: { _locale?: string } = {}
|
||||
): Promise<Record<string, unknown>> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params._locale) sp.set("_locale", params._locale);
|
||||
const qs = sp.toString();
|
||||
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}${qs ? `?${qs}` : ""}`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error((err as { message?: string }).message || `Create: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateEntry(
|
||||
collection: string,
|
||||
slug: string,
|
||||
data: Record<string, unknown>,
|
||||
params: { _locale?: string } = {}
|
||||
): Promise<Record<string, unknown>> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params._locale) sp.set("_locale", params._locale);
|
||||
const qs = sp.toString();
|
||||
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}${qs ? `?${qs}` : ""}`;
|
||||
const res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error((err as { message?: string }).message || `Update: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteEntry(
|
||||
collection: string,
|
||||
slug: string,
|
||||
params: { _locale?: string } = {}
|
||||
): Promise<void> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params._locale) sp.set("_locale", params._locale);
|
||||
const qs = sp.toString();
|
||||
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}${qs ? `?${qs}` : ""}`;
|
||||
const res = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: getHeaders(),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Delete: ${res.status}`);
|
||||
}
|
||||
Reference in New Issue
Block a user