From 6c84485ecd0c251c8599ad71ecad38d95f3d256c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 9 Oct 2025 17:43:40 -0400 Subject: [PATCH] Include schema methods and statics in model descriptions --- backend/helpers/getModelDescriptions.js | 70 +++++++++++++++++++---- test/helpers.getModelDescriptions.test.js | 43 ++++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/backend/helpers/getModelDescriptions.js b/backend/helpers/getModelDescriptions.js index 2fecfe6..6db76e3 100644 --- a/backend/helpers/getModelDescriptions.js +++ b/backend/helpers/getModelDescriptions.js @@ -21,18 +21,68 @@ const formatSchemaPath = (path, schemaType) => `- ${path}: ${formatSchemaTypeIns formatRef(schemaType) + (schemaType.schema ? formatNestedSchema(schemaType) : ''); -const listModelPaths = Model => [ - ...Object.entries(Model.schema.paths).map( +const indentLines = (value, spaces = 2) => value.split('\n').map(line => `${' '.repeat(spaces)}${line}`).join('\n'); + +const normalizeFunctionSource = fn => { + const source = fn.toString(); + const lines = source.split('\n'); + + if (lines.length <= 1) { + return source; + } + + const [firstLine, ...rest] = lines; + const indentLengths = rest + .filter(line => line.trim().length > 0) + .map(line => line.match(/^\s*/)[0].length); + const minIndent = indentLengths.length > 0 ? Math.min(...indentLengths) : 0; + const normalizedRest = rest.map(line => line.slice(Math.min(line.length, minIndent))); + + return [firstLine, ...normalizedRest].join('\n'); +}; + +const formatFieldSection = Model => { + const fieldLines = Object.entries(Model.schema.paths).map( ([path, schemaType]) => formatSchemaPath(path, schemaType) - ), - ...Object.entries(Model.schema.virtuals).filter(([path, virtual]) => virtual.options?.ref).map( + ); + return fieldLines.length ? ['Fields:', ...fieldLines] : []; +}; + +const formatVirtualSection = Model => { + const virtualLines = Object.entries(Model.schema.virtuals).filter(([path, virtual]) => virtual.options?.ref).map( ([path, virtual]) => `- ${path}: Virtual (ref: ${virtual.options.ref})` - ) -].join('\n'); + ); + return virtualLines.length ? ['Virtuals:', ...virtualLines] : []; +}; + +const formatMethodSection = Model => { + const methodEntries = Object.entries(Model.schema.methods || {}); + if (!methodEntries.length) { + return []; + } + + return ['Methods:', ...methodEntries.flatMap(([name, fn]) => [`- ${name}:`, indentLines(normalizeFunctionSource(fn), 2)])]; +}; + +const formatStaticSection = Model => { + const staticEntries = Object.entries(Model.schema.statics || {}); + if (!staticEntries.length) { + return []; + } + + return ['Statics:', ...staticEntries.flatMap(([name, fn]) => [`- ${name}:`, indentLines(normalizeFunctionSource(fn), 2)])]; +}; + +const getModelDescriptions = db => Object.values(db.models).filter(Model => !Model.modelName.startsWith('__Studio')).map(Model => { + const sections = [ + `${Model.modelName} (collection: ${Model.collection.collectionName})`, + ...formatFieldSection(Model), + ...formatVirtualSection(Model), + ...formatMethodSection(Model), + ...formatStaticSection(Model) + ]; -const getModelDescriptions = db => Object.values(db.models).filter(Model => !Model.modelName.startsWith('__Studio')).map(Model => ` -${Model.modelName} (collection: ${Model.collection.collectionName}) -${listModelPaths(Model)} -`.trim()).join('\n\n'); + return sections.join('\n'); +}).join('\n\n'); module.exports = getModelDescriptions; diff --git a/test/helpers.getModelDescriptions.test.js b/test/helpers.getModelDescriptions.test.js index 5f89d5d..4bc212d 100644 --- a/test/helpers.getModelDescriptions.test.js +++ b/test/helpers.getModelDescriptions.test.js @@ -33,6 +33,7 @@ describe('getModelDescriptions', function() { result, dedent(` User (collection: users) + Fields: - name: String - age: Number - email: String @@ -57,6 +58,7 @@ describe('getModelDescriptions', function() { result, dedent(` Book (collection: books) + Fields: - title: String - author: ObjectId (ref: User) - _id: ObjectId @@ -82,9 +84,11 @@ describe('getModelDescriptions', function() { result, dedent(` User (collection: users) + Fields: - name: String - _id: ObjectId - __v: Number + Virtuals: - books: Virtual (ref: Book) `) ); @@ -117,11 +121,13 @@ describe('getModelDescriptions', function() { result, dedent(` User (collection: users) + Fields: - name: String - _id: ObjectId - __v: Number Book (collection: books) + Fields: - title: String - author: ObjectId (ref: User) - _id: ObjectId @@ -147,6 +153,7 @@ describe('getModelDescriptions', function() { result, dedent(` Book (collection: books) + Fields: - title: String - tags: String[] - authors: Subdocument[] @@ -159,4 +166,40 @@ describe('getModelDescriptions', function() { `) ); }); + + it('should include methods and statics with their source code', function() { + conn = mongoose.createConnection(); + const UserSchema = new Schema({ name: String }); + UserSchema.methods.greet = function(prefix) { + return `${prefix} ${this.name}`; + }; + UserSchema.statics.findByName = function(name) { + return this.findOne({ name }); + }; + + conn.model('User', UserSchema, 'users'); + + const result = getModelDescriptions(conn); + + assert.strictEqual( + result, + dedent(` + User (collection: users) + Fields: + - name: String + - _id: ObjectId + - __v: Number + Methods: + - greet: + function(prefix) { + return \`\${prefix} \${this.name}\`; + } + Statics: + - findByName: + function(name) { + return this.findOne({ name }); + } + `) + ); + }); });