UNPKG

mongoose-tsgen

Version:

A Typescript interface generator for Mongoose that works out of the box.

295 lines (294 loc) 18.5 kB
"use strict"; 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;