vovk 3.7.0 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/client/create-rpc.d.ts +3 -3
  3. package/dist/client/create-rpc.js +6 -8
  4. package/dist/client/default-stream-handler.d.ts +1 -5
  5. package/dist/client/default-stream-handler.js +53 -45
  6. package/dist/client/fetcher.d.ts +1 -1
  7. package/dist/client/fetcher.js +53 -4
  8. package/dist/client/progressive.d.ts +1 -5
  9. package/dist/client/progressive.js +19 -9
  10. package/dist/client/serialize-query.d.ts +0 -11
  11. package/dist/client/serialize-query.js +4 -28
  12. package/dist/core/controllers-to-static-params.d.ts +1 -2
  13. package/dist/core/controllers-to-static-params.js +1 -2
  14. package/dist/core/create-decorator.d.ts +1 -1
  15. package/dist/core/decorate.d.ts +2 -6
  16. package/dist/core/decorators.js +3 -3
  17. package/dist/core/get-schema.d.ts +1 -1
  18. package/dist/core/get-schema.js +1 -3
  19. package/dist/core/http-exception.d.ts +1 -1
  20. package/dist/core/http-exception.js +1 -1
  21. package/dist/core/init-segment.js +1 -1
  22. package/dist/core/json-lines-responder.d.ts +8 -12
  23. package/dist/core/json-lines-responder.js +42 -20
  24. package/dist/core/vovk-app.d.ts +1 -1
  25. package/dist/core/vovk-app.js +62 -16
  26. package/dist/index.d.ts +16 -18
  27. package/dist/index.js +14 -15
  28. package/dist/internal.d.ts +14 -12
  29. package/dist/internal.js +8 -7
  30. package/dist/openapi/error.js +2 -2
  31. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +1 -12
  32. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +4 -10
  33. package/dist/openapi/openapi-to-vovk-schema/index.d.ts +1 -1
  34. package/dist/openapi/openapi-to-vovk-schema/index.js +20 -6
  35. package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +0 -7
  36. package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +2 -13
  37. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +0 -5
  38. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +3 -8
  39. package/dist/openapi/vovk-schema-to-openapi.d.ts +1 -1
  40. package/dist/openapi/vovk-schema-to-openapi.js +9 -3
  41. package/dist/req/parse-query.d.ts +0 -23
  42. package/dist/req/parse-query.js +51 -43
  43. package/dist/req/validate-content-type.js +1 -1
  44. package/dist/samples/create-code-samples.d.ts +1 -1
  45. package/dist/samples/create-code-samples.js +4 -3
  46. package/dist/samples/schema-to-code.d.ts +1 -1
  47. package/dist/samples/schema-to-code.js +18 -15
  48. package/dist/samples/schema-to-object.d.ts +1 -1
  49. package/dist/samples/schema-to-object.js +18 -15
  50. package/dist/tools/create-tool-factory.d.ts +2 -2
  51. package/dist/tools/derive-tools.d.ts +9 -13
  52. package/dist/tools/derive-tools.js +48 -47
  53. package/dist/tools/to-model-output-default.d.ts +2 -2
  54. package/dist/tools/to-model-output-default.js +20 -6
  55. package/dist/tools/to-model-output-mcp.d.ts +3 -2
  56. package/dist/tools/to-model-output.d.ts +1 -1
  57. package/dist/types/client.d.ts +4 -4
  58. package/dist/types/config.d.ts +5 -6
  59. package/dist/types/core.d.ts +4 -1
  60. package/dist/types/enums.d.ts +1 -1
  61. package/dist/types/enums.js +1 -1
  62. package/dist/types/inference.d.ts +2 -3
  63. package/dist/types/request.d.ts +1 -5
  64. package/dist/types/standard-schema.js +0 -2
  65. package/dist/types/standard-tool.d.ts +8 -12
  66. package/dist/types/tools.d.ts +2 -40
  67. package/dist/types/validation.d.ts +3 -3
  68. package/dist/utils/camel-case.d.ts +0 -5
  69. package/dist/utils/camel-case.js +2 -10
  70. package/dist/utils/deep-extend.d.ts +1 -10
  71. package/dist/utils/deep-extend.js +16 -4
  72. package/dist/validation/create-standard-validation.d.ts +3 -126
  73. package/dist/validation/create-standard-validation.js +2 -3
  74. package/dist/validation/create-validate-on-client.d.ts +3 -3
  75. package/dist/validation/json-schema-only-spec.d.ts +14 -0
  76. package/dist/validation/json-schema-only-spec.js +57 -0
  77. package/dist/validation/procedure.d.ts +1 -124
  78. package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +0 -9
  79. package/dist/validation/validation-schemas-object-to-single-validation-schema.js +11 -14
  80. package/dist/validation/with-validation-library.d.ts +2 -2
  81. package/dist/validation/with-validation-library.js +11 -5
  82. package/package.json +3 -10
@@ -1,6 +1,4 @@
1
- // Collect the trailing name of every `$ref` in the tree. Covers both pointer styles
2
- // the transform emits: `#/components/schemas/X` (response slots, raw operation objects)
3
- // and `#/$defs/X` (request slots embed components under their original names).
1
+ // collects the trailing name of every $ref in the tree, both `#/components/schemas/X` and `#/$defs/X` styles
4
2
  function collectRefNames(node, into) {
5
3
  if (!node || typeof node !== 'object')
6
4
  return;
@@ -21,11 +19,8 @@ function collectRefNames(node, into) {
21
19
  }
22
20
  }
23
21
  }
24
- /**
25
- * Shrinks a `components.schemas` dict to the transitive `$ref` closure of `roots`
26
- * (BFS with a visited set — component graphs of large specs like Stripe are cyclic).
27
- * Preserves the original key order for deterministic output.
28
- */
22
+ // shrinks components.schemas to the transitive $ref closure of roots, BFS with a visited set
23
+ // (big specs like Stripe are cyclic); preserves key order for deterministic output
29
24
  export function pruneComponentsSchemas(roots, componentsSchemas) {
30
25
  const required = new Set();
31
26
  collectRefNames(roots, required);
@@ -1,5 +1,5 @@
1
1
  import type { OpenAPIObject } from 'openapi3-ts/oas31';
2
- import type { VovkOutputConfig, VovkStrictConfig, VovkReadmeConfig, VovkSamplesConfig, VovkPackageJson } from '../types/config.js';
2
+ import type { VovkOutputConfig, VovkPackageJson, VovkReadmeConfig, VovkSamplesConfig, VovkStrictConfig } from '../types/config.js';
3
3
  import type { VovkSchema } from '../types/core.js';
4
4
  export declare function vovkSchemaToOpenAPI({ config, rootEntry, schema: fullSchema, outputConfigs, forceOutputConfigs, isBundle, segmentName: givenSegmentName, projectPackageJson, }: {
5
5
  config: VovkStrictConfig | undefined;
@@ -1,6 +1,6 @@
1
+ import { resolveGeneratorConfigValues } from '../core/resolve-generator-config-values.js';
1
2
  import { createCodeSamples } from '../samples/create-code-samples.js';
2
3
  import { schemaToObject } from '../samples/schema-to-object.js';
3
- import { resolveGeneratorConfigValues } from '../core/resolve-generator-config-values.js';
4
4
  import { HttpStatus } from '../types/enums.js';
5
5
  function extractComponents(schema) {
6
6
  if (!schema)
@@ -119,7 +119,11 @@ export function vovkSchemaToOpenAPI({ config, rootEntry = 'api', schema: fullSch
119
119
  ],
120
120
  ...(queryParameters || pathParameters
121
121
  ? {
122
- parameters: h.operationObject?.parameters ?? [...(queryParameters || []), ...(pathParameters || [])],
122
+ // merge derived path/query parameters with user-declared ones, user wins on (name, in) collision
123
+ parameters: [
124
+ ...[...(queryParameters || []), ...(pathParameters || [])].filter((p) => !h.operationObject?.parameters?.some((up) => 'name' in up && up.name === p.name && up.in === p.in)),
125
+ ...(h.operationObject?.parameters ?? []),
126
+ ],
123
127
  }
124
128
  : {}),
125
129
  ...(paths[path][httpMethod].parameters
@@ -209,7 +213,9 @@ export function vovkSchemaToOpenAPI({ config, rootEntry = 'api', schema: fullSch
209
213
  components: {
210
214
  ...openAPIObject?.components,
211
215
  schemas: {
212
- ...(openAPIObject?.components?.schemas ?? components),
216
+ // merge so user-declared schemas extend the derived ones instead of replacing them
217
+ ...components,
218
+ ...openAPIObject?.components?.schemas,
213
219
  HttpStatus: {
214
220
  type: 'integer',
215
221
  description: 'HTTP status code',
@@ -1,24 +1 @@
1
- /**
2
- * Deserialize a bracket-based query string into an object.
3
- *
4
- * Supports:
5
- * - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
6
- * - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
7
- * - Mixed arrays of objects, etc.
8
- *
9
- * @example
10
- * parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
11
- * => {
12
- * x: "xx",
13
- * y: ["yy", "uu"],
14
- * z: {
15
- * f: "x",
16
- * u: ["uu", "xx"],
17
- * d: { x: "ee" }
18
- * }
19
- * }
20
- *
21
- * @param queryString - The raw query string (e.g. location.search.slice(1))
22
- * @returns - A nested object representing the query params
23
- */
24
1
  export declare function parseQuery(queryString: string): Record<string, unknown>;
@@ -1,10 +1,6 @@
1
- /**
2
- * Parse a bracket-based key (e.g. "z[d][0][x]" or "arr[]")
3
- * into an array of path segments (strings or special push-markers).
4
- *
5
- * Example: "z[d][0][x]" => ["z", "d", "0", "x"]
6
- * Example: "arr[]" => ["arr", "" ] // "" indicates "push" onto array
7
- */
1
+ // segments that would let a query string reach Object.prototype, such pairs are dropped like qs does
2
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
3
+ // bracket key to path segments: "z[d][0][x]" => ["z", "d", "0", "x"], "arr[]" => ["arr", ""] ("" means push)
8
4
  function parseKey(key) {
9
5
  // The first segment is everything up to the first '[' (or the entire key if no '[')
10
6
  const segments = [];
@@ -28,14 +24,34 @@ function parseKey(key) {
28
24
  }
29
25
  return segments;
30
26
  }
31
- /**
32
- * Recursively set a value in a nested object/array, given a path of segments.
33
- * - If segment is numeric => treat as array index
34
- * - If segment is empty "" => push to array
35
- * - Else => object property
36
- */
27
+ // past this an index becomes an object key, like qs does, so a short query cannot size a huge array
28
+ const ARRAY_LIMIT = 100;
29
+ // digits only, Number() would take "-1" and "1e2" and then drop the value on an array
30
+ function isArrayIndex(segment) {
31
+ return /^\d+$/.test(segment) && Number(segment) <= ARRAY_LIMIT;
32
+ }
33
+ // which container the next segment needs, "" is a push and so wants an array too
34
+ function wantsArray(segment) {
35
+ return typeof segment === 'string' && (segment === '' || isArrayIndex(segment));
36
+ }
37
+ // sets a value at a segment path: numeric => array index, "" => array push, else object property
37
38
  function setValue(obj, path, value) {
38
39
  let current = obj;
40
+ let parent = null;
41
+ let parentKey = '';
42
+ // an array only keeps numeric indices, any other key is dropped on serialization,
43
+ // so replace the array with an object before setting one
44
+ const demoteArray = () => {
45
+ if (!Array.isArray(current))
46
+ return;
47
+ const replacement = {};
48
+ const source = current;
49
+ for (const key of Object.keys(source))
50
+ replacement[key] = source[key];
51
+ if (parent)
52
+ parent[parentKey] = replacement;
53
+ current = replacement;
54
+ };
39
55
  for (let i = 0; i < path.length; i++) {
40
56
  const segment = path[i];
41
57
  // If we're at the last segment, set the value
@@ -47,7 +63,7 @@ function setValue(obj, path, value) {
47
63
  }
48
64
  current.push(value);
49
65
  }
50
- else if (!Number.isNaN(Number(segment))) {
66
+ else if (isArrayIndex(segment)) {
51
67
  // Numeric segment => array index
52
68
  const idx = Number(segment);
53
69
  if (!Array.isArray(current)) {
@@ -57,6 +73,7 @@ function setValue(obj, path, value) {
57
73
  }
58
74
  else {
59
75
  // Object property
76
+ demoteArray();
60
77
  current[segment] = value;
61
78
  }
62
79
  }
@@ -73,9 +90,9 @@ function setValue(obj, path, value) {
73
90
  // for the next segment. We'll push something and move current to that.
74
91
  if (current.length === 0) {
75
92
  // nothing in array yet
76
- current.push(typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {});
93
+ current.push(wantsArray(nextSegment) ? [] : {});
77
94
  }
78
- else if (typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment))) {
95
+ else if (wantsArray(nextSegment)) {
79
96
  // next is numeric => we want an array
80
97
  if (!Array.isArray(current[current.length - 1])) {
81
98
  current[current.length - 1] = [];
@@ -87,9 +104,11 @@ function setValue(obj, path, value) {
87
104
  current[current.length - 1] = {};
88
105
  }
89
106
  }
107
+ parent = current;
108
+ parentKey = current.length - 1;
90
109
  current = current[current.length - 1];
91
110
  }
92
- else if (!Number.isNaN(Number(segment))) {
111
+ else if (isArrayIndex(segment)) {
93
112
  // segment is numeric => array index
94
113
  const idx = Number(segment);
95
114
  if (!Array.isArray(current)) {
@@ -97,44 +116,28 @@ function setValue(obj, path, value) {
97
116
  }
98
117
  if (current[idx] === undefined) {
99
118
  // Create placeholder for next segment
100
- current[idx] = typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {};
119
+ current[idx] = wantsArray(nextSegment) ? [] : {};
101
120
  }
121
+ parent = current;
122
+ parentKey = idx;
102
123
  current = current[idx];
103
124
  }
104
125
  else {
105
126
  // segment is an object key
127
+ demoteArray();
106
128
  if (current[segment] === undefined) {
107
129
  // Create placeholder
108
- current[segment] = typeof nextSegment === 'string' && !Number.isNaN(Number(nextSegment)) ? [] : {};
130
+ current[segment] = wantsArray(nextSegment) ? [] : {};
109
131
  }
132
+ parent = current;
133
+ parentKey = segment;
110
134
  current = current[segment];
111
135
  }
112
136
  }
113
137
  }
114
138
  }
115
- /**
116
- * Deserialize a bracket-based query string into an object.
117
- *
118
- * Supports:
119
- * - Key/value pairs with nested brackets (e.g. "a[b][0]=value")
120
- * - Arrays with empty bracket (e.g. "arr[]=1&arr[]=2")
121
- * - Mixed arrays of objects, etc.
122
- *
123
- * @example
124
- * parseQuery("x=xx&y[0]=yy&y[1]=uu&z[f]=x&z[u][0]=uu&z[u][1]=xx&z[d][x]=ee")
125
- * => {
126
- * x: "xx",
127
- * y: ["yy", "uu"],
128
- * z: {
129
- * f: "x",
130
- * u: ["uu", "xx"],
131
- * d: { x: "ee" }
132
- * }
133
- * }
134
- *
135
- * @param queryString - The raw query string (e.g. location.search.slice(1))
136
- * @returns - A nested object representing the query params
137
- */
139
+ // bracket query string to a nested object, supports "a[b][0]=value", "arr[]=1&arr[]=2" etc,
140
+ // e.g. "x=xx&y[0]=yy&z[f]=x&z[d][x]=ee" => { x: "xx", y: ["yy"], z: { f: "x", d: { x: "ee" } } }
138
141
  export function parseQuery(queryString) {
139
142
  const result = {};
140
143
  if (!queryString)
@@ -144,11 +147,16 @@ export function parseQuery(queryString) {
144
147
  .replace(/^\?/, '') // Remove leading "?" if present
145
148
  .split('&');
146
149
  for (const pair of pairs) {
147
- const [rawKey, rawVal = ''] = pair.split('=');
150
+ // split at the first "=" only, unencoded "=" is legal inside values (base64, JWTs, signatures)
151
+ const eqIndex = pair.indexOf('=');
152
+ const rawKey = eqIndex === -1 ? pair : pair.slice(0, eqIndex);
153
+ const rawVal = eqIndex === -1 ? '' : pair.slice(eqIndex + 1);
148
154
  const decodedKey = decodeURIComponent(rawKey);
149
155
  const decodedVal = decodeURIComponent(rawVal);
150
156
  // Parse bracket notation
151
157
  const pathSegments = parseKey(decodedKey);
158
+ if (pathSegments.some((segment) => FORBIDDEN_KEYS.has(segment)))
159
+ continue;
152
160
  // Insert into the result object
153
161
  setValue(result, pathSegments, decodedVal);
154
162
  }
@@ -1,7 +1,7 @@
1
1
  import { HttpException } from '../core/http-exception.js';
2
2
  import { HttpStatus } from '../types/enums.js';
3
3
  export function validateContentType(request, allowed) {
4
- // Wildcard — skip validation
4
+ // wildcard, skip validation
5
5
  if (!request?.headers || allowed.includes('*/*'))
6
6
  return null;
7
7
  const raw = request.headers.get('content-type');
@@ -1,5 +1,5 @@
1
- import type { VovkControllerSchema, VovkHandlerSchema } from '../types/core.js';
2
1
  import type { VovkSamplesConfig } from '../types/config.js';
2
+ import type { VovkControllerSchema, VovkHandlerSchema } from '../types/core.js';
3
3
  export type CodeSamplePackageJson = {
4
4
  name?: string;
5
5
  version?: string;
@@ -1,5 +1,5 @@
1
- import { schemaToCode, getSampleValue } from './schema-to-code.js';
2
1
  import { objectToCode } from './object-to-code.js';
2
+ import { getSampleValue, schemaToCode } from './schema-to-code.js';
3
3
  const toSnakeCase = (str) => str
4
4
  .replace(/-/g, '_') // Replace hyphens with underscores
5
5
  .replace(/([a-z0-9])([A-Z])/g, '$1_$2') // Add underscore between lowercase/digit and uppercase
@@ -270,8 +270,9 @@ export function createCodeSamples({ handlerName, handlerSchema, controllerSchema
270
270
  const iterationValidation = handlerSchema?.validation?.iteration;
271
271
  const hasArg = !!queryValidation || !!bodyValidation || !!paramsValidation || !!config?.apiRoot || !!config?.headers;
272
272
  const rpcName = controllerSchema.rpcModuleName;
273
- const packageName = packageJson?.name || 'vovk-client';
274
- const packageNameSnake = toSnakeCase(packageName);
273
+ const packageName = packageJson?.name || '@/client';
274
+ // snake fallback avoids invalid "@/client" in py/rs imports
275
+ const packageNameSnake = toSnakeCase(packageJson?.name || 'client');
275
276
  const pyPackageName = packageJson?.py_name ?? packageNameSnake;
276
277
  const rsPackageName = packageJson?.rs_name ?? packageNameSnake;
277
278
  const commonParams = {
@@ -7,5 +7,5 @@ interface SamplerOptions {
7
7
  ignoreBinary?: boolean;
8
8
  }
9
9
  export declare function schemaToCode(schema: VovkJSONSchemaBase, options: SamplerOptions, rootSchema?: VovkJSONSchemaBase): string;
10
- export declare function getSampleValue(schema: VovkJSONSchemaBase, rootSchema?: VovkJSONSchemaBase, ignoreBinary?: boolean): unknown;
10
+ export declare function getSampleValue(schema: VovkJSONSchemaBase, rootSchema?: VovkJSONSchemaBase, ignoreBinary?: boolean, seen?: Set<string>): unknown;
11
11
  export {};
@@ -10,7 +10,7 @@ export function schemaToCode(schema, options, rootSchema) {
10
10
  return formatWithDescriptions(sampleValue, schema, rootSchema, comment, stripQuotes, indent, nestingIndent, ignoreBinary, true // isTopLevel
11
11
  );
12
12
  }
13
- export function getSampleValue(schema, rootSchema, ignoreBinary) {
13
+ export function getSampleValue(schema, rootSchema, ignoreBinary, seen = new Set()) {
14
14
  if (!schema || typeof schema !== 'object')
15
15
  return null;
16
16
  rootSchema = rootSchema || schema;
@@ -32,7 +32,7 @@ export function getSampleValue(schema, rootSchema, ignoreBinary) {
32
32
  }
33
33
  // Handle $ref if present
34
34
  if (schema.$ref) {
35
- return handleRef(schema.$ref, rootSchema, ignoreBinary);
35
+ return handleRef(schema.$ref, rootSchema, ignoreBinary, seen);
36
36
  }
37
37
  // Handle enum if present
38
38
  if (schema.enum && schema.enum.length > 0) {
@@ -40,15 +40,15 @@ export function getSampleValue(schema, rootSchema, ignoreBinary) {
40
40
  }
41
41
  // Handle oneOf, anyOf, allOf
42
42
  if (schema.oneOf && schema.oneOf.length > 0) {
43
- return getSampleValue(schema.oneOf[0], rootSchema, ignoreBinary);
43
+ return getSampleValue(schema.oneOf[0], rootSchema, ignoreBinary, seen);
44
44
  }
45
45
  if (schema.anyOf && schema.anyOf.length > 0) {
46
- return getSampleValue(schema.anyOf[0], rootSchema, ignoreBinary);
46
+ return getSampleValue(schema.anyOf[0], rootSchema, ignoreBinary, seen);
47
47
  }
48
48
  if (schema.allOf && schema.allOf.length > 0) {
49
49
  // Merge all schemas in allOf
50
50
  const mergedSchema = schema.allOf.reduce((acc, s) => Object.assign(acc, s), {});
51
- return getSampleValue(mergedSchema, rootSchema, ignoreBinary);
51
+ return getSampleValue(mergedSchema, rootSchema, ignoreBinary, seen);
52
52
  }
53
53
  // Handle different types
54
54
  if (schema.type) {
@@ -61,9 +61,9 @@ export function getSampleValue(schema, rootSchema, ignoreBinary) {
61
61
  case 'boolean':
62
62
  return handleBoolean();
63
63
  case 'object':
64
- return handleObject(schema, rootSchema, ignoreBinary);
64
+ return handleObject(schema, rootSchema, ignoreBinary, seen);
65
65
  case 'array':
66
- return handleArray(schema, rootSchema, ignoreBinary);
66
+ return handleArray(schema, rootSchema, ignoreBinary, seen);
67
67
  case 'null':
68
68
  return null;
69
69
  default:
@@ -72,7 +72,7 @@ export function getSampleValue(schema, rootSchema, ignoreBinary) {
72
72
  }
73
73
  // If type is not specified but properties are, treat it as an object
74
74
  if (schema.properties) {
75
- return handleObject(schema, rootSchema, ignoreBinary);
75
+ return handleObject(schema, rootSchema, ignoreBinary, seen);
76
76
  }
77
77
  // Default fallback
78
78
  return null;
@@ -154,9 +154,12 @@ function resolveRef(ref, rootSchema) {
154
154
  }
155
155
  return current;
156
156
  }
157
- function handleRef(ref, rootSchema, ignoreBinary) {
157
+ function handleRef(ref, rootSchema, ignoreBinary, seen) {
158
+ // a ref already being expanded means the schema is circular, stop instead of recursing forever
159
+ if (seen.has(ref))
160
+ return null;
158
161
  const resolved = resolveRef(ref, rootSchema);
159
- return getSampleValue(resolved, rootSchema, ignoreBinary);
162
+ return getSampleValue(resolved, rootSchema, ignoreBinary, new Set(seen).add(ref));
160
163
  }
161
164
  function handleString(schema) {
162
165
  if (schema.format) {
@@ -218,13 +221,13 @@ function handleNumber(schema) {
218
221
  function handleBoolean() {
219
222
  return true;
220
223
  }
221
- function handleObject(schema, rootSchema, ignoreBinary) {
224
+ function handleObject(schema, rootSchema, ignoreBinary, seen) {
222
225
  const result = {};
223
226
  if (schema.properties) {
224
227
  const required = schema.required || [];
225
228
  for (const [key, propSchema] of Object.entries(schema.properties)) {
226
229
  if (required.includes(key) || required.length === 0) {
227
- const value = getSampleValue(propSchema, rootSchema, ignoreBinary);
230
+ const value = getSampleValue(propSchema, rootSchema, ignoreBinary, seen);
228
231
  // Only add the property if it's not undefined (which happens when ignoreBinary is true and it's a binary field)
229
232
  if (value !== undefined) {
230
233
  result[key] = value;
@@ -233,14 +236,14 @@ function handleObject(schema, rootSchema, ignoreBinary) {
233
236
  }
234
237
  }
235
238
  if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
236
- const value = getSampleValue(schema.additionalProperties, rootSchema, ignoreBinary);
239
+ const value = getSampleValue(schema.additionalProperties, rootSchema, ignoreBinary, seen);
237
240
  if (value !== undefined) {
238
241
  result.additionalProp = value;
239
242
  }
240
243
  }
241
244
  return result;
242
245
  }
243
- function handleArray(schema, rootSchema, ignoreBinary) {
246
+ function handleArray(schema, rootSchema, ignoreBinary, seen) {
244
247
  if (schema.items) {
245
248
  // If items is a boolean, return empty array (true means any items allowed, false means no items)
246
249
  if (typeof schema.items === 'boolean') {
@@ -253,7 +256,7 @@ function handleArray(schema, rootSchema, ignoreBinary) {
253
256
  }
254
257
  const minItems = schema.minItems || 1;
255
258
  const numItems = Math.min(minItems, 3);
256
- const items = Array.from({ length: numItems }, () => getSampleValue(itemSchema, rootSchema, ignoreBinary)).filter((item) => item !== undefined); // Filter out undefined values from ignored binary items
259
+ const items = Array.from({ length: numItems }, () => getSampleValue(itemSchema, rootSchema, ignoreBinary, seen)).filter((item) => item !== undefined); // Filter out undefined values from ignored binary items
257
260
  // If all items were filtered out (e.g., all were binary), return undefined instead of empty array
258
261
  if (items.length === 0 && numItems > 0) {
259
262
  return undefined;
@@ -1,2 +1,2 @@
1
1
  import type { VovkJSONSchemaBase } from '../types/json-schema.js';
2
- export declare function schemaToObject(schema: VovkJSONSchemaBase, rootSchema?: VovkJSONSchemaBase): unknown;
2
+ export declare function schemaToObject(schema: VovkJSONSchemaBase, rootSchema?: VovkJSONSchemaBase, seen?: Set<string>): unknown;
@@ -1,4 +1,4 @@
1
- export function schemaToObject(schema, rootSchema) {
1
+ export function schemaToObject(schema, rootSchema, seen = new Set()) {
2
2
  if (!schema || typeof schema !== 'object')
3
3
  return null;
4
4
  // Use the input schema as the root if not provided
@@ -17,7 +17,7 @@ export function schemaToObject(schema, rootSchema) {
17
17
  }
18
18
  // Handle $ref if present
19
19
  if (schema.$ref) {
20
- return handleRef(schema.$ref, rootSchema);
20
+ return handleRef(schema.$ref, rootSchema, seen);
21
21
  }
22
22
  // Handle enum if present
23
23
  if (schema.enum && schema.enum.length > 0) {
@@ -25,15 +25,15 @@ export function schemaToObject(schema, rootSchema) {
25
25
  }
26
26
  // Handle oneOf, anyOf, allOf
27
27
  if (schema.oneOf && schema.oneOf.length > 0) {
28
- return schemaToObject(schema.oneOf[0], rootSchema);
28
+ return schemaToObject(schema.oneOf[0], rootSchema, seen);
29
29
  }
30
30
  if (schema.anyOf && schema.anyOf.length > 0) {
31
- return schemaToObject(schema.anyOf[0], rootSchema);
31
+ return schemaToObject(schema.anyOf[0], rootSchema, seen);
32
32
  }
33
33
  if (schema.allOf && schema.allOf.length > 0) {
34
34
  // Merge all schemas in allOf
35
35
  const mergedSchema = schema.allOf.reduce((acc, s) => Object.assign(acc, s), {});
36
- return schemaToObject(mergedSchema, rootSchema);
36
+ return schemaToObject(mergedSchema, rootSchema, seen);
37
37
  }
38
38
  // Handle different types
39
39
  if (schema.type) {
@@ -46,9 +46,9 @@ export function schemaToObject(schema, rootSchema) {
46
46
  case 'boolean':
47
47
  return handleBoolean();
48
48
  case 'object':
49
- return handleObject(schema, rootSchema);
49
+ return handleObject(schema, rootSchema, seen);
50
50
  case 'array':
51
- return handleArray(schema, rootSchema);
51
+ return handleArray(schema, rootSchema, seen);
52
52
  case 'null':
53
53
  return null;
54
54
  default:
@@ -57,12 +57,15 @@ export function schemaToObject(schema, rootSchema) {
57
57
  }
58
58
  // If type is not specified but properties are, treat it as an object
59
59
  if (schema.properties) {
60
- return handleObject(schema, rootSchema);
60
+ return handleObject(schema, rootSchema, seen);
61
61
  }
62
62
  // Default fallback
63
63
  return null;
64
64
  }
65
- function handleRef(ref, rootSchema) {
65
+ function handleRef(ref, rootSchema, seen) {
66
+ // a ref already being expanded means the schema is circular, stop instead of recursing forever
67
+ if (seen.has(ref))
68
+ return null;
66
69
  // Parse the reference path
67
70
  const path = ref.split('/').slice(1); // Remove the initial '#'
68
71
  // Navigate through the schema to find the referenced definition
@@ -74,7 +77,7 @@ function handleRef(ref, rootSchema) {
74
77
  }
75
78
  }
76
79
  // Process the referenced schema
77
- return schemaToObject(current, rootSchema);
80
+ return schemaToObject(current, rootSchema, new Set(seen).add(ref));
78
81
  }
79
82
  function handleString(schema) {
80
83
  if (schema.format) {
@@ -135,30 +138,30 @@ function handleNumber(schema) {
135
138
  function handleBoolean() {
136
139
  return true;
137
140
  }
138
- function handleObject(schema, rootSchema) {
141
+ function handleObject(schema, rootSchema, seen) {
139
142
  const result = {};
140
143
  if (schema.properties) {
141
144
  const required = schema.required || [];
142
145
  for (const [key, propSchema] of Object.entries(schema.properties)) {
143
146
  // Only include required properties or as a basic example
144
147
  if (required.includes(key) || required.length === 0) {
145
- result[key] = schemaToObject(propSchema, rootSchema);
148
+ result[key] = schemaToObject(propSchema, rootSchema, seen);
146
149
  }
147
150
  }
148
151
  }
149
152
  // Handle additionalProperties
150
153
  if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
151
- result.additionalProp = schemaToObject(schema.additionalProperties, rootSchema);
154
+ result.additionalProp = schemaToObject(schema.additionalProperties, rootSchema, seen);
152
155
  }
153
156
  return result;
154
157
  }
155
- function handleArray(schema, rootSchema) {
158
+ function handleArray(schema, rootSchema, seen) {
156
159
  if (schema.items && typeof schema.items === 'object') {
157
160
  const itemSchema = schema.items;
158
161
  const minItems = schema.minItems || 1;
159
162
  // Create minimum number of items (capped at a reasonable max for examples)
160
163
  const numItems = Math.min(minItems, 3);
161
- return Array.from({ length: numItems }, () => schemaToObject(itemSchema, rootSchema));
164
+ return Array.from({ length: numItems }, () => schemaToObject(itemSchema, rootSchema, seen));
162
165
  }
163
166
  return [];
164
167
  }
@@ -1,8 +1,8 @@
1
1
  import type { VovkValidationType } from '../types/core.js';
2
2
  import type { VovkTool } from '../types/tools.js';
3
- import type { DefaultModelOutput, ToModelOutputDefaultFn } from './to-model-output-default.js';
4
- import type { CombinedSpec } from '../types/validation.js';
5
3
  import type { KnownAny } from '../types/utils.js';
4
+ import type { CombinedSpec } from '../types/validation.js';
5
+ import type { DefaultModelOutput, ToModelOutputDefaultFn } from './to-model-output-default.js';
6
6
  type InferFormattedOutput<TFn, TOutput> = TFn extends ToModelOutputDefaultFn ? DefaultModelOutput<TOutput> : TFn extends (...args: KnownAny[]) => infer R ? R extends Promise<infer U> ? U : R : unknown;
7
7
  type AnyToModelOutputFn = (...args: KnownAny[]) => KnownAny;
8
8
  export declare function createToolFactory({ toJSONSchema, }: {
@@ -1,6 +1,7 @@
1
- import type { DefaultModelOutput } from './to-model-output-default.js';
2
1
  import type { VovkRequest } from '../types/request.js';
3
- import type { VovkTool, ToModelOutputFn } from '../types/tools.js';
2
+ import type { StandardToolV0 } from '../types/standard-tool.js';
3
+ import type { ToModelOutputFn } from '../types/tools.js';
4
+ import type { DefaultModelOutput } from './to-model-output-default.js';
4
5
  type DerivedToolInput = {
5
6
  body?: unknown;
6
7
  query?: unknown;
@@ -9,12 +10,8 @@ type DerivedToolInput = {
9
10
  type DeriveToolsBaseOptions<TOutput = unknown, TFormattedOutput = unknown> = {
10
11
  modules: Record<string, object>;
11
12
  meta?: Record<string, unknown>;
12
- onExecute?: (result: unknown, tool: VovkTool<DerivedToolInput, TOutput, TFormattedOutput>, req: Pick<VovkRequest, 'vovk'> | null) => void;
13
- onError?: (error: Error, tool: VovkTool<DerivedToolInput, TOutput, TFormattedOutput>, req: Pick<VovkRequest, 'vovk'> | null) => void;
14
- };
15
- type DeriveToolsResult<TOutput, TFormattedOutput> = {
16
- tools: VovkTool<DerivedToolInput, TOutput, TFormattedOutput>[];
17
- toolsByName: Record<string, VovkTool<DerivedToolInput, TOutput, TFormattedOutput>>;
13
+ onExecute?: (result: unknown, tool: StandardToolV0<DerivedToolInput, TOutput, TFormattedOutput>, req: Pick<VovkRequest, 'vovk'> | null) => void;
14
+ onError?: (error: Error, tool: StandardToolV0<DerivedToolInput, TOutput, TFormattedOutput>, req: Pick<VovkRequest, 'vovk'> | null) => void;
18
15
  };
19
16
  /**
20
17
  * Derives AI tools from controllers and RPC modules.
@@ -22,10 +19,9 @@ type DeriveToolsResult<TOutput, TFormattedOutput> = {
22
19
  * @example
23
20
  * ```ts
24
21
  * import { deriveTools, ToModelOutput } from 'vovk';
25
- * import { UserRPC } from 'vovk-client';
22
+ * import { UserRPC } from '@/client';
26
23
  *
27
- * // Derive AI tools from the UserRPC module
28
- * const { tools, toolsByName } = deriveTools({
24
+ * const tools = deriveTools({
29
25
  * modules: { UserRPC },
30
26
  * toModelOutput: ToModelOutput.MCP,
31
27
  * onExecute: (result, tool) => {
@@ -39,8 +35,8 @@ type DeriveToolsResult<TOutput, TFormattedOutput> = {
39
35
  */
40
36
  export declare function deriveTools<TOutput = unknown, TFormattedOutput = DefaultModelOutput<TOutput>>(options: DeriveToolsBaseOptions & {
41
37
  toModelOutput?: never;
42
- }): DeriveToolsResult<TOutput, TFormattedOutput>;
38
+ }): StandardToolV0<DerivedToolInput, TOutput, TFormattedOutput>[];
43
39
  export declare function deriveTools<TOutput = unknown, TFormattedOutput = unknown>(options: DeriveToolsBaseOptions & {
44
40
  toModelOutput: ToModelOutputFn<unknown, TOutput, TFormattedOutput>;
45
- }): DeriveToolsResult<TOutput, TFormattedOutput>;
41
+ }): StandardToolV0<DerivedToolInput, TOutput, TFormattedOutput>[];
46
42
  export {};