@x12i/memorix-client-api 0.0.0-stage → 0.16.0

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.
Files changed (108) hide show
  1. package/HANDOVER.md +97 -0
  2. package/dist/app.d.ts +64 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +412 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/authix.d.ts +9 -0
  7. package/dist/authix.d.ts.map +1 -0
  8. package/dist/authix.js +26 -0
  9. package/dist/authix.js.map +1 -0
  10. package/dist/catalog.d.ts +160 -0
  11. package/dist/catalog.d.ts.map +1 -0
  12. package/dist/catalog.js +371 -0
  13. package/dist/catalog.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +102 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +33 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +109 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/derived.d.ts +2 -0
  23. package/dist/derived.d.ts.map +1 -0
  24. package/dist/derived.js +35 -0
  25. package/dist/derived.js.map +1 -0
  26. package/dist/errors.d.ts +11 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +21 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fetch.d.ts +17 -0
  31. package/dist/fetch.d.ts.map +1 -0
  32. package/dist/fetch.js +741 -0
  33. package/dist/fetch.js.map +1 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +4 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/live-ops-nav.d.ts +2 -0
  39. package/dist/live-ops-nav.d.ts.map +1 -0
  40. package/dist/live-ops-nav.js +42 -0
  41. package/dist/live-ops-nav.js.map +1 -0
  42. package/dist/live.d.ts +20 -0
  43. package/dist/live.d.ts.map +1 -0
  44. package/dist/live.js +8 -0
  45. package/dist/live.js.map +1 -0
  46. package/dist/markdown.d.ts +19 -0
  47. package/dist/markdown.d.ts.map +1 -0
  48. package/dist/markdown.js +102 -0
  49. package/dist/markdown.js.map +1 -0
  50. package/dist/peers.d.ts +51 -0
  51. package/dist/peers.d.ts.map +1 -0
  52. package/dist/peers.js +127 -0
  53. package/dist/peers.js.map +1 -0
  54. package/dist/processed.d.ts +22 -0
  55. package/dist/processed.d.ts.map +1 -0
  56. package/dist/processed.js +397 -0
  57. package/dist/processed.js.map +1 -0
  58. package/dist/project.d.ts +25 -0
  59. package/dist/project.d.ts.map +1 -0
  60. package/dist/project.js +76 -0
  61. package/dist/project.js.map +1 -0
  62. package/dist/reads-live-routes.d.ts +4 -0
  63. package/dist/reads-live-routes.d.ts.map +1 -0
  64. package/dist/reads-live-routes.js +56 -0
  65. package/dist/reads-live-routes.js.map +1 -0
  66. package/dist/reads-live-ui.d.ts +3 -0
  67. package/dist/reads-live-ui.d.ts.map +1 -0
  68. package/dist/reads-live-ui.js +204 -0
  69. package/dist/reads-live-ui.js.map +1 -0
  70. package/dist/reads-ring.d.ts +29 -0
  71. package/dist/reads-ring.d.ts.map +1 -0
  72. package/dist/reads-ring.js +44 -0
  73. package/dist/reads-ring.js.map +1 -0
  74. package/dist/refs.d.ts +24 -0
  75. package/dist/refs.d.ts.map +1 -0
  76. package/dist/refs.js +354 -0
  77. package/dist/refs.js.map +1 -0
  78. package/dist/request-countex.d.ts +53 -0
  79. package/dist/request-countex.d.ts.map +1 -0
  80. package/dist/request-countex.js +171 -0
  81. package/dist/request-countex.js.map +1 -0
  82. package/dist/scope.d.ts +13 -0
  83. package/dist/scope.d.ts.map +1 -0
  84. package/dist/scope.js +46 -0
  85. package/dist/scope.js.map +1 -0
  86. package/dist/scoper.d.ts +75 -0
  87. package/dist/scoper.d.ts.map +1 -0
  88. package/dist/scoper.js +475 -0
  89. package/dist/scoper.js.map +1 -0
  90. package/dist/shaped-refs.d.ts +23 -0
  91. package/dist/shaped-refs.d.ts.map +1 -0
  92. package/dist/shaped-refs.js +395 -0
  93. package/dist/shaped-refs.js.map +1 -0
  94. package/dist/skills.d.ts +102 -0
  95. package/dist/skills.d.ts.map +1 -0
  96. package/dist/skills.js +609 -0
  97. package/dist/skills.js.map +1 -0
  98. package/dist/story.d.ts +43 -0
  99. package/dist/story.d.ts.map +1 -0
  100. package/dist/story.js +121 -0
  101. package/dist/story.js.map +1 -0
  102. package/dist/upstream.d.ts +13 -0
  103. package/dist/upstream.d.ts.map +1 -0
  104. package/dist/upstream.js +58 -0
  105. package/dist/upstream.js.map +1 -0
  106. package/openapi/openapi.json +480 -0
  107. package/package.json +60 -6
  108. package/README.md +0 -3
@@ -0,0 +1,204 @@
1
+ /** HTML for GET /_live/reads — which org and agents each caller read. */
2
+ export function readsLiveHtml() {
3
+ return `<!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <title>Memorix client API · Reads</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
12
+ <style>
13
+ :root {
14
+ --bg: #0a0c10; --bg-elev: #10141b; --bg-row: #141922; --line: #242b38;
15
+ --text: #e8ebe6; --muted: #8a9288; --dim: #5c645c;
16
+ --bad: #e25a5a; --accent: #c8f082;
17
+ --sans: "IBM Plex Sans", system-ui, sans-serif;
18
+ --mono: "IBM Plex Mono", ui-monospace, monospace;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ html, body { margin: 0; }
22
+ body {
23
+ font-family: var(--sans);
24
+ background:
25
+ radial-gradient(1200px 600px at 10% -10%, #1a2214 0%, transparent 55%),
26
+ radial-gradient(900px 500px at 100% 0%, #121820 0%, transparent 50%),
27
+ var(--bg);
28
+ color: var(--text); font-size: 14px; line-height: 1.45;
29
+ }
30
+ a { color: var(--accent); }
31
+ .shell { max-width: 1280px; margin: 0 auto; padding: 36px 28px 64px; }
32
+ header {
33
+ display: flex; align-items: flex-end; justify-content: space-between; gap: 24px;
34
+ margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid var(--line);
35
+ }
36
+ .brand { font-size: 26px; font-weight: 700; letter-spacing: -0.03em; }
37
+ .brand span { color: var(--accent); }
38
+ .sub { margin-top: 8px; color: var(--muted); font-size: 13px; max-width: 72ch; }
39
+ .meta { text-align: right; font-family: var(--mono); font-size: 11px; color: var(--dim); display: grid; gap: 4px; }
40
+ .toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 16px; }
41
+ .btn, input {
42
+ appearance: none; border: 1px solid var(--line); background: var(--bg-row); color: var(--text);
43
+ font: 500 12px/1 var(--sans); padding: 10px 14px; border-radius: 2px;
44
+ }
45
+ input { min-width: 280px; font-family: var(--mono); }
46
+ .btn { cursor: pointer; }
47
+ .btn:hover, input:focus { border-color: var(--accent); outline: none; }
48
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 18px; }
49
+ .card { background: var(--bg-elev); border: 1px solid var(--line); padding: 12px 14px; }
50
+ .card h3 {
51
+ margin: 0 0 6px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
52
+ text-transform: uppercase; color: var(--dim); font-weight: 500;
53
+ }
54
+ .card .val { font-family: var(--mono); font-size: 22px; font-weight: 600; }
55
+ .card .val.time { font-size: 13px; font-weight: 500; }
56
+ h2 {
57
+ margin: 22px 0 10px; font-size: 12px; font-family: var(--mono);
58
+ letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); font-weight: 500;
59
+ }
60
+ table { width: 100%; border-collapse: collapse; background: var(--bg-elev); border: 1px solid var(--line); margin-bottom: 18px; }
61
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
62
+ th {
63
+ font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
64
+ color: var(--dim); font-weight: 500; background: #0d1016;
65
+ }
66
+ tr:last-child td { border-bottom: 0; }
67
+ tr.group-head td {
68
+ background: #0d1016; color: var(--accent); font-family: var(--mono); font-size: 12px; font-weight: 600;
69
+ }
70
+ .mono { font-family: var(--mono); font-size: 12px; }
71
+ .muted { color: var(--muted); }
72
+ .bad { color: var(--bad); }
73
+ .chips { display: flex; flex-wrap: wrap; gap: 4px; }
74
+ .chip {
75
+ font-family: var(--mono); font-size: 11px; padding: 2px 6px;
76
+ border: 1px solid var(--line); color: var(--text); background: #0d1016;
77
+ }
78
+ .err { color: var(--bad); margin: 8px 0; }
79
+ footer { margin-top: 28px; color: var(--dim); font-size: 12px; }
80
+ .empty { color: var(--muted); padding: 16px; border: 1px solid var(--line); background: var(--bg-elev); }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="shell">
85
+ <header>
86
+ <div>
87
+ <div class="brand">Memorix client API <span>reads</span></div>
88
+ <p class="sub">Which org and which agents did each caller read?</p>
89
+ </div>
90
+ <div class="meta">
91
+ <div><strong>UI</strong> /_live/reads</div>
92
+ <div><strong>API</strong> /_live/api/reads</div>
93
+ <div><a href="/_live">← live view</a></div>
94
+ </div>
95
+ </header>
96
+ <div class="toolbar">
97
+ <input id="q" type="search" placeholder="caller, org, agent, path" aria-label="Search" />
98
+ <button class="btn" id="refresh" type="button">Refresh</button>
99
+ <span class="mono muted" id="asof"></span>
100
+ </div>
101
+ <div class="cards" id="cards"></div>
102
+ <div id="error" class="err" hidden></div>
103
+ <div id="table"></div>
104
+ <footer>Refreshes every 20 seconds. The table lists the newest 500 reads held in this process. Record bodies stay on the API.</footer>
105
+ </div>
106
+ <script>
107
+ const q = document.getElementById("q");
108
+ const cards = document.getElementById("cards");
109
+ const tableEl = document.getElementById("table");
110
+ const errorEl = document.getElementById("error");
111
+ const asof = document.getElementById("asof");
112
+ let snapshot = null;
113
+
114
+ function esc(s) {
115
+ return String(s ?? "").replace(/[&<>"']/g, (c) => ({
116
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
117
+ }[c]));
118
+ }
119
+
120
+ function when(iso) {
121
+ if (!iso) return "—";
122
+ const d = new Date(iso);
123
+ return Number.isNaN(d.getTime()) ? String(iso) : d.toLocaleString();
124
+ }
125
+
126
+ function card(label, value, time) {
127
+ const cls = time ? "val time" : "val";
128
+ return '<div class="card"><h3>' + esc(label) + '</h3><div class="' + cls + '">' + esc(value) + "</div></div>";
129
+ }
130
+
131
+ function render() {
132
+ const summary = (snapshot && snapshot.summary) || {};
133
+ const needle = q.value.trim().toLowerCase();
134
+ const reads = ((snapshot && snapshot.reads) || []).filter((row) => {
135
+ if (!needle) return true;
136
+ const hay = [row.caller, row.orgId, (row.agentIds || []).join(" "), row.path, row.method, row.statusCode]
137
+ .join(" ").toLowerCase();
138
+ return hay.includes(needle);
139
+ });
140
+ const cardsHtml =
141
+ card("Reads", summary.reads ?? 0) +
142
+ card("Callers", summary.callers ?? 0) +
143
+ card("Orgs", summary.orgs ?? 0) +
144
+ card("Agents", summary.agents ?? 0) +
145
+ card("Errors", summary.errors ?? 0) +
146
+ card("Last read", summary.lastAt ? when(summary.lastAt) : "—", true);
147
+ let tableHtml = "";
148
+ if (!snapshot || !(snapshot.reads || []).length) {
149
+ tableHtml = '<div class="empty">No reads yet. This process has not completed a /api/v1 call.</div>';
150
+ } else if (!reads.length) {
151
+ tableHtml = '<div class="empty">No reads match this search.</div>';
152
+ } else {
153
+ const groups = new Map();
154
+ for (const row of reads) {
155
+ const key = row.caller || "unknown";
156
+ const list = groups.get(key) || [];
157
+ list.push(row);
158
+ groups.set(key, list);
159
+ }
160
+ tableHtml = '<table><thead><tr><th>When</th><th>Org</th><th>Agents</th><th>Path</th><th>Status</th><th>ms</th></tr></thead><tbody>';
161
+ for (const [caller, rows] of groups) {
162
+ tableHtml += '<tr class="group-head"><td colspan="6">' + esc(caller) + " · " + rows.length + "</td></tr>";
163
+ for (const row of rows) {
164
+ const agents = (row.agentIds || []).length
165
+ ? '<div class="chips">' + row.agentIds.map((id) => '<span class="chip">' + esc(id) + "</span>").join("") + "</div>"
166
+ : '<span class="muted">—</span>';
167
+ const statusCls = row.statusCode >= 400 ? "mono bad" : "mono";
168
+ tableHtml += "<tr><td class='mono'>" + esc(when(row.at)) + "</td><td class='mono'>" + esc(row.orgId || "—") +
169
+ "</td><td>" + agents + "</td><td class='mono'>" + esc(row.method) + " " + esc(row.path) +
170
+ "</td><td class='" + statusCls + "'>" + esc(row.statusCode) + "</td><td class='mono'>" + esc(row.durationMs) + "</td></tr>";
171
+ }
172
+ }
173
+ tableHtml += "</tbody></table>";
174
+ }
175
+ const markup = cardsHtml + "\\n" + tableHtml;
176
+ if (tableEl.dataset.markup === markup) return;
177
+ tableEl.dataset.markup = markup;
178
+ cards.innerHTML = cardsHtml;
179
+ tableEl.innerHTML = tableHtml;
180
+ }
181
+
182
+ async function load() {
183
+ try {
184
+ const res = await fetch("/_live/api/reads");
185
+ if (!res.ok) throw new Error("reads " + res.status);
186
+ snapshot = await res.json();
187
+ errorEl.hidden = true;
188
+ asof.textContent = snapshot.generatedAt ? new Date(snapshot.generatedAt).toLocaleString() : "";
189
+ render();
190
+ } catch (err) {
191
+ errorEl.hidden = false;
192
+ errorEl.textContent = err && err.message ? err.message : String(err);
193
+ }
194
+ }
195
+
196
+ q.addEventListener("input", render);
197
+ document.getElementById("refresh").addEventListener("click", load);
198
+ load();
199
+ setInterval(load, 20000);
200
+ </script>
201
+ </body>
202
+ </html>`;
203
+ }
204
+ //# sourceMappingURL=reads-live-ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads-live-ui.js","sourceRoot":"","sources":["../src/reads-live-ui.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuMD,CAAC;AACT,CAAC"}
@@ -0,0 +1,29 @@
1
+ /** Recent /api/v1 reads for /_live/reads. Ids and status only. No bodies, tokens, or grants. */
2
+ export type ReadEvent = {
3
+ at: string;
4
+ caller: string;
5
+ orgId: string;
6
+ agentIds: string[];
7
+ method: string;
8
+ path: string;
9
+ statusCode: number;
10
+ durationMs: number;
11
+ };
12
+ export type ReadsSnapshot = {
13
+ summary: {
14
+ reads: number;
15
+ callers: number;
16
+ orgs: number;
17
+ agents: number;
18
+ errors: number;
19
+ lastAt: string | null;
20
+ shown: number;
21
+ };
22
+ reads: ReadEvent[];
23
+ };
24
+ export type ReadRing = {
25
+ record: (event: ReadEvent) => void;
26
+ snapshot: () => ReadsSnapshot;
27
+ };
28
+ export declare function createReadRing(capacity?: number): ReadRing;
29
+ //# sourceMappingURL=reads-ring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads-ring.d.ts","sourceRoot":"","sources":["../src/reads-ring.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAKhG,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,aAAa,CAAC;CAC/B,CAAC;AAEF,wBAAgB,cAAc,CAAC,QAAQ,SAAmB,GAAG,QAAQ,CAkCpE"}
@@ -0,0 +1,44 @@
1
+ /** Recent /api/v1 reads for /_live/reads. Ids and status only. No bodies, tokens, or grants. */
2
+ const DEFAULT_CAPACITY = 10_000;
3
+ const SHOWN = 500;
4
+ export function createReadRing(capacity = DEFAULT_CAPACITY) {
5
+ const events = [];
6
+ return {
7
+ record(event) {
8
+ events.push(event);
9
+ while (events.length > capacity)
10
+ events.shift();
11
+ },
12
+ snapshot() {
13
+ const callers = new Set();
14
+ const orgs = new Set();
15
+ const agents = new Set();
16
+ let errors = 0;
17
+ for (const event of events) {
18
+ if (event.caller)
19
+ callers.add(event.caller);
20
+ if (event.orgId)
21
+ orgs.add(event.orgId);
22
+ for (const agentId of event.agentIds)
23
+ agents.add(agentId);
24
+ if (event.statusCode >= 400)
25
+ errors += 1;
26
+ }
27
+ const reads = events.slice(-SHOWN).reverse();
28
+ const last = events[events.length - 1];
29
+ return {
30
+ summary: {
31
+ reads: events.length,
32
+ callers: callers.size,
33
+ orgs: orgs.size,
34
+ agents: agents.size,
35
+ errors,
36
+ lastAt: last?.at ?? null,
37
+ shown: reads.length,
38
+ },
39
+ reads,
40
+ };
41
+ },
42
+ };
43
+ }
44
+ //# sourceMappingURL=reads-ring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reads-ring.js","sourceRoot":"","sources":["../src/reads-ring.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAEhG,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,KAAK,GAAG,GAAG,CAAC;AA+BlB,MAAM,UAAU,cAAc,CAAC,QAAQ,GAAG,gBAAgB;IACxD,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,OAAO;QACL,MAAM,CAAC,KAAK;YACV,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,MAAM,CAAC,MAAM,GAAG,QAAQ;gBAAE,MAAM,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;QACD,QAAQ;YACN,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;YAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;YACjC,IAAI,MAAM,GAAG,CAAC,CAAC;YACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,IAAI,KAAK,CAAC,MAAM;oBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;gBAC5C,IAAI,KAAK,CAAC,KAAK;oBAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACvC,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ;oBAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1D,IAAI,KAAK,CAAC,UAAU,IAAI,GAAG;oBAAE,MAAM,IAAI,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACvC,OAAO;gBACL,OAAO,EAAE;oBACP,KAAK,EAAE,MAAM,CAAC,MAAM;oBACpB,OAAO,EAAE,OAAO,CAAC,IAAI;oBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,MAAM;oBACN,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI;oBACxB,KAAK,EAAE,KAAK,CAAC,MAAM;iBACpB;gBACD,KAAK;aACN,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/refs.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { type FieldView, type ObjectTypeView } from "./catalog.js";
2
+ import type { ClientScope } from "./scope.js";
3
+ import { type Upstream } from "./upstream.js";
4
+ /** A referred record inlined in place of a record id. */
5
+ export type ResolvedRecord = {
6
+ recordId: string;
7
+ objectType: string;
8
+ contentType: string;
9
+ data: unknown;
10
+ };
11
+ export declare function referenceKey(objectType: string, recordId: string): string;
12
+ export declare function isResolvedRecord(value: unknown): value is ResolvedRecord;
13
+ export declare function mergeReferenceTargets(into: Map<string, Set<string>>, extra: ReadonlyMap<string, ReadonlySet<string>>): void;
14
+ /** Record ids named by `refersTo` fields on this response. */
15
+ export declare function collectReferenceTargets(contents: Record<string, unknown> | null, fieldsByType: Map<string, FieldView[]>, shape: "record" | "envelope" | "data"): Map<string, Set<string>>;
16
+ /** Replace record ids with the records already loaded for those ids. */
17
+ export declare function expandReferenceData(data: unknown, fields: FieldView[], resolved?: ReadonlyMap<string, ResolvedRecord>): unknown;
18
+ /**
19
+ * Load the records named by `refersTo` fields.
20
+ * Match `recordId` first, then identifier fields declared on the leading content type.
21
+ * A failed lookup leaves that id unresolved.
22
+ */
23
+ export declare function loadResolvedReferences(upstream: Upstream, scope: ClientScope, catalog: ObjectTypeView[], targets: ReadonlyMap<string, ReadonlySet<string>>): Promise<Map<string, ResolvedRecord>>;
24
+ //# sourceMappingURL=refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../src/refs.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAExD,yDAAyD;AACzD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAaF,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAUxE;AA6FD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EAC9B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAC9C,IAAI,CASN;AAED,8DAA8D;AAC9D,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACxC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EACtC,KAAK,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GACpC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAiB1B;AA+ED,wEAAwE;AACxE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,SAAS,EAAE,EACnB,QAAQ,GAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAe,GAC1D,OAAO,CAaT;AAsFD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,cAAc,EAAE,EACzB,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAChD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAqBtC"}
package/dist/refs.js ADDED
@@ -0,0 +1,354 @@
1
+ import { findContentType, findObjectType, } from "./catalog.js";
2
+ import { UpstreamError } from "./errors.js";
3
+ import { asRecord } from "./upstream.js";
4
+ const REF_ID_LIMIT = 200;
5
+ const REF_CHUNK = 25;
6
+ const REF_PAGES = 5;
7
+ const CONTENT_TYPE_LIMIT = 5;
8
+ const IDENTITY_FIELD_LIMIT = 8;
9
+ const NO_RESOLVED = new Map();
10
+ function isPlain(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ export function referenceKey(objectType, recordId) {
14
+ return `${objectType}\u0000${recordId}`;
15
+ }
16
+ export function isResolvedRecord(value) {
17
+ if (!isPlain(value))
18
+ return false;
19
+ const keys = Object.keys(value);
20
+ if (keys.length !== 4)
21
+ return false;
22
+ return (typeof value.recordId === "string" &&
23
+ typeof value.objectType === "string" &&
24
+ typeof value.contentType === "string" &&
25
+ "data" in value);
26
+ }
27
+ function dataSegments(path) {
28
+ if (!path.startsWith("data."))
29
+ return null;
30
+ const segments = path.slice("data.".length).split(".").filter(Boolean);
31
+ return segments.length > 0 ? segments : null;
32
+ }
33
+ function referenceFields(fields) {
34
+ return fields.filter((field) => typeof field.refersTo?.objectType === "string" &&
35
+ field.refersTo.objectType.length > 0 &&
36
+ !(field.shapes && field.shapes.length > 0));
37
+ }
38
+ /** `displayName` / `title` / `name`, or a camelCase id such as `cveId`. */
39
+ function identityLeaf(leaf) {
40
+ if (leaf === "displayName" || leaf === "title" || leaf === "name")
41
+ return true;
42
+ return leaf === "id" || leaf.endsWith("Id") || leaf.endsWith("_id") || leaf.endsWith("-id");
43
+ }
44
+ function identityFields(fields) {
45
+ return fields
46
+ .filter((field) => {
47
+ if (field.refersTo || field.enum?.length)
48
+ return false;
49
+ if (field.valueType !== "string")
50
+ return false;
51
+ const segments = dataSegments(field.path);
52
+ if (!segments || segments.length !== 1)
53
+ return false;
54
+ return identityLeaf(segments[0]);
55
+ })
56
+ .slice(0, IDENTITY_FIELD_LIMIT);
57
+ }
58
+ function eachId(value, visit) {
59
+ if (typeof value === "string") {
60
+ if (value.trim())
61
+ visit(value);
62
+ return;
63
+ }
64
+ if (!Array.isArray(value))
65
+ return;
66
+ for (const item of value) {
67
+ if (typeof item === "string" && item.trim())
68
+ visit(item);
69
+ }
70
+ }
71
+ function addIds(value, objectType, into) {
72
+ eachId(value, (id) => {
73
+ let set = into.get(objectType);
74
+ if (!set) {
75
+ set = new Set();
76
+ into.set(objectType, set);
77
+ }
78
+ set.add(id);
79
+ });
80
+ }
81
+ function collectDeepKey(value, key, objectType, into) {
82
+ if (Array.isArray(value)) {
83
+ for (const item of value)
84
+ collectDeepKey(item, key, objectType, into);
85
+ return;
86
+ }
87
+ if (!isPlain(value))
88
+ return;
89
+ for (const [childKey, child] of Object.entries(value)) {
90
+ if (childKey === key)
91
+ addIds(child, objectType, into);
92
+ else
93
+ collectDeepKey(child, key, objectType, into);
94
+ }
95
+ }
96
+ function collectAt(value, segments, objectType, into) {
97
+ if (segments.length === 0) {
98
+ addIds(value, objectType, into);
99
+ return;
100
+ }
101
+ if (Array.isArray(value)) {
102
+ for (const item of value)
103
+ collectAt(item, segments, objectType, into);
104
+ return;
105
+ }
106
+ if (!isPlain(value))
107
+ return;
108
+ collectAt(value[segments[0]], segments.slice(1), objectType, into);
109
+ }
110
+ function readInstanceData(value, shape) {
111
+ if (shape === "data")
112
+ return value;
113
+ if (!isPlain(value))
114
+ return undefined;
115
+ return value.data;
116
+ }
117
+ export function mergeReferenceTargets(into, extra) {
118
+ for (const [objectType, ids] of extra) {
119
+ let set = into.get(objectType);
120
+ if (!set) {
121
+ set = new Set();
122
+ into.set(objectType, set);
123
+ }
124
+ for (const id of ids)
125
+ set.add(id);
126
+ }
127
+ }
128
+ /** Record ids named by `refersTo` fields on this response. */
129
+ export function collectReferenceTargets(contents, fieldsByType, shape) {
130
+ const into = new Map();
131
+ for (const [contentType, list] of Object.entries(contents ?? {})) {
132
+ const fields = referenceFields(fieldsByType.get(contentType) ?? []);
133
+ if (fields.length === 0)
134
+ continue;
135
+ const values = Array.isArray(list) ? list : [];
136
+ for (const value of values) {
137
+ const data = readInstanceData(value, shape);
138
+ for (const field of fields) {
139
+ const segments = dataSegments(field.path);
140
+ if (!segments)
141
+ continue;
142
+ if (segments.length === 1)
143
+ collectDeepKey(data, segments[0], field.refersTo.objectType, into);
144
+ else
145
+ collectAt(data, segments, field.refersTo.objectType, into);
146
+ }
147
+ }
148
+ }
149
+ return into;
150
+ }
151
+ function cloneResolved(hit) {
152
+ try {
153
+ return structuredClone(hit);
154
+ }
155
+ catch {
156
+ return { ...hit };
157
+ }
158
+ }
159
+ function substitute(value, objectType, resolved) {
160
+ if (typeof value === "string") {
161
+ const hit = resolved.get(referenceKey(objectType, value));
162
+ return hit ? cloneResolved(hit) : value;
163
+ }
164
+ if (!Array.isArray(value))
165
+ return value;
166
+ let changed = false;
167
+ const next = value.map((item) => {
168
+ if (typeof item !== "string")
169
+ return item;
170
+ const hit = resolved.get(referenceKey(objectType, item));
171
+ if (!hit)
172
+ return item;
173
+ changed = true;
174
+ return cloneResolved(hit);
175
+ });
176
+ return changed ? next : value;
177
+ }
178
+ function expandDeepKey(value, key, objectType, resolved) {
179
+ if (Array.isArray(value)) {
180
+ let changed = false;
181
+ const next = value.map((item) => {
182
+ const updated = expandDeepKey(item, key, objectType, resolved);
183
+ if (updated !== item)
184
+ changed = true;
185
+ return updated;
186
+ });
187
+ return changed ? next : value;
188
+ }
189
+ if (!isPlain(value))
190
+ return value;
191
+ let changed = false;
192
+ const next = {};
193
+ for (const [childKey, child] of Object.entries(value)) {
194
+ const updated = childKey === key
195
+ ? substitute(child, objectType, resolved)
196
+ : expandDeepKey(child, key, objectType, resolved);
197
+ if (updated !== child)
198
+ changed = true;
199
+ next[childKey] = updated;
200
+ }
201
+ return changed ? next : value;
202
+ }
203
+ function expandAt(value, segments, objectType, resolved) {
204
+ if (segments.length === 0)
205
+ return substitute(value, objectType, resolved);
206
+ if (Array.isArray(value)) {
207
+ let changed = false;
208
+ const next = value.map((item) => {
209
+ const updated = expandAt(item, segments, objectType, resolved);
210
+ if (updated !== item)
211
+ changed = true;
212
+ return updated;
213
+ });
214
+ return changed ? next : value;
215
+ }
216
+ if (!isPlain(value) || !Object.prototype.hasOwnProperty.call(value, segments[0]))
217
+ return value;
218
+ const key = segments[0];
219
+ const updated = expandAt(value[key], segments.slice(1), objectType, resolved);
220
+ if (updated === value[key])
221
+ return value;
222
+ return { ...value, [key]: updated };
223
+ }
224
+ /** Replace record ids with the records already loaded for those ids. */
225
+ export function expandReferenceData(data, fields, resolved = NO_RESOLVED) {
226
+ const refs = referenceFields(fields);
227
+ if (refs.length === 0 || resolved.size === 0)
228
+ return data;
229
+ let current = data;
230
+ for (const field of refs) {
231
+ const segments = dataSegments(field.path);
232
+ if (!segments)
233
+ continue;
234
+ current =
235
+ segments.length === 1
236
+ ? expandDeepKey(current, segments[0], field.refersTo.objectType, resolved)
237
+ : expandAt(current, segments, field.refersTo.objectType, resolved);
238
+ }
239
+ return current;
240
+ }
241
+ function contentTypesToSearch(view) {
242
+ const ids = view.contentTypes.map((ct) => ct.id).filter((id) => id !== "relationships");
243
+ const leading = view.leadingContentType;
244
+ const ordered = leading && leading !== "relationships" ? [leading, ...ids.filter((id) => id !== leading)] : ids;
245
+ return ordered.slice(0, CONTENT_TYPE_LIMIT);
246
+ }
247
+ function chunks(items, size) {
248
+ const out = [];
249
+ for (let index = 0; index < items.length; index += size)
250
+ out.push(items.slice(index, index + size));
251
+ return out;
252
+ }
253
+ function contentPath(objectType, contentType, field, ids, cursor) {
254
+ const params = new URLSearchParams();
255
+ params.set("objectType", objectType);
256
+ params.set("contentType", contentType);
257
+ params.set("limit", String(Math.min(Math.max(ids.length, 1), 200)));
258
+ params.set("filter", JSON.stringify([{ field, op: "in", value: ids }]));
259
+ if (cursor)
260
+ params.set("cursor", cursor);
261
+ return `/api/data/content?${params.toString()}`;
262
+ }
263
+ function readDataPath(data, path) {
264
+ const segments = dataSegments(path);
265
+ if (!segments)
266
+ return null;
267
+ let current = data;
268
+ for (const segment of segments) {
269
+ if (!isPlain(current))
270
+ return null;
271
+ current = current[segment];
272
+ }
273
+ return typeof current === "string" && current.trim() ? current : null;
274
+ }
275
+ async function matchByField(upstream, scope, objectType, contentType, field, pending, resolved) {
276
+ const ids = [...pending];
277
+ for (const group of chunks(ids, REF_CHUNK)) {
278
+ const wanted = group.filter((id) => pending.has(id));
279
+ if (wanted.length === 0)
280
+ continue;
281
+ let cursor;
282
+ const seen = new Set();
283
+ for (let page = 0; page < REF_PAGES; page += 1) {
284
+ let body;
285
+ try {
286
+ body = await upstream.get(contentPath(objectType, contentType, field, wanted, cursor), scope);
287
+ }
288
+ catch (err) {
289
+ if (err instanceof UpstreamError)
290
+ return;
291
+ throw err;
292
+ }
293
+ const root = asRecord(body);
294
+ const records = Array.isArray(root?.records) ? root.records : [];
295
+ for (const item of records) {
296
+ const row = asRecord(item);
297
+ if (!row)
298
+ continue;
299
+ const recordId = typeof row.recordId === "string" ? row.recordId : "";
300
+ const matched = field === "recordId" ? recordId : readDataPath(row.data, field);
301
+ if (!matched || !pending.has(matched))
302
+ continue;
303
+ pending.delete(matched);
304
+ const key = referenceKey(objectType, matched);
305
+ if (resolved.has(key))
306
+ continue;
307
+ resolved.set(key, {
308
+ recordId: recordId || matched,
309
+ objectType,
310
+ contentType,
311
+ data: row.data ?? null,
312
+ });
313
+ }
314
+ const next = asRecord(root?.pageInfo)?.nextCursor;
315
+ if (typeof next !== "string" || !next || seen.has(next))
316
+ break;
317
+ seen.add(next);
318
+ cursor = next;
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Load the records named by `refersTo` fields.
324
+ * Match `recordId` first, then identifier fields declared on the leading content type.
325
+ * A failed lookup leaves that id unresolved.
326
+ */
327
+ export async function loadResolvedReferences(upstream, scope, catalog, targets) {
328
+ const resolved = new Map();
329
+ for (const [objectType, rawIds] of targets) {
330
+ const view = findObjectType(catalog, objectType);
331
+ if (!view)
332
+ continue;
333
+ const pending = new Set([...rawIds].filter((id) => id.trim()).slice(0, REF_ID_LIMIT));
334
+ if (pending.size === 0)
335
+ continue;
336
+ const contentTypes = contentTypesToSearch(view);
337
+ for (const contentType of contentTypes) {
338
+ if (pending.size === 0)
339
+ break;
340
+ await matchByField(upstream, scope, objectType, contentType, "recordId", pending, resolved);
341
+ }
342
+ const leading = contentTypes[0];
343
+ if (!leading || pending.size === 0)
344
+ continue;
345
+ const fields = findContentType(view, leading)?.fields ?? [];
346
+ for (const field of identityFields(fields)) {
347
+ if (pending.size === 0)
348
+ break;
349
+ await matchByField(upstream, scope, objectType, leading, field.path, pending, resolved);
350
+ }
351
+ }
352
+ return resolved;
353
+ }
354
+ //# sourceMappingURL=refs.js.map