feat(journals): extract decryption logic to helper and improve Quick Add behavior
- Extracted journal entry decryption workflow to 'ae_journals_decryption.ts' to decouple it from Svelte reactivity and address loop issues. - Updated 'ae_comp__journal_entry_obj_id_view.svelte' to use the new decryption helper. - Refactored 'Quick Add' to use the first line as the title and remove it from the content before saving.
This commit is contained in:
@@ -188,3 +188,49 @@ const downloadUrl = `${BASE_URL}/v3/crud/hosted_file/${fileId}/?jwt=${jwtToken}`
|
||||
1. **Use `view` for Rich Data**: Instead of manually joining data in separate calls, use `?view=enriched` or `?view=detail`.
|
||||
2. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`).
|
||||
3. **Strict Typing**: Ensure your `data` objects match the backend models to avoid `400 Bad Request` validation errors.
|
||||
|
||||
---
|
||||
|
||||
## 8. Standard Patterns & Common Pitfalls (2026 Update)
|
||||
|
||||
To ensure stability across the Aether mesh, all frontend components must adhere to these established patterns.
|
||||
|
||||
### A. The "Whitelist" Standard for Saves
|
||||
**Problem:** Sending the entire object returned by a `GET` request back to a `PATCH` endpoint will cause a `400 Bad Request`. This happens because the object contains technical metadata (like `journal_id`, `created_on`, `for_type`) that the API cannot "SET".
|
||||
|
||||
**Standard:** When preparing a save payload, explicitly whitelist ONLY the user-editable fields.
|
||||
```typescript
|
||||
// ✅ CORRECT: Whitelist only editable fields
|
||||
const data_kv = {
|
||||
name: tmp_obj.name,
|
||||
content: tmp_obj.content,
|
||||
tags: tmp_obj.tags,
|
||||
category_code: tmp_obj.category_code
|
||||
};
|
||||
|
||||
// ❌ WRONG: Passing the whole object or just blacklisting a few
|
||||
const data_kv = { ...tmp_obj };
|
||||
delete data_kv.id; // Still contains other computed columns!
|
||||
```
|
||||
|
||||
### B. The Triple-ID Save Pattern
|
||||
**Standard:** When sending an ID in a POST/PATCH payload (e.g. creating a child object), use the random string version with the `_id_random` suffix.
|
||||
* **Property:** `[obj_type]_id_random`
|
||||
* **Value:** A string (e.g., `qpgvOh5nOYI`)
|
||||
|
||||
**Warning:** Sending a string value under an integer key (e.g. `journal_id: "qpgvOh5nOYI"`) will trigger a Pydantic validation error on the backend.
|
||||
|
||||
### C. Handle 'NULL' vs '0' for Booleans
|
||||
**Pitfall:** Fields like `hide` or `priority` can be `NULL` in the database.
|
||||
**Standard:** Ensure your synchronization logic in components handles `null`, `undefined`, and `0` identically for boolean checks to prevent "ghost" changes being detected by Svelte 5.
|
||||
|
||||
---
|
||||
|
||||
## 9. Planned API Improvements (2026 Roadmap)
|
||||
|
||||
The following refinements have been requested from the Backend Agent to further improve frontend productivity:
|
||||
|
||||
1. **Permissive Update Mode**: A new header (`x-ae-ignore-extra-fields: true`) is planned to allow the API to ignore non-writable columns in `PATCH` requests instead of returning a `400 Bad Request`.
|
||||
2. **Automated ID Resolution**: Backend will soon support automatic resolution of `_id_random` strings to integer IDs during `POST/PATCH`, reducing the need for manual lookups in the frontend.
|
||||
3. **Schema Evolution Tools**: New orchestration tools are being developed to automate field creation and renaming, including the automatic regeneration of the enriched SQL views.
|
||||
4. **Structured Validation Errors**: Move from string-based error details to a machine-readable format for better inline form validation.
|
||||
|
||||
90
src/lib/ae_journals/ae_journals_decryption.ts
Normal file
90
src/lib/ae_journals/ae_journals_decryption.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
|
||||
|
||||
/**
|
||||
* Result structure for decryption operations.
|
||||
*/
|
||||
export interface DecryptionResult {
|
||||
success: boolean;
|
||||
content?: string;
|
||||
history?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a journal entry's content and history using the provided or stored passcode.
|
||||
*
|
||||
* @param entry The journal entry object to decrypt.
|
||||
* @param journal The parent journal object (provides base passcode and private_passcode).
|
||||
* @param typed_passcode Optional: A user-entered passcode override.
|
||||
* @returns A Promise resolving to a DecryptionResult.
|
||||
*/
|
||||
export async function decrypt_journal_entry(
|
||||
entry: ae_JournalEntry,
|
||||
journal: ae_Journal,
|
||||
typed_passcode?: string
|
||||
): Promise<DecryptionResult> {
|
||||
|
||||
// Safety check: if not encrypted, return as-is
|
||||
if (!entry.content_encrypted) {
|
||||
return {
|
||||
success: true,
|
||||
content: entry.content ?? '',
|
||||
history: entry.history ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
// Determine which key to use
|
||||
let journal_key = typed_passcode;
|
||||
|
||||
// If no override, try the private passcode stored on the journal object
|
||||
if (!journal_key?.length) {
|
||||
journal_key = journal.private_passcode;
|
||||
}
|
||||
|
||||
if (!journal_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No passcode provided or available for decryption.'
|
||||
};
|
||||
}
|
||||
|
||||
// Aether standard: combine the journal's public passcode with the private key
|
||||
const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`;
|
||||
|
||||
try {
|
||||
// Decrypt Primary Content
|
||||
const result = await ae_util.decrypt_wrapper(entry.content_encrypted, decrypt_key);
|
||||
|
||||
if (result === false) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Decryption failed. Incorrect passcode or corrupted data.'
|
||||
};
|
||||
}
|
||||
|
||||
const decrypted_text = typeof result === 'string' ? result : '';
|
||||
let decrypted_history = '';
|
||||
|
||||
// Decrypt History (if it exists)
|
||||
if (entry.history_encrypted) {
|
||||
const h_res = await ae_util.decrypt_wrapper(entry.history_encrypted, decrypt_key);
|
||||
if (h_res !== false) {
|
||||
decrypted_history = typeof h_res === 'string' ? h_res : '';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: decrypted_text,
|
||||
history: decrypted_history
|
||||
};
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('decrypt_journal_entry error:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: `System error during decryption: ${err.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import { journals_loc, journals_sess, journals_slct } from '$lib/ae_journals/ae_journals_stores';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
import { decrypt_journal_entry } from '$lib/ae_journals/ae_journals_decryption';
|
||||
|
||||
import JournalEntry_Editor from './JournalEntry_Editor.svelte';
|
||||
import JournalEntry_Header from './JournalEntry_Header.svelte';
|
||||
@@ -125,9 +126,7 @@
|
||||
if (!journal?.id) return;
|
||||
|
||||
let journal_key = $journals_sess.journal_kv[journal.id]?.typed_journal_passcode;
|
||||
if (!journal_key?.length) journal_key = journal.private_passcode;
|
||||
if (!journal_key) return;
|
||||
|
||||
|
||||
is_processing = true;
|
||||
decryption_error = null;
|
||||
|
||||
@@ -137,11 +136,10 @@
|
||||
return s;
|
||||
});
|
||||
|
||||
const decrypt_key = `${journal.passcode ?? ''}:${journal_key}`;
|
||||
const result = await ae_util.decrypt_wrapper($lq__journal_entry_obj?.content_encrypted, decrypt_key);
|
||||
const result = await decrypt_journal_entry($lq__journal_entry_obj, journal, journal_key);
|
||||
|
||||
if (result === false) {
|
||||
decryption_error = 'Decryption failed. Incorrect passcode or corrupted data.';
|
||||
if (!result.success) {
|
||||
decryption_error = result.error || 'Decryption failed.';
|
||||
journals_sess.update(s => {
|
||||
s.journal_kv[journal.id].journal_passcode_verified = false;
|
||||
s.journal_kv[journal.id].journal_passcode_decrypted = false;
|
||||
@@ -152,18 +150,13 @@
|
||||
}
|
||||
|
||||
// SUCCESS
|
||||
const decrypted_text = typeof result === 'string' ? result : '';
|
||||
tmp_entry_obj.content = decrypted_text;
|
||||
tmp_entry_obj.content_md_html = handle_marked(decrypted_text);
|
||||
if (orig_entry_obj) orig_entry_obj.content = decrypted_text;
|
||||
tmp_entry_obj.content = result.content;
|
||||
tmp_entry_obj.content_md_html = handle_marked(result.content || '');
|
||||
if (orig_entry_obj) orig_entry_obj.content = result.content;
|
||||
|
||||
// Decrypt History
|
||||
if ($lq__journal_entry_obj?.history_encrypted) {
|
||||
const h_res = await ae_util.decrypt_wrapper($lq__journal_entry_obj.history_encrypted, decrypt_key);
|
||||
if (h_res !== false) {
|
||||
tmp_entry_obj.history = h_res;
|
||||
if (orig_entry_obj) orig_entry_obj.history = h_res;
|
||||
}
|
||||
if (result.history) {
|
||||
tmp_entry_obj.history = result.history;
|
||||
if (orig_entry_obj) orig_entry_obj.history = result.history;
|
||||
}
|
||||
|
||||
journals_sess.update(s => {
|
||||
|
||||
@@ -36,9 +36,12 @@
|
||||
let name = lines[0].substring(0, 100);
|
||||
if (lines[0].length > 100) name += "...";
|
||||
|
||||
// Remove the first line (title) from the content
|
||||
const entry_content = lines.slice(1).join('\n').trim();
|
||||
|
||||
const data_kv = {
|
||||
name: name,
|
||||
content: note_content,
|
||||
content: entry_content,
|
||||
type_code: 'note',
|
||||
private: false, // Ensure notes are public/decrypted by default
|
||||
enable: true,
|
||||
|
||||
Reference in New Issue
Block a user