Skip to content

Commit

Permalink
API Generator: Fix imports in generated code for more than one level …
Browse files Browse the repository at this point in the history
…deep relations (#2091)

Fix two related issues:
- input class: for more than one level deep nested impouts use the last
entry from nestedInputFiles as that is the correct file
- resolver imports: collect injectRepositories not only for generating
constructor injects but also generate imports from it

---------

Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
  • Loading branch information
nsams and johnnyomair authored Jun 11, 2024
1 parent 696aefd commit ebdd108
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-hotels-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/cms-api": patch
---

API Generator: Fix imports in generated code for more than one level deep relations
2 changes: 1 addition & 1 deletion packages/api/cms-api/src/generator/generate-crud-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export async function generateCrudInput(
generatedFiles.push(...nestedInputFiles);
imports.push({
name: inputNameClassName,
importPath: nestedInputFiles[0].name.replace(/^dto/, ".").replace(/\.ts$/, ""),
importPath: nestedInputFiles[nestedInputFiles.length - 1].name.replace(/^dto/, ".").replace(/\.ts$/, ""),
});
}
decorators.push(`@Field(() => ${inputNameClassName}${prop.nullable ? ", { nullable: true }" : ""})`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { BaseEntity, Collection, Entity, ManyToOne, OneToMany, OneToOne, PrimaryKey, Property, Ref } from "@mikro-orm/core";
import { MikroORM } from "@mikro-orm/postgresql";
import { LazyMetadataStorage } from "@nestjs/graphql/dist/schema-builder/storages/lazy-metadata.storage";
import { v4 as uuid } from "uuid";

import { generateCrud } from "./generate-crud";
import { lintGeneratedFiles, parseSource } from "./utils/test-helper";

@Entity()
class ProductVariant extends BaseEntity<ProductVariant, "id"> {
@PrimaryKey({ type: "uuid" })
id: string = uuid();

@Property()
title: string;

@ManyToOne(() => ProductData, { ref: true })
productData: Ref<ProductData>;
}

@Entity()
class ProductData extends BaseEntity<ProductData, "id"> {
@PrimaryKey({ type: "uuid" })
id: string = uuid();

@OneToOne(() => Product, { ref: true })
product: Ref<Product>;

@OneToMany(() => ProductVariant, (variant) => variant.productData, { orphanRemoval: true })
variants = new Collection<ProductVariant>(this);
}

@Entity()
class Product extends BaseEntity<Product, "id"> {
@PrimaryKey({ type: "uuid" })
id: string = uuid();

@Property()
title: string;

@OneToOne(() => ProductData, { ref: true })
data: Ref<ProductData>;
}

describe("generate-crud relations two levels", () => {
it("should be a valid generated ts file", async () => {
LazyMetadataStorage.load();
const orm = await MikroORM.init({
type: "postgresql",
dbName: "test-db",
entities: [Product, ProductData, ProductVariant],
});

const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("Product"));
const lintedOut = await lintGeneratedFiles(out);

{
const file = lintedOut.find((file) => file.name === "product.resolver.ts");
if (!file) throw new Error("File not found");
const source = parseSource(file.content);

const classes = source.getClasses();
expect(classes.length).toBe(1);

const cls = classes[0];
expect(cls.getName()).toBe("ProductResolver");
const structure = cls.getStructure();

expect(structure.properties?.length).toBe(0);
expect(structure.methods?.length).toBe(6);

const imports: string[] = [];
for (const tsImport of source.getImportDeclarations()) {
for (const namedImport of tsImport.getNamedImports()) {
imports.push(namedImport.getNameNode().getText());
}
}
expect(imports).toContain("ProductData"); //import for repository
expect(imports).toContain("ProductVariant"); //import for repository
}

{
const file = lintedOut.find((file) => file.name === "dto/product.input.ts");
if (!file) throw new Error("File not found");
const source = parseSource(file.content);

const classes = source.getClasses();
expect(classes.length).toBe(2);

expect(classes[0].getName()).toBe("ProductInput");
expect(classes[1].getName()).toBe("ProductUpdateInput");

const structure = classes[0].getStructure();

expect(structure.properties?.length).toBe(2);
expect(structure.properties?.[1].type).toBe("ProductNestedProductDataInput");

const imports: Record<string, string> = {};
for (const tsImport of source.getImportDeclarations()) {
for (const namedImport of tsImport.getNamedImports()) {
imports[namedImport.getNameNode().getText()] = tsImport.getModuleSpecifierValue();
}
}
expect(imports["ProductNestedProductDataInput"]).toBe("./product-nested-product-data.input");
}

{
const file = lintedOut.find((file) => file.name === "dto/product-data-nested-product-variant.input.ts");
if (!file) throw new Error("File not found");
const source = parseSource(file.content);

const classes = source.getClasses();
expect(classes.length).toBe(1);

expect(classes[0].getName()).toBe("ProductDataNestedProductVariantInput");

const structure = classes[0].getStructure();

expect(structure.properties?.length).toBe(1);
}

{
const file = lintedOut.find((file) => file.name === "dto/product-data-nested-product-variant.input.ts");
if (!file) throw new Error("File not found");
const source = parseSource(file.content);

const classes = source.getClasses();
expect(classes.length).toBe(1);

expect(classes[0].getName()).toBe("ProductDataNestedProductVariantInput");

const structure = classes[0].getStructure();

expect(structure.properties?.length).toBe(1);
}

orm.close();
});
});
18 changes: 10 additions & 8 deletions packages/api/cms-api/src/generator/generate-crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,11 @@ function generateEntityImport(targetMetadata: EntityMetadata<any>, relativeTo: s
function generateInputHandling(
options: { mode: "create" | "update" | "updateNested"; inputName: string; assignEntityCode: string; excludeFields?: string[] },
metadata: EntityMetadata<any>,
): { code: string; injectRepositories: string[] } {
): { code: string; injectRepositories: EntityMetadata<any>[] } {
const { instanceNameSingular } = buildNameVariants(metadata);
const { blockProps, hasVisibleProp, scopeProp } = buildOptions(metadata);

const injectRepositories: string[] = [];
const injectRepositories: EntityMetadata<any>[] = [];

const props = metadata.props.filter((prop) => !options.excludeFields || !options.excludeFields.includes(prop.name));

Expand All @@ -396,7 +396,9 @@ function generateInputHandling(
const inputRelationManyToOneProps = relationManyToOneProps
.filter((prop) => hasFieldFeature(metadata.class, prop.name, "input"))
.map((prop) => {
injectRepositories.push(prop.type);
const targetMeta = prop.targetMeta;
if (!targetMeta) throw new Error("targetMeta is not set for relation");
injectRepositories.push(targetMeta);
return {
name: prop.name,
singularName: singular(prop.name),
Expand All @@ -411,7 +413,7 @@ function generateInputHandling(
.map((prop) => {
const targetMeta = prop.targetMeta;
if (!targetMeta) throw new Error("targetMeta is not set for relation");
injectRepositories.push(prop.type);
injectRepositories.push(targetMeta);
return {
name: prop.name,
singularName: singular(prop.name),
Expand All @@ -426,7 +428,7 @@ function generateInputHandling(
.map((prop) => {
const targetMeta = prop.targetMeta;
if (!targetMeta) throw new Error("targetMeta is not set for relation");
injectRepositories.push(prop.type);
injectRepositories.push(targetMeta);
return {
name: prop.name,
singularName: singular(prop.name),
Expand Down Expand Up @@ -563,7 +565,6 @@ ${
: ""
}
`;

return { code, injectRepositories };
}

Expand Down Expand Up @@ -700,7 +701,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr
const outputRelationOneToOneProps = relationOneToOneProps.filter((prop) => hasFieldFeature(metadata.class, prop.name, "resolveField"));

const imports: Imports = [];
const injectRepositories = new Array<string>();
const injectRepositories = new Array<EntityMetadata<any>>();

const { code: createInputHandlingCode, injectRepositories: createInputHandlingInjectRepositories } = generateInputHandling(
{ mode: "create", inputName: "input", assignEntityCode: `const ${instanceNameSingular} = this.repository.create({` },
Expand Down Expand Up @@ -728,6 +729,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr
if (scopeProp && scopeProp.targetMeta) {
imports.push(generateEntityImport(scopeProp.targetMeta, generatorOptions.targetDirectory));
}
imports.push(...injectRepositories.map((meta) => generateEntityImport(meta, generatorOptions.targetDirectory)));

const resolverOut = `import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityRepository, EntityManager } from "@mikro-orm/postgresql";
Expand All @@ -749,7 +751,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr
private readonly entityManager: EntityManager,
private readonly ${instanceNamePlural}Service: ${classNamePlural}Service,
@InjectRepository(${metadata.className}) private readonly repository: EntityRepository<${metadata.className}>,
${[...new Set<string>(injectRepositories)]
${[...new Set<string>(injectRepositories.map((meta) => meta.className))]
.map((type) => `@InjectRepository(${type}) private readonly ${classNameToInstanceName(type)}Repository: EntityRepository<${type}>`)
.join(", ")}
) {}
Expand Down

0 comments on commit ebdd108

Please sign in to comment.