UNPKG

@firefliesai/schema-forge

Version:

Transform TypeScript classes into JSON Schema definitions with automatic support for OpenAI, Anthropic, and Google Gemini function calling (tool) formats

381 lines (380 loc) 17.1 kB
"use strict"; /** * Decorators for schema-forge */ Object.defineProperty(exports, "__esModule", { value: true }); exports.applyPropertyUpdates = applyPropertyUpdates; exports.ToolProp = ToolProp; exports.ToolMeta = ToolMeta; exports.updateSchemaProperty = updateSchemaProperty; exports.addSchemaProperty = addSchemaProperty; const class_validator_integration_1 = require("./class-validator-integration"); const core_1 = require("./core"); const types_1 = require("./types"); const utils_1 = require("./utils"); /** * Applies property updates for a given property path */ function applyPropertyUpdates(properties, paths, updates, target) { const [current, ...rest] = paths; // Handle nested path if (rest.length > 0) { if (!properties[current]) { properties[current] = {}; } // Get all metadata along the target property's full path const originalMetadata = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {}; let currentMetadata = originalMetadata; for (const path of paths) { currentMetadata = currentMetadata?.[path] || currentMetadata?.items?.properties?.[path] || currentMetadata?.properties?.[path] || {}; } // Handle enum update for nested property if (updates.enum) { const enumValues = (0, utils_1.extractEnumValues)(updates.enum); const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number'; const targetProp = properties[current]; if (targetProp.type === 'array') { // Update in items.properties if (!targetProp.items.properties) { targetProp.items.properties = {}; } let current = targetProp.items.properties; for (let i = 0; i < rest.length - 1; i++) { const path = rest[i]; if (!current[path]) { current[path] = { type: 'object', properties: {} }; } // If this is an array type property if (current[path].type === 'array') { if (!current[path].items.properties) { current[path].items.properties = {}; } current = current[path].items.properties; } else { // Object type property if (!current[path].properties) { current[path].properties = {}; } current = current[path].properties; } } // Get the last path segment const lastPath = rest[rest.length - 1]; // Check if the target property should be an array of enums or just an enum if (current[lastPath] && current[lastPath].type === 'array') { // Update items of an array property current[lastPath].items = { type: enumType, enum: enumValues, }; } else { // Direct enum property current[lastPath] = { ...(current[lastPath] || {}), type: enumType, enum: enumValues, description: current[lastPath]?.description || currentMetadata.description, }; } } else if (targetProp.type === 'object') { // Update in object properties if (!targetProp.properties) { targetProp.properties = {}; } let current = targetProp.properties; for (let i = 0; i < rest.length - 1; i++) { const path = rest[i]; if (!current[path]) { current[path] = { type: 'object', properties: {} }; } // If this is an array type property if (current[path].type === 'array') { if (!current[path].items.properties) { current[path].items.properties = {}; } current = current[path].items.properties; } else { // Object type property if (!current[path].properties) { current[path].properties = {}; } current = current[path].properties; } } // Get the last path segment const lastPath = rest[rest.length - 1]; // Update the enum property current[lastPath] = { ...(current[lastPath] || {}), type: enumType, enum: enumValues, description: current[lastPath]?.description || currentMetadata.description, }; } } else { // Update general nested path if (properties[current].type === 'array') { if (!properties[current].items.properties) { properties[current].items.properties = {}; } applyPropertyUpdates(properties[current].items.properties, rest, updates, target); } // Handle nested object properties else if (properties[current].type === 'object') { if (!properties[current].properties) { properties[current].properties = {}; } applyPropertyUpdates(properties[current].properties, rest, updates, target); } } return; } // Handle non-nested path const currentProperty = properties[current] || {}; if (updates.enum) { const enumValues = (0, utils_1.extractEnumValues)(updates.enum); const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number'; const type = Reflect.getMetadata('design:type', target.prototype, current); if (type === Array) { properties[current] = { ...currentProperty, type: 'array', items: { type: enumType, enum: enumValues, }, }; } else { properties[current] = { ...currentProperty, type: enumType, enum: enumValues, }; } const { enum: _, ...remainingUpdates } = updates; // Normalize items if present in updates if (remainingUpdates.items) { remainingUpdates.items = (0, utils_1.normalizeItemsType)(remainingUpdates.items); } Object.assign(properties[current], remainingUpdates); } else { const normalizedUpdates = { ...updates }; // Normalize items if present in updates if (normalizedUpdates.items) { normalizedUpdates.items = (0, utils_1.normalizeItemsType)(normalizedUpdates.items); } properties[current] = { ...currentProperty, ...normalizedUpdates, }; } } /** * Decorator for adding schema metadata to a class property */ function ToolProp(options = {}) { return function (target, propertyKey) { const type = Reflect.getMetadata('design:type', target, propertyKey); const classValidatorProps = (0, class_validator_integration_1.inferClassValidatorProperties)(target, propertyKey); // Exclude isOptional, keep only other options const { isOptional: _isOptional, ...finalOptions } = options; const ownProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target) || {}; const ownRequiredProps = Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, target) || []; const parentTarget = Object.getPrototypeOf(target); let currentProperties = {}; let currentRequiredProps = []; if (parentTarget && parentTarget !== Object.prototype) { const parentProperties = (0, utils_1.cloneMetadata)(Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, parentTarget) || {}); const parentRequiredProps = [ ...(Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, parentTarget) || []), ]; currentProperties = { ...parentProperties }; currentRequiredProps = [...parentRequiredProps]; } currentProperties = { ...currentProperties, ...ownProperties }; currentRequiredProps = [...new Set([...currentRequiredProps, ...ownRequiredProps])]; if (type === Array || classValidatorProps.isArray) { if (options.enum) { const enumValues = (0, utils_1.extractEnumValues)(options.enum); const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number'; finalOptions.type = 'array'; finalOptions.items = { type: enumType, enum: enumValues, }; delete finalOptions.enum; } else if (classValidatorProps.items && !options.items) { // Use class-validator inferred items (e.g. from @ArrayContains, or each: true decorators) finalOptions.type = 'array'; finalOptions.items = classValidatorProps.items; } else if (!options.items) { throw new Error(`Array property "${propertyKey}" needs explicit type information.`); } else if ((0, utils_1.isCustomClass)(options.items.type)) { const nestedSchema = (0, core_1.classToJsonSchema)(options.items.type); finalOptions.type = 'array'; finalOptions.items = nestedSchema; } else { finalOptions.type = 'array'; // Normalize constructor types (Date, String, Number, Boolean) to string literals const normalizedItems = (0, utils_1.normalizeItemsType)(options.items); // Merge class-validator item constraints (e.g. from each: true) into explicit items const explicitItems = normalizedItems; finalOptions.items = classValidatorProps.items && Object.keys(classValidatorProps.items).length > 0 ? { ...classValidatorProps.items, ...explicitItems } : explicitItems; } } else if (options.enum) { const enumValues = (0, utils_1.extractEnumValues)(options.enum); const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number'; finalOptions.type = enumType; finalOptions.enum = enumValues; } else if ((0, utils_1.isCustomClass)(type)) { const nestedSchema = (0, core_1.classToJsonSchema)(type); finalOptions.type = 'object'; finalOptions.properties = nestedSchema.properties; finalOptions.required = nestedSchema.required; } else if ((0, utils_1.isDateType)(type)) { finalOptions.type = 'string'; finalOptions.format = 'date-time'; } // Build the property schema const propertySchema = { ...finalOptions, type: classValidatorProps.type || finalOptions.type || (0, utils_1.getJsonSchemaType)(type), }; // Apply class-validator inferred properties only if not explicitly set in ToolProp options if (classValidatorProps.maxItems !== undefined && propertySchema.maxItems === undefined) { propertySchema.maxItems = classValidatorProps.maxItems; } if (classValidatorProps.minItems !== undefined && propertySchema.minItems === undefined) { propertySchema.minItems = classValidatorProps.minItems; } if (classValidatorProps.maximum !== undefined && propertySchema.maximum === undefined) { propertySchema.maximum = classValidatorProps.maximum; } if (classValidatorProps.minimum !== undefined && propertySchema.minimum === undefined) { propertySchema.minimum = classValidatorProps.minimum; } if (classValidatorProps.minLength !== undefined && propertySchema.minLength === undefined) { propertySchema.minLength = classValidatorProps.minLength; } if (classValidatorProps.maxLength !== undefined && propertySchema.maxLength === undefined) { propertySchema.maxLength = classValidatorProps.maxLength; } if (classValidatorProps.format !== undefined && propertySchema.format === undefined) { propertySchema.format = classValidatorProps.format; } if (classValidatorProps.uniqueItems !== undefined && propertySchema.uniqueItems === undefined) { propertySchema.uniqueItems = classValidatorProps.uniqueItems; } currentProperties[propertyKey] = propertySchema; // Handle required props only, don't store isOptional if (!options.isOptional) { if (!currentRequiredProps.includes(propertyKey)) { currentRequiredProps.push(propertyKey); } } else { currentRequiredProps = currentRequiredProps.filter((prop) => prop !== propertyKey); } Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, currentProperties, target); Reflect.defineMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, currentRequiredProps, target); }; } /** * Decorator for adding schema metadata to a class */ function ToolMeta(options = {}) { return function (target) { Reflect.defineMetadata('jsonSchema:options', options, target); }; } /** * Updates a property in a schema with new options */ function updateSchemaProperty(target, propertyPath, updates) { const paths = propertyPath.split('.'); const existingProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {}; applyPropertyUpdates(existingProperties, paths, updates, target); Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, existingProperties, target.prototype); } /** * Adds a new property to a schema */ function addSchemaProperty(target, propertyPath, options) { const finalOptions = { ...options }; if (options.enum) { const enumValues = (0, utils_1.extractEnumValues)(options.enum); const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number'; finalOptions.type = enumType; } else if (!options.type) { throw new Error('Either type or enum must be specified'); } const paths = propertyPath.split('.'); const existingProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {}; let current = existingProperties; for (let i = 0; i < paths.length - 1; i++) { const path = paths[i]; if (current[path]?.type === 'array') { if (!current[path].items.properties) { current[path].items.properties = {}; } current = current[path].items.properties; } else { if (!current[path]?.properties) { current[path] = { type: 'object', properties: {}, }; } current = current[path].properties; } } const propertyKey = paths[paths.length - 1]; current[propertyKey] = { type: finalOptions.type, description: finalOptions.description, }; if (finalOptions.enum) { const enumValues = (0, utils_1.extractEnumValues)(finalOptions.enum); current[propertyKey].enum = enumValues; } if (finalOptions.items) { // Normalize constructor types (Date, String, Number, Boolean) to string literals const normalizedItems = (0, utils_1.normalizeItemsType)(finalOptions.items); current[propertyKey] = { type: 'array', description: finalOptions.description, items: normalizedItems, }; } Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, existingProperties, target.prototype); if (!finalOptions.isOptional) { const requiredProps = Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, target.prototype) || []; if (!requiredProps.includes(propertyKey)) { requiredProps.push(propertyKey); Reflect.defineMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, requiredProps, target.prototype); } } }