@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.
@@ -0,0 +1,405 @@
1
+ ---
2
+ description: "Data-fetching UI recipes: entity search, news feed, filings, gateway helpers. Read with the `data` rule when building pages that use the Query Server or platform data."
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Data-fetching cookbook
7
+
8
+ Copy-paste patterns that call the Elemental API, gateway, or helpers. For platform API details see the `data` rule. Pure UI patterns (tables, forms, charts) are in the `cookbook` rule.
9
+
10
+ ## 1. Entity Search Page
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 `data` 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
+ ```
96
+
97
+ ## 2. News Feed — Recent Articles with Sentiment
98
+
99
+ Fetch recent articles from the knowledge graph. Uses `useElementalSchema()`
100
+ for runtime flavor/PID discovery and `buildGatewayUrl()` for gateway access.
101
+
102
+ ```vue
103
+ <template>
104
+ <div class="d-flex flex-column fill-height pa-4">
105
+ <h1 class="text-h5 mb-4">Recent News</h1>
106
+ <v-alert v-if="error" type="error" variant="tonal" class="mb-4" closable>
107
+ {{ error }}
108
+ </v-alert>
109
+ <v-progress-linear v-if="loading" indeterminate class="mb-4" />
110
+ <v-list v-if="articles.length" lines="three">
111
+ <v-list-item v-for="a in articles" :key="a.neid">
112
+ <template #title>
113
+ <span>{{ a.name || a.neid }}</span>
114
+ <v-chip
115
+ v-if="a.sentiment"
116
+ size="x-small"
117
+ class="ml-2"
118
+ :color="a.sentiment > 0 ? 'success' : a.sentiment < 0 ? 'error' : 'grey'"
119
+ >
120
+ {{ a.sentiment > 0 ? 'Bullish' : a.sentiment < 0 ? 'Bearish' : 'Neutral' }}
121
+ </v-chip>
122
+ </template>
123
+ <template #subtitle>{{ a.neid }}</template>
124
+ </v-list-item>
125
+ </v-list>
126
+ <v-empty-state
127
+ v-else-if="!loading"
128
+ headline="No articles found"
129
+ icon="mdi-newspaper-variant-outline"
130
+ />
131
+ </div>
132
+ </template>
133
+
134
+ <script setup lang="ts">
135
+ import { useElementalClient } from '@yottagraph-app/elemental-api/client';
136
+ import { padNeid } from '~/utils/elementalHelpers';
137
+
138
+ const client = useElementalClient();
139
+ const { flavorByName, pidByName, refresh: loadSchema } = useElementalSchema();
140
+
141
+ const articles = ref<{ neid: string; name: string; sentiment: number | null }[]>([]);
142
+ const loading = ref(false);
143
+ const error = ref<string | null>(null);
144
+
145
+ onMounted(async () => {
146
+ loading.value = true;
147
+ try {
148
+ await loadSchema();
149
+ const articleFid = flavorByName('article');
150
+ if (!articleFid) {
151
+ error.value = 'Article entity type not found in schema';
152
+ return;
153
+ }
154
+
155
+ const res = await client.findEntities({
156
+ expression: JSON.stringify({ type: 'is_type', is_type: { fid: articleFid } }),
157
+ limit: 20,
158
+ });
159
+ const neids: string[] = (res as any).eids ?? [];
160
+
161
+ if (!neids.length) {
162
+ return;
163
+ }
164
+
165
+ const namePid = pidByName('name');
166
+ const sentimentPid = pidByName('sentiment');
167
+ const pids = [namePid, sentimentPid].filter((p): p is number => p !== null);
168
+
169
+ const props = await client.getPropertyValues({
170
+ eids: JSON.stringify(neids),
171
+ pids: JSON.stringify(pids),
172
+ });
173
+
174
+ const valueMap = new Map<string, Record<number, any>>();
175
+ for (const v of (props as any).values ?? []) {
176
+ const eid = padNeid(v.eid ?? v.entity_id ?? '');
177
+ if (!valueMap.has(eid)) valueMap.set(eid, {});
178
+ valueMap.get(eid)![v.pid] = v.value;
179
+ }
180
+
181
+ articles.value = neids.map((neid) => {
182
+ const vals = valueMap.get(neid) ?? {};
183
+ return {
184
+ neid,
185
+ name: namePid ? (vals[namePid] as string) ?? neid : neid,
186
+ sentiment: sentimentPid ? (vals[sentimentPid] as number) ?? null : null,
187
+ };
188
+ });
189
+ } catch (e: any) {
190
+ error.value = e.message || 'Failed to load articles';
191
+ } finally {
192
+ loading.value = false;
193
+ }
194
+ });
195
+ </script>
196
+ ```
197
+
198
+ ## 3. Entity Search with Gateway Helpers
199
+
200
+ Simpler version of recipe #1 using the pre-built `searchEntities()` helper.
201
+
202
+ ```vue
203
+ <template>
204
+ <div class="d-flex flex-column fill-height pa-4">
205
+ <h1 class="text-h5 mb-4">Entity Search</h1>
206
+ <v-text-field
207
+ v-model="query"
208
+ label="Search entities"
209
+ prepend-inner-icon="mdi-magnify"
210
+ variant="outlined"
211
+ @keyup.enter="search"
212
+ :loading="loading"
213
+ />
214
+ <v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
215
+ {{ error }}
216
+ </v-alert>
217
+ <v-list v-if="results.length" class="mt-4">
218
+ <v-list-item
219
+ v-for="r in results"
220
+ :key="r.neid"
221
+ :title="r.name"
222
+ :subtitle="r.neid"
223
+ />
224
+ </v-list>
225
+ <v-empty-state
226
+ v-else-if="searched && !loading"
227
+ headline="No results"
228
+ icon="mdi-magnify-remove-outline"
229
+ />
230
+ </div>
231
+ </template>
232
+
233
+ <script setup lang="ts">
234
+ import { searchEntities } from '~/utils/elementalHelpers';
235
+
236
+ const query = ref('');
237
+ const results = ref<{ neid: string; name: string }[]>([]);
238
+ const loading = ref(false);
239
+ const error = ref<string | null>(null);
240
+ const searched = ref(false);
241
+
242
+ async function search() {
243
+ if (!query.value.trim()) return;
244
+ loading.value = true;
245
+ error.value = null;
246
+ searched.value = true;
247
+ try {
248
+ results.value = await searchEntities(query.value.trim());
249
+ } catch (e: any) {
250
+ error.value = e.message || 'Search failed';
251
+ results.value = [];
252
+ } finally {
253
+ loading.value = false;
254
+ }
255
+ }
256
+ </script>
257
+ ```
258
+
259
+ ## 4. Get Filings for a Company
260
+
261
+ Fetch Edgar filings (or any relationship-linked documents) for an organization.
262
+ Uses `$fetch` for the initial entity search because `POST /entities/search`
263
+ is not wrapped by the generated client (same as recipe #1). Filing
264
+ properties are then fetched via `useElementalClient()`.
265
+
266
+ **Important:** For graph-layer entities (person, organization, location),
267
+ use `findEntities` with a `linked` expression. For property-layer entities
268
+ (documents, filings, articles), use `getPropertyValues` with the
269
+ relationship PID. See the `data` rule for the two-layer architecture.
270
+
271
+ ```vue
272
+ <template>
273
+ <div class="d-flex flex-column fill-height pa-4">
274
+ <h1 class="text-h5 mb-4">Company Filings</h1>
275
+ <v-text-field
276
+ v-model="query"
277
+ label="Company name"
278
+ prepend-inner-icon="mdi-magnify"
279
+ @keyup.enter="search"
280
+ :loading="loading"
281
+ />
282
+ <v-alert v-if="error" type="error" variant="tonal" class="mt-2" closable>
283
+ {{ error }}
284
+ </v-alert>
285
+ <v-data-table
286
+ v-if="filings.length"
287
+ :headers="headers"
288
+ :items="filings"
289
+ :loading="loading"
290
+ density="comfortable"
291
+ hover
292
+ class="mt-4"
293
+ />
294
+ <v-empty-state
295
+ v-else-if="searched && !loading"
296
+ headline="No filings found"
297
+ icon="mdi-file-document-off"
298
+ />
299
+ </div>
300
+ </template>
301
+
302
+ <script setup lang="ts">
303
+ import { useElementalClient } from '@yottagraph-app/elemental-api/client';
304
+
305
+ const client = useElementalClient();
306
+ const query = ref('');
307
+ const filings = ref<{ neid: string; name: string }[]>([]);
308
+ const loading = ref(false);
309
+ const error = ref<string | null>(null);
310
+ const searched = ref(false);
311
+
312
+ const headers = [
313
+ { title: 'NEID', key: 'neid', sortable: true },
314
+ { title: 'Name', key: 'name', sortable: true },
315
+ ];
316
+
317
+ async function getPropertyPidMap(client: ReturnType<typeof useElementalClient>) {
318
+ const schemaRes = await client.getSchema();
319
+ const properties = schemaRes.schema?.properties ?? (schemaRes as any).properties ?? [];
320
+ return new Map(properties.map((p: any) => [p.name, p.pid]));
321
+ }
322
+
323
+ function getSearchUrl() {
324
+ const config = useRuntimeConfig();
325
+ const gw = (config.public as any).gatewayUrl as string;
326
+ const org = (config.public as any).tenantOrgId as string;
327
+ return `${gw}/api/qs/${org}/entities/search`;
328
+ }
329
+
330
+ function getApiKey() {
331
+ return (useRuntimeConfig().public as any).qsApiKey as string;
332
+ }
333
+
334
+ async function search() {
335
+ if (!query.value.trim()) return;
336
+ loading.value = true;
337
+ error.value = null;
338
+ searched.value = true;
339
+ try {
340
+ const res = await $fetch<any>(getSearchUrl(), {
341
+ method: 'POST',
342
+ headers: { 'Content-Type': 'application/json', 'X-Api-Key': getApiKey() },
343
+ body: {
344
+ queries: [{ queryId: 1, query: query.value.trim(), flavors: ['organization'] }],
345
+ maxResults: 1,
346
+ includeNames: true,
347
+ },
348
+ });
349
+ const matches = res?.results?.[0]?.matches ?? [];
350
+ if (!matches.length) {
351
+ filings.value = [];
352
+ return;
353
+ }
354
+ const orgNeid = matches[0].neid;
355
+
356
+ const pidMap = await getPropertyPidMap(client);
357
+ const filedPid = pidMap.get('filed');
358
+ if (!filedPid) {
359
+ error.value = '"filed" relationship not found in schema';
360
+ return;
361
+ }
362
+
363
+ const propRes = await client.getPropertyValues({
364
+ eids: JSON.stringify([orgNeid]),
365
+ pids: JSON.stringify([filedPid]),
366
+ });
367
+
368
+ const docNeids = (propRes.values ?? []).map((v: any) =>
369
+ String(v.value).padStart(20, '0'),
370
+ );
371
+
372
+ function getEntityNameUrl(neid: string) {
373
+ const config = useRuntimeConfig();
374
+ const gw = (config.public as any).gatewayUrl as string;
375
+ const org = (config.public as any).tenantOrgId as string;
376
+ return `${gw}/api/qs/${org}/entities/${neid}/name`;
377
+ }
378
+
379
+ const names = await Promise.all(
380
+ docNeids.map(async (neid: string) => {
381
+ try {
382
+ const res = await $fetch<{ name: string }>(
383
+ getEntityNameUrl(neid),
384
+ { headers: { 'X-Api-Key': getApiKey() } },
385
+ );
386
+ return res.name || neid;
387
+ } catch {
388
+ return neid;
389
+ }
390
+ }),
391
+ );
392
+
393
+ filings.value = docNeids.map((neid: string, i: number) => ({
394
+ neid,
395
+ name: names[i],
396
+ }));
397
+ } catch (e: any) {
398
+ error.value = e.message || 'Failed to load filings';
399
+ filings.value = [];
400
+ } finally {
401
+ loading.value = false;
402
+ }
403
+ }
404
+ </script>
405
+ ```