ont-run 0.0.3 → 0.0.5

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.
@@ -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
 
@@ -122,6 +146,12 @@ export async function startBrowserServer(options: BrowserServerOptions): Promise
122
146
  }
123
147
 
124
148
  function generateBrowserUI(graphData: EnhancedGraphData): string {
149
+ const userContextFilterBtn = graphData.meta.totalUserContextFunctions > 0
150
+ ? `<button class="filter-btn" data-filter="userContext" title="Functions using userContext()">
151
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>User Context (${graphData.meta.totalUserContextFunctions})
152
+ </button>`
153
+ : '';
154
+
125
155
  return `<!DOCTYPE html>
126
156
  <html lang="en">
127
157
  <head>
@@ -699,6 +729,89 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
699
729
  color: var(--change-added);
700
730
  }
701
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
+
702
815
  /* No Changes State */
703
816
  .no-changes {
704
817
  text-align: center;
@@ -1427,6 +1540,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1427
1540
  <div class="view-tabs">
1428
1541
  <button class="view-tab active" data-view="graph">Graph</button>
1429
1542
  <button class="view-tab" data-view="table">Table</button>
1543
+ <button class="view-tab" data-view="source">Source</button>
1430
1544
  </div>
1431
1545
 
1432
1546
  <div class="filter-buttons" id="graphFilters">
@@ -1440,6 +1554,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1440
1554
  <button class="filter-btn" data-filter="accessGroup">
1441
1555
  <span class="dot access"></span> Access
1442
1556
  </button>
1557
+ ${userContextFilterBtn}
1443
1558
  </div>
1444
1559
 
1445
1560
  <div class="layout-selector">
@@ -1546,6 +1661,20 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1546
1661
  <div class="table-view" id="tableView">
1547
1662
  <div id="tableContent"></div>
1548
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>
1549
1678
  </div>
1550
1679
 
1551
1680
  <!-- Review Footer -->
@@ -1584,6 +1713,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1584
1713
  metadata: node.metadata,
1585
1714
  changeStatus: node.changeStatus || 'unchanged',
1586
1715
  changeDetails: node.changeDetails || null,
1716
+ usesUserContext: node.metadata?.usesUserContext || false,
1587
1717
  },
1588
1718
  });
1589
1719
  }
@@ -1636,6 +1766,19 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
1636
1766
  'height': 55,
1637
1767
  },
1638
1768
  },
1769
+ // Function nodes with userContext - show indicator below label
1770
+ {
1771
+ selector: 'node[type="function"][?usesUserContext]',
1772
+ style: {
1773
+ 'background-image': 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#e8f4f8" stroke="#023d60" stroke-width="1.5"/><g transform="translate(4, 4)" fill="none" stroke="#023d60" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></g></svg>'),
1774
+ 'background-width': '18px',
1775
+ 'background-height': '18px',
1776
+ 'background-position-x': '50%',
1777
+ 'background-position-y': '75%',
1778
+ 'text-valign': 'center',
1779
+ 'text-margin-y': -8,
1780
+ },
1781
+ },
1639
1782
  // Entity nodes - Teal
1640
1783
  {
1641
1784
  selector: 'node[type="entity"]',
@@ -2177,6 +2320,16 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
2177
2320
  if (filter === 'all') {
2178
2321
  cy.nodes().removeClass('hidden');
2179
2322
  cy.edges().removeClass('hidden');
2323
+ } else if (filter === 'userContext') {
2324
+ // Special filter: show only functions with userContext
2325
+ cy.nodes().forEach(node => {
2326
+ if (node.data('type') === 'function' && node.data('usesUserContext')) {
2327
+ node.removeClass('hidden');
2328
+ } else {
2329
+ node.addClass('hidden');
2330
+ }
2331
+ });
2332
+ cy.edges().addClass('hidden');
2180
2333
  } else {
2181
2334
  cy.nodes().forEach(node => {
2182
2335
  if (node.data('type') === filter) {
@@ -2398,6 +2551,7 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
2398
2551
  const graphContainer = document.querySelector('.graph-container');
2399
2552
  const detailPanel = document.getElementById('detailPanel');
2400
2553
  const tableView = document.getElementById('tableView');
2554
+ const sourceView = document.getElementById('sourceView');
2401
2555
  const graphFilters = document.getElementById('graphFilters');
2402
2556
  const layoutSelector = document.querySelector('.layout-selector');
2403
2557
 
@@ -2405,15 +2559,25 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
2405
2559
  graphContainer.style.display = 'block';
2406
2560
  detailPanel.style.display = 'block';
2407
2561
  tableView.classList.remove('active');
2562
+ sourceView.classList.remove('active');
2408
2563
  if (graphFilters) graphFilters.style.display = 'flex';
2409
2564
  if (layoutSelector) layoutSelector.style.display = 'flex';
2410
- } else {
2565
+ } else if (view === 'table') {
2411
2566
  graphContainer.style.display = 'none';
2412
2567
  detailPanel.style.display = 'none';
2413
2568
  tableView.classList.add('active');
2569
+ sourceView.classList.remove('active');
2414
2570
  if (graphFilters) graphFilters.style.display = 'none';
2415
2571
  if (layoutSelector) layoutSelector.style.display = 'none';
2416
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();
2417
2581
  }
2418
2582
  }
2419
2583
 
@@ -2455,6 +2619,74 @@ function generateBrowserUI(graphData: EnhancedGraphData): string {
2455
2619
  });
2456
2620
  }
2457
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
+
2458
2690
  function renderTableSection(title, items, type) {
2459
2691
  const changedCount = items.filter(n => n.changeStatus !== 'unchanged').length;
2460
2692
 
@@ -16,7 +16,6 @@ export interface GraphNode {
16
16
  metadata: {
17
17
  inputs?: Record<string, unknown>;
18
18
  outputs?: Record<string, unknown>;
19
- resolver?: string;
20
19
  functionCount?: number;
21
20
  usesUserContext?: boolean;
22
21
  };
@@ -38,6 +37,7 @@ export interface GraphData {
38
37
  totalFunctions: number;
39
38
  totalEntities: number;
40
39
  totalAccessGroups: number;
40
+ totalUserContextFunctions: number;
41
41
  };
42
42
  }
43
43
 
@@ -132,6 +132,7 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
132
132
  // Track function counts for access groups and entities
133
133
  const accessGroupCounts: Record<string, number> = {};
134
134
  const entityCounts: Record<string, number> = {};
135
+ let userContextFunctionCount = 0;
135
136
 
136
137
  // Initialize counts
137
138
  for (const groupName of Object.keys(config.accessGroups)) {
@@ -158,6 +159,9 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
158
159
  // Check if function uses userContext
159
160
  const userContextFields = getUserContextFields(fn.inputs);
160
161
  const usesUserContext = userContextFields.length > 0;
162
+ if (usesUserContext) {
163
+ userContextFunctionCount++;
164
+ }
161
165
 
162
166
  // Create function node
163
167
  nodes.push({
@@ -168,7 +172,6 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
168
172
  metadata: {
169
173
  inputs: safeZodToJsonSchema(fn.inputs),
170
174
  outputs: fn.outputs ? safeZodToJsonSchema(fn.outputs) : undefined,
171
- resolver: fn.resolver,
172
175
  usesUserContext: usesUserContext || undefined,
173
176
  },
174
177
  });
@@ -242,6 +245,7 @@ export function transformToGraphData(config: OntologyConfig): GraphData {
242
245
  totalFunctions: Object.keys(config.functions).length,
243
246
  totalEntities: config.entities ? Object.keys(config.entities).length : 0,
244
247
  totalAccessGroups: Object.keys(config.accessGroups).length,
248
+ totalUserContextFunctions: userContextFunctionCount,
245
249
  },
246
250
  };
247
251
  }
@@ -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/cli/index.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { defineCommand, runMain } from "citty";
2
2
  import { initCommand } from "./commands/init.js";
3
3
  import { reviewCommand } from "./commands/review.js";
4
+ import pkg from "../../package.json";
4
5
 
5
6
  const main = defineCommand({
6
7
  meta: {
7
8
  name: "ont",
8
9
  description: "Ontology - Ontology-first backends with human-approved AI access & edits",
9
- version: "0.1.0",
10
+ version: pkg.version,
10
11
  },
11
12
  subCommands: {
12
13
  init: initCommand,
@@ -174,12 +174,17 @@ export function hasUserContextMetadata(
174
174
 
175
175
  /**
176
176
  * Get all userContext field names from a Zod object schema
177
+ *
178
+ * Note: Uses _def.typeName check instead of instanceof to work across
179
+ * module boundaries in bundled CLI.
177
180
  */
178
181
  export function getUserContextFields(schema: z.ZodType): string[] {
179
182
  const fields: string[] = [];
180
183
 
181
- if (schema instanceof z.ZodObject) {
182
- const shape = schema.shape;
184
+ // Use _def.typeName check for bundler compatibility (instanceof fails across module boundaries)
185
+ const def = (schema as unknown as { _def?: { typeName?: string; shape?: () => Record<string, unknown> } })._def;
186
+ if (def?.typeName === "ZodObject" && typeof def.shape === "function") {
187
+ const shape = def.shape();
183
188
  for (const [key, value] of Object.entries(shape)) {
184
189
  if (hasUserContextMetadata(value)) {
185
190
  fields.push(key);
@@ -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: './resolvers/getUser.ts',
50
+ * resolver: getUser, // Direct function reference for type safety
48
51
  * },
49
52
  * },
50
53
  * });
@@ -76,3 +79,48 @@ export function defineOntology<
76
79
 
77
80
  return config as OntologyConfig<TGroups, TEntities, TFunctions>;
78
81
  }
82
+
83
+ /**
84
+ * Define a function with full type inference for resolver type safety.
85
+ *
86
+ * This helper ensures that the resolver function's return type matches
87
+ * the outputs Zod schema at compile time.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { defineFunction, z } from 'ont-run';
92
+ * import type { ResolverContext } from 'ont-run';
93
+ *
94
+ * const getUser = defineFunction({
95
+ * description: 'Get a user by ID',
96
+ * access: ['public', 'admin'] as const,
97
+ * entities: ['User'] as const,
98
+ * inputs: z.object({ id: z.string() }),
99
+ * outputs: z.object({ id: z.string(), name: z.string() }),
100
+ * resolver: async (ctx, args) => {
101
+ * // TypeScript knows args is { id: string }
102
+ * // TypeScript enforces return type is { id: string, name: string }
103
+ * return { id: args.id, name: 'Example User' };
104
+ * },
105
+ * });
106
+ * ```
107
+ */
108
+ export function defineFunction<
109
+ TGroups extends string,
110
+ TEntities extends string,
111
+ TInputs extends z.ZodType,
112
+ TOutputs extends z.ZodType,
113
+ >(config: {
114
+ description: string;
115
+ access: readonly TGroups[];
116
+ entities: readonly TEntities[];
117
+ inputs: TInputs;
118
+ outputs?: TOutputs;
119
+ resolver: ResolverFunction<z.infer<TInputs>, z.infer<TOutputs>>;
120
+ }): FunctionDefinition<TGroups, TEntities, TInputs, TOutputs> {
121
+ return {
122
+ ...config,
123
+ access: [...config.access],
124
+ entities: [...config.entities],
125
+ };
126
+ }
@@ -53,7 +53,7 @@ export const FunctionDefinitionSchema = z.object({
53
53
  message: "outputs must be a Zod schema",
54
54
  })
55
55
  .optional(),
56
- resolver: z.string(),
56
+ resolver: z.function(),
57
57
  });
58
58
 
59
59
  /**
@@ -37,12 +37,41 @@ export interface FieldOption {
37
37
  label: string;
38
38
  }
39
39
 
40
+ /**
41
+ * Context passed to resolvers
42
+ */
43
+ export interface ResolverContext {
44
+ /** Current environment name */
45
+ env: string;
46
+ /** Environment configuration */
47
+ envConfig: EnvironmentConfig;
48
+ /** Logger instance */
49
+ logger: {
50
+ info: (message: string, ...args: unknown[]) => void;
51
+ warn: (message: string, ...args: unknown[]) => void;
52
+ error: (message: string, ...args: unknown[]) => void;
53
+ debug: (message: string, ...args: unknown[]) => void;
54
+ };
55
+ /** Access groups for the current request */
56
+ accessGroups: string[];
57
+ }
58
+
59
+ /**
60
+ * Resolver function signature
61
+ */
62
+ export type ResolverFunction<TArgs = unknown, TResult = unknown> = (
63
+ ctx: ResolverContext,
64
+ args: TArgs
65
+ ) => Promise<TResult> | TResult;
66
+
40
67
  /**
41
68
  * Definition of a function in the ontology
42
69
  */
43
70
  export interface FunctionDefinition<
44
71
  TGroups extends string = string,
45
72
  TEntities extends string = string,
73
+ TInputs extends z.ZodType = z.ZodType<unknown>,
74
+ TOutputs extends z.ZodType = z.ZodType<unknown>,
46
75
  > {
47
76
  /** Human-readable description of what this function does */
48
77
  description: string;
@@ -51,11 +80,11 @@ export interface FunctionDefinition<
51
80
  /** Which entities this function relates to (use empty array [] if none) */
52
81
  entities: TEntities[];
53
82
  /** Zod schema for input validation */
54
- inputs: z.ZodType<unknown>;
83
+ inputs: TInputs;
55
84
  /** Zod schema for output validation/documentation */
56
- outputs?: z.ZodType<unknown>;
57
- /** Path to the resolver file (relative to ontology.config.ts) */
58
- resolver: string;
85
+ outputs?: TOutputs;
86
+ /** Resolver function that handles this function's logic */
87
+ resolver: ResolverFunction<z.infer<TInputs>, z.infer<TOutputs>>;
59
88
  }
60
89
 
61
90
  /**
@@ -105,30 +134,3 @@ export interface OntologyConfig<
105
134
  /** Function definitions */
106
135
  functions: TFunctions;
107
136
  }
108
-
109
- /**
110
- * Context passed to resolvers
111
- */
112
- export interface ResolverContext {
113
- /** Current environment name */
114
- env: string;
115
- /** Environment configuration */
116
- envConfig: EnvironmentConfig;
117
- /** Logger instance */
118
- logger: {
119
- info: (message: string, ...args: unknown[]) => void;
120
- warn: (message: string, ...args: unknown[]) => void;
121
- error: (message: string, ...args: unknown[]) => void;
122
- debug: (message: string, ...args: unknown[]) => void;
123
- };
124
- /** Access groups for the current request */
125
- accessGroups: string[];
126
- }
127
-
128
- /**
129
- * Resolver function signature
130
- */
131
- export type ResolverFunction<TArgs = unknown, TResult = unknown> = (
132
- ctx: ResolverContext,
133
- args: TArgs
134
- ) => Promise<TResult> | TResult;
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * ```ts
6
6
  * import { defineOntology, fieldFrom } from 'ont-run';
7
7
  * import { z } from 'zod';
8
+ * import { hello } from './resolvers/hello.js';
8
9
  *
9
10
  * export default defineOntology({
10
11
  * name: 'my-api',
@@ -23,7 +24,7 @@
23
24
  * access: ['public'],
24
25
  * entities: [],
25
26
  * inputs: z.object({ name: z.string() }),
26
- * resolver: './resolvers/hello.ts',
27
+ * resolver: hello, // Direct function reference for type safety
27
28
  * },
28
29
  * },
29
30
  * });
@@ -31,7 +32,7 @@
31
32
  */
32
33
 
33
34
  // Main API
34
- export { defineOntology } from "./config/define.js";
35
+ export { defineOntology, defineFunction } from "./config/define.js";
35
36
  export { fieldFrom, userContext } from "./config/categorical.js";
36
37
  export { startOnt } from "./server/start.js";
37
38
  export type { StartOntOptions, StartOntResult } from "./server/start.js";