@mocking-bird/mongoose
Version:
Generates fixtures for `mongoose`. Simply provide the schema or model, and it will generate mock data based on the types and constraints of the schema.
452 lines • 19.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongooseFixture = void 0;
const mongoose_1 = require("mongoose");
const core_1 = require("@mocking-bird/core");
const mongoose_type_mapper_1 = require("./mongoose-type-mapper");
const mongoose_validator_1 = require("./mongoose-validator");
/**
* ### Overview
*
* MongooseFixture class extending abstract CoreFixture class for generating mock data based on Mongoose models
* or schemas.
*
* @see CoreFixture
*
* @typeParam T The type of the document corresponding to the Mongoose model or schema.
*
* @example
* const Model = mongoose.model<IDocumentType>('ModelName', schema);
* const Schema = new mongoose.Schema({ name: String, age: Number });
*
* const modelFixture = new MongooseFixture(Model);
* const schemaFixture = new MongooseFixture(Schema);
*
* const data = modelFixture.generate();
* const multiple = schemaFixture.bulkGenerate(10);
*/
class MongooseFixture extends core_1.CoreFixture {
/**
* Constructs a MongooseFixture instance.
*
* @param target The Mongoose model or schema to generate fixtures for. If a model is provided, the schema is
* extracted from it. If a schema is provided, it is used directly.
*/
constructor(target) {
const pathfinder = new core_1.GlobPathFinder();
const mongooseTypeMapper = new mongoose_type_mapper_1.MongooseTypeMapper();
const mongooseValidator = new mongoose_validator_1.MongooseValidator();
super(pathfinder, mongooseTypeMapper, mongooseValidator);
this.schema = this.isModel(target) ? target.schema : target;
this.mongooseValidator = mongooseValidator;
}
/**
* Sets global options for the fixture generation.
*
* @param options Fixture options to be set globally. If an option is provided per function call, options
* provided via the parameter will override the global options.
*/
static setGlobalOptions(options) {
Object.assign(MongooseFixture.globalOptions, options);
}
/**
* Generates an array of mock data based on the schema.
*
* @inheritDoc
* @override
*
* @typeParam T The type of the document corresponding to the Mongoose model or schema.
*
* @throws {Error} same as {@link MongooseFixture#generate}
*
* @public
*/
bulkGenerate(size, overrideValues, options) {
const combinedOptions = this.preGeneration(overrideValues, options);
return Array.from({ length: size }, () => this.recursivelyGenerateValue(this.schema, undefined, overrideValues, combinedOptions));
}
/**
* Generates a single mock data based on the schema.
*
* @inheritDoc
* @override
*
* @typeParam T The type of the document corresponding to the Mongoose model or schema.
*
* @throws {Error}
* For the following reasons:
*
* - If multiple override values are found for the same path. For example, { 'address.*.buildingNo': 42,
* 'address.street.buildingNo': 43 }.
* - If override value breaks the schema rule.
* - If a required field is excluded.
* - If a custom rule has conflict with the schema rule.
* - If the value generated does not match the custom rule or the schema rule.
*
* @public
*/
generate(overrideValues, options) {
const combinedOptions = this.preGeneration(overrideValues, options);
return this.recursivelyGenerateValue(this.schema, undefined, overrideValues, combinedOptions);
}
/**
* Recursively generates values for nested schema.
* First, it iterates through each field in the schema and generates a value for each field.
* If a field is a nested schema, it will recursively generate values for the nested schema.
* During each iteration, it builds a sub path, e.g., 'address.street', 'address.street.city', etc.
*
* @example
* const schema = new Schema({
* name: String,
* address: {
* street: String,
* city: String,
* country: {
* name: String,
* code: String,
* }
* },
* phone: String,
* });
*
* For such a nested schema above, data will be generated for each child field in the schema.
*
* @param schemaNode The current level of the schema being processed.
* @param rootPath The root path for the current schema node. If undefined, the current schema node is the root.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns The generated mock data for the current schema node.
*
* @private
*/
recursivelyGenerateValue(schemaNode, rootPath, overrideValues, options) {
const generated = {};
schemaNode.eachPath((path, schemaType) => {
if (this.isExcluded(path, schemaType, options)) {
return;
}
const subPath = rootPath
? this.pathfinder.createPath(rootPath, path)
: path;
const generatedValue = this.generateValueForSchemaType(subPath, schemaType, overrideValues, options);
Object.assign(generated, { [path]: generatedValue });
});
return generated;
}
/**
* Generates a value based on the schema type.
*
* We check if a field is nested by checking if it has a `schema` property.
* If a field has basic types such as String, Number, etc., it will not have a schema property.
* Whereas, if a field is an array or an embedded schema, it will have a schema property and will be recursively
* processed.
* Map types are also supported and processed in a special way, as they can be little more complex.
*
* @param path The path to the field.
* @param schemaType The schema type of the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns A mock value for the field or `undefined` if the field is either excluded or explicitly set to
* `undefined` in the override values.
*
* @private
*/
generateValueForSchemaType(path, schemaType, overrideValues, options) {
if (schemaType.instance === MongooseFixture.MAP_SCHEMA_INSTANCE) {
return this.generateValueForMapType(path, schemaType, overrideValues, options);
}
if (schemaType.schema) {
return this.generateValueForNestedSchema(path, schemaType, overrideValues, options);
}
return this.generateValue(path, schemaType, overrideValues, options);
}
/**
* Generates a value for a nested schema.
* Nested schema is a schema that either has an embedded schema or an array schema.
* Values are generated recursively.
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns A mock value for the field in the nested schema; `undefined` if the schema type is not a nested schema.
*
* @private
*/
generateValueForNestedSchema(path, schemaType, overrideValues, options) {
if (schemaType.instance === MongooseFixture.NESTED_SCHEMA_INSTANCE) {
return this.recursivelyGenerateValue(schemaType.schema, path, overrideValues, options);
}
if (schemaType.instance === MongooseFixture.ARRAY_SCHEMA_INSTANCE) {
return this.generateValueForSchemaArray(path, schemaType, overrideValues, options);
}
return undefined;
}
/**
* Generates a value for an array schema.
* The process is similar to generating a value for a nested schema, except that it returns an array of values.
* If a size is provided in the `rule`, the array will be generated with the specified size.
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns An array of mock values
*
* @private
*/
generateValueForSchemaArray(path, schemaType, overrideValues, options) {
const rule = this.pathfinder.findRule(path, options === null || options === void 0 ? void 0 : options.rules);
if (rule === null || rule === void 0 ? void 0 : rule.size) {
return Array.from({ length: rule.size }, () => this.recursivelyGenerateValue(schemaType.schema, path, overrideValues, options));
}
return [
this.recursivelyGenerateValue(schemaType.schema, path, overrideValues, options),
];
}
/**
* Generates a value for a map schema.
* The types used in the map is separated into three main categories: array, schema and basic types.
*
* There are four special cases for processing a map:
* 1. A map is of an array of schema -> { type: Map, of: [new Schema({ name: String, age: Number })] }
* 2. A map is of an array of basic types -> { type: Map, of: [String] }
* 3. A map is of a schema type -> { type: Map, of: new Schema({ name: String, age: Number }) }
* 4. A map is of a basic type -> { type: Map, of: String }
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns A key-value pair of mock values. Returns `undefined` if the map type is not defined.
*
* @private
*/
generateValueForMapType(path, schemaType, overrideValues, options) {
const { of: mapDefinition } = schemaType.options;
if (!mapDefinition) {
return undefined;
}
// Generate a mock vale for the map key
const mapKey = this.generateMapKey();
let value;
if (Array.isArray(mapDefinition)) {
// 1. processing of array types -> { type: Map, of: [String] } or { type: Map, of: [Schema] }
value = this.generateArrayMapValues(mapDefinition[0], path, overrideValues, options);
}
else if (mapDefinition instanceof mongoose_1.Schema) {
// 2. processing of schema type, which is just a nested schema generation
value = this.recursivelyGenerateValue(mapDefinition, path, overrideValues, options);
}
else {
// 3. processing of basic map types -> { type: Map, of: String }
value = this.generateBasicMapValue(mapDefinition.name);
}
return { [mapKey]: value };
}
/**
* Generates a value for an array map type.
*
* @example
* {
* basicArray: [1, 2, 3],
* schemaArray: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }],
* }
*
* @param mapDefinition Either based on schema or basic type. If based on basic type, we access the `name` property.
* @param path The path to the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns An array of mock values, either an object of values or primitive values.
*
* @private
*/
generateArrayMapValues(mapDefinition, path, overrideValues, options) {
var _a;
const rule = this.pathfinder.findRule(path, options === null || options === void 0 ? void 0 : options.rules);
const size = (_a = rule === null || rule === void 0 ? void 0 : rule.size) !== null && _a !== void 0 ? _a : 1; // Default to 1 if size is not specified
const isSchema = mapDefinition instanceof mongoose_1.Schema;
return Array.from({ length: size }, () => {
if (isSchema) {
return this.recursivelyGenerateValue(mapDefinition, path, overrideValues, options);
}
return this.generateBasicMapValue(mapDefinition.name);
});
}
/**
* Generates a map value based on the basic type -> { type: Map, of: String }
*
* @param basicFieldType The basic type of the map value. String, Number, etc.
*
* @returns A mock value for the map value.
*
* @private
*/
generateBasicMapValue(basicFieldType) {
const fieldType = this.typeMapper.getType(basicFieldType);
// The field name is irrelevant here, as it will return a random value regardless.
return this.generateSingleValue('value', fieldType, undefined, false);
}
/**
* Generates a random map value which will be used for the map key.
*
* @returns A random map key.
*
* @private
*/
generateMapKey() {
return this.generateSingleValue('mapKey', // The field name is irrelevant here, as it will return a random value regardless.
core_1.FieldType.STRING, undefined, false);
}
/**
* Generates a value for a field based on the schema type.
* If a value is provided in the `overrideValues`, it will be used instead of generating a new value.
* If a `rule` is provided, a value will be generated based on it.
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param overrideValues Values to override in the schema.
* @param options Fixture generation options.
*
* @returns A mock value for the field or a value in the `overrideValues` if it exists.
*
* @throws {Error} If the value generated does not match the custom rule or the schema rule.
*
* @private
*/
generateValue(path, schemaType, overrideValues, options) {
const rule = this.pathfinder.findRule(path, options === null || options === void 0 ? void 0 : options.rules);
const value = this.shouldOverride(path, overrideValues)
? this.overrideValue(path, overrideValues)
: this.generateMockValue(path, schemaType, rule, options === null || options === void 0 ? void 0 : options.isAccurate);
this.mongooseValidator.validateValue(schemaType, value, rule);
return value;
}
/**
* Generates a mock value for a field based on the schema rule and the custom rule.
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param rule The custom rule to apply for the field.
* @param isAccurate If set to `true`, the value will be generated based on the field name. {@link FakerFinder#search}
*
* @returns A mock value for the field or `undefined`.
*
* @throws {Error} If the schema rules and custom rules conflict each other
*
* @private
*/
generateMockValue(path, schemaType, rule, isAccurate = true) {
const type = this.typeMapper.getType(schemaType.instance);
const schemaRule = this.mongooseValidator.parseValidators(schemaType.validators);
if (schemaRule && rule) {
this.mongooseValidator.validateRules(schemaRule, rule);
}
const combinedRule = this.mongooseValidator.combineRules(schemaRule, rule);
const fieldName = this.pathfinder.extractFieldName(path);
if (type === core_1.FieldType.ARRAY) {
// caster is the schema type of the array elements, from which we can get the array type, e.g., String, Number, etc.
const { caster } = schemaType;
return (caster === null || caster === void 0 ? void 0 : caster.instance)
? this.generateArrayValue(fieldName, this.typeMapper.getArrayType(caster.instance), combinedRule === null || combinedRule === void 0 ? void 0 : combinedRule.size,
/**
* The parsed rule here is the rule for the array elements.
* @example
* const schema = new Schema({ values: [type: String, enum: ['A', 'B', 'C']] });
*/
this.mongooseValidator.parseValidators(caster.validators), isAccurate)
: undefined;
}
return this.generateSingleValue(fieldName, type, combinedRule, isAccurate);
}
/**
* Pre-generation routine to validate and extract paths and to set global options.
*
* @param overrideValues Values to override in the schema.in the schema
* @param options Fixture generation options.
*
* @returns Merged fixture options.
*
* @throws {Error} If paths defined both in the `overrideValues` and the `options` are invalid.
*
* @private
*/
preGeneration(overrideValues, options) {
const extractedPaths = this.extractPaths(overrideValues, options);
this.pathfinder.validatePaths(extractedPaths);
if (!options) {
return MongooseFixture.globalOptions;
}
return Object.assign({}, MongooseFixture.globalOptions, options);
}
/**
* Checks if a given field (path) is excluded from the fixture generation.
*
* @param path The path to the field.
* @param schemaType The mongoose schema type of the field.
* @param options Fixture generation options.
*
* @returns `true` if the field is excluded, `false` otherwise.
*
* @throws {Error} If a required field is excluded.
*
* @private
*/
isExcluded(path, schemaType, options) {
var _a;
// Immediately return true if only required fields are needed and the current field is not required.
// Also, return true if the current field is the version key (__v) or contains special characters (.$*).
if (path === MongooseFixture.VERSION_KEY ||
this.containsSpecialChars(path) ||
((options === null || options === void 0 ? void 0 : options.requiredOnly) && !schemaType.isRequired)) {
return true;
}
if (!((_a = options === null || options === void 0 ? void 0 : options.exclude) === null || _a === void 0 ? void 0 : _a.length)) {
return false;
}
const isExist = this.pathfinder.exists(path, options.exclude);
if (isExist && schemaType.isRequired) {
throw new Error(`Cannot exclude required field: ${path}`);
}
return isExist;
}
/**
* Checks if the path contains special characters.
*
* @param path The path to the field.
*
* @returns `true` if the path contains special characters, `false` otherwise.
*
* @private
*/
containsSpecialChars(path) {
return MongooseFixture.MONGOOSE_SPECIAL_CHARS_REGEX.test(path);
}
/**
* Checks if the target is a Mongoose model.
* Model instances have a `schema` property, by which we can distinguish between a model and a schema.
*
* @param target Either a Mongoose model or a schema.
*
* @returns `true` if the target is a Mongoose model, `false` otherwise.
*
* @private
*/
isModel(target) {
return target.schema !== undefined;
}
}
exports.MongooseFixture = MongooseFixture;
MongooseFixture.globalOptions = {};
MongooseFixture.MONGOOSE_SPECIAL_CHARS_REGEX = /[.*$]/g;
MongooseFixture.NESTED_SCHEMA_INSTANCE = 'Embedded';
MongooseFixture.ARRAY_SCHEMA_INSTANCE = 'Array';
MongooseFixture.MAP_SCHEMA_INSTANCE = 'Map';
MongooseFixture.VERSION_KEY = '__v';
//# sourceMappingURL=mongoose-fixture.js.map