1use crate::audit;
24use crate::authz::Action;
25use crate::error::CoreError;
26use crate::request::ReqCtx;
27use crate::resource::Resource;
28use crate::response::ApiResponse;
29use crate::storage::{storage, CreateOutcome};
30use serde_json::{json, Value};
31
32pub async fn create<R: Resource + ?Sized>(res: &R, ctx: &ReqCtx, body: Value) -> ApiResponse {
34 if !res.authorize(ctx, Action::Create) {
35 return CoreError::Unauthorized.into();
36 }
37 let data = match res.filter_writable(body) {
38 Ok(d) => d,
39 Err(resp) => return resp,
40 };
41
42 let snapshot = audit::is_enabled().then(|| data.clone());
44 match storage().create(res.table_name(), data).await {
45 Ok(CreateOutcome { last_insert_id }) => {
46 if let Some(written) = snapshot {
47 let entry = audit::AuditEntry::new(
48 ctx,
49 res.base_path(),
50 last_insert_id.clone().unwrap_or_default(),
51 audit::Event::Create,
52 audit::diff(None, &written),
53 );
54 if let Some(reject) = audit::emit(entry).await {
55 return reject;
56 }
57 }
58 reindex_after_write(res, last_insert_id.as_deref()).await;
62
63 ApiResponse::created(json!({
64 "success": true,
65 "message": format!("{} created successfully", res.resource_name()),
66 "last_insert_id": last_insert_id,
67 }))
68 }
69 Err(e) => CoreError::from(e).into(),
70 }
71}
72
73async fn reindex_after_write<R: Resource + ?Sized>(res: &R, id: Option<&str>) {
77 let fields = res.search_fields();
78 if fields.is_empty() || !crate::search::is_enabled() {
79 return;
80 }
81 let Some(id) = id else { return };
82 if let Ok(Some(row)) = storage().get(res.table_name(), res.primary_key(), id).await {
83 let doc = crate::search::document_for(&row, &fields);
84 crate::search::index_record(res.base_path(), id, doc).await;
85 }
86}
87
88pub async fn update<R: Resource + ?Sized>(
91 res: &R,
92 ctx: &ReqCtx,
93 id: &str,
94 body: Value,
95) -> ApiResponse {
96 if !res.authorize(ctx, Action::Update) {
97 return CoreError::Unauthorized.into();
98 }
99 let data = match res.filter_writable(body) {
100 Ok(d) => d,
101 Err(resp) => return resp,
102 };
103
104 let (before, snapshot) = if audit::is_enabled() {
108 let before = storage()
109 .get(res.table_name(), res.primary_key(), id)
110 .await
111 .unwrap_or(None);
112 (before, Some(data.clone()))
113 } else {
114 (None, None)
115 };
116
117 match storage()
118 .update(res.table_name(), res.primary_key(), id, data)
119 .await
120 {
121 Ok(n) if n > 0 => {
122 if let Some(written) = snapshot {
123 let changes = audit::diff(before.as_ref(), &written);
124 if !changes.is_empty() {
126 let entry = audit::AuditEntry::new(
127 ctx,
128 res.base_path(),
129 id,
130 audit::Event::Update,
131 changes,
132 );
133 if let Some(reject) = audit::emit(entry).await {
134 return reject;
135 }
136 }
137 }
138 reindex_after_write(res, Some(id)).await;
141 ApiResponse::ok(json!({
142 "success": true,
143 "message": format!("{} updated successfully", res.resource_name()),
144 "modified_count": n,
145 }))
146 }
147 Ok(_) => CoreError::NotFound.into(),
148 Err(e) => CoreError::from(e).into(),
149 }
150}
151
152pub async fn delete<R: Resource + ?Sized>(res: &R, ctx: &ReqCtx, id: &str) -> ApiResponse {
155 if !res.authorize(ctx, Action::Delete) {
156 return CoreError::Unauthorized.into();
157 }
158 let soft = res.soft_delete();
159
160 let before = if audit::is_enabled() {
163 storage()
164 .get(res.table_name(), res.primary_key(), id)
165 .await
166 .unwrap_or(None)
167 } else {
168 None
169 };
170
171 match storage()
172 .delete(res.table_name(), res.primary_key(), id, soft)
173 .await
174 {
175 Ok(n) if n > 0 => {
176 if audit::is_enabled() {
177 let changes = before.as_ref().map(audit::diff_removed).unwrap_or_default();
178 let entry =
179 audit::AuditEntry::new(ctx, res.base_path(), id, audit::Event::Delete, changes);
180 if let Some(reject) = audit::emit(entry).await {
181 return reject;
182 }
183 }
184 if !soft && crate::attach::is_enabled() {
189 crate::attach::purge(res.base_path(), id).await;
190 }
191 if crate::search::is_enabled() && !res.search_fields().is_empty() {
194 crate::search::remove_record(res.base_path(), id).await;
195 }
196 ApiResponse::ok(json!({
197 "success": true,
198 "message": format!("{} deleted successfully", res.resource_name()),
199 "soft_delete": soft,
200 "affected": n,
201 }))
202 }
203 Ok(_) => CoreError::NotFound.into(),
204 Err(e) => CoreError::from(e).into(),
205 }
206}