ont-run 0.0.4 → 0.0.6
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/README.md +8 -4
- package/dist/bin/ont.js +14000 -5172
- package/dist/index.js +15820 -7206
- package/dist/src/browser/server.d.ts +2 -0
- package/dist/src/browser/transform.d.ts +0 -1
- package/dist/src/config/define.d.ts +38 -3
- package/dist/src/config/schema.d.ts +20 -116
- package/dist/src/config/types.d.ts +27 -27
- package/dist/src/config/zod-utils.d.ts +44 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/server/api/index.d.ts +0 -2
- package/dist/src/server/api/router.d.ts +1 -1
- package/dist/src/server/mcp/index.d.ts +0 -2
- package/dist/src/server/mcp/tools.d.ts +2 -2
- package/dist/src/server/resolver.d.ts +4 -15
- package/package.json +2 -3
- package/src/browser/server.ts +203 -2
- package/src/browser/transform.ts +40 -19
- package/src/cli/commands/init.ts +10 -5
- package/src/cli/commands/review.ts +2 -1
- package/src/config/define.ts +52 -2
- package/src/config/schema.ts +49 -34
- package/src/config/types.ts +33 -31
- package/src/config/zod-utils.ts +144 -0
- package/src/index.ts +3 -2
- package/src/lockfile/hasher.ts +46 -22
- package/src/server/api/index.ts +2 -15
- package/src/server/api/router.ts +3 -6
- package/src/server/mcp/index.ts +2 -4
- package/src/server/mcp/tools.ts +48 -27
- package/src/server/resolver.ts +5 -78
- package/src/server/start.ts +0 -2
package/src/browser/server.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { basename } from "path";
|
|
2
4
|
import open from "open";
|
|
3
5
|
import type { OntologyConfig } from "../config/types.js";
|
|
4
6
|
import type { OntologyDiff } from "../lockfile/types.js";
|
|
@@ -12,6 +14,8 @@ export interface BrowserServerOptions {
|
|
|
12
14
|
diff?: OntologyDiff | null;
|
|
13
15
|
/** Directory to write the lockfile to on approval */
|
|
14
16
|
configDir?: string;
|
|
17
|
+
/** Path to the ontology.config.ts file */
|
|
18
|
+
configPath?: string;
|
|
15
19
|
port?: number;
|
|
16
20
|
openBrowser?: boolean;
|
|
17
21
|
}
|
|
@@ -22,7 +26,7 @@ export interface BrowserServerResult {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export async function startBrowserServer(options: BrowserServerOptions): Promise<BrowserServerResult> {
|
|
25
|
-
const { config, diff = null, configDir, port: preferredPort, openBrowser = true } = options;
|
|
29
|
+
const { config, diff = null, configDir, configPath, port: preferredPort, openBrowser = true } = options;
|
|
26
30
|
|
|
27
31
|
// Transform config to graph data and enhance with diff info
|
|
28
32
|
const baseGraphData = transformToGraphData(config);
|
|
@@ -90,6 +94,26 @@ export async function startBrowserServer(options: BrowserServerOptions): Promise
|
|
|
90
94
|
return c.json({ success: true });
|
|
91
95
|
});
|
|
92
96
|
|
|
97
|
+
// API: Get raw TypeScript source
|
|
98
|
+
app.get("/api/source", (c) => {
|
|
99
|
+
if (!configPath) {
|
|
100
|
+
return c.json({ error: "Config path not available" }, 400);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const source = readFileSync(configPath, "utf-8");
|
|
104
|
+
const filename = basename(configPath);
|
|
105
|
+
return c.json({ source, filename, path: configPath });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return c.json(
|
|
108
|
+
{
|
|
109
|
+
error: "Failed to read config file",
|
|
110
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
111
|
+
},
|
|
112
|
+
500
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
93
117
|
// Serve UI
|
|
94
118
|
app.get("/", (c) => c.html(generateBrowserUI(graphData)));
|
|
95
119
|
|
|
@@ -705,6 +729,89 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
705
729
|
color: var(--change-added);
|
|
706
730
|
}
|
|
707
731
|
|
|
732
|
+
/* Source View */
|
|
733
|
+
.source-view {
|
|
734
|
+
display: none;
|
|
735
|
+
grid-column: 2 / 4;
|
|
736
|
+
padding: 24px;
|
|
737
|
+
overflow-y: auto;
|
|
738
|
+
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(231, 225, 207, 0.3));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.source-view.active {
|
|
742
|
+
display: flex;
|
|
743
|
+
flex-direction: column;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.source-header {
|
|
747
|
+
display: flex;
|
|
748
|
+
align-items: center;
|
|
749
|
+
justify-content: space-between;
|
|
750
|
+
padding: 12px 20px;
|
|
751
|
+
background: rgba(2, 61, 96, 0.95);
|
|
752
|
+
border-radius: 12px 12px 0 0;
|
|
753
|
+
color: white;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.source-filename {
|
|
757
|
+
font-family: 'Space Mono', monospace;
|
|
758
|
+
font-size: 13px;
|
|
759
|
+
font-weight: 500;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.copy-btn {
|
|
763
|
+
display: flex;
|
|
764
|
+
align-items: center;
|
|
765
|
+
gap: 6px;
|
|
766
|
+
padding: 6px 12px;
|
|
767
|
+
background: rgba(255, 255, 255, 0.1);
|
|
768
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
769
|
+
border-radius: 6px;
|
|
770
|
+
color: white;
|
|
771
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
772
|
+
font-size: 12px;
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
transition: all 0.2s ease;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.copy-btn:hover {
|
|
778
|
+
background: rgba(255, 255, 255, 0.2);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.copy-btn.copied {
|
|
782
|
+
background: rgba(21, 168, 168, 0.3);
|
|
783
|
+
border-color: var(--vanna-teal);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.source-code {
|
|
787
|
+
flex: 1;
|
|
788
|
+
margin: 0;
|
|
789
|
+
padding: 20px;
|
|
790
|
+
background: #1e1e1e;
|
|
791
|
+
border-radius: 0 0 12px 12px;
|
|
792
|
+
overflow: auto;
|
|
793
|
+
font-family: 'Space Mono', monospace;
|
|
794
|
+
font-size: 13px;
|
|
795
|
+
line-height: 1.6;
|
|
796
|
+
color: #d4d4d4;
|
|
797
|
+
tab-size: 2;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.source-code code {
|
|
801
|
+
display: block;
|
|
802
|
+
white-space: pre;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* Syntax highlighting classes */
|
|
806
|
+
.source-code .keyword { color: #569cd6; }
|
|
807
|
+
.source-code .string { color: #ce9178; }
|
|
808
|
+
.source-code .number { color: #b5cea8; }
|
|
809
|
+
.source-code .comment { color: #6a9955; }
|
|
810
|
+
.source-code .function { color: #dcdcaa; }
|
|
811
|
+
.source-code .type { color: #4ec9b0; }
|
|
812
|
+
.source-code .property { color: #9cdcfe; }
|
|
813
|
+
.source-code .punctuation { color: #d4d4d4; }
|
|
814
|
+
|
|
708
815
|
/* No Changes State */
|
|
709
816
|
.no-changes {
|
|
710
817
|
text-align: center;
|
|
@@ -1433,6 +1540,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
1433
1540
|
<div class="view-tabs">
|
|
1434
1541
|
<button class="view-tab active" data-view="graph">Graph</button>
|
|
1435
1542
|
<button class="view-tab" data-view="table">Table</button>
|
|
1543
|
+
<button class="view-tab" data-view="source">Source</button>
|
|
1436
1544
|
</div>
|
|
1437
1545
|
|
|
1438
1546
|
<div class="filter-buttons" id="graphFilters">
|
|
@@ -1553,6 +1661,20 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
1553
1661
|
<div class="table-view" id="tableView">
|
|
1554
1662
|
<div id="tableContent"></div>
|
|
1555
1663
|
</div>
|
|
1664
|
+
|
|
1665
|
+
<!-- Source View -->
|
|
1666
|
+
<div class="source-view" id="sourceView">
|
|
1667
|
+
<div class="source-header">
|
|
1668
|
+
<span class="source-filename" id="sourceFilename">ontology.config.ts</span>
|
|
1669
|
+
<button class="copy-btn" id="copySourceBtn" title="Copy to clipboard">
|
|
1670
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1671
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
1672
|
+
</svg>
|
|
1673
|
+
Copy
|
|
1674
|
+
</button>
|
|
1675
|
+
</div>
|
|
1676
|
+
<pre class="source-code" id="sourceCode"><code>Loading...</code></pre>
|
|
1677
|
+
</div>
|
|
1556
1678
|
</div>
|
|
1557
1679
|
|
|
1558
1680
|
<!-- Review Footer -->
|
|
@@ -2429,6 +2551,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
2429
2551
|
const graphContainer = document.querySelector('.graph-container');
|
|
2430
2552
|
const detailPanel = document.getElementById('detailPanel');
|
|
2431
2553
|
const tableView = document.getElementById('tableView');
|
|
2554
|
+
const sourceView = document.getElementById('sourceView');
|
|
2432
2555
|
const graphFilters = document.getElementById('graphFilters');
|
|
2433
2556
|
const layoutSelector = document.querySelector('.layout-selector');
|
|
2434
2557
|
|
|
@@ -2436,15 +2559,25 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
2436
2559
|
graphContainer.style.display = 'block';
|
|
2437
2560
|
detailPanel.style.display = 'block';
|
|
2438
2561
|
tableView.classList.remove('active');
|
|
2562
|
+
sourceView.classList.remove('active');
|
|
2439
2563
|
if (graphFilters) graphFilters.style.display = 'flex';
|
|
2440
2564
|
if (layoutSelector) layoutSelector.style.display = 'flex';
|
|
2441
|
-
} else {
|
|
2565
|
+
} else if (view === 'table') {
|
|
2442
2566
|
graphContainer.style.display = 'none';
|
|
2443
2567
|
detailPanel.style.display = 'none';
|
|
2444
2568
|
tableView.classList.add('active');
|
|
2569
|
+
sourceView.classList.remove('active');
|
|
2445
2570
|
if (graphFilters) graphFilters.style.display = 'none';
|
|
2446
2571
|
if (layoutSelector) layoutSelector.style.display = 'none';
|
|
2447
2572
|
renderTableView();
|
|
2573
|
+
} else if (view === 'source') {
|
|
2574
|
+
graphContainer.style.display = 'none';
|
|
2575
|
+
detailPanel.style.display = 'none';
|
|
2576
|
+
tableView.classList.remove('active');
|
|
2577
|
+
sourceView.classList.add('active');
|
|
2578
|
+
if (graphFilters) graphFilters.style.display = 'none';
|
|
2579
|
+
if (layoutSelector) layoutSelector.style.display = 'none';
|
|
2580
|
+
loadSourceView();
|
|
2448
2581
|
}
|
|
2449
2582
|
}
|
|
2450
2583
|
|
|
@@ -2486,6 +2619,74 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
|
|
|
2486
2619
|
});
|
|
2487
2620
|
}
|
|
2488
2621
|
|
|
2622
|
+
// Source view
|
|
2623
|
+
let sourceLoaded = false;
|
|
2624
|
+
let sourceContent = '';
|
|
2625
|
+
|
|
2626
|
+
async function loadSourceView() {
|
|
2627
|
+
if (sourceLoaded) return;
|
|
2628
|
+
|
|
2629
|
+
const codeEl = document.getElementById('sourceCode').querySelector('code');
|
|
2630
|
+
const filenameEl = document.getElementById('sourceFilename');
|
|
2631
|
+
|
|
2632
|
+
try {
|
|
2633
|
+
const res = await fetch('/api/source');
|
|
2634
|
+
if (!res.ok) throw new Error('Failed to load source');
|
|
2635
|
+
const data = await res.json();
|
|
2636
|
+
|
|
2637
|
+
sourceContent = data.source;
|
|
2638
|
+
filenameEl.textContent = data.filename;
|
|
2639
|
+
codeEl.innerHTML = highlightTypeScript(data.source);
|
|
2640
|
+
sourceLoaded = true;
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
codeEl.textContent = 'Error loading source: ' + err.message;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
function highlightTypeScript(code) {
|
|
2647
|
+
// Escape HTML first
|
|
2648
|
+
code = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2649
|
+
|
|
2650
|
+
// Comments (single and multi-line)
|
|
2651
|
+
code = code.replace(/(\\/\\/.*$)/gm, '<span class="comment">$1</span>');
|
|
2652
|
+
code = code.replace(/(\\/\\*[\\s\\S]*?\\*\\/)/g, '<span class="comment">$1</span>');
|
|
2653
|
+
|
|
2654
|
+
// Strings (double, single, and template)
|
|
2655
|
+
code = code.replace(/("(?:[^"\\\\]|\\\\.)*")/g, '<span class="string">$1</span>');
|
|
2656
|
+
code = code.replace(/('(?:[^'\\\\]|\\\\.)*')/g, '<span class="string">$1</span>');
|
|
2657
|
+
code = code.replace(/(\`(?:[^\`\\\\]|\\\\.)*\`)/g, '<span class="string">$1</span>');
|
|
2658
|
+
|
|
2659
|
+
// Keywords
|
|
2660
|
+
const keywords = ['import', 'export', 'from', 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'class', 'extends', 'new', 'this', 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'async', 'await', 'default', 'as', 'type', 'interface'];
|
|
2661
|
+
keywords.forEach(kw => {
|
|
2662
|
+
code = code.replace(new RegExp('\\\\b(' + kw + ')\\\\b', 'g'), '<span class="keyword">$1</span>');
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
// Numbers
|
|
2666
|
+
code = code.replace(/\\b(\\d+\\.?\\d*)\\b/g, '<span class="number">$1</span>');
|
|
2667
|
+
|
|
2668
|
+
// Function calls
|
|
2669
|
+
code = code.replace(/\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(/g, '<span class="function">$1</span>(');
|
|
2670
|
+
|
|
2671
|
+
return code;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Copy source button
|
|
2675
|
+
document.getElementById('copySourceBtn').addEventListener('click', async () => {
|
|
2676
|
+
const btn = document.getElementById('copySourceBtn');
|
|
2677
|
+
try {
|
|
2678
|
+
await navigator.clipboard.writeText(sourceContent);
|
|
2679
|
+
btn.classList.add('copied');
|
|
2680
|
+
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Copied!';
|
|
2681
|
+
setTimeout(() => {
|
|
2682
|
+
btn.classList.remove('copied');
|
|
2683
|
+
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy';
|
|
2684
|
+
}, 2000);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
console.error('Failed to copy:', err);
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2489
2690
|
function renderTableSection(title, items, type) {
|
|
2490
2691
|
const changedCount = items.filter(n => n.changeStatus !== 'unchanged').length;
|
|
2491
2692
|
|
package/src/browser/transform.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3
2
|
import type { OntologyConfig } from "../config/types.js";
|
|
4
3
|
import type { OntologyDiff, FunctionChange } from "../lockfile/types.js";
|
|
5
4
|
import { getFieldFromMetadata, getUserContextFields } from "../config/categorical.js";
|
|
5
|
+
import {
|
|
6
|
+
isZodObject,
|
|
7
|
+
isZodOptional,
|
|
8
|
+
isZodNullable,
|
|
9
|
+
isZodArray,
|
|
10
|
+
isZodDefault,
|
|
11
|
+
getObjectShape,
|
|
12
|
+
getInnerSchema,
|
|
13
|
+
getArrayElement,
|
|
14
|
+
} from "../config/zod-utils.js";
|
|
6
15
|
|
|
7
16
|
export type NodeType = "entity" | "function" | "accessGroup";
|
|
8
17
|
export type EdgeType = "operates-on" | "requires-access" | "depends-on";
|
|
@@ -16,7 +25,6 @@ export interface GraphNode {
|
|
|
16
25
|
metadata: {
|
|
17
26
|
inputs?: Record<string, unknown>;
|
|
18
27
|
outputs?: Record<string, unknown>;
|
|
19
|
-
resolver?: string;
|
|
20
28
|
functionCount?: number;
|
|
21
29
|
usesUserContext?: boolean;
|
|
22
30
|
};
|
|
@@ -81,30 +89,44 @@ function extractFieldReferences(
|
|
|
81
89
|
});
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
if (schema
|
|
85
|
-
const shape = schema
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
if (isZodObject(schema)) {
|
|
93
|
+
const shape = getObjectShape(schema);
|
|
94
|
+
if (shape) {
|
|
95
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
96
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
97
|
+
results.push(
|
|
98
|
+
...extractFieldReferences(value as z.ZodType<unknown>, fieldPath)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
if (schema
|
|
95
|
-
|
|
104
|
+
if (isZodOptional(schema)) {
|
|
105
|
+
const inner = getInnerSchema(schema);
|
|
106
|
+
if (inner) {
|
|
107
|
+
results.push(...extractFieldReferences(inner as z.ZodType<unknown>, path));
|
|
108
|
+
}
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
if (schema
|
|
99
|
-
|
|
111
|
+
if (isZodNullable(schema)) {
|
|
112
|
+
const inner = getInnerSchema(schema);
|
|
113
|
+
if (inner) {
|
|
114
|
+
results.push(...extractFieldReferences(inner as z.ZodType<unknown>, path));
|
|
115
|
+
}
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
if (schema
|
|
103
|
-
|
|
118
|
+
if (isZodArray(schema)) {
|
|
119
|
+
const element = getArrayElement(schema);
|
|
120
|
+
if (element) {
|
|
121
|
+
results.push(...extractFieldReferences(element as z.ZodType<unknown>, `${path}[]`));
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
if (schema
|
|
107
|
-
|
|
125
|
+
if (isZodDefault(schema)) {
|
|
126
|
+
const inner = getInnerSchema(schema);
|
|
127
|
+
if (inner) {
|
|
128
|
+
results.push(...extractFieldReferences(inner as z.ZodType<unknown>, path));
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
return results;
|
|
@@ -115,7 +137,7 @@ function extractFieldReferences(
|
|
|
115
137
|
*/
|
|
116
138
|
function safeZodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> | undefined {
|
|
117
139
|
try {
|
|
118
|
-
const result =
|
|
140
|
+
const result = z.toJSONSchema(schema, { reused: "inline", unrepresentable: "any" }) as Record<string, unknown>;
|
|
119
141
|
delete result.$schema;
|
|
120
142
|
return result;
|
|
121
143
|
} catch {
|
|
@@ -173,7 +195,6 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
|
|
|
173
195
|
metadata: {
|
|
174
196
|
inputs: safeZodToJsonSchema(fn.inputs),
|
|
175
197
|
outputs: fn.outputs ? safeZodToJsonSchema(fn.outputs) : undefined,
|
|
176
|
-
resolver: fn.resolver,
|
|
177
198
|
usesUserContext: usesUserContext || undefined,
|
|
178
199
|
},
|
|
179
200
|
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -54,6 +54,11 @@ export const initCommand = defineCommand({
|
|
|
54
54
|
const configTemplate = `import { defineOntology, userContext } from 'ont-run';
|
|
55
55
|
import { z } from 'zod';
|
|
56
56
|
|
|
57
|
+
// Import resolver functions - TypeScript enforces return types match outputs
|
|
58
|
+
import healthCheck from './resolvers/healthCheck.js';
|
|
59
|
+
import getUser from './resolvers/getUser.js';
|
|
60
|
+
import deleteUser from './resolvers/deleteUser.js';
|
|
61
|
+
|
|
57
62
|
export default defineOntology({
|
|
58
63
|
name: 'my-api',
|
|
59
64
|
|
|
@@ -98,7 +103,7 @@ export default defineOntology({
|
|
|
98
103
|
access: ['public', 'support', 'admin'],
|
|
99
104
|
entities: [],
|
|
100
105
|
inputs: z.object({}),
|
|
101
|
-
resolver:
|
|
106
|
+
resolver: healthCheck,
|
|
102
107
|
},
|
|
103
108
|
|
|
104
109
|
// Example: Restricted function with row-level access
|
|
@@ -114,7 +119,7 @@ export default defineOntology({
|
|
|
114
119
|
email: z.string(),
|
|
115
120
|
})),
|
|
116
121
|
}),
|
|
117
|
-
resolver:
|
|
122
|
+
resolver: getUser,
|
|
118
123
|
},
|
|
119
124
|
|
|
120
125
|
// Example: Admin-only function
|
|
@@ -126,7 +131,7 @@ export default defineOntology({
|
|
|
126
131
|
userId: z.string().uuid(),
|
|
127
132
|
reason: z.string().optional(),
|
|
128
133
|
}),
|
|
129
|
-
resolver:
|
|
134
|
+
resolver: deleteUser,
|
|
130
135
|
},
|
|
131
136
|
},
|
|
132
137
|
});
|
|
@@ -138,7 +143,7 @@ export default defineOntology({
|
|
|
138
143
|
// Write example resolvers
|
|
139
144
|
const healthCheckResolver = `import type { ResolverContext } from 'ont-run';
|
|
140
145
|
|
|
141
|
-
export default async function healthCheck(ctx: ResolverContext
|
|
146
|
+
export default async function healthCheck(ctx: ResolverContext) {
|
|
142
147
|
ctx.logger.info('Health check called');
|
|
143
148
|
|
|
144
149
|
return {
|
|
@@ -237,7 +242,7 @@ await startOnt();
|
|
|
237
242
|
packageJson.dependencies = {
|
|
238
243
|
...(packageJson.dependencies as Record<string, string> || {}),
|
|
239
244
|
"ont-run": "latest",
|
|
240
|
-
zod: "^
|
|
245
|
+
zod: "^4.0.0",
|
|
241
246
|
};
|
|
242
247
|
|
|
243
248
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
@@ -48,7 +48,7 @@ export const reviewCommand = defineCommand({
|
|
|
48
48
|
|
|
49
49
|
// Load config
|
|
50
50
|
consola.info("Loading ontology config...");
|
|
51
|
-
const { config, configDir } = await loadConfig();
|
|
51
|
+
const { config, configDir, configPath } = await loadConfig();
|
|
52
52
|
|
|
53
53
|
// Compute current ontology snapshot
|
|
54
54
|
const { ontology: newOntology, hash: newHash } = computeOntologyHash(config);
|
|
@@ -107,6 +107,7 @@ export const reviewCommand = defineCommand({
|
|
|
107
107
|
config,
|
|
108
108
|
diff: diff.hasChanges ? diff : null,
|
|
109
109
|
configDir,
|
|
110
|
+
configPath,
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
if (diff.hasChanges) {
|
package/src/config/define.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import {
|
|
2
3
|
OntologyConfigSchema,
|
|
3
4
|
validateAccessGroups,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
EnvironmentConfig,
|
|
12
13
|
EntityDefinition,
|
|
13
14
|
AuthFunction,
|
|
15
|
+
ResolverFunction,
|
|
14
16
|
} from "./types.js";
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -20,6 +22,7 @@ import type {
|
|
|
20
22
|
* ```ts
|
|
21
23
|
* import { defineOntology, fieldFrom } from 'ont-run';
|
|
22
24
|
* import { z } from 'zod';
|
|
25
|
+
* import { getUser } from './resolvers/getUser.js';
|
|
23
26
|
*
|
|
24
27
|
* export default defineOntology({
|
|
25
28
|
* name: 'my-api',
|
|
@@ -44,7 +47,7 @@ import type {
|
|
|
44
47
|
* access: ['public', 'admin'],
|
|
45
48
|
* entities: ['User'],
|
|
46
49
|
* inputs: z.object({ id: z.string() }),
|
|
47
|
-
* resolver:
|
|
50
|
+
* resolver: getUser, // Direct function reference for type safety
|
|
48
51
|
* },
|
|
49
52
|
* },
|
|
50
53
|
* });
|
|
@@ -53,7 +56,9 @@ import type {
|
|
|
53
56
|
export function defineOntology<
|
|
54
57
|
TGroups extends string,
|
|
55
58
|
TEntities extends string,
|
|
56
|
-
|
|
59
|
+
// Use `any` for input/output schema types to avoid contravariance issues with resolver functions.
|
|
60
|
+
// Without this, ResolverFunction<unknown, unknown> won't accept more specific resolver types.
|
|
61
|
+
TFunctions extends Record<string, FunctionDefinition<TGroups, TEntities, any, any>>,
|
|
57
62
|
>(config: {
|
|
58
63
|
name: string;
|
|
59
64
|
environments: Record<string, EnvironmentConfig>;
|
|
@@ -76,3 +81,48 @@ export function defineOntology<
|
|
|
76
81
|
|
|
77
82
|
return config as OntologyConfig<TGroups, TEntities, TFunctions>;
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Define a function with full type inference for resolver type safety.
|
|
87
|
+
*
|
|
88
|
+
* This helper ensures that the resolver function's return type matches
|
|
89
|
+
* the outputs Zod schema at compile time.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { defineFunction, z } from 'ont-run';
|
|
94
|
+
* import type { ResolverContext } from 'ont-run';
|
|
95
|
+
*
|
|
96
|
+
* const getUser = defineFunction({
|
|
97
|
+
* description: 'Get a user by ID',
|
|
98
|
+
* access: ['public', 'admin'] as const,
|
|
99
|
+
* entities: ['User'] as const,
|
|
100
|
+
* inputs: z.object({ id: z.string() }),
|
|
101
|
+
* outputs: z.object({ id: z.string(), name: z.string() }),
|
|
102
|
+
* resolver: async (ctx, args) => {
|
|
103
|
+
* // TypeScript knows args is { id: string }
|
|
104
|
+
* // TypeScript enforces return type is { id: string, name: string }
|
|
105
|
+
* return { id: args.id, name: 'Example User' };
|
|
106
|
+
* },
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function defineFunction<
|
|
111
|
+
TGroups extends string,
|
|
112
|
+
TEntities extends string,
|
|
113
|
+
TInputs extends z.ZodType,
|
|
114
|
+
TOutputs extends z.ZodType,
|
|
115
|
+
>(config: {
|
|
116
|
+
description: string;
|
|
117
|
+
access: readonly TGroups[];
|
|
118
|
+
entities: readonly TEntities[];
|
|
119
|
+
inputs: TInputs;
|
|
120
|
+
outputs?: TOutputs;
|
|
121
|
+
resolver: ResolverFunction<z.infer<TInputs>, z.infer<TOutputs>>;
|
|
122
|
+
}): FunctionDefinition<TGroups, TEntities, TInputs, TOutputs> {
|
|
123
|
+
return {
|
|
124
|
+
...config,
|
|
125
|
+
access: [...config.access],
|
|
126
|
+
entities: [...config.entities],
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getFieldFromMetadata, getUserContextFields } from "./categorical.js";
|
|
3
|
+
import {
|
|
4
|
+
isZodSchema,
|
|
5
|
+
isZodObject,
|
|
6
|
+
isZodOptional,
|
|
7
|
+
isZodNullable,
|
|
8
|
+
isZodArray,
|
|
9
|
+
isZodDefault,
|
|
10
|
+
getObjectShape,
|
|
11
|
+
getInnerSchema,
|
|
12
|
+
getArrayElement,
|
|
13
|
+
} from "./zod-utils.js";
|
|
3
14
|
import type { OntologyConfig } from "./types.js";
|
|
4
15
|
|
|
5
16
|
/**
|
|
@@ -26,16 +37,10 @@ export const EntityDefinitionSchema = z.object({
|
|
|
26
37
|
});
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
|
-
* Check if a value is a
|
|
40
|
+
* Check if a value is a function
|
|
30
41
|
*/
|
|
31
|
-
function
|
|
32
|
-
return
|
|
33
|
-
val !== null &&
|
|
34
|
-
typeof val === "object" &&
|
|
35
|
-
"_def" in val &&
|
|
36
|
-
"safeParse" in val &&
|
|
37
|
-
typeof (val as { safeParse: unknown }).safeParse === "function"
|
|
38
|
-
);
|
|
42
|
+
function isFunction(val: unknown): val is (...args: unknown[]) => unknown {
|
|
43
|
+
return typeof val === "function";
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -53,24 +58,20 @@ export const FunctionDefinitionSchema = z.object({
|
|
|
53
58
|
message: "outputs must be a Zod schema",
|
|
54
59
|
})
|
|
55
60
|
.optional(),
|
|
56
|
-
resolver: z.
|
|
61
|
+
resolver: z.custom<(...args: unknown[]) => unknown>(isFunction, {
|
|
62
|
+
message: "resolver must be a function",
|
|
63
|
+
}),
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
/**
|
|
60
|
-
* Schema for auth function
|
|
61
|
-
*/
|
|
62
|
-
export const AuthFunctionSchema = z
|
|
63
|
-
.function()
|
|
64
|
-
.args(z.custom<Request>())
|
|
65
|
-
.returns(z.union([z.array(z.string()), z.promise(z.array(z.string()))]));
|
|
66
|
-
|
|
67
66
|
/**
|
|
68
67
|
* Schema for the full ontology configuration
|
|
69
68
|
*/
|
|
70
69
|
export const OntologyConfigSchema = z.object({
|
|
71
70
|
name: z.string().min(1),
|
|
72
71
|
environments: z.record(z.string(), EnvironmentConfigSchema),
|
|
73
|
-
auth: z.
|
|
72
|
+
auth: z.custom<(req: Request) => unknown>(isFunction, {
|
|
73
|
+
message: "auth must be a function",
|
|
74
|
+
}),
|
|
74
75
|
accessGroups: z.record(z.string(), AccessGroupConfigSchema),
|
|
75
76
|
entities: z.record(z.string(), EntityDefinitionSchema).optional(),
|
|
76
77
|
functions: z.record(z.string(), FunctionDefinitionSchema),
|
|
@@ -141,34 +142,48 @@ function extractFieldFromRefs(
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// Handle ZodObject - recurse into properties
|
|
144
|
-
if (schema
|
|
145
|
-
const shape = schema
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
if (isZodObject(schema)) {
|
|
146
|
+
const shape = getObjectShape(schema);
|
|
147
|
+
if (shape) {
|
|
148
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
149
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
150
|
+
results.push(
|
|
151
|
+
...extractFieldFromRefs(value as z.ZodType<unknown>, fieldPath)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
151
154
|
}
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
// Handle ZodOptional - unwrap
|
|
155
|
-
if (schema
|
|
156
|
-
|
|
158
|
+
if (isZodOptional(schema)) {
|
|
159
|
+
const inner = getInnerSchema(schema);
|
|
160
|
+
if (inner) {
|
|
161
|
+
results.push(...extractFieldFromRefs(inner as z.ZodType<unknown>, path));
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
// Handle ZodNullable - unwrap
|
|
160
|
-
if (schema
|
|
161
|
-
|
|
166
|
+
if (isZodNullable(schema)) {
|
|
167
|
+
const inner = getInnerSchema(schema);
|
|
168
|
+
if (inner) {
|
|
169
|
+
results.push(...extractFieldFromRefs(inner as z.ZodType<unknown>, path));
|
|
170
|
+
}
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
// Handle ZodArray - recurse into element
|
|
165
|
-
if (schema
|
|
166
|
-
|
|
174
|
+
if (isZodArray(schema)) {
|
|
175
|
+
const element = getArrayElement(schema);
|
|
176
|
+
if (element) {
|
|
177
|
+
results.push(...extractFieldFromRefs(element as z.ZodType<unknown>, `${path}[]`));
|
|
178
|
+
}
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
// Handle ZodDefault - unwrap
|
|
170
|
-
if (schema
|
|
171
|
-
|
|
182
|
+
if (isZodDefault(schema)) {
|
|
183
|
+
const inner = getInnerSchema(schema);
|
|
184
|
+
if (inner) {
|
|
185
|
+
results.push(...extractFieldFromRefs(inner as z.ZodType<unknown>, path));
|
|
186
|
+
}
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
return results;
|