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.
@@ -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, '&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
+
2489
2690
  function renderTableSection(title, items, type) {
2490
2691
  const changedCount = items.filter(n => n.changeStatus !== 'unchanged').length;
2491
2692
 
@@ -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 instanceof z.ZodObject) {
85
- const shape = schema.shape;
86
- for (const [key, value] of Object.entries(shape)) {
87
- const fieldPath = path ? `${path}.${key}` : key;
88
- results.push(
89
- ...extractFieldReferences(value as z.ZodType<unknown>, fieldPath)
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 instanceof z.ZodOptional) {
95
- results.push(...extractFieldReferences(schema.unwrap(), path));
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 instanceof z.ZodNullable) {
99
- results.push(...extractFieldReferences(schema.unwrap(), path));
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 instanceof z.ZodArray) {
103
- results.push(...extractFieldReferences(schema.element, `${path}[]`));
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 instanceof z.ZodDefault) {
107
- results.push(...extractFieldReferences(schema._def.innerType, path));
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 = zodToJsonSchema(schema, { $refStrategy: "none" }) as Record<string, unknown>;
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
  });
@@ -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: './resolvers/healthCheck.ts',
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: './resolvers/getUser.ts',
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: './resolvers/deleteUser.ts',
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, args: {}) {
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: "^3.24.0",
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) {
@@ -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
  * });
@@ -53,7 +56,9 @@ import type {
53
56
  export function defineOntology<
54
57
  TGroups extends string,
55
58
  TEntities extends string,
56
- TFunctions extends Record<string, FunctionDefinition<TGroups, TEntities>>,
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
+ }
@@ -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 Zod schema (duck typing to work across bundle boundaries)
40
+ * Check if a value is a function
30
41
  */
31
- function isZodSchema(val: unknown): boolean {
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.string(),
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.function(),
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 instanceof z.ZodObject) {
145
- const shape = schema.shape;
146
- for (const [key, value] of Object.entries(shape)) {
147
- const fieldPath = path ? `${path}.${key}` : key;
148
- results.push(
149
- ...extractFieldFromRefs(value as z.ZodType<unknown>, fieldPath)
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 instanceof z.ZodOptional) {
156
- results.push(...extractFieldFromRefs(schema.unwrap(), path));
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 instanceof z.ZodNullable) {
161
- results.push(...extractFieldFromRefs(schema.unwrap(), path));
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 instanceof z.ZodArray) {
166
- results.push(...extractFieldFromRefs(schema.element, `${path}[]`));
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 instanceof z.ZodDefault) {
171
- results.push(...extractFieldFromRefs(schema._def.innerType, path));
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;