UNPKG

ts-forged

Version:

Type-safe fake data generation from DTOs for TypeScript and NestJS

463 lines (458 loc) 14.9 kB
import { faker } from '@faker-js/faker'; import { getMetadataStorage } from 'class-validator'; import 'reflect-metadata'; // src/core/field-generator.ts // src/generators/base-generator.ts var BaseGenerator = class { /** * Gets the generator options from the context */ getOptions(context) { if (typeof context === "object" && context !== null && "options" in context) { return context.options.overrides || {}; } return {}; } /** * Gets a specific option value */ getOption(context, key) { return context.options[key]; } }; // src/generators/string-generator.ts var StringGenerator = class extends BaseGenerator { /** * Generates a string value based on the field context */ generate(context) { const { fieldName, metadata } = context; const isEmail = metadata.decorators.some((d) => d.name === "IsEmail"); if (isEmail) { return faker.internet.email(); } if (fieldName.toLowerCase().includes("password")) { return faker.internet.password(); } if (fieldName.toLowerCase().includes("username")) { return faker.internet.userName(); } if (fieldName.toLowerCase().includes("domain")) { return faker.internet.domainName(); } if (fieldName.toLowerCase().includes("ip")) { return faker.internet.ip(); } if (fieldName.toLowerCase().includes("color")) { return faker.internet.color(); } if (fieldName.toLowerCase().includes("uuid")) { return faker.string.uuid(); } return faker.word.sample(); } }; var NumberGenerator = class extends BaseGenerator { /** * Generates a number value based on the field context */ generate(context) { const { fieldName } = context; const validationMetadatas = getMetadataStorage().getTargetValidationMetadatas( context.target || Object, fieldName, false, false ); const constraintNumbers = validationMetadatas.filter( (vm) => vm.type === "customValidation" && vm.propertyName === fieldName && typeof vm.constraints?.[0] === "number" ).map((vm) => vm.constraints[0]); let min = void 0; let max = void 0; if (constraintNumbers.length > 0) { min = Math.min(...constraintNumbers); max = Math.max(...constraintNumbers); } if (min !== void 0 && max !== void 0) { return faker.number.int({ min, max }); } if (fieldName.toLowerCase().includes("age")) { return faker.number.int({ min: 18, max: 80 }); } if (fieldName.toLowerCase().includes("price")) { return faker.number.float({ min: 0, max: 1e3, precision: 0.01 }); } if (fieldName.toLowerCase().includes("percent")) { return faker.number.float({ min: 0, max: 100, precision: 0.01 }); } if (fieldName.toLowerCase().includes("rating")) { return faker.number.float({ min: 1, max: 5, precision: 0.1 }); } if (fieldName.toLowerCase().includes("count")) { return faker.number.int({ min: 0, max: 100 }); } return faker.number.int(); } }; var DateGenerator = class extends BaseGenerator { /** * Generates a date value based on the field context */ generate(context) { const { fieldName, metadata } = context; const isDate = metadata.decorators.some((d) => d.name === "IsDate"); if (isDate) { return faker.date.anytime(); } if (fieldName.toLowerCase().includes("birth")) { return faker.date.birthdate(); } if (fieldName.toLowerCase().includes("future")) { return faker.date.future(); } if (fieldName.toLowerCase().includes("past")) { return faker.date.past(); } if (fieldName.toLowerCase().includes("recent")) { return faker.date.recent(); } return faker.date.anytime(); } }; var SWAGGER_PROPS_KEY = "swagger/apiModelPropertiesArray"; var FAKE_PROPS_KEY = "ts-forged:props"; function FakeProperty(typeFn) { return (target, propertyKey) => { const props = Reflect.getMetadata(FAKE_PROPS_KEY, target) || []; props.push(propertyKey.toString()); Reflect.defineMetadata(FAKE_PROPS_KEY, props, target); if (typeFn) { Reflect.defineMetadata("ts-forged:elementType", typeFn(), target, propertyKey); } }; } function getFieldMetadata(type) { const metadata = []; const prototype = type.prototype; let swaggerProps = Reflect.getMetadata(SWAGGER_PROPS_KEY, prototype) || []; swaggerProps = swaggerProps.map((p) => p.startsWith(":") ? p.slice(1) : p); const fakeProps = Reflect.getMetadata(FAKE_PROPS_KEY, prototype) || []; const properties = [.../* @__PURE__ */ new Set([...swaggerProps, ...fakeProps])]; for (const property of properties) { const designType = Reflect.getMetadata("design:type", prototype, property); let typeName = "unknown"; let isArray = false; let elementType = void 0; const validationMetadatas = getMetadataStorage().getTargetValidationMetadatas( type, property, false, false ); const propertyValidationMetadatas = validationMetadatas.filter( (vm) => vm.propertyName === property ); const decorators = propertyValidationMetadatas.map((vm) => ({ name: vm.type, args: vm.constraints || [] })); const isOptional = propertyValidationMetadatas.some( (vm) => vm.type === "conditionalValidation" && vm.name === "isOptional" ); const swaggerMeta = Reflect.getMetadata("swagger/apiModelProperties", prototype, property); if (swaggerMeta) { if (swaggerMeta.enum) { typeName = "String"; elementType = swaggerMeta.enum; } if (swaggerMeta.isArray === true || Array.isArray(swaggerMeta.type) && swaggerMeta.type.length > 0 || typeof swaggerMeta.type === "function" && swaggerMeta.type === Array) { isArray = true; typeName = "Array"; if (Array.isArray(swaggerMeta.type) && swaggerMeta.type.length > 0) { elementType = swaggerMeta.type[0]; } else if (typeof swaggerMeta.type === "function" && swaggerMeta.type === Array) { const explicitElementType = Reflect.getMetadata( "ts-forged:elementType", prototype, property ); if (explicitElementType) { elementType = explicitElementType; } } } else if (swaggerMeta.type) { if (typeof swaggerMeta.type === "function") { typeName = swaggerMeta.type.name; } else if (typeof swaggerMeta.type === "string") { typeName = swaggerMeta.type; } if (["String", "Number", "Boolean", "Date"].includes(typeName)) { typeName = typeName.toLowerCase(); } } } else if (designType && typeof designType === "function" && "name" in designType) { typeName = designType.name; if (typeName === "Array") { isArray = true; typeName = "Array"; const explicitElementType = Reflect.getMetadata( "ts-forged:elementType", prototype, property ); if (explicitElementType) { elementType = explicitElementType; } else { const arrayType = Reflect.getMetadata("design:type", prototype, property); if (arrayType && arrayType !== Array) { elementType = arrayType; } } } else { const explicitNestedType = Reflect.getMetadata( "ts-forged:elementType", prototype, property ); if (explicitNestedType) { elementType = explicitNestedType; } } if (["String", "Number", "Boolean", "Date"].includes(typeName)) { typeName = typeName.toLowerCase(); } } metadata.push({ name: property, type: typeName, isOptional, isArray, elementType, decorators }); } return metadata; } function getElementType(type, property) { const prototype = type.prototype; const explicitElementType = Reflect.getMetadata("ts-forged:elementType", prototype, property); if (explicitElementType) { return explicitElementType; } const swaggerMetadata = Reflect.getMetadata("swagger/apiProperties", prototype, property); if (swaggerMetadata?.items?.type) { return swaggerMetadata.items.type; } const explicitNestedType = Reflect.getMetadata("ts-forged:elementType", prototype, property); if (explicitNestedType) { return explicitNestedType; } return void 0; } function isClassType(value) { return typeof value === "function" && value.prototype && value.prototype.constructor === value; } var FieldGenerator = class { constructor() { this.stringGenerator = new StringGenerator(); this.numberGenerator = new NumberGenerator(); this.dateGenerator = new DateGenerator(); } /** * Generates fake data for all fields in a type */ generateFields(type, options = {}) { const fields = {}; const metadata = getFieldMetadata(type); for (const field of metadata) { const context = { fieldName: field.name, fieldType: field.type, metadata: field, options, target: type }; fields[field.name] = this.generateFieldValue(context); } return fields; } /** * Generates a single field value based on its context */ generateFieldValue(context) { const { fieldType, metadata } = context; if (metadata.isOptional) { if (faker.number.int({ min: 1, max: 10 }) <= 3) { return void 0; } } if (metadata.isArray) { return this.generateArrayValue(context); } if (Array.isArray(metadata.elementType) && metadata.elementType.length > 0) { if (metadata.elementType.every((v) => typeof v === "string" || typeof v === "number")) { return faker.helpers.arrayElement(metadata.elementType); } } if (isClassType(metadata.elementType)) { return this.generateFields(metadata.elementType, context.options); } const smartValue = this.generateSmartValue(context); if (smartValue !== void 0) { return smartValue; } switch (fieldType.toLowerCase()) { case "string": return this.stringGenerator.generate(context); case "number": return this.numberGenerator.generate(context); case "boolean": return faker.datatype.boolean(); case "date": return this.dateGenerator.generate(context); default: return this.generateObjectValue(context); } } /** * Generates an array of values */ generateArrayValue(context) { const { metadata } = context; const length = faker.number.int({ min: 1, max: 5 }); const newContext = { ...context, depth: (context.depth || 0) + 1 }; if (newContext.depth > 3) { return []; } if (isClassType(metadata.elementType)) { const elementType = metadata.elementType; return Array.from({ length }, () => this.generateFields(elementType, context.options)); } const elementContext = { ...newContext, fieldType: metadata.type.replace("[]", "") }; return Array.from({ length }, () => this.generateFieldValue(elementContext)); } /** * Generates a value based on smart field name detection */ generateSmartValue(context) { const { fieldName } = context; if (fieldName.toLowerCase().includes("name")) { if (fieldName.toLowerCase().includes("first")) { return faker.person.firstName(); } if (fieldName.toLowerCase().includes("last")) { return faker.person.lastName(); } return faker.person.fullName(); } if (fieldName.toLowerCase().includes("email")) { return faker.internet.email(); } if (fieldName.toLowerCase().includes("phone")) { return faker.phone.number(); } if (fieldName.toLowerCase().includes("address")) { return faker.location.streetAddress(); } if (fieldName.toLowerCase().includes("city")) { return faker.location.city(); } if (fieldName.toLowerCase().includes("country")) { return faker.location.country(); } if (fieldName.toLowerCase().includes("url")) { return faker.internet.url(); } return void 0; } /** * Generates an object value */ generateObjectValue(context) { const { fieldName, fieldType, metadata } = context; if (isClassType(metadata.elementType)) { return this.generateFields(metadata.elementType, context.options); } const prototype = context.options?.prototype || void 0; let nestedType = void 0; if (prototype) { nestedType = Reflect.getMetadata("design:type", prototype, fieldName); } if (nestedType && typeof nestedType === "function" && nestedType !== Object) { return this.generateFields(nestedType, context.options); } const fallbackType = this.getNestedType(fieldType); if (fallbackType && typeof fallbackType === "function") { return this.generateFields(fallbackType, context.options); } return {}; } /** * Gets the nested type for a field */ getNestedType(fieldType) { const baseType = fieldType.replace("[]", ""); const globalType = global[baseType]; if (globalType && typeof globalType === "function") { return globalType; } const moduleExports = module.exports; const moduleType = moduleExports[baseType]; if (moduleType && typeof moduleType === "function") { return moduleType; } return void 0; } }; // src/core/factory.ts var Factory = class _Factory { constructor(type) { this.type = type; this.fieldGenerator = new FieldGenerator(); } /** * Creates a new factory instance for the given type */ static create(type) { return new _Factory(type); } /** * Builds a single instance with optional overrides */ build(overrides = {}) { const instance = new this.type(); const fields = this.fieldGenerator.generateFields(this.type); for (const [key, value] of Object.entries(fields)) { if (key in overrides) { instance[key] = overrides[key]; } else { instance[key] = value; } } return instance; } /** * Builds multiple instances */ buildMany(count, overrides = {}) { return Array.from({ length: count }, () => this.build(overrides)); } /** * Creates a sequence of instances with unique values */ sequence(count, overrides = {}) { return Array.from({ length: count }, (_, index) => { const sequenceOverrides = { ...overrides, id: index + 1 }; return this.build(sequenceOverrides); }); } }; export { BaseGenerator, DateGenerator, Factory, FakeProperty, FieldGenerator, NumberGenerator, StringGenerator, getElementType, getFieldMetadata }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map