diff --git a/.changeset/pretty-kangaroos-repair.md b/.changeset/pretty-kangaroos-repair.md new file mode 100644 index 0000000000..49343b4ef5 --- /dev/null +++ b/.changeset/pretty-kangaroos-repair.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-api": minor +--- + +API Generator: Generate field resolver for root blocks + +This allows skipping the `@Field` annotation for root blocks in the entity and it doesn't need the field middleware anymore. diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 7513014aca..d9d7f5f7d5 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -507,10 +507,10 @@ type ProductTag { type ProductVariant { id: ID! name: String! - image: DamImageBlockData! createdAt: DateTime! updatedAt: DateTime! product: Product! + image: DamImageBlockData! } type ProductDiscounts { diff --git a/demo/api/src/news/generated/news.resolver.ts b/demo/api/src/news/generated/news.resolver.ts index 0441dc76d9..3695154158 100644 --- a/demo/api/src/news/generated/news.resolver.ts +++ b/demo/api/src/news/generated/news.resolver.ts @@ -1,12 +1,20 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. -import { AffectedEntity, extractGraphqlFields, RequiredPermission } from "@comet/cms-api"; +import { + AffectedEntity, + BlocksTransformerService, + DamImageBlock, + extractGraphqlFields, + RequiredPermission, + RootBlockDataScalar, +} from "@comet/cms-api"; import { FindOptions } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { GraphQLResolveInfo } from "graphql"; +import { NewsContentBlock } from "../blocks/news-content.block"; import { News, NewsContentScope } from "../entities/news.entity"; import { NewsComment } from "../entities/news-comment.entity"; import { NewsInput, NewsUpdateInput } from "./dto/news.input"; @@ -21,6 +29,7 @@ export class NewsResolver { private readonly entityManager: EntityManager, private readonly newsService: NewsService, @InjectRepository(News) private readonly repository: EntityRepository, + private readonly blocksTransformer: BlocksTransformerService, ) {} @Query(() => News) @@ -124,4 +133,14 @@ export class NewsResolver { async comments(@Parent() news: News): Promise { return news.comments.loadItems(); } + + @ResolveField(() => RootBlockDataScalar(DamImageBlock)) + async image(@Parent() news: News): Promise { + return this.blocksTransformer.transformToPlain(news.image); + } + + @ResolveField(() => RootBlockDataScalar(NewsContentBlock)) + async content(@Parent() news: News): Promise { + return this.blocksTransformer.transformToPlain(news.content); + } } diff --git a/demo/api/src/products/entities/product-variant.entity.ts b/demo/api/src/products/entities/product-variant.entity.ts index 1a00c4c91b..32a856b4c7 100644 --- a/demo/api/src/products/entities/product-variant.entity.ts +++ b/demo/api/src/products/entities/product-variant.entity.ts @@ -1,5 +1,5 @@ import { BlockDataInterface, RootBlock, RootBlockEntity } from "@comet/blocks-api"; -import { CrudField, CrudGenerator, DamImageBlock, RootBlockDataScalar, RootBlockType } from "@comet/cms-api"; +import { CrudField, CrudGenerator, DamImageBlock, RootBlockType } from "@comet/cms-api"; import { BaseEntity, Entity, ManyToOne, OptionalProps, PrimaryKey, Property, Ref } from "@mikro-orm/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; import { v4 as uuid } from "uuid"; @@ -22,7 +22,6 @@ export class ProductVariant extends BaseEntity { name: string; @Property({ customType: new RootBlockType(DamImageBlock) }) - @Field(() => RootBlockDataScalar(DamImageBlock)) @RootBlock(DamImageBlock) image: BlockDataInterface; diff --git a/demo/api/src/products/entities/product.entity.ts b/demo/api/src/products/entities/product.entity.ts index c453e81a2e..a3593610a4 100644 --- a/demo/api/src/products/entities/product.entity.ts +++ b/demo/api/src/products/entities/product.entity.ts @@ -1,5 +1,5 @@ import { BlockDataInterface, RootBlock, RootBlockEntity } from "@comet/blocks-api"; -import { CrudField, CrudGenerator, DamImageBlock, RootBlockDataScalar, RootBlockType } from "@comet/cms-api"; +import { CrudField, CrudGenerator, DamImageBlock, RootBlockType } from "@comet/cms-api"; import { BaseEntity, Collection, @@ -125,7 +125,6 @@ export class Product extends BaseEntity { availableSince?: Date = undefined; @Property({ customType: new RootBlockType(DamImageBlock) }) - @Field(() => RootBlockDataScalar(DamImageBlock)) @RootBlock(DamImageBlock) image: BlockDataInterface; diff --git a/demo/api/src/products/generated/product-variant.resolver.ts b/demo/api/src/products/generated/product-variant.resolver.ts index 462b4597a8..4843e6a289 100644 --- a/demo/api/src/products/generated/product-variant.resolver.ts +++ b/demo/api/src/products/generated/product-variant.resolver.ts @@ -1,6 +1,13 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. -import { AffectedEntity, extractGraphqlFields, RequiredPermission } from "@comet/cms-api"; +import { + AffectedEntity, + BlocksTransformerService, + DamImageBlock, + extractGraphqlFields, + RequiredPermission, + RootBlockDataScalar, +} from "@comet/cms-api"; import { FindOptions, Reference } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; @@ -22,6 +29,7 @@ export class ProductVariantResolver { private readonly productVariantsService: ProductVariantsService, @InjectRepository(ProductVariant) private readonly repository: EntityRepository, @InjectRepository(Product) private readonly productRepository: EntityRepository, + private readonly blocksTransformer: BlocksTransformerService, ) {} @Query(() => ProductVariant) @@ -117,4 +125,9 @@ export class ProductVariantResolver { async product(@Parent() productVariant: ProductVariant): Promise { return productVariant.product.load(); } + + @ResolveField(() => RootBlockDataScalar(DamImageBlock)) + async image(@Parent() productVariant: ProductVariant): Promise { + return this.blocksTransformer.transformToPlain(productVariant.image); + } } diff --git a/demo/api/src/products/generated/product.resolver.ts b/demo/api/src/products/generated/product.resolver.ts index 854e75175d..df4449cca8 100644 --- a/demo/api/src/products/generated/product.resolver.ts +++ b/demo/api/src/products/generated/product.resolver.ts @@ -1,6 +1,13 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. -import { AffectedEntity, extractGraphqlFields, RequiredPermission } from "@comet/cms-api"; +import { + AffectedEntity, + BlocksTransformerService, + DamImageBlock, + extractGraphqlFields, + RequiredPermission, + RootBlockDataScalar, +} from "@comet/cms-api"; import { FindOptions, Reference } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; @@ -33,6 +40,7 @@ export class ProductResolver { @InjectRepository(ProductColor) private readonly productColorRepository: EntityRepository, @InjectRepository(ProductToTag) private readonly productToTagRepository: EntityRepository, @InjectRepository(ProductTag) private readonly productTagRepository: EntityRepository, + private readonly blocksTransformer: BlocksTransformerService, ) {} @Query(() => Product) @@ -281,4 +289,9 @@ export class ProductResolver { async statistics(@Parent() product: Product): Promise { return product.statistics?.load(); } + + @ResolveField(() => RootBlockDataScalar(DamImageBlock)) + async image(@Parent() product: Product): Promise { + return this.blocksTransformer.transformToPlain(product.image); + } } diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index bf43ff78d9..e40f7f0a00 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -8,7 +8,7 @@ import { generateCrudInput } from "./generate-crud-input"; import { buildNameVariants, classNameToInstanceName } from "./utils/build-name-variants"; import { integerTypes } from "./utils/constants"; import { generateImportsCode, Imports } from "./utils/generate-imports-code"; -import { findEnumImportPath, findEnumName, morphTsProperty } from "./utils/ts-morph-helper"; +import { findBlockImportPath, findBlockName, findEnumImportPath, findEnumName, morphTsProperty } from "./utils/ts-morph-helper"; import { GeneratedFile } from "./utils/write-generated-files"; // TODO move into own file @@ -729,20 +729,26 @@ function generateNestedEntityResolver({ generatorOptions, metadata }: { generato const imports: Imports = []; - const { imports: fieldImports, code, hasOutputRelations } = generateRelationsFieldResolver({ generatorOptions, metadata }); + const { + imports: fieldImports, + code, + hasOutputRelations, + needsBlocksTransformer, + } = generateRelationsFieldResolver({ generatorOptions, metadata }); if (!hasOutputRelations) return null; imports.push(...fieldImports); imports.push(generateEntityImport(metadata, generatorOptions.targetDirectory)); return ` - import { RequiredPermission } from "@comet/cms-api"; + import { RequiredPermission, RootBlockDataScalar, BlocksTransformerService } from "@comet/cms-api"; import { Args, ID, Info, Mutation, Query, Resolver, ResolveField, Parent } from "@nestjs/graphql"; ${generateImportsCode(imports)} @Resolver(() => ${metadata.className}) @RequiredPermission(${JSON.stringify(generatorOptions.requiredPermission)}${skipScopeCheck ? `, { skipScopeCheck: true }` : ""}) export class ${classNameSingular}Resolver { + ${needsBlocksTransformer ? `constructor(private readonly blocksTransformer: BlocksTransformerService) {}` : ""} ${code} } `; @@ -773,6 +779,10 @@ function generateRelationsFieldResolver({ generatorOptions, metadata }: { genera } } + const resolveFieldBlockProps = metadata.props.filter((prop) => { + return hasFieldFeature(metadata.class, prop.name, "resolveField") && prop.type === "RootBlockType"; + }); + const hasOutputRelations = outputRelationManyToOneProps.length > 0 || outputRelationOneToManyProps.length > 0 || @@ -786,6 +796,12 @@ function generateRelationsFieldResolver({ generatorOptions, metadata }: { genera imports.push(generateEntityImport(prop.targetMeta, generatorOptions.targetDirectory)); } + for (const prop of resolveFieldBlockProps) { + const blockName = findBlockName(prop.name, metadata); + const importPath = findBlockImportPath(blockName, `${generatorOptions.targetDirectory}`, metadata); + imports.push({ name: blockName, importPath }); + } + const code = ` ${outputRelationManyToOneProps .map( @@ -831,12 +847,24 @@ function generateRelationsFieldResolver({ generatorOptions, metadata }: { genera ) .join("\n")} + ${resolveFieldBlockProps + .map( + (prop) => ` + @ResolveField(() => RootBlockDataScalar(${findBlockName(prop.name, metadata)})) + async ${prop.name}(@Parent() ${instanceNameSingular}: ${metadata.className}): Promise { + return this.blocksTransformer.transformToPlain(${instanceNameSingular}.${prop.name}); + } + `, + ) + .join("\n")} + `.trim(); return { code, imports, hasOutputRelations, + needsBlocksTransformer: resolveFieldBlockProps.length > 0, }; } @@ -892,6 +920,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr imports: relationsFieldResolverImports, code: relationsFieldResolverCode, hasOutputRelations, + needsBlocksTransformer, } = generateRelationsFieldResolver({ generatorOptions, metadata, @@ -921,11 +950,18 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr } } + imports.push({ name: "extractGraphqlFields", importPath: "@comet/cms-api" }); + imports.push({ name: "SortDirection", importPath: "@comet/cms-api" }); + imports.push({ name: "RequiredPermission", importPath: "@comet/cms-api" }); + imports.push({ name: "AffectedEntity", importPath: "@comet/cms-api" }); + imports.push({ name: "validateNotModified", importPath: "@comet/cms-api" }); + imports.push({ name: "RootBlockDataScalar", importPath: "@comet/cms-api" }); + imports.push({ name: "BlocksTransformerService", importPath: "@comet/cms-api" }); + const resolverOut = `import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityRepository, EntityManager } from "@mikro-orm/postgresql"; import { FindOptions, ObjectQuery, Reference } from "@mikro-orm/core"; import { Args, ID, Info, Mutation, Query, Resolver, ResolveField, Parent } from "@nestjs/graphql"; - import { extractGraphqlFields, SortDirection, RequiredPermission, AffectedEntity, validateNotModified } from "@comet/cms-api"; import { GraphQLResolveInfo } from "graphql"; import { ${classNamePlural}Service } from "./${fileNamePlural}.service"; @@ -942,8 +978,8 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr private readonly ${instanceNamePlural}Service: ${classNamePlural}Service, @InjectRepository(${metadata.className}) private readonly repository: EntityRepository<${metadata.className}>, ${[...new Set(injectRepositories.map((meta) => meta.className))] - .map((type) => `@InjectRepository(${type}) private readonly ${classNameToInstanceName(type)}Repository: EntityRepository<${type}>`) - .join(", ")} + .map((type) => `@InjectRepository(${type}) private readonly ${classNameToInstanceName(type)}Repository: EntityRepository<${type}>,`) + .join("")}${needsBlocksTransformer ? `private readonly blocksTransformer: BlocksTransformerService,` : ""} ) {} @Query(() => ${metadata.className}) diff --git a/packages/api/cms-api/src/generator/utils/generate-imports-code.ts b/packages/api/cms-api/src/generator/utils/generate-imports-code.ts index e4fc011206..277e8e61d0 100644 --- a/packages/api/cms-api/src/generator/utils/generate-imports-code.ts +++ b/packages/api/cms-api/src/generator/utils/generate-imports-code.ts @@ -19,10 +19,17 @@ export function generateImportsCode(imports: Imports): string { return true; }); - const importsString = filteredImports - .map((imp) => { - return `import { ${imp.name} } from "${imp.importPath}";`; - }) - .join("\n"); + const importsPathToName: Record = {}; + for (const imp of filteredImports) { + if (!importsPathToName[imp.importPath]) { + importsPathToName[imp.importPath] = []; + } + importsPathToName[imp.importPath].push(imp.name); + } + + let importsString = ""; + for (const [importPath, importNames] of Object.entries(importsPathToName)) { + importsString += `import { ${importNames.sort().join(", ")} } from "${importPath}";\n`; + } return importsString; }