mongoose-tsgen
Version:
A Typescript interface generator for Mongoose that works out of the box.
295 lines (294 loc) • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveFile = exports.generateTypes = exports.getSchemaTypes = exports.parseFunctions = exports.createSourceFile = exports.overloadQueryPopulate = exports.addPopulateHelpers = exports.replaceModelTypes = exports.convertFuncSignatureToType = exports.sanitizeModelName = exports.cleanComment = void 0;
const tslib_1 = require("tslib");
const ts_morph_1 = require("ts-morph");
const templates = (0, tslib_1.__importStar)(require("./templates"));
const schema_1 = require("../parser/schema");
const utils_1 = require("../parser/utils");
const stringBuilder_1 = require("../writer/stringBuilder");
const typeSanitization_1 = require("./typeSanitization");
// TODO next: Pull this file apart. Create a new "file writer" file, move all the ts stuff somewhere else,
const cleanComment = (comment) => {
if (!comment)
return "";
if (comment.trim() === "/** */")
return "";
return comment
.replace(/^\/\*\*[^\S\r\n]?/, "") // Remove opening /**
.replace(/[^\S\r\n]+\*\s/g, "") // Remove * at start of lines
.replace(/(\n)?[^\S\r\n]+\*\/$/, ""); // Remove closing */
};
exports.cleanComment = cleanComment;
// Needs to be exported by generator Module
const sanitizeModelName = (name) => (0, typeSanitization_1.sanitizeTypeIdentifier)(name);
exports.sanitizeModelName = sanitizeModelName;
const funcTypeToThisSuffix = {
query: "Query",
methods: "Document",
statics: "Model"
};
const parseSignature = (signature, modelName, funcType) => {
const thisSuffix = funcTypeToThisSuffix[funcType];
const thisType = `${modelName}${thisSuffix}`;
const queryReturnType = `${modelName}Query`;
const match = signature === null || signature === void 0 ? void 0 : signature.match(/\((?:this: \w*(?:, )?)?(?<params>.*)\) => (?<returnType>.*)/);
if (!(match === null || match === void 0 ? void 0 : match.groups)) {
console.warn(`Failed to extract types from function signature: ${signature}, falling back to defaults`);
const defaultReturnType = funcType === "query" ? queryReturnType : "any";
const defaultParams = "...args: any[]";
return {
params: defaultParams,
returnType: defaultReturnType,
thisType
};
}
const finalReturnType = funcType === "query" ? queryReturnType : match.groups.returnType;
return {
params: match.groups.params,
returnType: finalReturnType,
thisType
};
};
const convertFuncSignatureToType = (funcSignature, funcType, modelName) => {
const sanitizedModelName = (0, exports.sanitizeModelName)(modelName);
const { params, returnType, thisType } = parseSignature(funcSignature, sanitizedModelName, funcType);
const paramsString = (params === null || params === void 0 ? void 0 : params.length) > 0 ? `, ${params}` : "";
return `(this: ${thisType}${paramsString}) => ${returnType}`;
};
exports.convertFuncSignatureToType = convertFuncSignatureToType;
const replaceModelTypes = (sourceFile, modelTypes, models) => {
Object.entries(modelTypes).forEach(([modelName, types]) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
const sanitizedModelName = (0, exports.sanitizeModelName)(modelName);
const { methods, statics, query, virtuals, comments } = types;
// methods
if (Object.keys(methods).length > 0) {
(_b = (_a = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}Methods`)) === null || _a === void 0 ? void 0 : _a.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _b === void 0 ? void 0 : _b.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature).forEach((prop) => {
const signature = methods[prop.getName()];
if (signature) {
const funcType = (0, exports.convertFuncSignatureToType)(signature, "methods", modelName);
prop.setType(funcType);
}
});
}
// statics
if (Object.keys(statics).length > 0) {
(_d = (_c = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}Statics`)) === null || _c === void 0 ? void 0 : _c.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _d === void 0 ? void 0 : _d.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature).forEach((prop) => {
const signature = statics[prop.getName()];
if (signature) {
const funcType = (0, exports.convertFuncSignatureToType)(signature, "statics", modelName);
prop.setType(funcType);
}
});
}
// queries
if (Object.keys(query).length > 0) {
(_f = (_e = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}Queries`)) === null || _e === void 0 ? void 0 : _e.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _f === void 0 ? void 0 : _f.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature).forEach((prop) => {
const signature = query[prop.getName()];
if (signature) {
const funcType = (0, exports.convertFuncSignatureToType)(signature, "query", modelName);
prop.setType(funcType);
}
});
}
// virtuals
const virtualNames = Object.keys(virtuals);
if (virtualNames.length > 0) {
const documentProperties = (_j = (_h = (_g = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}Document`)) === null || _g === void 0 ? void 0 : _g.getFirstChildByKind(ts_morph_1.SyntaxKind.IntersectionType)) === null || _h === void 0 ? void 0 : _h.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _j === void 0 ? void 0 : _j.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
const { schema } = models.find((model) => model.modelName === modelName);
const leanProperties = (0, utils_1.getShouldLeanIncludeVirtuals)(schema) &&
((_l = (_k = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}`)) === null || _k === void 0 ? void 0 : _k.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _l === void 0 ? void 0 : _l.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature));
if (documentProperties || leanProperties) {
virtualNames.forEach((virtualName) => {
const virtualNameComponents = virtualName.split(".");
let nestedDocProps;
let nestedLeanProps;
virtualNameComponents.forEach((nameComponent, i) => {
var _a, _b, _c, _d;
if (i === virtualNameComponents.length - 1) {
if (documentProperties) {
const docPropMatch = (nestedDocProps !== null && nestedDocProps !== void 0 ? nestedDocProps : documentProperties).find((prop) => prop.getName() === nameComponent);
docPropMatch === null || docPropMatch === void 0 ? void 0 : docPropMatch.setType(virtuals[virtualName]);
}
if (leanProperties) {
const leanPropMatch = (nestedLeanProps !== null && nestedLeanProps !== void 0 ? nestedLeanProps : leanProperties).find((prop) => prop.getName() === nameComponent);
leanPropMatch === null || leanPropMatch === void 0 ? void 0 : leanPropMatch.setType(virtuals[virtualName]);
}
return;
}
if (documentProperties) {
nestedDocProps = (_b = (_a = (nestedDocProps !== null && nestedDocProps !== void 0 ? nestedDocProps : documentProperties)
.find((prop) => prop.getName() === nameComponent)) === null || _a === void 0 ? void 0 : _a.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _b === void 0 ? void 0 : _b.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
}
if (leanProperties) {
nestedLeanProps = (_d = (_c = (nestedLeanProps !== null && nestedLeanProps !== void 0 ? nestedLeanProps : leanProperties)
.find((prop) => prop.getName() === nameComponent)) === null || _c === void 0 ? void 0 : _c.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _d === void 0 ? void 0 : _d.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
}
});
});
}
}
// TODO: this section is almost identical to the virtual property section above, refactor
if (comments.length > 0) {
const documentProperties = (_p = (_o = (_m = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}Document`)) === null || _m === void 0 ? void 0 : _m.getFirstChildByKind(ts_morph_1.SyntaxKind.IntersectionType)) === null || _o === void 0 ? void 0 : _o.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _p === void 0 ? void 0 : _p.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
const leanProperties = (_r = (_q = sourceFile === null || sourceFile === void 0 ? void 0 : sourceFile.getTypeAlias(`${sanitizedModelName}`)) === null || _q === void 0 ? void 0 : _q.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _r === void 0 ? void 0 : _r.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
comments.forEach(({ path, comment }) => {
const pathComponents = path.split(".");
let nestedDocProps;
let nestedLeanProps;
pathComponents.forEach((nameComponent, i) => {
var _a, _b, _c, _d;
if (i === pathComponents.length - 1) {
if (documentProperties) {
const docPropMatch = (nestedDocProps !== null && nestedDocProps !== void 0 ? nestedDocProps : documentProperties).find((prop) => prop.getName() === nameComponent);
docPropMatch === null || docPropMatch === void 0 ? void 0 : docPropMatch.addJsDoc((0, exports.cleanComment)(comment));
}
if (leanProperties) {
const leanPropMatch = (nestedLeanProps !== null && nestedLeanProps !== void 0 ? nestedLeanProps : leanProperties).find((prop) => prop.getName() === nameComponent);
leanPropMatch === null || leanPropMatch === void 0 ? void 0 : leanPropMatch.addJsDoc((0, exports.cleanComment)(comment));
}
return;
}
if (documentProperties) {
nestedDocProps = (_b = (_a = (nestedDocProps !== null && nestedDocProps !== void 0 ? nestedDocProps : documentProperties)
.find((prop) => prop.getName() === nameComponent)) === null || _a === void 0 ? void 0 : _a.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _b === void 0 ? void 0 : _b.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
}
if (leanProperties) {
nestedLeanProps = (_d = (_c = (nestedLeanProps !== null && nestedLeanProps !== void 0 ? nestedLeanProps : leanProperties)
.find((prop) => prop.getName() === nameComponent)) === null || _c === void 0 ? void 0 : _c.getFirstChildByKind(ts_morph_1.SyntaxKind.TypeLiteral)) === null || _d === void 0 ? void 0 : _d.getChildrenOfKind(ts_morph_1.SyntaxKind.PropertySignature);
}
});
});
}
});
};
exports.replaceModelTypes = replaceModelTypes;
const addPopulateHelpers = (sourceFile) => {
sourceFile.addStatements("\n" + templates.POPULATE_HELPERS);
};
exports.addPopulateHelpers = addPopulateHelpers;
const overloadQueryPopulate = (sourceFile) => {
sourceFile.addStatements("\n" + templates.QUERY_POPULATE);
};
exports.overloadQueryPopulate = overloadQueryPopulate;
const createSourceFile = (genPath) => {
const project = new ts_morph_1.Project();
const sourceFile = project.createSourceFile(genPath, "", { overwrite: true });
return sourceFile;
};
exports.createSourceFile = createSourceFile;
// TODO: statics, query, methods should all be parsed in the parser, and then written in the stringBuilder
const parseFunctions = (funcs, modelName, funcType) => {
let interfaceString = "";
Object.keys(funcs).forEach((key) => {
if (["initializeTimestamps"].includes(key))
return;
const funcSignature = "(...args: any[]) => any";
const type = (0, exports.convertFuncSignatureToType)(funcSignature, funcType, modelName);
interfaceString += (0, stringBuilder_1.convertKeyValueToLine)({ key, valueType: type });
});
return interfaceString;
};
exports.parseFunctions = parseFunctions;
const getSchemaTypes = (model) => {
var _a;
const { modelName, schema } = model;
const sanitizedModelName = (0, exports.sanitizeModelName)(modelName);
let schemaTypes = "";
// add type alias to modelName so that it can be imported without clashing with the mongoose model
schemaTypes += templates.getObjectDocs(sanitizedModelName);
schemaTypes += `\nexport type ${sanitizedModelName}Object = ${sanitizedModelName}\n\n`;
schemaTypes += templates.getQueryDocs();
schemaTypes += `\nexport type ${sanitizedModelName}Query = mongoose.Query<any, ${sanitizedModelName}Document, ${sanitizedModelName}Queries> & ${sanitizedModelName}Queries\n\n`;
schemaTypes += templates.getQueryHelpersDocs(sanitizedModelName);
schemaTypes += `\nexport type ${sanitizedModelName}Queries = {\n`;
schemaTypes += (0, exports.parseFunctions)((_a = schema.query) !== null && _a !== void 0 ? _a : {}, modelName, "query");
schemaTypes += "}\n";
schemaTypes += `\nexport type ${sanitizedModelName}Methods = {\n`;
schemaTypes += (0, exports.parseFunctions)(schema.methods, modelName, "methods");
schemaTypes += "}\n";
schemaTypes += `\nexport type ${sanitizedModelName}Statics = {\n`;
schemaTypes += (0, exports.parseFunctions)(schema.statics, modelName, "statics");
schemaTypes += "}\n\n";
const modelExtend = `mongoose.Model<${sanitizedModelName}Document, ${sanitizedModelName}Queries>`;
schemaTypes += templates.getModelDocs(sanitizedModelName);
schemaTypes += `\nexport type ${sanitizedModelName}Model = ${modelExtend} & ${sanitizedModelName}Statics\n\n`;
schemaTypes += templates.getSchemaDocs(sanitizedModelName);
schemaTypes += `\nexport type ${sanitizedModelName}Schema = mongoose.Schema<${sanitizedModelName}Document, ${sanitizedModelName}Model, ${sanitizedModelName}Methods, ${sanitizedModelName}Queries>\n\n`;
return schemaTypes;
};
exports.getSchemaTypes = getSchemaTypes;
// TODO: This should be split up, shouldn't be writing to file and parsing schema simultaneously. Instead parse schema first then write later.
const generateTypes = ({ sourceFile, imports = [], modelsPaths, noMongoose, datesAsStrings }) => {
const models = (0, utils_1.loadModels)(modelsPaths);
sourceFile.addStatements((writer) => {
writer.write(templates.MAIN_HEADER).blankLine();
// mongoose import
if (!noMongoose)
writer.write(templates.MONGOOSE_IMPORT);
// custom, user-defined imports
if (imports.length > 0)
writer.write(imports.join("\n"));
writer.blankLine();
models.forEach((model) => {
const { modelName, schema } = model;
const sanitizedModelName = (0, exports.sanitizeModelName)(modelName);
const leanHeader = templates.getLeanDocs(sanitizedModelName) + `\nexport type ${sanitizedModelName} = {\n`;
const leanFooter = "}";
const parserSchema = new schema_1.ParserSchema({
mongooseSchema: schema,
modelName: sanitizedModelName,
model
});
const leanInterfaceStr = parserSchema.generateTemplate({
isDocument: false,
noMongoose,
datesAsStrings,
header: leanHeader,
footer: leanFooter
});
writer.write(leanInterfaceStr).blankLine();
// if noMongoose, skip adding document types
if (noMongoose) {
return;
}
// get type of _id to pass to mongoose.Document
const _idType = schema.tree._id
? (0, utils_1.convertBaseTypeToTs)({
key: "_id",
val: schema.tree._id,
isDocument: true,
noMongoose,
datesAsStrings
})
: "any";
const mongooseDocExtend = `mongoose.Document<${_idType}, ${sanitizedModelName}Queries>`;
let documentInterfaceStr = "";
documentInterfaceStr += (0, exports.getSchemaTypes)(model);
const documentHeader = templates.getDocumentDocs(sanitizedModelName) +
`\nexport type ${sanitizedModelName}Document = ${mongooseDocExtend} & ${sanitizedModelName}Methods & {\n`;
const documentFooter = "}";
documentInterfaceStr += parserSchema.generateTemplate({
isDocument: true,
noMongoose,
datesAsStrings,
header: documentHeader,
footer: documentFooter
});
writer.write(documentInterfaceStr).blankLine();
});
});
return sourceFile;
};
exports.generateTypes = generateTypes;
const saveFile = ({ sourceFile }) => {
try {
sourceFile.saveSync();
}
catch (err) {
console.error(err);
throw err;
}
};
exports.saveFile = saveFile;