fix(imports): point to element_data_store_v3 and restore Data Store v3; commit workspace updates

This commit is contained in:
Scott Idem
2026-03-17 18:57:27 -04:00
parent 3038be0686
commit 9fc3ee0198
22 changed files with 841 additions and 91 deletions

View File

@@ -0,0 +1,430 @@
<script lang="ts">
import { browser } from '$app/environment';
import { onMount, untrack } from 'svelte';
import { Modal } from 'flowbite-svelte';
import { liveQuery } from 'dexie';
import { api } from '$lib/api/api';
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
import { db_core } from '$lib/ae_core/db_core';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { key_val } from '$lib/stores/ae_stores';
import type { ae_DataStore } from '$lib/types/ae_types';
interface Props {
log_lvl?: number;
expire_minutes?: number;
mount_reload_sec?: number;
ds_code: string;
ds_name?: null | string;
ds_type?: string;
for_type?: null | string;
for_id?: null | string;
class_li?: string;
display?: string;
try_cache?: boolean;
hide?: boolean;
show_edit?: boolean;
show_edit_btn?: boolean;
show_view?: boolean;
ds_loaded?: boolean;
debug?: boolean;
ds_loading_status?: string;
val_sql?: null | any;
}
let {
log_lvl = 0,
expire_minutes = 15,
mount_reload_sec = 0,
ds_code,
ds_name = null,
ds_type = 'text',
for_type = null,
for_id = null,
class_li = '',
display = undefined as string | undefined,
try_cache = true,
hide = false,
show_edit = $bindable(false),
show_edit_btn = false,
show_view = $bindable(true),
ds_loaded = $bindable(false),
debug = false,
ds_loading_status = $bindable('starting'),
val_sql = $bindable(null)
}: Props = $props();
// Local reactive state
let trigger: null | string = $state(null);
let ds_submit_results: Promise<any> | key_val | undefined = $state();
// Dexie LiveQuery for data store
// This derived observable will automatically update when dependencies change
let lq__ds_obj = $derived(
liveQuery(async () => {
const current_code = ds_code;
const account_id = $ae_loc.account_id;
const current_for_type = for_type;
const current_for_id = for_id;
if (!current_code) return null;
if (log_lvl) console.log(`ae_e_data_store [${current_code}]: LQ Lookup...`, { account_id, current_for_type, current_for_id });
// Hierarchical Local Lookup (Specific -> Account -> Global)
// Mimics backend SQL priority: WHERE code = :code ORDER BY for_id DESC, account_id DESC
if (log_lvl) console.log(`ae_e_data_store [${current_code}]: Fetching all matching codes for priority sorting...`);
const results = await db_core.data_store
.where('code')
.equals(current_code)
.toArray();
if (!results || results.length === 0) return null;
// Sort by specificity
results.sort((a, b) => {
// 1. Priority: Specific Context match (for_type + for_id)
const a_context = (current_for_id && a.for_id === current_for_id && a.for_type === current_for_type) ? 1 : 0;
const b_context = (current_for_id && b.for_id === current_for_id && b.for_type === current_for_type) ? 1 : 0;
if (a_context !== b_context) return b_context - a_context;
// 2. Priority: Account-specific match
const a_account = (account_id && a.account_id === account_id) ? 1 : 0;
const b_account = (account_id && b.account_id === account_id) ? 1 : 0;
if (a_account !== b_account) return b_account - a_account;
// 3. Tie-breaker: Newest updated
const a_time = new Date(a.updated_on || a.created_on || 0).getTime();
const b_time = new Date(b.updated_on || b.created_on || 0).getTime();
return b_time - a_time;
});
if (log_lvl) console.log(`ae_e_data_store [${current_code}]: Best match found (ID: ${results[0].id}, Account: ${results[0].account_id})`);
return results[0];
})
);
// Sync status and bound props when the live data changes
$effect(() => {
const entry = $lq__ds_obj as ae_DataStore | null;
untrack(() => {
ds_loaded = !!entry;
if (entry) {
ds_loading_status = 'loaded';
// Handle val_sql binding if type is sql
if (ds_type === 'sql') {
val_sql = entry.text || entry.html || null;
}
}
});
});
// Initial Trigger & Context Change Guard
$effect(() => {
const account_id = $slct.account_id;
const api_ready = !!$ae_api?.base_url;
const entry = $lq__ds_obj;
if (browser && api_ready && !entry && ds_loading_status === 'starting') {
trigger = 'load__ds__code';
}
});
// Fetch handler
$effect(() => {
if (trigger === 'load__ds__code') {
untrack(() => {
trigger = null;
load_data_store();
});
}
});
// Mount reload logic
onMount(() => {
if (mount_reload_sec > 0) {
const random_ms = Math.floor(Math.random() * mount_reload_sec * 1000);
setTimeout(() => { trigger = 'load__ds__code'; }, random_ms);
}
});
async function load_data_store() {
if (ds_loading_status === 'loading') return;
ds_loading_status = 'loading';
const api_cfg = untrack(() => $ae_api);
if (log_lvl) console.log(`ae_e_data_store [${ds_code}]: Fetching...`);
try {
// Attempt 1: Context-specific fetch
let ds_results = await api.get_data_store_v3({
api_cfg,
code: ds_code,
for_type: for_type,
for_id: for_id,
log_lvl: log_lvl
});
// V3 API structured check
const is_error = ds_results?.meta?.success === false;
const status_code = ds_results?.meta?.status_code || (ds_results === false ? 500 : 200);
// Fallback to Global if not found (404), unauthorized (403/401), or explicitly failed
if (!ds_results || is_error || status_code === 404 || status_code === 403 || status_code === 401) {
if (log_lvl) console.log(`ae_e_data_store [${ds_code}]: Not found in context (Status ${status_code}). Trying global fallback.`);
ds_results = await api.get_data_store_v3({
api_cfg,
code: ds_code,
no_account_id: true,
log_lvl: log_lvl
});
}
const ds_id = ds_results?.data_store_id || ds_results?.id;
if (ds_results && ds_id) {
// Map fields correctly for V3 alignment
const text_val = ds_results.text || '';
const json_val = ds_results.json || (ds_results.json_str ? JSON.parse(ds_results.json_str) : null);
// Save to Dexie
const ds_to_save: ae_DataStore = {
...ds_results,
id: ds_id,
data_store_id: ds_results.data_store_id || ds_id,
// data_store_id: ds_id,
account_id: ds_results.account_id || ds_results.account_id,
// account_id: ds_results.account_id || ds_results.account_id,
updated_on: ds_results.updated_on || new Date().toISOString(),
text: text_val,
html: text_val, // Default map text to html
json: json_val
};
await db_core.data_store.put(ds_to_save);
if (log_lvl) console.log(`ae_e_data_store [${ds_code}]: Saved to Dexie. ID: ${ds_id}`);
} else {
ds_loading_status = 'not found';
if (log_lvl) console.warn(`ae_e_data_store [${ds_code}]: Result had no valid ID.`);
}
} catch (err) {
console.error(`ae_e_data_store [${ds_code}]: Fetch failed.`, err);
ds_loading_status = 'error';
}
}
async function handle_submit_form(event: Event) {
const target = event.target as HTMLFormElement;
$ae_sess.ds.submit_status = 'processing';
const form_data = new FormData(target);
const data_store_di = ae_util.extract_prefixed_form_data({
prefix: null,
form_data,
trim_values: true,
bool_tf_str: true
});
const data_store_do: key_val = {
code: data_store_di.ds_code ?? ds_code,
name: data_store_di.ds_name ?? ds_name,
type: data_store_di.ds_type ?? ds_type,
for_type: data_store_di.ds_for_type ?? null,
for_id: data_store_di.ds_for_id ?? null,
access_read: data_store_di.ds_access_read,
access_write: data_store_di.ds_access_write,
access_delete: data_store_di.ds_access_delete,
enable: data_store_di.ds_enable ?? true,
account_id: data_store_di.ds_use_account_id ? (data_store_di.ds_account_id ?? $slct.account_id) : null
};
const content_val = data_store_di.ds_value;
if (data_store_do.type === 'json') {
data_store_do.json = content_val;
try {
// Ensure it's valid JSON if stringified
if (typeof content_val === 'string') JSON.parse(content_val);
} catch (e) {
console.error("Invalid JSON content");
}
} else {
data_store_do.text = content_val;
}
const api_cfg = untrack(() => $ae_api);
if ($lq__ds_obj?.id) {
ds_submit_results = api.update_ae_obj_v3({
api_cfg,
obj_type: 'data_store',
obj_id: $lq__ds_obj.id,
fields: data_store_do
}).then((res) => {
if (res) {
$ae_sess.ds.submit_status = 'updated';
trigger = 'load__ds__code';
}
return res;
});
} else {
ds_submit_results = api.create_ae_obj_v3({
api_cfg,
obj_type: 'data_store',
fields: data_store_do
}).then((res) => {
if (res) {
$ae_sess.ds.submit_status = 'created';
trigger = 'load__ds__code';
}
return res;
});
}
}
async function handle_delete() {
if (!$lq__ds_obj?.id || !confirm('Are you sure you want to delete this data store?')) return;
const api_cfg = untrack(() => $ae_api);
const res = await api.delete_ae_obj_v3({
api_cfg,
obj_type: 'data_store',
obj_id: $lq__ds_obj.id,
method: 'delete'
});
if (res) {
await db_core.data_store.delete($lq__ds_obj.id);
ds_loading_status = 'not found';
show_edit = false;
}
}
</script>
<div class="ae__elem__data_store relative {class_li}" class:hidden={hide} style={display ? `display: ${display}` : undefined}>
{#if $lq__ds_obj}
{#if debug || $ae_loc.debug === 'debug'}
Debug is ON!
<pre class="text-[10px] bg-black/10 p-2 rounded mb-2 overflow-x-auto">
ID: {$lq__ds_obj.id}
Code: {$lq__ds_obj.code}
Name: {$lq__ds_obj.name}
Type: {$lq__ds_obj.type}
Account: {$lq__ds_obj.account_id || 'Global / NULL'}
Created: {$lq__ds_obj.created_on}
Updated: {$lq__ds_obj.updated_on}
</pre>
<hr />
{/if}
<Modal
title="{$lq__ds_obj.name || 'Unnamed'} - {$lq__ds_obj.code}"
bind:open={show_edit}
autoclose={false}
size="xl"
class="w-full max-w-6xl"
>
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); handle_submit_form(e); }}>
<input type="hidden" name="ds_id_random" value={$lq__ds_obj.id} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<label class="label">
<span class="text-xs font-bold opacity-70">Code</span>
<input type="text" name="ds_code" class="input font-mono" value={$lq__ds_obj.code} readonly={!$ae_loc.manager_access} required />
</label>
<label class="label">
<span class="text-xs font-bold opacity-70">Name</span>
<input type="text" name="ds_name" class="input" value={$lq__ds_obj.name} required />
</label>
</div>
<div class="space-y-2">
<label class="label">
<span class="text-xs font-bold opacity-70">Type</span>
<select name="ds_type" class="select" value={$lq__ds_obj.type}>
<option value="text">Text</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
<option value="md">Markdown</option>
<option value="sql">SQL</option>
</select>
</label>
<div class="flex items-center gap-2 pt-6">
<input type="checkbox" name="ds_use_account_id" class="checkbox" checked={!!$lq__ds_obj.account_id} />
<span class="text-xs">Account Specific</span>
</div>
</div>
</div>
<div class="space-y-2">
<span class="text-xs font-bold opacity-70">Content</span>
<textarea
name="ds_value"
class="textarea font-mono text-sm"
rows="15"
placeholder="Enter content here..."
>{$lq__ds_obj.type === 'json' ? (typeof $lq__ds_obj.json === 'string' ? $lq__ds_obj.json : JSON.stringify($lq__ds_obj.json, null, 2)) : ($lq__ds_obj.text || $lq__ds_obj.html || '')}</textarea>
</div>
<div class="text-xs text-surface-500">
Created on: {$lq__ds_obj.created_on} | Last Updated: {$lq__ds_obj.updated_on}
</div>
<div class="flex justify-between items-center pt-4">
<button type="button" class="btn variant-filled-error" onclick={handle_delete}>
<span class="fas fa-trash mr-2"></span> Delete
</button>
<div class="flex gap-2">
<button type="button" class="btn variant-soft" onclick={() => show_edit = false}>Cancel</button>
<button type="submit" class="btn variant-filled-primary">
<span class="fas fa-save mr-2"></span> Save
</button>
</div>
</div>
</form>
</Modal>
{#if show_view}
{#if $lq__ds_obj.type === 'html' && $lq__ds_obj.html}
{@html $lq__ds_obj.html}
{:else if $lq__ds_obj.type === 'text' && $lq__ds_obj.text}
<div class="whitespace-pre-wrap">{$lq__ds_obj.text}</div>
{:else if $lq__ds_obj.type === 'sql' && $lq__ds_obj.text}
{#if debug}<div class="font-mono text-xs opacity-50">SQL: {$lq__ds_obj.text}</div>{/if}
{/if}
{/if}
{#if $ae_loc.edit_mode && ($ae_loc.manager_access || (show_edit_btn && $ae_loc.administrator_access))}
<button
type="button"
class="absolute top-0 right-0 btn btn-sm variant-soft-warning opacity-20 hover:opacity-100 z-10"
ondblclick={() => { show_edit = true; show_view = false; }}
title="Edit Data Store: {ds_code}"
>
<span class="fas fa-edit"></span>
</button>
{/if}
{:else if ds_loading_status === 'not found'}
<!-- Only show diagnostic to administrator+ (no edit_mode needed) or trusted staff in edit mode.
Anonymous/user/public visitors must never see internal data store codes or gaps. -->
{#if $ae_loc.administrator_access || ($ae_loc.trusted_access && $ae_loc.edit_mode)}
<div class="p-2 border border-dashed border-surface-500/30 rounded text-xs opacity-50">
Data Store not found: {ds_code}
</div>
{/if}
{/if}
{#if ds_loading_status === 'loading'}
<div class="absolute bottom-0 left-0 p-1 opacity-50">
<span class="fas fa-spinner fa-spin text-xs"></span>
</div>
{/if}
</div>