Enhance documentation and admin UI: Add detailed implementation guidelines in CLAUDE.md, introduce a referrer index in README.md, and update admin UI translations for improved user experience. Update package dependencies for better functionality and performance.
This commit is contained in:
@@ -11,6 +11,7 @@ use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::referrers::{Referrer, ReferrerIndex};
|
||||
use crate::schema::types::{SchemaDefinition, VALID_FIELD_TYPES};
|
||||
use crate::schema::validator;
|
||||
use crate::schema::SchemaRegistry;
|
||||
@@ -53,6 +54,10 @@ pub struct AppState {
|
||||
pub stores: Option<HashMap<String, Arc<dyn ContentStore>>>,
|
||||
/// Assets dir per environment when environments is set. Key = env name.
|
||||
pub assets_dirs: Option<HashMap<String, PathBuf>>,
|
||||
/// Reverse index for "who references this entry?". Updated on create/update/delete. None when environments are used.
|
||||
pub referrer_index: Option<Arc<RwLock<ReferrerIndex>>>,
|
||||
/// Path to persist referrer index (e.g. content/_referrers.json).
|
||||
pub referrer_index_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -726,6 +731,7 @@ pub async fn create_entry(
|
||||
|
||||
// Normalize reference arrays: objects with _slug → slug strings (so clients can send resolved refs)
|
||||
validator::normalize_reference_arrays(&schema, &mut body);
|
||||
validator::normalize_multi_select(&schema, &mut body);
|
||||
|
||||
// Validate against schema (type checks, constraints, strict mode, …)
|
||||
let errors = validator::validate_content(&schema, &body);
|
||||
@@ -766,6 +772,27 @@ pub async fn create_entry(
|
||||
.map_err(ApiError::from)?;
|
||||
state.cache.invalidate_collection(&env, &collection).await;
|
||||
|
||||
// Update referrer index: this entry references (ref_coll, ref_slug)
|
||||
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
|
||||
let refs = validator::extract_references(&schema, &body);
|
||||
let referrer = Referrer {
|
||||
collection: collection.clone(),
|
||||
slug: slug.clone(),
|
||||
field: String::new(), // we add per (ref_coll, ref_slug, field) below
|
||||
locale: locale_ref.map(str::to_string),
|
||||
};
|
||||
let mut index = idx.write().await;
|
||||
for (ref_coll, ref_slug, field) in refs {
|
||||
let mut r = referrer.clone();
|
||||
r.field = field;
|
||||
index.add_referrer(&ref_coll, &ref_slug, r);
|
||||
}
|
||||
drop(index);
|
||||
if let Err(e) = idx.read().await.save(path) {
|
||||
tracing::warn!("Failed to save referrer index: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Return created entry (with reference format)
|
||||
let entry = store
|
||||
.get(&collection, &slug, locale_ref)
|
||||
@@ -846,6 +873,7 @@ pub async fn update_entry(
|
||||
|
||||
// Normalize reference arrays: objects with _slug → slug strings (so clients can send resolved refs)
|
||||
validator::normalize_reference_arrays(&schema, &mut body);
|
||||
validator::normalize_multi_select(&schema, &mut body);
|
||||
|
||||
// Load existing content for readonly check
|
||||
let existing = store
|
||||
@@ -902,6 +930,28 @@ pub async fn update_entry(
|
||||
.map_err(ApiError::from)?;
|
||||
state.cache.invalidate_collection(&env, &collection).await;
|
||||
|
||||
// Update referrer index: remove old refs from this entry, add new refs
|
||||
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
|
||||
let mut index = idx.write().await;
|
||||
index.remove_all_referrers_from(&collection, &slug);
|
||||
let refs = validator::extract_references(&schema, &body);
|
||||
let referrer = Referrer {
|
||||
collection: collection.clone(),
|
||||
slug: slug.clone(),
|
||||
field: String::new(),
|
||||
locale: locale_ref.map(str::to_string),
|
||||
};
|
||||
for (ref_coll, ref_slug, field) in refs {
|
||||
let mut r = referrer.clone();
|
||||
r.field = field;
|
||||
index.add_referrer(&ref_coll, &ref_slug, r);
|
||||
}
|
||||
drop(index);
|
||||
if let Err(e) = idx.read().await.save(path) {
|
||||
tracing::warn!("Failed to save referrer index: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Return updated entry (with reference format)
|
||||
let entry = store
|
||||
.get(&collection, &slug, locale_ref)
|
||||
@@ -933,6 +983,22 @@ pub async fn update_entry(
|
||||
Ok(Json(formatted))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /api/content/:collection/:slug/referrers
|
||||
// ---------------------------------------------------------------------------
|
||||
pub async fn get_referrers(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((collection, slug)): Path<(String, String)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let referrers = match &state.referrer_index {
|
||||
Some(idx) => idx.read().await.get_referrers(&collection, &slug),
|
||||
None => vec![],
|
||||
};
|
||||
Ok(Json(
|
||||
serde_json::to_value(&referrers).unwrap_or_else(|_| json!([])),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /api/locales
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -982,6 +1048,17 @@ pub async fn delete_entry(
|
||||
)));
|
||||
}
|
||||
|
||||
// Update referrer index: remove this entry from every (ref_coll, ref_slug) it referenced; and remove key (collection, slug)
|
||||
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
|
||||
let mut index = idx.write().await;
|
||||
index.remove_all_referrers_from(&collection, &slug);
|
||||
index.remove_referenced_entry(&collection, &slug);
|
||||
drop(index);
|
||||
if let Err(e) = idx.read().await.save(path) {
|
||||
tracing::warn!("Failed to save referrer index: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
store
|
||||
.delete(&collection, &slug, locale_ref)
|
||||
.await
|
||||
|
||||
@@ -235,6 +235,41 @@ pub fn generate_spec(registry: &SchemaRegistry, server_url: &str) -> Value {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// GET /api/content/:collection/:slug/referrers – who references this entry
|
||||
paths.insert(
|
||||
format!("/api/content/{}/{{slug}}/referrers", name),
|
||||
json!({
|
||||
"get": {
|
||||
"summary": format!("List referrers of '{}' entry", name),
|
||||
"description": "Returns all entries that reference this entry (reverse index). Empty when not using referrer index (e.g. with RUSTYCMS_ENVIRONMENTS).",
|
||||
"operationId": format!("get{}Referrers", pascal),
|
||||
"tags": [tag],
|
||||
"parameters": [
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Entry slug" }
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of referrers (collection, slug, field, locale)",
|
||||
"content": { "application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": { "type": "string", "description": "Collection of the referring entry" },
|
||||
"slug": { "type": "string", "description": "Slug of the referring entry" },
|
||||
"field": { "type": "string", "description": "Field that holds the reference" },
|
||||
"locale": { "type": "string", "nullable": true, "description": "Locale of the referring entry if applicable" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Asset management ─────────────────────────────────────────────────
|
||||
@@ -701,6 +736,7 @@ const API_INDEX_HTML: &str = r#"<!DOCTYPE html>
|
||||
<li><code>POST</code> /api/content/:type – Create entry</li>
|
||||
<li><code>PUT</code> /api/content/:type/:slug – Update entry</li>
|
||||
<li><code>DELETE</code> /api/content/:type/:slug – Delete entry</li>
|
||||
<li><code>GET</code> /api/content/:type/:slug/referrers – List entries that reference this entry (reverse index)</li>
|
||||
<li><code>GET</code> <a href="/api/transform?url=https://httpbin.org/image/png&w=80&h=80">/api/transform</a> – Transform image from URL (w, h, ar, fit, format)</li>
|
||||
<li><code>GET</code> <a href="/health">/health</a> – Health check</li>
|
||||
</ul>
|
||||
|
||||
@@ -41,6 +41,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
"/api/content/:collection",
|
||||
get(handlers::list_entries).post(handlers::create_entry),
|
||||
)
|
||||
.route(
|
||||
"/api/content/:collection/:slug/referrers",
|
||||
get(handlers::get_referrers),
|
||||
)
|
||||
.route(
|
||||
"/api/content/:collection/:slug",
|
||||
get(handlers::get_entry)
|
||||
|
||||
Reference in New Issue
Block a user