@yottagraph-app/aether-instructions 1.1.27 → 1.1.28
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/commands/build_my_app.md +3 -3
- package/commands/update_instructions.md +23 -0
- package/package.json +3 -2
- package/rules/aether.mdc +2 -2
- package/rules/agents-data.mdc +72 -0
- package/rules/agents.mdc +2 -54
- package/rules/architecture.mdc +9 -10
- package/rules/cookbook-data.mdc +405 -0
- package/rules/cookbook.mdc +2 -397
- package/rules/{api.mdc → data.mdc} +4 -4
- package/rules/instructions_warning.mdc +2 -2
- package/rules/server-data.mdc +54 -0
- package/rules/server.mdc +2 -48
- package/rules/something-broke.mdc +1 -1
- package/variants/mcp-only/commands/build_my_app.md +146 -0
- package/variants/mcp-only/rules/agents-data.mdc +45 -0
- package/variants/mcp-only/rules/cookbook-data.mdc +50 -0
- package/variants/mcp-only/rules/data.mdc +55 -0
- package/variants/mcp-only/rules/server-data.mdc +41 -0
package/rules/cookbook.mdc
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Copy-paste UI patterns for common pages:
|
|
2
|
+
description: "Copy-paste UI patterns for common pages: data table, form, chart, dialog, master-detail. For Query Server / data-fetching recipes see `cookbook-data`."
|
|
3
3
|
alwaysApply: false
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -7,92 +7,7 @@ alwaysApply: false
|
|
|
7
7
|
|
|
8
8
|
Copy-paste patterns using the project's actual composables and Vuetify components. Adapt to your needs.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Search for entities by name and display results. Uses `$fetch` directly
|
|
13
|
-
because `POST /entities/search` (batch name resolution with scored ranking)
|
|
14
|
-
is not wrapped by the generated `useElementalClient()` — see the `api` rule.
|
|
15
|
-
|
|
16
|
-
```vue
|
|
17
|
-
<template>
|
|
18
|
-
<div class="d-flex flex-column fill-height pa-4">
|
|
19
|
-
<h1 class="text-h5 mb-4">Entity Search</h1>
|
|
20
|
-
<v-text-field
|
|
21
|
-
v-model="query"
|
|
22
|
-
label="Search entities"
|
|
23
|
-
prepend-inner-icon="mdi-magnify"
|
|
24
|
-
variant="outlined"
|
|
25
|
-
@keyup.enter="search"
|
|
26
|
-
:loading="loading"
|
|
27
|
-
/>
|
|
28
|
-
<v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
|
|
29
|
-
{{ error }}
|
|
30
|
-
</v-alert>
|
|
31
|
-
<v-list v-if="results.length" class="mt-4">
|
|
32
|
-
<v-list-item
|
|
33
|
-
v-for="(neid, i) in results"
|
|
34
|
-
:key="neid"
|
|
35
|
-
:title="names[i] || neid"
|
|
36
|
-
:subtitle="neid"
|
|
37
|
-
/>
|
|
38
|
-
</v-list>
|
|
39
|
-
<v-empty-state
|
|
40
|
-
v-else-if="searched && !loading"
|
|
41
|
-
headline="No results"
|
|
42
|
-
icon="mdi-magnify-remove-outline"
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
</template>
|
|
46
|
-
|
|
47
|
-
<script setup lang="ts">
|
|
48
|
-
import { useElementalClient } from '@yottagraph-app/elemental-api/client';
|
|
49
|
-
|
|
50
|
-
const client = useElementalClient();
|
|
51
|
-
const query = ref('');
|
|
52
|
-
const results = ref<string[]>([]);
|
|
53
|
-
const names = ref<string[]>([]);
|
|
54
|
-
const loading = ref(false);
|
|
55
|
-
const error = ref<string | null>(null);
|
|
56
|
-
const searched = ref(false);
|
|
57
|
-
|
|
58
|
-
function getSearchUrl() {
|
|
59
|
-
const config = useRuntimeConfig();
|
|
60
|
-
const gw = (config.public as any).gatewayUrl as string;
|
|
61
|
-
const org = (config.public as any).tenantOrgId as string;
|
|
62
|
-
return `${gw}/api/qs/${org}/entities/search`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getApiKey() {
|
|
66
|
-
return (useRuntimeConfig().public as any).qsApiKey as string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function search() {
|
|
70
|
-
if (!query.value.trim()) return;
|
|
71
|
-
loading.value = true;
|
|
72
|
-
error.value = null;
|
|
73
|
-
searched.value = true;
|
|
74
|
-
try {
|
|
75
|
-
const res = await $fetch<any>(getSearchUrl(), {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
|
|
78
|
-
body: {
|
|
79
|
-
queries: [{ queryId: 1, query: query.value.trim() }],
|
|
80
|
-
maxResults: 10,
|
|
81
|
-
includeNames: true,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
const matches = res?.results?.[0]?.matches ?? [];
|
|
85
|
-
results.value = matches.map((m: any) => m.neid);
|
|
86
|
-
names.value = matches.map((m: any) => m.name || m.neid);
|
|
87
|
-
} catch (e: any) {
|
|
88
|
-
error.value = e.message || 'Search failed';
|
|
89
|
-
results.value = [];
|
|
90
|
-
} finally {
|
|
91
|
-
loading.value = false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
</script>
|
|
95
|
-
```
|
|
10
|
+
**Data-fetching recipes** (entity search, news feed, filings, gateway helpers) live in the `cookbook-data` rule.
|
|
96
11
|
|
|
97
12
|
## 2. Data Table Page
|
|
98
13
|
|
|
@@ -387,313 +302,3 @@ Two-column layout with selectable list and detail panel.
|
|
|
387
302
|
});
|
|
388
303
|
</script>
|
|
389
304
|
```
|
|
390
|
-
|
|
391
|
-
## 7. News Feed — Recent Articles with Sentiment
|
|
392
|
-
|
|
393
|
-
Fetch recent articles from the knowledge graph. Uses `useElementalSchema()`
|
|
394
|
-
for runtime flavor/PID discovery and `buildGatewayUrl()` for gateway access.
|
|
395
|
-
|
|
396
|
-
```vue
|
|
397
|
-
<template>
|
|
398
|
-
<div class="d-flex flex-column fill-height pa-4">
|
|
399
|
-
<h1 class="text-h5 mb-4">Recent News</h1>
|
|
400
|
-
<v-alert v-if="error" type="error" variant="tonal" class="mb-4" closable>
|
|
401
|
-
{{ error }}
|
|
402
|
-
</v-alert>
|
|
403
|
-
<v-progress-linear v-if="loading" indeterminate class="mb-4" />
|
|
404
|
-
<v-list v-if="articles.length" lines="three">
|
|
405
|
-
<v-list-item v-for="a in articles" :key="a.neid">
|
|
406
|
-
<template #title>
|
|
407
|
-
<span>{{ a.name || a.neid }}</span>
|
|
408
|
-
<v-chip
|
|
409
|
-
v-if="a.sentiment"
|
|
410
|
-
size="x-small"
|
|
411
|
-
class="ml-2"
|
|
412
|
-
:color="a.sentiment > 0 ? 'success' : a.sentiment < 0 ? 'error' : 'grey'"
|
|
413
|
-
>
|
|
414
|
-
{{ a.sentiment > 0 ? 'Bullish' : a.sentiment < 0 ? 'Bearish' : 'Neutral' }}
|
|
415
|
-
</v-chip>
|
|
416
|
-
</template>
|
|
417
|
-
<template #subtitle>{{ a.neid }}</template>
|
|
418
|
-
</v-list-item>
|
|
419
|
-
</v-list>
|
|
420
|
-
<v-empty-state
|
|
421
|
-
v-else-if="!loading"
|
|
422
|
-
headline="No articles found"
|
|
423
|
-
icon="mdi-newspaper-variant-outline"
|
|
424
|
-
/>
|
|
425
|
-
</div>
|
|
426
|
-
</template>
|
|
427
|
-
|
|
428
|
-
<script setup lang="ts">
|
|
429
|
-
import { useElementalClient } from '@yottagraph-app/elemental-api/client';
|
|
430
|
-
import { padNeid } from '~/utils/elementalHelpers';
|
|
431
|
-
|
|
432
|
-
const client = useElementalClient();
|
|
433
|
-
const { flavorByName, pidByName, refresh: loadSchema } = useElementalSchema();
|
|
434
|
-
|
|
435
|
-
const articles = ref<{ neid: string; name: string; sentiment: number | null }[]>([]);
|
|
436
|
-
const loading = ref(false);
|
|
437
|
-
const error = ref<string | null>(null);
|
|
438
|
-
|
|
439
|
-
onMounted(async () => {
|
|
440
|
-
loading.value = true;
|
|
441
|
-
try {
|
|
442
|
-
await loadSchema();
|
|
443
|
-
const articleFid = flavorByName('article');
|
|
444
|
-
if (!articleFid) {
|
|
445
|
-
error.value = 'Article entity type not found in schema';
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const res = await client.findEntities({
|
|
450
|
-
expression: JSON.stringify({ type: 'is_type', is_type: { fid: articleFid } }),
|
|
451
|
-
limit: 20,
|
|
452
|
-
});
|
|
453
|
-
const neids: string[] = (res as any).eids ?? [];
|
|
454
|
-
|
|
455
|
-
if (!neids.length) {
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const namePid = pidByName('name');
|
|
460
|
-
const sentimentPid = pidByName('sentiment');
|
|
461
|
-
const pids = [namePid, sentimentPid].filter((p): p is number => p !== null);
|
|
462
|
-
|
|
463
|
-
const props = await client.getPropertyValues({
|
|
464
|
-
eids: JSON.stringify(neids),
|
|
465
|
-
pids: JSON.stringify(pids),
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
const valueMap = new Map<string, Record<number, any>>();
|
|
469
|
-
for (const v of (props as any).values ?? []) {
|
|
470
|
-
const eid = padNeid(v.eid ?? v.entity_id ?? '');
|
|
471
|
-
if (!valueMap.has(eid)) valueMap.set(eid, {});
|
|
472
|
-
valueMap.get(eid)![v.pid] = v.value;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
articles.value = neids.map((neid) => {
|
|
476
|
-
const vals = valueMap.get(neid) ?? {};
|
|
477
|
-
return {
|
|
478
|
-
neid,
|
|
479
|
-
name: namePid ? (vals[namePid] as string) ?? neid : neid,
|
|
480
|
-
sentiment: sentimentPid ? (vals[sentimentPid] as number) ?? null : null,
|
|
481
|
-
};
|
|
482
|
-
});
|
|
483
|
-
} catch (e: any) {
|
|
484
|
-
error.value = e.message || 'Failed to load articles';
|
|
485
|
-
} finally {
|
|
486
|
-
loading.value = false;
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
</script>
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
## 8. Entity Search with Gateway Helpers
|
|
493
|
-
|
|
494
|
-
Simpler version of recipe #1 using the pre-built `searchEntities()` helper.
|
|
495
|
-
|
|
496
|
-
```vue
|
|
497
|
-
<template>
|
|
498
|
-
<div class="d-flex flex-column fill-height pa-4">
|
|
499
|
-
<h1 class="text-h5 mb-4">Entity Search</h1>
|
|
500
|
-
<v-text-field
|
|
501
|
-
v-model="query"
|
|
502
|
-
label="Search entities"
|
|
503
|
-
prepend-inner-icon="mdi-magnify"
|
|
504
|
-
variant="outlined"
|
|
505
|
-
@keyup.enter="search"
|
|
506
|
-
:loading="loading"
|
|
507
|
-
/>
|
|
508
|
-
<v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
|
|
509
|
-
{{ error }}
|
|
510
|
-
</v-alert>
|
|
511
|
-
<v-list v-if="results.length" class="mt-4">
|
|
512
|
-
<v-list-item
|
|
513
|
-
v-for="r in results"
|
|
514
|
-
:key="r.neid"
|
|
515
|
-
:title="r.name"
|
|
516
|
-
:subtitle="r.neid"
|
|
517
|
-
/>
|
|
518
|
-
</v-list>
|
|
519
|
-
<v-empty-state
|
|
520
|
-
v-else-if="searched && !loading"
|
|
521
|
-
headline="No results"
|
|
522
|
-
icon="mdi-magnify-remove-outline"
|
|
523
|
-
/>
|
|
524
|
-
</div>
|
|
525
|
-
</template>
|
|
526
|
-
|
|
527
|
-
<script setup lang="ts">
|
|
528
|
-
import { searchEntities } from '~/utils/elementalHelpers';
|
|
529
|
-
|
|
530
|
-
const query = ref('');
|
|
531
|
-
const results = ref<{ neid: string; name: string }[]>([]);
|
|
532
|
-
const loading = ref(false);
|
|
533
|
-
const error = ref<string | null>(null);
|
|
534
|
-
const searched = ref(false);
|
|
535
|
-
|
|
536
|
-
async function search() {
|
|
537
|
-
if (!query.value.trim()) return;
|
|
538
|
-
loading.value = true;
|
|
539
|
-
error.value = null;
|
|
540
|
-
searched.value = true;
|
|
541
|
-
try {
|
|
542
|
-
results.value = await searchEntities(query.value.trim());
|
|
543
|
-
} catch (e: any) {
|
|
544
|
-
error.value = e.message || 'Search failed';
|
|
545
|
-
results.value = [];
|
|
546
|
-
} finally {
|
|
547
|
-
loading.value = false;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
</script>
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
## 9. Get Filings for a Company
|
|
554
|
-
|
|
555
|
-
Fetch Edgar filings (or any relationship-linked documents) for an organization.
|
|
556
|
-
Uses `$fetch` for the initial entity search because `POST /entities/search`
|
|
557
|
-
is not wrapped by the generated client (same as recipe #1). Filing
|
|
558
|
-
properties are then fetched via `useElementalClient()`.
|
|
559
|
-
|
|
560
|
-
**Important:** For graph-layer entities (person, organization, location),
|
|
561
|
-
use `findEntities` with a `linked` expression. For property-layer entities
|
|
562
|
-
(documents, filings, articles), use `getPropertyValues` with the
|
|
563
|
-
relationship PID. See the `api` rule for the two-layer architecture.
|
|
564
|
-
|
|
565
|
-
```vue
|
|
566
|
-
<template>
|
|
567
|
-
<div class="d-flex flex-column fill-height pa-4">
|
|
568
|
-
<h1 class="text-h5 mb-4">Company Filings</h1>
|
|
569
|
-
<v-text-field
|
|
570
|
-
v-model="query"
|
|
571
|
-
label="Company name"
|
|
572
|
-
prepend-inner-icon="mdi-magnify"
|
|
573
|
-
@keyup.enter="search"
|
|
574
|
-
:loading="loading"
|
|
575
|
-
/>
|
|
576
|
-
<v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
|
|
577
|
-
{{ error }}
|
|
578
|
-
</v-alert>
|
|
579
|
-
<v-data-table
|
|
580
|
-
v-if="filings.length"
|
|
581
|
-
:headers="headers"
|
|
582
|
-
:items="filings"
|
|
583
|
-
:loading="loading"
|
|
584
|
-
density="comfortable"
|
|
585
|
-
hover
|
|
586
|
-
class="mt-4"
|
|
587
|
-
/>
|
|
588
|
-
<v-empty-state
|
|
589
|
-
v-else-if="searched && !loading"
|
|
590
|
-
headline="No filings found"
|
|
591
|
-
icon="mdi-file-document-off"
|
|
592
|
-
/>
|
|
593
|
-
</div>
|
|
594
|
-
</template>
|
|
595
|
-
|
|
596
|
-
<script setup lang="ts">
|
|
597
|
-
import { useElementalClient } from '@yottagraph-app/elemental-api/client';
|
|
598
|
-
|
|
599
|
-
const client = useElementalClient();
|
|
600
|
-
const query = ref('');
|
|
601
|
-
const filings = ref<{ neid: string; name: string }[]>([]);
|
|
602
|
-
const loading = ref(false);
|
|
603
|
-
const error = ref<string | null>(null);
|
|
604
|
-
const searched = ref(false);
|
|
605
|
-
|
|
606
|
-
const headers = [
|
|
607
|
-
{ title: 'NEID', key: 'neid', sortable: true },
|
|
608
|
-
{ title: 'Name', key: 'name', sortable: true },
|
|
609
|
-
];
|
|
610
|
-
|
|
611
|
-
async function getPropertyPidMap(client: ReturnType<typeof useElementalClient>) {
|
|
612
|
-
const schemaRes = await client.getSchema();
|
|
613
|
-
const properties = schemaRes.schema?.properties ?? (schemaRes as any).properties ?? [];
|
|
614
|
-
return new Map(properties.map((p: any) => [p.name, p.pid]));
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function getSearchUrl() {
|
|
618
|
-
const config = useRuntimeConfig();
|
|
619
|
-
const gw = (config.public as any).gatewayUrl as string;
|
|
620
|
-
const org = (config.public as any).tenantOrgId as string;
|
|
621
|
-
return `${gw}/api/qs/${org}/entities/search`;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function getApiKey() {
|
|
625
|
-
return (useRuntimeConfig().public as any).qsApiKey as string;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
async function search() {
|
|
629
|
-
if (!query.value.trim()) return;
|
|
630
|
-
loading.value = true;
|
|
631
|
-
error.value = null;
|
|
632
|
-
searched.value = true;
|
|
633
|
-
try {
|
|
634
|
-
const res = await $fetch<any>(getSearchUrl(), {
|
|
635
|
-
method: 'POST',
|
|
636
|
-
headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
|
|
637
|
-
body: {
|
|
638
|
-
queries: [{ queryId: 1, query: query.value.trim(), flavors: ['organization'] }],
|
|
639
|
-
maxResults: 1,
|
|
640
|
-
includeNames: true,
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
const matches = res?.results?.[0]?.matches ?? [];
|
|
644
|
-
if (!matches.length) {
|
|
645
|
-
filings.value = [];
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
const orgNeid = matches[0].neid;
|
|
649
|
-
|
|
650
|
-
const pidMap = await getPropertyPidMap(client);
|
|
651
|
-
const filedPid = pidMap.get('filed');
|
|
652
|
-
if (!filedPid) {
|
|
653
|
-
error.value = '"filed" relationship not found in schema';
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const propRes = await client.getPropertyValues({
|
|
658
|
-
eids: JSON.stringify([orgNeid]),
|
|
659
|
-
pids: JSON.stringify([filedPid]),
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
const docNeids = (propRes.values ?? []).map((v: any) =>
|
|
663
|
-
String(v.value).padStart(20, '0'),
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
function getEntityNameUrl(neid: string) {
|
|
667
|
-
const config = useRuntimeConfig();
|
|
668
|
-
const gw = (config.public as any).gatewayUrl as string;
|
|
669
|
-
const org = (config.public as any).tenantOrgId as string;
|
|
670
|
-
return `${gw}/api/qs/${org}/entities/${neid}/name`;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const names = await Promise.all(
|
|
674
|
-
docNeids.map(async (neid: string) => {
|
|
675
|
-
try {
|
|
676
|
-
const res = await $fetch<{ name: string }>(
|
|
677
|
-
getEntityNameUrl(neid),
|
|
678
|
-
{ headers: { 'X-Api-Key': getApiKey() } },
|
|
679
|
-
);
|
|
680
|
-
return res.name || neid;
|
|
681
|
-
} catch {
|
|
682
|
-
return neid;
|
|
683
|
-
}
|
|
684
|
-
}),
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
filings.value = docNeids.map((neid: string, i: number) => ({
|
|
688
|
-
neid,
|
|
689
|
-
name: names[i],
|
|
690
|
-
}));
|
|
691
|
-
} catch (e: any) {
|
|
692
|
-
error.value = e.message || 'Failed to load filings';
|
|
693
|
-
filings.value = [];
|
|
694
|
-
} finally {
|
|
695
|
-
loading.value = false;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
</script>
|
|
699
|
-
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Elemental API client usage, schema discovery, entity search, API gotchas, MCP servers. Read when building features that fetch or display data from the Query Server."
|
|
2
|
+
description: "Data access via Elemental API: client usage, schema discovery, entity search, API gotchas, MCP servers. Read when building features that fetch or display data from the Query Server."
|
|
3
3
|
alwaysApply: false
|
|
4
4
|
---
|
|
5
|
-
# Elemental API
|
|
5
|
+
# Data — Elemental API (platform data source)
|
|
6
6
|
|
|
7
7
|
**This app is built on the Lovelace platform.** The Query Server is the
|
|
8
8
|
primary data source — use it first for any data needs (entities, news,
|
|
@@ -379,7 +379,7 @@ const res = await client.getPropertyValues({
|
|
|
379
379
|
const docNeids = (res.values ?? []).map((v) => String(v.value).padStart(20, '0'));
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
-
See the **cookbook** rule for a full "Get filings for a company" recipe.
|
|
382
|
+
See the **cookbook-data** rule for a full "Get filings for a company" recipe.
|
|
383
383
|
|
|
384
384
|
### Expression language pitfalls
|
|
385
385
|
|
|
@@ -425,7 +425,7 @@ source-specific schemas.
|
|
|
425
425
|
`getPropertyValues()` with the relationship PID. Values are entity IDs
|
|
426
426
|
that must be zero-padded to 20 characters.
|
|
427
427
|
|
|
428
|
-
See the **cookbook** rule (recipe
|
|
428
|
+
See the **cookbook-data** rule (news feed recipe) for a full example.
|
|
429
429
|
|
|
430
430
|
## Error Handling
|
|
431
431
|
|
|
@@ -27,7 +27,7 @@ Example:
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
# Copy the rule you want to customize
|
|
30
|
-
cp .cursor/rules/
|
|
30
|
+
cp .cursor/rules/data.mdc .cursor/rules/data_custom.mdc
|
|
31
31
|
|
|
32
32
|
# Edit your copy — it won't be affected by instruction updates
|
|
33
33
|
```
|
|
@@ -35,7 +35,7 @@ cp .cursor/rules/api.mdc .cursor/rules/api_custom.mdc
|
|
|
35
35
|
## How It Works
|
|
36
36
|
|
|
37
37
|
- `.cursor/.aether-instructions-manifest` lists every file installed by the
|
|
38
|
-
package (one relative path per line, e.g. `rules/
|
|
38
|
+
package (one relative path per line, e.g. `rules/data.mdc`)
|
|
39
39
|
- `/update_instructions` deletes manifest entries, extracts fresh files from
|
|
40
40
|
the latest package, and writes a new manifest
|
|
41
41
|
- Files not in the manifest are never touched
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Calling the Elemental API from Nitro server routes via the Portal Gateway. Read when proxying Query Server calls server-side."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
globs: server/**
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Server routes: Elemental API (Query Server)
|
|
8
|
+
|
|
9
|
+
Server routes can call the Elemental API through the Portal Gateway proxy,
|
|
10
|
+
just like client-side code does. The gateway URL, tenant org ID, and API key
|
|
11
|
+
are available via `useRuntimeConfig()`.
|
|
12
|
+
|
|
13
|
+
**NEVER use `readFileSync('broadchurch.yaml')` in server routes.** The YAML
|
|
14
|
+
file is read at build time by `nuxt.config.ts` and its values flow into
|
|
15
|
+
`runtimeConfig`. Nitro serverless functions (Vercel) don't bundle arbitrary
|
|
16
|
+
project files — `readFileSync` will crash with ENOENT in production even
|
|
17
|
+
though it works locally.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
export default defineEventHandler(async (event) => {
|
|
21
|
+
const { public: config } = useRuntimeConfig();
|
|
22
|
+
|
|
23
|
+
const gatewayUrl = config.gatewayUrl; // Portal Gateway base URL
|
|
24
|
+
const orgId = config.tenantOrgId; // Tenant org ID (path segment)
|
|
25
|
+
const apiKey = config.qsApiKey; // API key for X-Api-Key header
|
|
26
|
+
|
|
27
|
+
if (!gatewayUrl || !orgId) {
|
|
28
|
+
throw createError({ statusCode: 503, statusMessage: 'Gateway not configured' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const res = await $fetch(`${gatewayUrl}/api/qs/${orgId}/entities/search`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
...(apiKey && { 'X-Api-Key': apiKey }),
|
|
36
|
+
},
|
|
37
|
+
body: { queries: [{ queryId: 1, query: 'Microsoft' }], maxResults: 5 },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return res;
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Available runtime config keys (all under `runtimeConfig.public`):
|
|
45
|
+
|
|
46
|
+
| Key | Source | Purpose |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `gatewayUrl` | `broadchurch.yaml` → `gateway.url` | Portal Gateway base URL |
|
|
49
|
+
| `tenantOrgId` | `broadchurch.yaml` → `tenant.org_id` | Tenant ID for API path |
|
|
50
|
+
| `qsApiKey` | `broadchurch.yaml` → `gateway.qs_api_key` | API key sent as `X-Api-Key` |
|
|
51
|
+
| `queryServerAddress` | `broadchurch.yaml` → `query_server.url` | Direct QS URL (prefer gateway) |
|
|
52
|
+
|
|
53
|
+
Build the request URL as `{gatewayUrl}/api/qs/{tenantOrgId}/{endpoint}`.
|
|
54
|
+
See the `data` rule for endpoint reference and response shapes.
|
package/rules/server.mdc
CHANGED
|
@@ -158,54 +158,8 @@ export function getDb(): NeonQueryFunction | null {
|
|
|
158
158
|
}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
Server routes can call the Elemental API through the Portal Gateway proxy,
|
|
164
|
-
just like client-side code does. The gateway URL, tenant org ID, and API key
|
|
165
|
-
are available via `useRuntimeConfig()`.
|
|
166
|
-
|
|
167
|
-
**NEVER use `readFileSync('broadchurch.yaml')` in server routes.** The YAML
|
|
168
|
-
file is read at build time by `nuxt.config.ts` and its values flow into
|
|
169
|
-
`runtimeConfig`. Nitro serverless functions (Vercel) don't bundle arbitrary
|
|
170
|
-
project files — `readFileSync` will crash with ENOENT in production even
|
|
171
|
-
though it works locally.
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
export default defineEventHandler(async (event) => {
|
|
175
|
-
const { public: config } = useRuntimeConfig();
|
|
176
|
-
|
|
177
|
-
const gatewayUrl = config.gatewayUrl; // Portal Gateway base URL
|
|
178
|
-
const orgId = config.tenantOrgId; // Tenant org ID (path segment)
|
|
179
|
-
const apiKey = config.qsApiKey; // API key for X-Api-Key header
|
|
180
|
-
|
|
181
|
-
if (!gatewayUrl || !orgId) {
|
|
182
|
-
throw createError({ statusCode: 503, statusMessage: 'Gateway not configured' });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const res = await $fetch(`${gatewayUrl}/api/qs/${orgId}/entities/search`, {
|
|
186
|
-
method: 'POST',
|
|
187
|
-
headers: {
|
|
188
|
-
'Content-Type': 'application/json',
|
|
189
|
-
...(apiKey && { 'X-Api-Key': apiKey }),
|
|
190
|
-
},
|
|
191
|
-
body: { queries: [{ queryId: 1, query: 'Microsoft' }], maxResults: 5 },
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
return res;
|
|
195
|
-
});
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Available runtime config keys (all under `runtimeConfig.public`):
|
|
199
|
-
|
|
200
|
-
| Key | Source | Purpose |
|
|
201
|
-
|---|---|---|
|
|
202
|
-
| `gatewayUrl` | `broadchurch.yaml` → `gateway.url` | Portal Gateway base URL |
|
|
203
|
-
| `tenantOrgId` | `broadchurch.yaml` → `tenant.org_id` | Tenant ID for API path |
|
|
204
|
-
| `qsApiKey` | `broadchurch.yaml` → `gateway.qs_api_key` | API key sent as `X-Api-Key` |
|
|
205
|
-
| `queryServerAddress` | `broadchurch.yaml` → `query_server.url` | Direct QS URL (prefer gateway) |
|
|
206
|
-
|
|
207
|
-
Build the request URL as `{gatewayUrl}/api/qs/{tenantOrgId}/{endpoint}`.
|
|
208
|
-
See the `api` rule for endpoint reference and response shapes.
|
|
161
|
+
For **Query Server / Elemental API** calls from server routes (gateway URL,
|
|
162
|
+
`X-Api-Key`, request shapes), see the `server-data` rule.
|
|
209
163
|
|
|
210
164
|
## Neon Postgres: Handle Missing Tables in GET Routes
|
|
211
165
|
|
|
@@ -49,7 +49,7 @@ import { myHelper } from '~/utils/myHelper';
|
|
|
49
49
|
|
|
50
50
|
### `Type 'X' is not assignable to type 'Y'`
|
|
51
51
|
|
|
52
|
-
Usually an API response shape mismatch. Common case: `getSchema()` nests data under `response.schema` but TypeScript types suggest top-level access. See the `
|
|
52
|
+
Usually an API response shape mismatch. Common case: `getSchema()` nests data under `response.schema` but TypeScript types suggest top-level access. See the `data` rule's API Gotchas section.
|
|
53
53
|
|
|
54
54
|
### `SyntaxError` or blank page with "missing export"
|
|
55
55
|
|