@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
JavaScript
;
/**
* 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);
}
}
}