From dea39fa848919827e57443504a6a48f0c918e958 Mon Sep 17 00:00:00 2001 From: javan Date: Sat, 10 May 2025 17:40:34 +0300 Subject: [PATCH 1/2] feat: :sparkles: add enum value-level descriptions and deprecation metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhances the `registerEnum` function to include per-enum-value descriptions and deprecation reasons when registering enums using `registerEnumType`. - Introduces a `valuesMap` generated from the `SchemaEnum` value documentation (e.g., comments in schema.prisma). - Deprecation reasons are parsed from `@deprecated` annotations. - Regular comments are used as descriptions for enum values. - Values without comments are skipped in the `valuesMap`. - Includes a helper `extractEnumValueDocs` to isolate parsing logic. Example: /// @deprecated Use USER instead ADMIN → becomes: ADMIN: { deprecationReason: "Use USER instead" } This allows richer metadata in GraphQL schema introspection and enhances tooling and client generation. Usage remains backward compatible for enums without documentation. --- prisma/schema.prisma | 8 ++++++++ src/handlers/prisma-enum-doc.ts | 24 +++++++++++++++++++++++ src/handlers/register-enum.ts | 34 +++++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 src/handlers/prisma-enum-doc.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 974b7800..a34af201 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -104,11 +104,19 @@ model Comment { articleId String? } +/// user access control enum Role { + /// default user access control USER + /// have full access control + NINGA + /// @deprecated Use USER instead + ADMIN + REVIEWER // no comment and won't show in registerenum valuemaps } model Profile { + /// @deprecated Use new name instead id Int @id @default(autoincrement()) user User @relation(fields: [userId], references: [id]) userId String @unique diff --git a/src/handlers/prisma-enum-doc.ts b/src/handlers/prisma-enum-doc.ts new file mode 100644 index 00000000..ff3f59c2 --- /dev/null +++ b/src/handlers/prisma-enum-doc.ts @@ -0,0 +1,24 @@ +// utils/prisma-enum-doc.ts + +type EnumValueDocInfo = { description: string } | { deprecationReason: string }; + +export function extractEnumValueDocs( + values: readonly { name: string; [key: string]: any }[], +): Record { + return Object.fromEntries( + values + .map((value): [string, EnumValueDocInfo] | null => { + const { name } = value; + const documentation: unknown = value.documentation; + + if (typeof documentation !== 'string') return null; + + if (documentation.startsWith('@deprecated')) { + return [name, { deprecationReason: documentation.slice(11).trim() }]; + } + + return [name, { description: documentation }]; + }) + .filter((entry): entry is [string, EnumValueDocInfo] => entry !== null), + ); +} diff --git a/src/handlers/register-enum.ts b/src/handlers/register-enum.ts index b2649ce0..9f71501c 100644 --- a/src/handlers/register-enum.ts +++ b/src/handlers/register-enum.ts @@ -2,13 +2,16 @@ import { EnumDeclarationStructure, StructureKind } from 'ts-morph'; import { ImportDeclarationMap } from '../helpers/import-declaration-map'; import { EventArguments, SchemaEnum } from '../types'; +import { extractEnumValueDocs } from './prisma-enum-doc'; export function registerEnum(enumType: SchemaEnum, args: EventArguments) { - const { getSourceFile, enums, config } = args; + const { config, enums, getSourceFile } = args; if (!config.emitBlocks.prismaEnums && !enums[enumType.name]) return; const dataModelEnum = enums[enumType.name]; + const enumTypesData = dataModelEnum?.values || []; + console.log('enumTypesData', enumTypesData); const sourceFile = getSourceFile({ name: enumType.name, type: 'enum', @@ -17,18 +20,37 @@ export function registerEnum(enumType: SchemaEnum, args: EventArguments) { const importDeclarations = new ImportDeclarationMap(); importDeclarations.set('registerEnumType', { - namedImports: [{ name: 'registerEnumType' }], moduleSpecifier: '@nestjs/graphql', + namedImports: [{ name: 'registerEnumType' }], }); + // Create valuesMap based on documentation + const valuesMap = extractEnumValueDocs(enumTypesData); + + const valuesMapString = + Object.keys(valuesMap).length > 0 + ? `, valuesMap: ${JSON.stringify(valuesMap, null, 2).replace(/"([^"]+)":/g, '$1:')}` + : ''; + + // Filter out empty entries (those that don't have description or deprecationReason) + const filteredValuesMap = Object.fromEntries( + Object.entries(valuesMap).filter(([key, value]) => Object.keys(value).length > 0), + ); + + // Format valuesMap for the final output + const formattedValuesMap = JSON.stringify(filteredValuesMap, null, 2).replace( + /"([^"]+)":/g, + '$1:', + ); + const enumStructure: EnumDeclarationStructure = { - kind: StructureKind.Enum, isExported: true, - name: enumType.name, + kind: StructureKind.Enum, members: enumType.values.map(v => ({ - name: v, initializer: JSON.stringify(v), + name: v, })), + name: enumType.name, }; sourceFile.set({ @@ -38,7 +60,7 @@ export function registerEnum(enumType: SchemaEnum, args: EventArguments) { '\n', `registerEnumType(${enumType.name}, { name: '${ enumType.name - }', description: ${JSON.stringify(dataModelEnum?.documentation)} })`, + }', description: ${JSON.stringify(dataModelEnum?.documentation)}, valuesMap: ${formattedValuesMap} })`, ], }); } From 2eee11973cd2b7dd05d55e170296d5dc0ac98858 Mon Sep 17 00:00:00 2001 From: javan Date: Sat, 10 May 2025 18:04:39 +0300 Subject: [PATCH 2/2] update --- .../user-update-without-articles.input.ts | 73 ++++++++++--------- src/handlers/register-enum.ts | 35 ++++----- 2 files changed, 52 insertions(+), 56 deletions(-) diff --git a/@generated/user/user-update-without-articles.input.ts b/@generated/user/user-update-without-articles.input.ts index db2b9835..f1a1ab43 100644 --- a/@generated/user/user-update-without-articles.input.ts +++ b/@generated/user/user-update-without-articles.input.ts @@ -15,54 +15,55 @@ import { ProfileUpdateOneWithoutUserNestedInput } from '../profile/profile-updat @InputType() export class UserUpdateWithoutArticlesInput { - @Field(() => StringFieldUpdateOperationsInput, { nullable: true }) - id?: StringFieldUpdateOperationsInput; - @Field(() => StringFieldUpdateOperationsInput, { nullable: true }) - email?: StringFieldUpdateOperationsInput; + @Field(() => StringFieldUpdateOperationsInput, {nullable:true}) + id?: StringFieldUpdateOperationsInput; - @Field(() => StringFieldUpdateOperationsInput, { nullable: true }) - name?: StringFieldUpdateOperationsInput; + @Field(() => StringFieldUpdateOperationsInput, {nullable:true}) + email?: StringFieldUpdateOperationsInput; - @Field(() => StringFieldUpdateOperationsInput, { nullable: true }) - password?: StringFieldUpdateOperationsInput; + @Field(() => StringFieldUpdateOperationsInput, {nullable:true}) + name?: StringFieldUpdateOperationsInput; - @Field(() => NullableStringFieldUpdateOperationsInput, { nullable: true }) - bio?: NullableStringFieldUpdateOperationsInput; + @Field(() => StringFieldUpdateOperationsInput, {nullable:true}) + password?: StringFieldUpdateOperationsInput; - @Field(() => NullableStringFieldUpdateOperationsInput, { nullable: true }) - image?: NullableStringFieldUpdateOperationsInput; + @Field(() => NullableStringFieldUpdateOperationsInput, {nullable:true}) + bio?: NullableStringFieldUpdateOperationsInput; - @Field(() => NullableIntFieldUpdateOperationsInput, { nullable: true }) - countComments?: NullableIntFieldUpdateOperationsInput; + @Field(() => NullableStringFieldUpdateOperationsInput, {nullable:true}) + image?: NullableStringFieldUpdateOperationsInput; - @Field(() => NullableFloatFieldUpdateOperationsInput, { nullable: true }) - rating?: NullableFloatFieldUpdateOperationsInput; + @Field(() => NullableIntFieldUpdateOperationsInput, {nullable:true}) + countComments?: NullableIntFieldUpdateOperationsInput; - @Field(() => NullableDecimalFieldUpdateOperationsInput, { nullable: true }) - @Type(() => NullableDecimalFieldUpdateOperationsInput) - money?: NullableDecimalFieldUpdateOperationsInput; + @Field(() => NullableFloatFieldUpdateOperationsInput, {nullable:true}) + rating?: NullableFloatFieldUpdateOperationsInput; - @Field(() => NullableEnumRoleFieldUpdateOperationsInput, { nullable: true }) - role?: NullableEnumRoleFieldUpdateOperationsInput; + @Field(() => NullableDecimalFieldUpdateOperationsInput, {nullable:true}) + @Type(() => NullableDecimalFieldUpdateOperationsInput) + money?: NullableDecimalFieldUpdateOperationsInput; - @Field(() => UserUpdateManyWithoutFollowersNestedInput, { nullable: true }) - @Type(() => UserUpdateManyWithoutFollowersNestedInput) - following?: UserUpdateManyWithoutFollowersNestedInput; + @Field(() => NullableEnumRoleFieldUpdateOperationsInput, {nullable:true}) + role?: NullableEnumRoleFieldUpdateOperationsInput; - @Field(() => UserUpdateManyWithoutFollowingNestedInput, { nullable: true }) - @Type(() => UserUpdateManyWithoutFollowingNestedInput) - followers?: UserUpdateManyWithoutFollowingNestedInput; + @Field(() => UserUpdateManyWithoutFollowersNestedInput, {nullable:true}) + @Type(() => UserUpdateManyWithoutFollowersNestedInput) + following?: UserUpdateManyWithoutFollowersNestedInput; - @Field(() => ArticleUpdateManyWithoutFavoritedByNestedInput, { nullable: true }) - @Type(() => ArticleUpdateManyWithoutFavoritedByNestedInput) - favoriteArticles?: ArticleUpdateManyWithoutFavoritedByNestedInput; + @Field(() => UserUpdateManyWithoutFollowingNestedInput, {nullable:true}) + @Type(() => UserUpdateManyWithoutFollowingNestedInput) + followers?: UserUpdateManyWithoutFollowingNestedInput; - @Field(() => CommentUpdateManyWithoutAuthorNestedInput, { nullable: true }) - @Type(() => CommentUpdateManyWithoutAuthorNestedInput) - comments?: CommentUpdateManyWithoutAuthorNestedInput; + @Field(() => ArticleUpdateManyWithoutFavoritedByNestedInput, {nullable:true}) + @Type(() => ArticleUpdateManyWithoutFavoritedByNestedInput) + favoriteArticles?: ArticleUpdateManyWithoutFavoritedByNestedInput; - @Field(() => ProfileUpdateOneWithoutUserNestedInput, { nullable: true }) - @Type(() => ProfileUpdateOneWithoutUserNestedInput) - profile?: ProfileUpdateOneWithoutUserNestedInput; + @Field(() => CommentUpdateManyWithoutAuthorNestedInput, {nullable:true}) + @Type(() => CommentUpdateManyWithoutAuthorNestedInput) + comments?: CommentUpdateManyWithoutAuthorNestedInput; + + @Field(() => ProfileUpdateOneWithoutUserNestedInput, {nullable:true}) + @Type(() => ProfileUpdateOneWithoutUserNestedInput) + profile?: ProfileUpdateOneWithoutUserNestedInput; } diff --git a/src/handlers/register-enum.ts b/src/handlers/register-enum.ts index 9f71501c..bed40956 100644 --- a/src/handlers/register-enum.ts +++ b/src/handlers/register-enum.ts @@ -11,7 +11,6 @@ export function registerEnum(enumType: SchemaEnum, args: EventArguments) { const dataModelEnum = enums[enumType.name]; const enumTypesData = dataModelEnum?.values || []; - console.log('enumTypesData', enumTypesData); const sourceFile = getSourceFile({ name: enumType.name, type: 'enum', @@ -24,33 +23,29 @@ export function registerEnum(enumType: SchemaEnum, args: EventArguments) { namedImports: [{ name: 'registerEnumType' }], }); - // Create valuesMap based on documentation + // Extract valuesMap from enum documentation const valuesMap = extractEnumValueDocs(enumTypesData); - const valuesMapString = - Object.keys(valuesMap).length > 0 - ? `, valuesMap: ${JSON.stringify(valuesMap, null, 2).replace(/"([^"]+)":/g, '$1:')}` - : ''; - - // Filter out empty entries (those that don't have description or deprecationReason) + // Remove entries with no description or deprecationReason const filteredValuesMap = Object.fromEntries( - Object.entries(valuesMap).filter(([key, value]) => Object.keys(value).length > 0), + Object.entries(valuesMap).filter(([_, v]) => Object.keys(v).length > 0), ); - // Format valuesMap for the final output - const formattedValuesMap = JSON.stringify(filteredValuesMap, null, 2).replace( - /"([^"]+)":/g, - '$1:', - ); + // Format only if needed + const hasValuesMap = Object.keys(filteredValuesMap).length > 0; + const formattedValuesMap = hasValuesMap + ? JSON.stringify(filteredValuesMap, null, 2).replace(/"([^"]+)":/g, '$1:') + : ''; + const valuesMapEntry = hasValuesMap ? `, valuesMap: ${formattedValuesMap}` : ''; const enumStructure: EnumDeclarationStructure = { - isExported: true, kind: StructureKind.Enum, + isExported: true, + name: enumType.name, members: enumType.values.map(v => ({ - initializer: JSON.stringify(v), name: v, + initializer: JSON.stringify(v), })), - name: enumType.name, }; sourceFile.set({ @@ -58,9 +53,9 @@ export function registerEnum(enumType: SchemaEnum, args: EventArguments) { ...importDeclarations.toStatements(), enumStructure, '\n', - `registerEnumType(${enumType.name}, { name: '${ - enumType.name - }', description: ${JSON.stringify(dataModelEnum?.documentation)}, valuesMap: ${formattedValuesMap} })`, + `registerEnumType(${enumType.name}, { name: '${enumType.name}', description: ${JSON.stringify( + dataModelEnum?.documentation, + )}${valuesMapEntry} })`, ], }); }