Skip to content

Commit

Permalink
API Generator: Generate field resolver for root blocks (#2167)
Browse files Browse the repository at this point in the history
This allows skipping the `@Field` annotation for root blocks in the
entity and it doesn't need the field middleware anymore.

---------

Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
  • Loading branch information
nsams and johnnyomair authored Jul 8, 2024
1 parent a31be9c commit bfa94b7
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 19 deletions.
7 changes: 7 additions & 0 deletions .changeset/pretty-kangaroos-repair.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -507,10 +507,10 @@ type ProductTag {
type ProductVariant {
id: ID!
name: String!
image: DamImageBlockData!
createdAt: DateTime!
updatedAt: DateTime!
product: Product!
image: DamImageBlockData!
}

type ProductDiscounts {
Expand Down
21 changes: 20 additions & 1 deletion demo/api/src/news/generated/news.resolver.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -21,6 +29,7 @@ export class NewsResolver {
private readonly entityManager: EntityManager,
private readonly newsService: NewsService,
@InjectRepository(News) private readonly repository: EntityRepository<News>,
private readonly blocksTransformer: BlocksTransformerService,
) {}

@Query(() => News)
Expand Down Expand Up @@ -124,4 +133,14 @@ export class NewsResolver {
async comments(@Parent() news: News): Promise<NewsComment[]> {
return news.comments.loadItems();
}

@ResolveField(() => RootBlockDataScalar(DamImageBlock))
async image(@Parent() news: News): Promise<object> {
return this.blocksTransformer.transformToPlain(news.image);
}

@ResolveField(() => RootBlockDataScalar(NewsContentBlock))
async content(@Parent() news: News): Promise<object> {
return this.blocksTransformer.transformToPlain(news.content);
}
}
3 changes: 1 addition & 2 deletions demo/api/src/products/entities/product-variant.entity.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,7 +22,6 @@ export class ProductVariant extends BaseEntity<ProductVariant, "id"> {
name: string;

@Property({ customType: new RootBlockType(DamImageBlock) })
@Field(() => RootBlockDataScalar(DamImageBlock))
@RootBlock(DamImageBlock)
image: BlockDataInterface;

Expand Down
3 changes: 1 addition & 2 deletions demo/api/src/products/entities/product.entity.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -125,7 +125,6 @@ export class Product extends BaseEntity<Product, "id"> {
availableSince?: Date = undefined;

@Property({ customType: new RootBlockType(DamImageBlock) })
@Field(() => RootBlockDataScalar(DamImageBlock))
@RootBlock(DamImageBlock)
image: BlockDataInterface;

Expand Down
15 changes: 14 additions & 1 deletion demo/api/src/products/generated/product-variant.resolver.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,6 +29,7 @@ export class ProductVariantResolver {
private readonly productVariantsService: ProductVariantsService,
@InjectRepository(ProductVariant) private readonly repository: EntityRepository<ProductVariant>,
@InjectRepository(Product) private readonly productRepository: EntityRepository<Product>,
private readonly blocksTransformer: BlocksTransformerService,
) {}

@Query(() => ProductVariant)
Expand Down Expand Up @@ -117,4 +125,9 @@ export class ProductVariantResolver {
async product(@Parent() productVariant: ProductVariant): Promise<Product> {
return productVariant.product.load();
}

@ResolveField(() => RootBlockDataScalar(DamImageBlock))
async image(@Parent() productVariant: ProductVariant): Promise<object> {
return this.blocksTransformer.transformToPlain(productVariant.image);
}
}
15 changes: 14 additions & 1 deletion demo/api/src/products/generated/product.resolver.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -33,6 +40,7 @@ export class ProductResolver {
@InjectRepository(ProductColor) private readonly productColorRepository: EntityRepository<ProductColor>,
@InjectRepository(ProductToTag) private readonly productToTagRepository: EntityRepository<ProductToTag>,
@InjectRepository(ProductTag) private readonly productTagRepository: EntityRepository<ProductTag>,
private readonly blocksTransformer: BlocksTransformerService,
) {}

@Query(() => Product)
Expand Down Expand Up @@ -281,4 +289,9 @@ export class ProductResolver {
async statistics(@Parent() product: Product): Promise<ProductStatistics | undefined> {
return product.statistics?.load();
}

@ResolveField(() => RootBlockDataScalar(DamImageBlock))
async image(@Parent() product: Product): Promise<object> {
return this.blocksTransformer.transformToPlain(product.image);
}
}
48 changes: 42 additions & 6 deletions packages/api/cms-api/src/generator/generate-crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
}
`;
Expand Down Expand Up @@ -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 ||
Expand All @@ -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(
Expand Down Expand Up @@ -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<object> {
return this.blocksTransformer.transformToPlain(${instanceNameSingular}.${prop.name});
}
`,
)
.join("\n")}
`.trim();

return {
code,
imports,
hasOutputRelations,
needsBlocksTransformer: resolveFieldBlockProps.length > 0,
};
}

Expand Down Expand Up @@ -892,6 +920,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr
imports: relationsFieldResolverImports,
code: relationsFieldResolverCode,
hasOutputRelations,
needsBlocksTransformer,
} = generateRelationsFieldResolver({
generatorOptions,
metadata,
Expand Down Expand Up @@ -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";
Expand All @@ -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<string>(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})
Expand Down
17 changes: 12 additions & 5 deletions packages/api/cms-api/src/generator/utils/generate-imports-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]> = {};
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;
}

0 comments on commit bfa94b7

Please sign in to comment.