From 9ebfcf81c0ab7d4e3090c86bf60d856efd8f1d4a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:52:05 -0700 Subject: [PATCH 1/3] fix(zmodel): prefer to use triple-slash comments as ZModel documentation fixes #1816 --- .../zmodel-documentation-provider.ts | 16 +++++++++ .../src/language-server/zmodel-module.ts | 6 +++- .../zmodel-workspace-manager.ts | 2 +- .../src/plugins/prisma/schema-generator.ts | 29 +++++++-------- .../tests/generator/prisma-generator.test.ts | 35 ++++++++++++++++++- 5 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 packages/schema/src/language-server/zmodel-documentation-provider.ts diff --git a/packages/schema/src/language-server/zmodel-documentation-provider.ts b/packages/schema/src/language-server/zmodel-documentation-provider.ts new file mode 100644 index 000000000..e7da12ee5 --- /dev/null +++ b/packages/schema/src/language-server/zmodel-documentation-provider.ts @@ -0,0 +1,16 @@ +import { AstNode, JSDocDocumentationProvider } from 'langium'; + +/** + * Documentation provider that first tries to use triple-slash comments and falls back to JSDoc comments. + */ +export class ZModelDocumentationProvider extends JSDocDocumentationProvider { + getDocumentation(node: AstNode): string | undefined { + // prefer to user triple-slash comments + if ('comments' in node && Array.isArray(node.comments) && node.comments.length > 0) { + return node.comments.map((c: string) => c.replace(/^[/]*\s*/, '')).join('\n'); + } + + // fall back to JSDoc comments + return super.getDocumentation(node); + } +} diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts index 116d486da..701d31d87 100644 --- a/packages/schema/src/language-server/zmodel-module.ts +++ b/packages/schema/src/language-server/zmodel-module.ts @@ -27,13 +27,14 @@ import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-va import { ZModelCodeActionProvider } from './zmodel-code-action'; import { ZModelCompletionProvider } from './zmodel-completion-provider'; import { ZModelDefinitionProvider } from './zmodel-definition'; +import { ZModelDocumentationProvider } from './zmodel-documentation-provider'; import { ZModelFormatter } from './zmodel-formatter'; import { ZModelHighlightProvider } from './zmodel-highlight'; import { ZModelHoverProvider } from './zmodel-hover'; import { ZModelLinker } from './zmodel-linker'; import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; import { ZModelSemanticTokenProvider } from './zmodel-semantic'; -import ZModelWorkspaceManager from './zmodel-workspace-manager'; +import { ZModelWorkspaceManager } from './zmodel-workspace-manager'; /** * Declaration of custom services - add your own service classes here. @@ -77,6 +78,9 @@ export const ZModelModule: Module createGrammarConfig(services), }, + documentation: { + DocumentationProvider: (services) => new ZModelDocumentationProvider(services), + }, }; // this duplicates createDefaultSharedModule except that a custom WorkspaceManager is used diff --git a/packages/schema/src/language-server/zmodel-workspace-manager.ts b/packages/schema/src/language-server/zmodel-workspace-manager.ts index 79b5bfb5e..734a785cd 100644 --- a/packages/schema/src/language-server/zmodel-workspace-manager.ts +++ b/packages/schema/src/language-server/zmodel-workspace-manager.ts @@ -9,7 +9,7 @@ import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** * Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel */ -export default class ZModelWorkspaceManager extends DefaultWorkspaceManager { +export class ZModelWorkspaceManager extends DefaultWorkspaceManager { public pluginModels = new Set(); protected async loadAdditionalDocuments( diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 54d111d88..2d42abd90 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -282,13 +282,13 @@ export class PrismaSchemaGenerator { this.generateContainerAttribute(model, attr); } - decl.attributes - .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => model.addComment('/// ' + this.zModelGenerator.generate(attr))); - // user defined comments pass-through decl.comments.forEach((c) => model.addComment(c)); + decl.attributes + .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) + .forEach((attr) => model.addComment('/// - _' + this.zModelGenerator.generate(attr) + '_')); + // generate relation fields on base models linking to concrete models this.generateDelegateRelationForBase(model, decl); @@ -765,9 +765,11 @@ export class PrismaSchemaGenerator { const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr)); + // user defined comments pass-through + const docs: string[] = [...field.comments]; + docs.push(...nonPrismaAttributes.map((attr) => '/// - _' + this.zModelGenerator.generate(attr) + '_')); - const result = model.addField(field.name, type, attributes, documentations, addToFront); + const result = model.addField(field.name, type, attributes, docs, addToFront); if (this.mode === 'logical') { if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { @@ -777,8 +779,6 @@ export class PrismaSchemaGenerator { } } - // user defined comments pass-through - field.comments.forEach((c) => result.addComment(c)); return result; } @@ -898,12 +898,12 @@ export class PrismaSchemaGenerator { this.generateContainerAttribute(_enum, attr); } - decl.attributes - .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => _enum.addComment('/// ' + this.zModelGenerator.generate(attr))); - // user defined comments pass-through decl.comments.forEach((c) => _enum.addComment(c)); + + decl.attributes + .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) + .forEach((attr) => _enum.addComment('/// - _' + this.zModelGenerator.generate(attr) + '_')); } private generateEnumField(_enum: PrismaEnum, field: EnumField) { @@ -913,8 +913,9 @@ export class PrismaSchemaGenerator { const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr)); - _enum.addField(field.name, attributes, documentations.concat(field.comments)); + const docs = [...field.comments]; + docs.push(...nonPrismaAttributes.map((attr) => '/// - _' + this.zModelGenerator.generate(attr) + '_')); + _enum.addField(field.name, attributes, docs); } } diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 5affcec77..de4ceba46 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -47,10 +47,35 @@ describe('Prisma generator test', () => { provider = '@core/prisma' } + /// User roles + enum Role { + /// Admin role + ADMIN + /// Regular role + USER + + @@schema("auth") + } + + /// My user model + /// defined here model User { - id String @id + /// the id field + id String @id @allow('read', this == auth()) + role Role @@schema("auth") + @@allow('all', true) + @@deny('update', this != auth()) + } + + /** + * My post model + * defined here + */ + model Post { + id String @id + @@schema("public") } `); @@ -70,6 +95,14 @@ describe('Prisma generator test', () => { 'extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")]' ); expect(content).toContain('schemas = ["auth", "public"]'); + expect(content).toContain('/// My user model'); + expect(content).toContain(`/// - _@@allow('all', true)_`); + expect(content).toContain(`/// the id field`); + expect(content).toContain(`/// - _@allow('read', this == auth())_`); + expect(content).not.toContain('/// My post model'); + expect(content).toContain('/// User roles'); + expect(content).toContain('/// Admin role'); + expect(content).toContain('/// Regular role'); await getDMMF({ datamodel: content }); }); From f7bf4718fe9f2dfc0442d813448bd5def2c7dde8 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:55:07 -0700 Subject: [PATCH 2/3] update starter --- packages/schema/src/res/starter.zmodel | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/schema/src/res/starter.zmodel b/packages/schema/src/res/starter.zmodel index c23cbbbeb..978724dc7 100644 --- a/packages/schema/src/res/starter.zmodel +++ b/packages/schema/src/res/starter.zmodel @@ -1,8 +1,6 @@ // This is a sample model to get you started. -/** - * A sample data source using local sqlite db. - */ +/// A sample data source using local sqlite db. datasource db { provider = 'sqlite' url = 'file:./dev.db' @@ -12,9 +10,7 @@ generator client { provider = "prisma-client-js" } -/** - * User model - */ +/// User model model User { id String @id @default(cuid()) email String @unique @email @length(6, 32) @@ -28,9 +24,7 @@ model User { @@allow('all', auth() == this) } -/** - * Post model - */ +/// Post model model Post { id String @id @default(cuid()) createdAt DateTime @default(now()) From c3ca4c6a7990e39b3ef51ebf4c3beff8c800b641 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:54:49 -0700 Subject: [PATCH 3/3] update --- .../zmodel-documentation-provider.ts | 2 +- .../src/plugins/prisma/schema-generator.ts | 40 +++++++++++-------- .../tests/generator/prisma-generator.test.ts | 10 ++++- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/packages/schema/src/language-server/zmodel-documentation-provider.ts b/packages/schema/src/language-server/zmodel-documentation-provider.ts index e7da12ee5..f960507bc 100644 --- a/packages/schema/src/language-server/zmodel-documentation-provider.ts +++ b/packages/schema/src/language-server/zmodel-documentation-provider.ts @@ -5,7 +5,7 @@ import { AstNode, JSDocDocumentationProvider } from 'langium'; */ export class ZModelDocumentationProvider extends JSDocDocumentationProvider { getDocumentation(node: AstNode): string | undefined { - // prefer to user triple-slash comments + // prefer to use triple-slash comments if ('comments' in node && Array.isArray(node.comments) && node.comments.length > 0) { return node.comments.map((c: string) => c.replace(/^[/]*\s*/, '')).join('\n'); } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 2d42abd90..28a886e47 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -100,6 +100,7 @@ export class PrismaSchemaGenerator { `; private mode: 'logical' | 'physical' = 'physical'; + private customAttributesAsComments = false; // a mapping from full names to shortened names private shortNameMap = new Map(); @@ -117,6 +118,14 @@ export class PrismaSchemaGenerator { this.mode = options.mode as 'logical' | 'physical'; } + if ( + options.customAttributesAsComments !== undefined && + typeof options.customAttributesAsComments !== 'boolean' + ) { + throw new PluginError(name, 'option "customAttributesAsComments" must be a boolean'); + } + this.customAttributesAsComments = options.customAttributesAsComments === true; + const prismaVersion = getPrismaVersion(); if (prismaVersion && semver.lt(prismaVersion, PRISMA_MINIMUM_VERSION)) { warnings.push( @@ -284,10 +293,7 @@ export class PrismaSchemaGenerator { // user defined comments pass-through decl.comments.forEach((c) => model.addComment(c)); - - decl.attributes - .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => model.addComment('/// - _' + this.zModelGenerator.generate(attr) + '_')); + this.getCustomAttributesAsComments(decl).forEach((c) => model.addComment(c)); // generate relation fields on base models linking to concrete models this.generateDelegateRelationForBase(model, decl); @@ -763,12 +769,8 @@ export class PrismaSchemaGenerator { ) .map((attr) => this.makeFieldAttribute(attr)); - const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - // user defined comments pass-through - const docs: string[] = [...field.comments]; - docs.push(...nonPrismaAttributes.map((attr) => '/// - _' + this.zModelGenerator.generate(attr) + '_')); - + const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)]; const result = model.addField(field.name, type, attributes, docs, addToFront); if (this.mode === 'logical') { @@ -900,10 +902,7 @@ export class PrismaSchemaGenerator { // user defined comments pass-through decl.comments.forEach((c) => _enum.addComment(c)); - - decl.attributes - .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => _enum.addComment('/// - _' + this.zModelGenerator.generate(attr) + '_')); + this.getCustomAttributesAsComments(decl).forEach((c) => _enum.addComment(c)); } private generateEnumField(_enum: PrismaEnum, field: EnumField) { @@ -911,12 +910,19 @@ export class PrismaSchemaGenerator { .filter((attr) => this.isPrismaAttribute(attr)) .map((attr) => this.makeFieldAttribute(attr)); - const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - - const docs = [...field.comments]; - docs.push(...nonPrismaAttributes.map((attr) => '/// - _' + this.zModelGenerator.generate(attr) + '_')); + const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)]; _enum.addField(field.name, attributes, docs); } + + private getCustomAttributesAsComments(decl: DataModel | DataModelField | Enum | EnumField) { + if (!this.customAttributesAsComments) { + return []; + } else { + return decl.attributes + .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) + .map((attr) => `/// ${this.zModelGenerator.generate(attr)}`); + } + } } function isDescendantOf(model: DataModel, superModel: DataModel): boolean { diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index de4ceba46..b4f58dcf1 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -85,6 +85,7 @@ describe('Prisma generator test', () => { schemaPath: 'schema.zmodel', output: 'schema.prisma', format: false, + customAttributesAsComments: true, }); const content = fs.readFileSync('schema.prisma', 'utf-8'); @@ -96,9 +97,9 @@ describe('Prisma generator test', () => { ); expect(content).toContain('schemas = ["auth", "public"]'); expect(content).toContain('/// My user model'); - expect(content).toContain(`/// - _@@allow('all', true)_`); + expect(content).toContain(`/// @@allow('all', true)`); expect(content).toContain(`/// the id field`); - expect(content).toContain(`/// - _@allow('read', this == auth())_`); + expect(content).toContain(`/// @allow('read', this == auth())`); expect(content).not.toContain('/// My post model'); expect(content).toContain('/// User roles'); expect(content).toContain('/// Admin role'); @@ -205,6 +206,7 @@ describe('Prisma generator test', () => { provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, + customAttributesAsComments: true, }); const content = fs.readFileSync(name, 'utf-8'); @@ -237,6 +239,7 @@ describe('Prisma generator test', () => { provider: '@core/prisma', schemaPath: 'schema.zmodel', output: name, + customAttributesAsComments: true, }); const content = fs.readFileSync(name, 'utf-8'); @@ -430,6 +433,7 @@ describe('Prisma generator test', () => { schemaPath: 'schema.zmodel', output: name, generateClient: false, + customAttributesAsComments: true, }); const content = fs.readFileSync(name, 'utf-8'); @@ -480,6 +484,7 @@ describe('Prisma generator test', () => { schemaPath: 'schema.zmodel', output: name, format: true, + customAttributesAsComments: true, }); const content = fs.readFileSync(name, 'utf-8'); @@ -511,6 +516,7 @@ describe('Prisma generator test', () => { schemaPath: 'schema.zmodel', output: name, format: true, + customAttributesAsComments: true, }); const content = fs.readFileSync(name, 'utf-8');