@yottagraph-app/aether-instructions 1.1.36 → 1.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/rules/cookbook-data.mdc +145 -0
- package/rules/data.mdc +20 -4
package/package.json
CHANGED
package/rules/cookbook-data.mdc
CHANGED
|
@@ -403,3 +403,148 @@ relationship PID. See the `data` rule for the two-layer architecture.
|
|
|
403
403
|
}
|
|
404
404
|
</script>
|
|
405
405
|
```
|
|
406
|
+
|
|
407
|
+
## 5. Async Entity Search with Live Suggestions
|
|
408
|
+
|
|
409
|
+
Type-ahead search that shows results in a dropdown as the user types. Uses
|
|
410
|
+
`searchEntities()` from the gateway helpers with a debounced watcher.
|
|
411
|
+
|
|
412
|
+
> **Do not use `v-autocomplete` for async search.** Vuetify's `v-autocomplete`
|
|
413
|
+
> with `hide-no-data` + `no-filter` + async item loading has a timing bug:
|
|
414
|
+
> the menu hides while items are empty (during the fetch) and does not reopen
|
|
415
|
+
> when results arrive. Use `v-text-field` with a manual dropdown instead.
|
|
416
|
+
|
|
417
|
+
```vue
|
|
418
|
+
<template>
|
|
419
|
+
<div class="entity-search" style="position: relative">
|
|
420
|
+
<v-text-field
|
|
421
|
+
v-model="searchQuery"
|
|
422
|
+
:label="label"
|
|
423
|
+
:prepend-inner-icon="icon"
|
|
424
|
+
variant="solo-filled"
|
|
425
|
+
rounded="lg"
|
|
426
|
+
clearable
|
|
427
|
+
density="comfortable"
|
|
428
|
+
:loading="searching"
|
|
429
|
+
@focus="showMenu = suggestions.length > 0"
|
|
430
|
+
@click:clear="onClear"
|
|
431
|
+
/>
|
|
432
|
+
|
|
433
|
+
<v-card
|
|
434
|
+
v-if="showMenu && suggestions.length > 0"
|
|
435
|
+
class="search-dropdown"
|
|
436
|
+
elevation="8"
|
|
437
|
+
>
|
|
438
|
+
<v-list density="compact">
|
|
439
|
+
<v-list-item
|
|
440
|
+
v-for="item in suggestions"
|
|
441
|
+
:key="item.neid"
|
|
442
|
+
:title="item.name"
|
|
443
|
+
:subtitle="item.neid"
|
|
444
|
+
@click="onSelect(item)"
|
|
445
|
+
/>
|
|
446
|
+
</v-list>
|
|
447
|
+
</v-card>
|
|
448
|
+
</div>
|
|
449
|
+
</template>
|
|
450
|
+
|
|
451
|
+
<script setup lang="ts">
|
|
452
|
+
import { searchEntities } from '~/utils/elementalHelpers';
|
|
453
|
+
|
|
454
|
+
const props = withDefaults(
|
|
455
|
+
defineProps<{
|
|
456
|
+
label?: string;
|
|
457
|
+
icon?: string;
|
|
458
|
+
flavors?: string[];
|
|
459
|
+
}>(),
|
|
460
|
+
{ label: 'Search', icon: 'mdi-magnify', flavors: undefined },
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const emit = defineEmits<{
|
|
464
|
+
selected: [entity: { neid: string; name: string }];
|
|
465
|
+
}>();
|
|
466
|
+
|
|
467
|
+
const searchQuery = ref('');
|
|
468
|
+
const suggestions = ref<{ neid: string; name: string }[]>([]);
|
|
469
|
+
const searching = ref(false);
|
|
470
|
+
const showMenu = ref(false);
|
|
471
|
+
let selectedName = '';
|
|
472
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
473
|
+
|
|
474
|
+
watch(searchQuery, (val) => {
|
|
475
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
476
|
+
if (val === selectedName) return;
|
|
477
|
+
if (!val || val.length < 2) {
|
|
478
|
+
suggestions.value = [];
|
|
479
|
+
showMenu.value = false;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
debounceTimer = setTimeout(() => doSearch(val), 300);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
async function doSearch(query: string) {
|
|
486
|
+
searching.value = true;
|
|
487
|
+
try {
|
|
488
|
+
suggestions.value = await searchEntities(query, {
|
|
489
|
+
maxResults: 8,
|
|
490
|
+
flavors: props.flavors,
|
|
491
|
+
});
|
|
492
|
+
showMenu.value = suggestions.value.length > 0;
|
|
493
|
+
} catch {
|
|
494
|
+
suggestions.value = [];
|
|
495
|
+
showMenu.value = false;
|
|
496
|
+
} finally {
|
|
497
|
+
searching.value = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function onSelect(item: { neid: string; name: string }) {
|
|
502
|
+
selectedName = item.name;
|
|
503
|
+
searchQuery.value = item.name;
|
|
504
|
+
suggestions.value = [];
|
|
505
|
+
showMenu.value = false;
|
|
506
|
+
emit('selected', item);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onClear() {
|
|
510
|
+
selectedName = '';
|
|
511
|
+
suggestions.value = [];
|
|
512
|
+
showMenu.value = false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
onMounted(() => {
|
|
516
|
+
document.addEventListener('click', (e) => {
|
|
517
|
+
const el = (e.target as HTMLElement)?.closest('.entity-search');
|
|
518
|
+
if (!el) showMenu.value = false;
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
</script>
|
|
522
|
+
|
|
523
|
+
<style scoped>
|
|
524
|
+
.entity-search {
|
|
525
|
+
max-width: 600px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.search-dropdown {
|
|
529
|
+
position: absolute;
|
|
530
|
+
top: 100%;
|
|
531
|
+
left: 0;
|
|
532
|
+
right: 0;
|
|
533
|
+
z-index: 100;
|
|
534
|
+
max-height: 300px;
|
|
535
|
+
overflow-y: auto;
|
|
536
|
+
margin-top: -8px;
|
|
537
|
+
}
|
|
538
|
+
</style>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Usage:
|
|
542
|
+
|
|
543
|
+
```vue
|
|
544
|
+
<EntitySearch
|
|
545
|
+
label="Search for a company"
|
|
546
|
+
icon="mdi-domain"
|
|
547
|
+
:flavors="['organization']"
|
|
548
|
+
@selected="onCompanySelected"
|
|
549
|
+
/>
|
|
550
|
+
```
|
package/rules/data.mdc
CHANGED
|
@@ -142,8 +142,8 @@ Elemental API patterns. **Use these instead of writing from scratch:**
|
|
|
142
142
|
```typescript
|
|
143
143
|
const { flavors, properties, flavorByName, pidByName, refresh } = useElementalSchema();
|
|
144
144
|
await refresh(); // fetches once, then cached
|
|
145
|
-
const articleFid = flavorByName('article'); // →
|
|
146
|
-
const namePid = pidByName('name'); // →
|
|
145
|
+
const articleFid = flavorByName('article'); // → string | null
|
|
146
|
+
const namePid = pidByName('name'); // → string | null
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Handles the dual response shapes (`res.schema.flavors` vs `res.flavors`)
|
|
@@ -297,15 +297,31 @@ Same value, different key. Always use a fallback:
|
|
|
297
297
|
|
|
298
298
|
```typescript
|
|
299
299
|
const articleFlavor = flavors.find(f => f.name === 'article');
|
|
300
|
-
|
|
300
|
+
// Always use String() — safe for small IDs (12) and required for large ones
|
|
301
|
+
const articleFid = String(articleFlavor?.fid ?? articleFlavor?.findex ?? '');
|
|
301
302
|
|
|
302
303
|
// When building a FID lookup map:
|
|
303
|
-
const fidMap = new Map(flavors.map(f => [f.fid ?? f.findex, f.name]));
|
|
304
|
+
const fidMap = new Map(flavors.map(f => [String(f.fid ?? f.findex), f.name]));
|
|
304
305
|
```
|
|
305
306
|
|
|
306
307
|
The `is_type` expression in `/elemental/find` always uses the `fid` key
|
|
307
308
|
regardless of which schema endpoint provided the value.
|
|
308
309
|
|
|
310
|
+
### Some FIDs and PIDs are 64-bit -- `JSON.parse` will silently corrupt them
|
|
311
|
+
|
|
312
|
+
FIDs and PIDs are stored as 64-bit signed integers. Many are small
|
|
313
|
+
(e.g. `12`), but others exceed JavaScript's `Number.MAX_SAFE_INTEGER`
|
|
314
|
+
(2^53 - 1) -- for example, `3466547124233281063`. `JSON.parse` silently
|
|
315
|
+
rounds these large values (that example becomes `3466547124233281000`). An `is_type` query with
|
|
316
|
+
the rounded FID returns empty results and no error, making it look like
|
|
317
|
+
the data doesn't exist.
|
|
318
|
+
|
|
319
|
+
**Always treat FIDs and PIDs as strings in TypeScript/JavaScript.**
|
|
320
|
+
Before `JSON.parse`, rewrite large numeric `fid`/`pid` fields to
|
|
321
|
+
quoted strings. Store them as `string`, not `number`. Build expressions
|
|
322
|
+
and `pids` arrays via string interpolation, not `JSON.stringify` of a
|
|
323
|
+
JS number. This is safe for small IDs too.
|
|
324
|
+
|
|
309
325
|
### Relationship property values need zero-padding to form valid NEIDs
|
|
310
326
|
|
|
311
327
|
Relationship properties (`data_nindex`) return linked entity IDs as raw
|