ont-run 0.0.1
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 +228 -0
- package/bin/ont.ts +5 -0
- package/dist/bin/ont.d.ts +2 -0
- package/dist/bin/ont.js +13667 -0
- package/dist/index.js +23152 -0
- package/dist/src/browser/server.d.ts +16 -0
- package/dist/src/browser/transform.d.ts +87 -0
- package/dist/src/cli/commands/init.d.ts +12 -0
- package/dist/src/cli/commands/review.d.ts +17 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/utils/config-loader.d.ts +13 -0
- package/dist/src/config/categorical.d.ts +76 -0
- package/dist/src/config/define.d.ts +46 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/schema.d.ts +162 -0
- package/dist/src/config/types.d.ts +94 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/lockfile/differ.d.ts +11 -0
- package/dist/src/lockfile/hasher.d.ts +31 -0
- package/dist/src/lockfile/index.d.ts +53 -0
- package/dist/src/lockfile/types.d.ts +90 -0
- package/dist/src/runtime/index.d.ts +28 -0
- package/dist/src/server/api/index.d.ts +20 -0
- package/dist/src/server/api/middleware.d.ts +34 -0
- package/dist/src/server/api/router.d.ts +18 -0
- package/dist/src/server/mcp/index.d.ts +23 -0
- package/dist/src/server/mcp/tools.d.ts +35 -0
- package/dist/src/server/resolver.d.ts +30 -0
- package/dist/src/server/start.d.ts +37 -0
- package/package.json +63 -0
- package/src/browser/server.ts +2567 -0
- package/src/browser/transform.ts +473 -0
- package/src/cli/commands/init.ts +226 -0
- package/src/cli/commands/review.ts +126 -0
- package/src/cli/index.ts +19 -0
- package/src/cli/utils/config-loader.ts +78 -0
- package/src/config/categorical.ts +101 -0
- package/src/config/define.ts +78 -0
- package/src/config/index.ts +23 -0
- package/src/config/schema.ts +196 -0
- package/src/config/types.ts +121 -0
- package/src/index.ts +53 -0
- package/src/lockfile/differ.ts +242 -0
- package/src/lockfile/hasher.ts +175 -0
- package/src/lockfile/index.ts +159 -0
- package/src/lockfile/types.ts +95 -0
- package/src/runtime/index.ts +114 -0
- package/src/server/api/index.ts +92 -0
- package/src/server/api/middleware.ts +118 -0
- package/src/server/api/router.ts +102 -0
- package/src/server/mcp/index.ts +182 -0
- package/src/server/mcp/tools.ts +199 -0
- package/src/server/resolver.ts +109 -0
- package/src/server/start.ts +151 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { loadConfig } from "../utils/config-loader.js";
|
|
4
|
+
import {
|
|
5
|
+
computeOntologyHash,
|
|
6
|
+
readLockfile,
|
|
7
|
+
diffOntology,
|
|
8
|
+
formatDiffForConsole,
|
|
9
|
+
writeLockfile,
|
|
10
|
+
} from "../../lockfile/index.js";
|
|
11
|
+
import { startBrowserServer } from "../../browser/server.js";
|
|
12
|
+
|
|
13
|
+
export const reviewCommand = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: "review",
|
|
16
|
+
description: "Review and approve ontology changes",
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
"auto-approve": {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
description: "Automatically approve changes (for CI/scripts)",
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
"print-only": {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
description: "Print diff and exit without prompting",
|
|
27
|
+
default: false,
|
|
28
|
+
},
|
|
29
|
+
cloud: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
description: "Sync with ont Cloud for team approvals",
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async run({ args }) {
|
|
36
|
+
try {
|
|
37
|
+
// Check for cloud mode
|
|
38
|
+
const cloudToken = process.env.ONT_CLOUD_TOKEN;
|
|
39
|
+
if (args.cloud || cloudToken) {
|
|
40
|
+
consola.info("");
|
|
41
|
+
consola.box(
|
|
42
|
+
"ont Cloud coming soon!\n\n" +
|
|
43
|
+
"Team approvals, audit trails, and compliance reporting.\n\n" +
|
|
44
|
+
"Sign up for early access: https://ont.dev/cloud"
|
|
45
|
+
);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load config
|
|
50
|
+
consola.info("Loading ontology config...");
|
|
51
|
+
const { config, configDir } = await loadConfig();
|
|
52
|
+
|
|
53
|
+
// Compute current ontology snapshot
|
|
54
|
+
const { ontology: newOntology, hash: newHash } = computeOntologyHash(config);
|
|
55
|
+
|
|
56
|
+
// Load existing lockfile
|
|
57
|
+
const lockfile = await readLockfile(configDir);
|
|
58
|
+
const oldOntology = lockfile?.ontology || null;
|
|
59
|
+
|
|
60
|
+
// Compute diff
|
|
61
|
+
const diff = diffOntology(oldOntology, newOntology);
|
|
62
|
+
|
|
63
|
+
// Handle print-only mode
|
|
64
|
+
if (args["print-only"]) {
|
|
65
|
+
if (diff.hasChanges) {
|
|
66
|
+
console.log("\n" + formatDiffForConsole(diff) + "\n");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
} else {
|
|
69
|
+
consola.success("No ontology changes detected.");
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle auto-approve mode
|
|
75
|
+
if (args["auto-approve"]) {
|
|
76
|
+
if (diff.hasChanges) {
|
|
77
|
+
console.log("\n" + formatDiffForConsole(diff) + "\n");
|
|
78
|
+
consola.info("Auto-approving changes...");
|
|
79
|
+
await writeLockfile(configDir, newOntology, newHash);
|
|
80
|
+
consola.success("Changes approved. Lockfile updated.");
|
|
81
|
+
} else {
|
|
82
|
+
consola.success("No ontology changes detected.");
|
|
83
|
+
if (!lockfile) {
|
|
84
|
+
consola.info("Writing initial lockfile...");
|
|
85
|
+
await writeLockfile(configDir, newOntology, newHash);
|
|
86
|
+
consola.success("Created ont.lock");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle initial lockfile creation (no previous state)
|
|
93
|
+
if (!lockfile && !diff.hasChanges) {
|
|
94
|
+
consola.info("Writing initial lockfile...");
|
|
95
|
+
await writeLockfile(configDir, newOntology, newHash);
|
|
96
|
+
consola.success("Created ont.lock");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Print diff summary to console if there are changes
|
|
100
|
+
if (diff.hasChanges) {
|
|
101
|
+
console.log("\n" + formatDiffForConsole(diff) + "\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Start unified browser/review UI
|
|
105
|
+
consola.info(diff.hasChanges ? "Starting review UI..." : "Starting ontology browser...");
|
|
106
|
+
const result = await startBrowserServer({
|
|
107
|
+
config,
|
|
108
|
+
diff: diff.hasChanges ? diff : null,
|
|
109
|
+
configDir,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (diff.hasChanges) {
|
|
113
|
+
if (result.approved) {
|
|
114
|
+
consola.success("Changes approved. Lockfile updated.");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
} else {
|
|
117
|
+
consola.warn("Changes rejected.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineCommand, runMain } from "citty";
|
|
2
|
+
import { initCommand } from "./commands/init.js";
|
|
3
|
+
import { reviewCommand } from "./commands/review.js";
|
|
4
|
+
|
|
5
|
+
const main = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "ont",
|
|
8
|
+
description: "Ontology - Ontology-first backends with human-approved AI access & edits",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
},
|
|
11
|
+
subCommands: {
|
|
12
|
+
init: initCommand,
|
|
13
|
+
review: reviewCommand,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function run() {
|
|
18
|
+
runMain(main);
|
|
19
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, dirname, resolve } from "path";
|
|
3
|
+
import type { OntologyConfig } from "../../config/types.js";
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILENAMES = ["ontology.config.ts", "ontology.config.js"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find the ontology config file in the given directory or its parents
|
|
9
|
+
*/
|
|
10
|
+
export function findConfigFile(startDir: string = process.cwd()): string | null {
|
|
11
|
+
let currentDir = resolve(startDir);
|
|
12
|
+
|
|
13
|
+
while (true) {
|
|
14
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
15
|
+
const configPath = join(currentDir, filename);
|
|
16
|
+
if (existsSync(configPath)) {
|
|
17
|
+
return configPath;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parentDir = dirname(currentDir);
|
|
22
|
+
if (parentDir === currentDir) {
|
|
23
|
+
// Reached root
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
currentDir = parentDir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the ontology config from a file
|
|
32
|
+
*/
|
|
33
|
+
export async function loadConfig(configPath?: string): Promise<{
|
|
34
|
+
config: OntologyConfig;
|
|
35
|
+
configDir: string;
|
|
36
|
+
configPath: string;
|
|
37
|
+
}> {
|
|
38
|
+
// Find config file if not provided
|
|
39
|
+
const resolvedPath = configPath || findConfigFile();
|
|
40
|
+
|
|
41
|
+
if (!resolvedPath) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Could not find ontology.config.ts in current directory or any parent directory.\n" +
|
|
44
|
+
"Run `ont init` to create a new project."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!existsSync(resolvedPath)) {
|
|
49
|
+
throw new Error(`Config file not found: ${resolvedPath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const configDir = dirname(resolvedPath);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Dynamic import the config
|
|
56
|
+
const module = await import(resolvedPath);
|
|
57
|
+
const config = module.default as OntologyConfig;
|
|
58
|
+
|
|
59
|
+
if (!config || typeof config !== "object") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Config file must export a default object created with defineOntology()"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!config.name || !config.functions) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Invalid config: missing required fields (name, functions)"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { config, configDir, configPath: resolvedPath };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if ((error as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") {
|
|
74
|
+
throw new Error(`Failed to load config: ${resolvedPath}`);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Symbol for storing fieldFrom metadata on Zod schemas
|
|
5
|
+
*/
|
|
6
|
+
export const FIELD_FROM_METADATA = Symbol.for("ont:fieldFrom");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metadata stored on fieldFrom Zod schemas
|
|
10
|
+
*/
|
|
11
|
+
export interface FieldFromMetadata {
|
|
12
|
+
/** Name of the function that provides options for this field */
|
|
13
|
+
functionName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type for a Zod string with fieldFrom metadata
|
|
18
|
+
*/
|
|
19
|
+
export type FieldFromString = z.ZodString & {
|
|
20
|
+
[FIELD_FROM_METADATA]: FieldFromMetadata;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a string field that gets its options from another function.
|
|
25
|
+
*
|
|
26
|
+
* The referenced function should return `{ value: string, label: string }[]`.
|
|
27
|
+
* - If the function has empty inputs `z.object({})`, it's treated as **bulk** (all options fetched at once)
|
|
28
|
+
* - If the function has a `query` input, it's treated as **autocomplete** (options searched)
|
|
29
|
+
*
|
|
30
|
+
* @param functionName - Name of the function that provides options
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* defineOntology({
|
|
35
|
+
* functions: {
|
|
36
|
+
* // Bulk options source (empty inputs)
|
|
37
|
+
* getUserStatuses: {
|
|
38
|
+
* description: 'Get available user statuses',
|
|
39
|
+
* access: ['admin'],
|
|
40
|
+
* entities: [],
|
|
41
|
+
* inputs: z.object({}),
|
|
42
|
+
* outputs: z.array(z.object({ value: z.string(), label: z.string() })),
|
|
43
|
+
* resolver: './resolvers/options/userStatuses.ts',
|
|
44
|
+
* },
|
|
45
|
+
*
|
|
46
|
+
* // Autocomplete source (has query input)
|
|
47
|
+
* searchTeams: {
|
|
48
|
+
* description: 'Search for teams',
|
|
49
|
+
* access: ['admin'],
|
|
50
|
+
* entities: [],
|
|
51
|
+
* inputs: z.object({ query: z.string() }),
|
|
52
|
+
* outputs: z.array(z.object({ value: z.string(), label: z.string() })),
|
|
53
|
+
* resolver: './resolvers/options/searchTeams.ts',
|
|
54
|
+
* },
|
|
55
|
+
*
|
|
56
|
+
* // Function using fieldFrom
|
|
57
|
+
* createUser: {
|
|
58
|
+
* description: 'Create a user',
|
|
59
|
+
* access: ['admin'],
|
|
60
|
+
* entities: ['User'],
|
|
61
|
+
* inputs: z.object({
|
|
62
|
+
* name: z.string(),
|
|
63
|
+
* status: fieldFrom('getUserStatuses'),
|
|
64
|
+
* team: fieldFrom('searchTeams'),
|
|
65
|
+
* }),
|
|
66
|
+
* resolver: './resolvers/createUser.ts',
|
|
67
|
+
* },
|
|
68
|
+
* },
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function fieldFrom(functionName: string): FieldFromString {
|
|
73
|
+
const schema = z.string() as FieldFromString;
|
|
74
|
+
schema[FIELD_FROM_METADATA] = { functionName };
|
|
75
|
+
return schema;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a Zod schema has fieldFrom metadata
|
|
80
|
+
*/
|
|
81
|
+
export function hasFieldFromMetadata(
|
|
82
|
+
schema: unknown
|
|
83
|
+
): schema is FieldFromString {
|
|
84
|
+
return (
|
|
85
|
+
schema !== null &&
|
|
86
|
+
typeof schema === "object" &&
|
|
87
|
+
FIELD_FROM_METADATA in schema
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract fieldFrom metadata from a Zod schema
|
|
93
|
+
*/
|
|
94
|
+
export function getFieldFromMetadata(
|
|
95
|
+
schema: unknown
|
|
96
|
+
): FieldFromMetadata | null {
|
|
97
|
+
if (hasFieldFromMetadata(schema)) {
|
|
98
|
+
return schema[FIELD_FROM_METADATA];
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OntologyConfigSchema,
|
|
3
|
+
validateAccessGroups,
|
|
4
|
+
validateEntityReferences,
|
|
5
|
+
validateFieldFromReferences,
|
|
6
|
+
} from "./schema.js";
|
|
7
|
+
import type {
|
|
8
|
+
OntologyConfig,
|
|
9
|
+
FunctionDefinition,
|
|
10
|
+
AccessGroupConfig,
|
|
11
|
+
EnvironmentConfig,
|
|
12
|
+
EntityDefinition,
|
|
13
|
+
AuthFunction,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Define an Ontology configuration with full type inference.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { defineOntology, fieldFrom } from 'ont-run';
|
|
22
|
+
* import { z } from 'zod';
|
|
23
|
+
*
|
|
24
|
+
* export default defineOntology({
|
|
25
|
+
* name: 'my-api',
|
|
26
|
+
* environments: {
|
|
27
|
+
* dev: { debug: true },
|
|
28
|
+
* prod: { debug: false },
|
|
29
|
+
* },
|
|
30
|
+
* auth: async (req) => {
|
|
31
|
+
* const token = req.headers.get('Authorization');
|
|
32
|
+
* return token ? ['admin'] : ['public'];
|
|
33
|
+
* },
|
|
34
|
+
* accessGroups: {
|
|
35
|
+
* public: { description: 'Unauthenticated users' },
|
|
36
|
+
* admin: { description: 'Administrators' },
|
|
37
|
+
* },
|
|
38
|
+
* entities: {
|
|
39
|
+
* User: { description: 'A user account' },
|
|
40
|
+
* },
|
|
41
|
+
* functions: {
|
|
42
|
+
* getUser: {
|
|
43
|
+
* description: 'Get a user by ID',
|
|
44
|
+
* access: ['public', 'admin'],
|
|
45
|
+
* entities: ['User'],
|
|
46
|
+
* inputs: z.object({ id: z.string() }),
|
|
47
|
+
* resolver: './resolvers/getUser.ts',
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function defineOntology<
|
|
54
|
+
TGroups extends string,
|
|
55
|
+
TEntities extends string,
|
|
56
|
+
TFunctions extends Record<string, FunctionDefinition<TGroups, TEntities>>,
|
|
57
|
+
>(config: {
|
|
58
|
+
name: string;
|
|
59
|
+
environments: Record<string, EnvironmentConfig>;
|
|
60
|
+
auth: AuthFunction;
|
|
61
|
+
accessGroups: Record<TGroups, AccessGroupConfig>;
|
|
62
|
+
entities?: Record<TEntities, EntityDefinition>;
|
|
63
|
+
functions: TFunctions;
|
|
64
|
+
}): OntologyConfig<TGroups, TEntities, TFunctions> {
|
|
65
|
+
// Validate the config structure
|
|
66
|
+
const parsed = OntologyConfigSchema.parse(config);
|
|
67
|
+
|
|
68
|
+
// Validate that all access groups referenced in functions exist
|
|
69
|
+
validateAccessGroups(parsed);
|
|
70
|
+
|
|
71
|
+
// Validate that all entities referenced in functions exist
|
|
72
|
+
validateEntityReferences(parsed);
|
|
73
|
+
|
|
74
|
+
// Validate that all fieldFrom() references point to existing functions
|
|
75
|
+
validateFieldFromReferences(parsed);
|
|
76
|
+
|
|
77
|
+
return config as OntologyConfig<TGroups, TEntities, TFunctions>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { defineOntology } from "./define.js";
|
|
2
|
+
export { fieldFrom } from "./categorical.js";
|
|
3
|
+
export type {
|
|
4
|
+
OntologyConfig,
|
|
5
|
+
FunctionDefinition,
|
|
6
|
+
AccessGroupConfig,
|
|
7
|
+
EnvironmentConfig,
|
|
8
|
+
EntityDefinition,
|
|
9
|
+
AuthFunction,
|
|
10
|
+
ResolverContext,
|
|
11
|
+
ResolverFunction,
|
|
12
|
+
FieldOption,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
export {
|
|
15
|
+
OntologyConfigSchema,
|
|
16
|
+
FunctionDefinitionSchema,
|
|
17
|
+
AccessGroupConfigSchema,
|
|
18
|
+
EnvironmentConfigSchema,
|
|
19
|
+
EntityDefinitionSchema,
|
|
20
|
+
validateAccessGroups,
|
|
21
|
+
validateEntityReferences,
|
|
22
|
+
validateFieldFromReferences,
|
|
23
|
+
} from "./schema.js";
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getFieldFromMetadata } from "./categorical.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Schema for environment configuration
|
|
6
|
+
*/
|
|
7
|
+
export const EnvironmentConfigSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
debug: z.boolean().optional(),
|
|
10
|
+
})
|
|
11
|
+
.passthrough();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Schema for access group configuration
|
|
15
|
+
*/
|
|
16
|
+
export const AccessGroupConfigSchema = z.object({
|
|
17
|
+
description: z.string(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Schema for entity definition
|
|
22
|
+
*/
|
|
23
|
+
export const EntityDefinitionSchema = z.object({
|
|
24
|
+
description: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a value is a Zod schema (duck typing to work across bundle boundaries)
|
|
29
|
+
*/
|
|
30
|
+
function isZodSchema(val: unknown): boolean {
|
|
31
|
+
return (
|
|
32
|
+
val !== null &&
|
|
33
|
+
typeof val === "object" &&
|
|
34
|
+
"_def" in val &&
|
|
35
|
+
"safeParse" in val &&
|
|
36
|
+
typeof (val as { safeParse: unknown }).safeParse === "function"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schema for function definition
|
|
42
|
+
*/
|
|
43
|
+
export const FunctionDefinitionSchema = z.object({
|
|
44
|
+
description: z.string(),
|
|
45
|
+
access: z.array(z.string()).min(1),
|
|
46
|
+
entities: z.array(z.string()),
|
|
47
|
+
inputs: z.custom<z.ZodType>(isZodSchema, {
|
|
48
|
+
message: "inputs must be a Zod schema",
|
|
49
|
+
}),
|
|
50
|
+
outputs: z
|
|
51
|
+
.custom<z.ZodType>(isZodSchema, {
|
|
52
|
+
message: "outputs must be a Zod schema",
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
55
|
+
resolver: z.string(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Schema for auth function
|
|
60
|
+
*/
|
|
61
|
+
export const AuthFunctionSchema = z
|
|
62
|
+
.function()
|
|
63
|
+
.args(z.custom<Request>())
|
|
64
|
+
.returns(z.union([z.array(z.string()), z.promise(z.array(z.string()))]));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schema for the full ontology configuration
|
|
68
|
+
*/
|
|
69
|
+
export const OntologyConfigSchema = z.object({
|
|
70
|
+
name: z.string().min(1),
|
|
71
|
+
environments: z.record(z.string(), EnvironmentConfigSchema),
|
|
72
|
+
auth: z.function(),
|
|
73
|
+
accessGroups: z.record(z.string(), AccessGroupConfigSchema),
|
|
74
|
+
entities: z.record(z.string(), EntityDefinitionSchema).optional(),
|
|
75
|
+
functions: z.record(z.string(), FunctionDefinitionSchema),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate that all function access groups exist in accessGroups
|
|
80
|
+
*/
|
|
81
|
+
export function validateAccessGroups(
|
|
82
|
+
config: z.infer<typeof OntologyConfigSchema>
|
|
83
|
+
): void {
|
|
84
|
+
const validGroups = new Set(Object.keys(config.accessGroups));
|
|
85
|
+
|
|
86
|
+
for (const [fnName, fn] of Object.entries(config.functions)) {
|
|
87
|
+
for (const group of fn.access) {
|
|
88
|
+
if (!validGroups.has(group)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Function "${fnName}" references unknown access group "${group}". ` +
|
|
91
|
+
`Valid groups: ${[...validGroups].join(", ")}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate that all function entity references exist in entities
|
|
100
|
+
*/
|
|
101
|
+
export function validateEntityReferences(
|
|
102
|
+
config: z.infer<typeof OntologyConfigSchema>
|
|
103
|
+
): void {
|
|
104
|
+
const validEntities = config.entities
|
|
105
|
+
? new Set(Object.keys(config.entities))
|
|
106
|
+
: new Set<string>();
|
|
107
|
+
|
|
108
|
+
for (const [fnName, fn] of Object.entries(config.functions)) {
|
|
109
|
+
for (const entity of fn.entities) {
|
|
110
|
+
if (!validEntities.has(entity)) {
|
|
111
|
+
const validList =
|
|
112
|
+
validEntities.size > 0
|
|
113
|
+
? [...validEntities].join(", ")
|
|
114
|
+
: "(none defined)";
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Function "${fnName}" references unknown entity "${entity}". ` +
|
|
117
|
+
`Valid entities: ${validList}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recursively extract fieldFrom references from a Zod schema
|
|
126
|
+
*/
|
|
127
|
+
function extractFieldFromRefs(
|
|
128
|
+
schema: z.ZodType<unknown>,
|
|
129
|
+
path: string = ""
|
|
130
|
+
): Array<{ path: string; functionName: string }> {
|
|
131
|
+
const results: Array<{ path: string; functionName: string }> = [];
|
|
132
|
+
|
|
133
|
+
// Check if this schema has fieldFrom metadata
|
|
134
|
+
const metadata = getFieldFromMetadata(schema);
|
|
135
|
+
if (metadata) {
|
|
136
|
+
results.push({
|
|
137
|
+
path: path || "(root)",
|
|
138
|
+
functionName: metadata.functionName,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle ZodObject - recurse into properties
|
|
143
|
+
if (schema instanceof z.ZodObject) {
|
|
144
|
+
const shape = schema.shape;
|
|
145
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
146
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
147
|
+
results.push(
|
|
148
|
+
...extractFieldFromRefs(value as z.ZodType<unknown>, fieldPath)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle ZodOptional - unwrap
|
|
154
|
+
if (schema instanceof z.ZodOptional) {
|
|
155
|
+
results.push(...extractFieldFromRefs(schema.unwrap(), path));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle ZodNullable - unwrap
|
|
159
|
+
if (schema instanceof z.ZodNullable) {
|
|
160
|
+
results.push(...extractFieldFromRefs(schema.unwrap(), path));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle ZodArray - recurse into element
|
|
164
|
+
if (schema instanceof z.ZodArray) {
|
|
165
|
+
results.push(...extractFieldFromRefs(schema.element, `${path}[]`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle ZodDefault - unwrap
|
|
169
|
+
if (schema instanceof z.ZodDefault) {
|
|
170
|
+
results.push(...extractFieldFromRefs(schema._def.innerType, path));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Validate that all fieldFrom() references point to existing functions
|
|
178
|
+
*/
|
|
179
|
+
export function validateFieldFromReferences(
|
|
180
|
+
config: z.infer<typeof OntologyConfigSchema>
|
|
181
|
+
): void {
|
|
182
|
+
const validFunctions = new Set(Object.keys(config.functions));
|
|
183
|
+
|
|
184
|
+
for (const [fnName, fn] of Object.entries(config.functions)) {
|
|
185
|
+
const refs = extractFieldFromRefs(fn.inputs);
|
|
186
|
+
|
|
187
|
+
for (const ref of refs) {
|
|
188
|
+
if (!validFunctions.has(ref.functionName)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Function "${fnName}" field "${ref.path}" references unknown function "${ref.functionName}" via fieldFrom(). ` +
|
|
191
|
+
`Valid functions: ${[...validFunctions].join(", ")}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|