mongoose-tsgen
Version:
A Typescript interface generator for Mongoose that works out of the box.
195 lines (194 loc) • 10.2 kB
JavaScript
"use strict";
/**
* Parser types
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParserSchema = void 0;
const tslib_1 = require("tslib");
const utils_1 = require("./utils");
const lodash_1 = (0, tslib_1.__importDefault)(require("lodash"));
const templates = (0, tslib_1.__importStar)(require("../helpers/templates"));
const generator_1 = require("../helpers/generator");
// old TODOs:
// - Handle statics method issue
// - Switch to using HydratedDocument, https://mongoosejs.com/docs/migrating_to_7.html. Also update query helpers https://mongoosejs.com/docs/typescript/query-helpers.html
// TODO: Look into new inference of types https://mongoosejs.com/docs/typescript/schemas.html
class ParserSchema {
constructor({ mongooseSchema, modelName, model }) {
this.methods = {};
this.statics = {};
this.queries = {};
this.virtuals = {};
this.comments = [];
// schema.tree with custom field _aliasRootField, TODO: Create explicit type for this
this.schemaTree = {};
/**
* Parses the schema tree, and adds _aliasRootField to the tree for aliases.
* @param schema The schema to parse.
* @returns The parsed schema tree.
*/
this.parseTree = (schema) => {
const tree = lodash_1.default.cloneDeep(schema.tree);
// TODO: Rename this to what it was before, related to adding type aliases
// Add alias types to tree
if (!lodash_1.default.isEmpty(this.mongooseSchema.aliases) && this.modelName) {
Object.entries(this.mongooseSchema.aliases).forEach(([alias, path]) => {
lodash_1.default.set(tree, `${alias}._aliasRootField`, lodash_1.default.get(tree, path));
});
}
return tree;
};
this.parseChildSchemas = (schema) => {
var _a, _b, _c, _d, _e, _f;
const childSchemas = [];
// NOTE: The for loop below is a hack for Schema maps. For some reason, when a map of a schema exists, the schema is not included
// in childSchemas. So we add it manually and add a few extra properties to ensure the processChild works correctly.
// UPDSTE: Newer versions of Mongoose do include the schema map in the child schemas, but in a weird format with "*$" postfix in the path. We can just filter those out which is what were doing directly below.
const mongooseChildSchemas = lodash_1.default.cloneDeep(schema.childSchemas).filter((child) => !child.model.path.endsWith("$*"));
for (const [path, type] of Object.entries(this.mongooseSchema.paths)) {
// This check tells us that this is a map of a separate schema
if (((_a = type) === null || _a === void 0 ? void 0 : _a.$isSchemaMap) && ((_b = type) === null || _b === void 0 ? void 0 : _b.$__schemaType.schema)) {
const childSchema = type.$__schemaType;
childSchema.model = {
path: path,
// TODO: Augment the mongoose schema with these, or dont update them in place would be even better
$isArraySubdocument: (_e = (_d = (_c = childSchema.Constructor) === null || _c === void 0 ? void 0 : _c.$isArraySubdocument) !== null && _d !== void 0 ? _d : childSchema.$isMongooseDocumentArray) !== null && _e !== void 0 ? _e : false,
$isSchemaMap: true
};
mongooseChildSchemas.push(childSchema);
}
}
for (const child of mongooseChildSchemas) {
const path = child.model.path;
const isSubdocArray = child.model.$isArraySubdocument;
const isSchemaMap = (_f = child.model.$isSchemaMap) !== null && _f !== void 0 ? _f : false;
const name = (0, utils_1.getSubdocName)(path, this.modelName);
const sanitizedName = (0, generator_1.sanitizeModelName)(name);
child.schema._isReplacedWithSchema = true;
child.schema._inferredInterfaceName = sanitizedName;
child.schema._isSubdocArray = isSubdocArray;
child.schema._isSchemaMap = isSchemaMap;
const requiredValuePath = `${path}.required`;
if (lodash_1.default.get(this.mongooseSchema.tree, requiredValuePath) === true) {
child.schema.required = true;
}
/**
* for subdocument arrays, mongoose supports passing `default: undefined` to disable the default empty array created.
* here we indicate this on the child schema using _isDefaultSetToUndefined so that the parser properly sets the `isOptional` flag
*/
if (isSubdocArray) {
const defaultValuePath = `${path}.default`;
if (lodash_1.default.has(this.mongooseSchema.tree, defaultValuePath) &&
lodash_1.default.get(this.mongooseSchema.tree, defaultValuePath) === undefined) {
child.schema._isDefaultSetToUndefined = true;
}
}
if (isSchemaMap) {
lodash_1.default.set(this.mongooseSchema.tree, path, {
type: Map,
of: isSubdocArray ? [child.schema] : child.schema
});
}
else if (isSubdocArray) {
lodash_1.default.set(this.mongooseSchema.tree, path, [child.schema]);
}
else {
lodash_1.default.set(this.mongooseSchema.tree, path, child.schema);
}
const childSchema = new ParserSchema({
mongooseSchema: child.schema,
modelName: sanitizedName,
model: child.model
});
childSchemas.push(childSchema);
}
return childSchemas;
};
this.model = model;
this.modelName = modelName;
this.mongooseSchema = mongooseSchema;
this.childSchemas = this.parseChildSchemas(mongooseSchema);
this.schemaTree = this.parseTree(mongooseSchema);
this.fields = this.generateFields(mongooseSchema);
this.shouldLeanIncludeVirtuals = (0, utils_1.getShouldLeanIncludeVirtuals)(mongooseSchema);
}
// TODO: Generate own representation
generateFields(_mongooseSchema) {
// return Object.entries(mongooseSchema.tree).map(([name, field]) => ({
// name: field.name,
// type: field.type,
// isOptional: field.isOptional || false,
// isArray: field.isArray || false,
// isMap: field.isMap || false,
// ref: field.ref,
// virtual: field.virtual,
// comment: field.comment
// }));
return [];
}
generateTemplate({ isDocument, noMongoose, datesAsStrings, header, footer }) {
let template = "";
if (this.mongooseSchema.childSchemas && this.modelName) {
// TODO: Splint into functuon
this.childSchemas.forEach((child) => {
const path = child.model.path;
const name = (0, utils_1.getSubdocName)(path, this.modelName);
let header = "";
if (isDocument)
// TODO: Does this make sense for child docs?
header += child.mongooseSchema._isSubdocArray
? templates.getSubdocumentDocs(this.modelName, path)
: templates.getDocumentDocs(this.modelName);
else
header += templates.getLeanDocs(this.modelName, name);
header += "\nexport ";
if (isDocument) {
header += `type ${name}Document = `;
// get type of _id to pass to mongoose.Document
// this is likely unecessary, since non-subdocs are not allowed to have option _id: false (https://mongoosejs.com/docs/guide.html#_id)
// TODO: Fix type manually
const _idType = child.mongooseSchema.tree._id
? (0, utils_1.convertBaseTypeToTs)({
key: "_id",
val: child.mongooseSchema.tree._id,
isDocument: true,
noMongoose,
datesAsStrings
})
: "any";
// TODO: this should extend `${name}Methods` like normal docs, but generator will only have methods, statics, etc. under the model name, not the subdoc model name
// so after this is generated, we should do a pass and see if there are any child schemas that have non-subdoc definitions.
// or could just wait until we dont need duplicate subdoc versions of docs (use the same one for both embedded doc and non-subdoc)
header += child.mongooseSchema._isSubdocArray
? `mongoose.Types.Subdocument<${_idType}>`
: `mongoose.Document<${_idType}>`;
header += " & {\n";
}
else
header += `type ${name} = {\n`;
const footer = `}\n\n`;
template += child.generateTemplate({
isDocument,
noMongoose,
datesAsStrings,
header,
footer
});
});
}
template += header;
Object.entries(this.schemaTree).forEach(([key, val]) => {
template += (0, utils_1.getTypeFromKeyValue)({
key,
val,
isDocument,
noMongoose,
datesAsStrings,
shouldLeanIncludeVirtuals: this.shouldLeanIncludeVirtuals
});
});
template += footer;
return template;
}
}
exports.ParserSchema = ParserSchema;