ts-forged
Version:
Type-safe fake data generation from DTOs for TypeScript and NestJS
463 lines (458 loc) • 14.9 kB
JavaScript
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