From cb9b1215ae16ce35faa82b058f14295459c7107a Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Tue, 8 Oct 2024 12:44:35 +0200 Subject: [PATCH 1/7] Fix crud-generator position group for ref-fields --- .../cms-api/src/generator/generate-crud.ts | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 2543de185a..467a75e25a 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EntityMetadata } from "@mikro-orm/core"; +import { EntityMetadata, ReferenceType } from "@mikro-orm/core"; import * as path from "path"; import { singular } from "pluralize"; @@ -462,7 +462,16 @@ function generateService({ generatorOptions, metadata }: { generatorOptions: Cru const { classNameSingular, fileNameSingular, classNamePlural } = buildNameVariants(metadata); const { hasPositionProp, positionGroupProps } = buildOptions(metadata, generatorOptions); - const positionGroupType = positionGroupProps.length ? `{ ${positionGroupProps.map((prop) => `${prop.name}: ${prop.type}`).join(",")} }` : false; + const positionGroupType = positionGroupProps.length + ? `{ ${positionGroupProps + .map( + (prop) => + `${prop.name}${prop.nullable ? `?` : ``}: ${ + [ReferenceType.EMBEDDED, ReferenceType.SCALAR].includes(prop.reference) ? prop.type : "string" + }`, + ) + .join(",")} }` + : false; const serviceOut = `import { FilterQuery } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; @@ -1207,7 +1216,16 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr if (input.position !== undefined) { const lastPosition = await this.${instanceNamePlural}Service.getLastPosition(${ positionGroupProps.length - ? `{ ${positionGroupProps.map((prop) => `${prop.name}: ${instanceNameSingular}.${prop.name}`).join(",")} }` + ? `{ ${positionGroupProps + .map( + (prop) => + `${prop.name}: ${instanceNameSingular}.${prop.name}${ + [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) + ? `${prop.nullable ? `?` : ``}.id` + : `` + }`, + ) + .join(",")} }` : `` }); if (input.position > lastPosition + 1) { @@ -1216,13 +1234,31 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr if (${instanceNameSingular}.position < input.position) { await this.${instanceNamePlural}Service.decrementPositions(${ positionGroupProps.length - ? `{ ${positionGroupProps.map((prop) => `${prop.name}: ${instanceNameSingular}.${prop.name}`).join(",")} },` + ? `{ ${positionGroupProps + .map( + (prop) => + `${prop.name}: ${instanceNameSingular}.${prop.name}${ + [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) + ? `${prop.nullable ? `?` : ``}.id` + : `` + }`, + ) + .join(",")} },` : `` }${instanceNameSingular}.position, input.position); } else if (${instanceNameSingular}.position > input.position) { await this.${instanceNamePlural}Service.incrementPositions(${ positionGroupProps.length - ? `{ ${positionGroupProps.map((prop) => `${prop.name}: ${instanceNameSingular}.${prop.name}`).join(",")} },` + ? `{ ${positionGroupProps + .map( + (prop) => + `${prop.name}: ${instanceNameSingular}.${prop.name}${ + [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) + ? `${prop.nullable ? `?` : ``}.id` + : `` + }`, + ) + .join(",")} },` : `` }input.position, ${instanceNameSingular}.position); } @@ -1251,7 +1287,16 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr hasPositionProp ? `await this.${instanceNamePlural}Service.decrementPositions(${ positionGroupProps.length - ? `{ ${positionGroupProps.map((prop) => `${prop.name}: ${instanceNameSingular}.${prop.name}`).join(",")} },` + ? `{ ${positionGroupProps + .map( + (prop) => + `${prop.name}: ${instanceNameSingular}.${prop.name}${ + [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) + ? `${prop.nullable ? `?` : ``}.id` + : `` + }`, + ) + .join(",")} },` : `` }${instanceNameSingular}.position);` : "" From 844cd46e85bbb2b479881da173e473e871749329 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Wed, 30 Oct 2024 11:28:24 +0100 Subject: [PATCH 2/7] Add example for pos-group based on relation, fix bug with dedicatedResolverArg-prop --- .../entities/product-variant.entity.ts | 10 +++- .../generated/dto/product-variant.input.ts | 10 +++- .../generated/dto/product-variant.sort.ts | 1 + .../generated/product-variant.resolver.ts | 25 ++++++++- .../generated/product-variants.service.ts | 52 +++++++++++++++++++ .../cms-api/src/generator/generate-crud.ts | 20 ++++++- 6 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 demo/api/src/products/generated/product-variants.service.ts diff --git a/demo/api/src/products/entities/product-variant.entity.ts b/demo/api/src/products/entities/product-variant.entity.ts index 32a856b4c7..3913850669 100644 --- a/demo/api/src/products/entities/product-variant.entity.ts +++ b/demo/api/src/products/entities/product-variant.entity.ts @@ -1,7 +1,8 @@ import { BlockDataInterface, RootBlock, RootBlockEntity } from "@comet/blocks-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 { Field, ID, Int, ObjectType } from "@nestjs/graphql"; +import { Min } from "class-validator"; import { v4 as uuid } from "uuid"; import { Product } from "./product.entity"; @@ -9,7 +10,7 @@ import { Product } from "./product.entity"; @ObjectType() @Entity() @RootBlockEntity() -@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "products" }) +@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "products", position: { groupByFields: ["product"] } }) export class ProductVariant extends BaseEntity { [OptionalProps]?: "createdAt" | "updatedAt"; @@ -25,6 +26,11 @@ export class ProductVariant extends BaseEntity { @RootBlock(DamImageBlock) image: BlockDataInterface; + @Property({ columnType: "integer" }) + @Field(() => Int) + @Min(1) + position: number; + @ManyToOne(() => Product, { ref: true }) @CrudField({ resolveField: true, // default is true diff --git a/demo/api/src/products/generated/dto/product-variant.input.ts b/demo/api/src/products/generated/dto/product-variant.input.ts index f9d5a6dbd2..876e0bef8f 100644 --- a/demo/api/src/products/generated/dto/product-variant.input.ts +++ b/demo/api/src/products/generated/dto/product-variant.input.ts @@ -2,9 +2,9 @@ // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api"; import { DamImageBlock, PartialType, RootBlockInputScalar } from "@comet/cms-api"; -import { Field, InputType } from "@nestjs/graphql"; +import { Field, InputType, Int } from "@nestjs/graphql"; import { Transform } from "class-transformer"; -import { IsNotEmpty, IsString, ValidateNested } from "class-validator"; +import { IsInt, IsNotEmpty, IsOptional, IsString, Min, ValidateNested } from "class-validator"; @InputType() export class ProductVariantInput { @@ -18,6 +18,12 @@ export class ProductVariantInput { @Transform(({ value }) => (isBlockInputInterface(value) ? value : DamImageBlock.blockInputFactory(value)), { toClassOnly: true }) @ValidateNested() image: BlockInputInterface; + + @IsOptional() + @Min(1) + @IsInt() + @Field(() => Int, { nullable: true }) + position?: number; } @InputType() diff --git a/demo/api/src/products/generated/dto/product-variant.sort.ts b/demo/api/src/products/generated/dto/product-variant.sort.ts index 41b8738148..5bf93b7a3c 100644 --- a/demo/api/src/products/generated/dto/product-variant.sort.ts +++ b/demo/api/src/products/generated/dto/product-variant.sort.ts @@ -6,6 +6,7 @@ import { IsEnum } from "class-validator"; export enum ProductVariantSortField { name = "name", + position = "position", product = "product", createdAt = "createdAt", updatedAt = "updatedAt", diff --git a/demo/api/src/products/generated/product-variant.resolver.ts b/demo/api/src/products/generated/product-variant.resolver.ts index e0b20dfc6b..10f35e4d5e 100644 --- a/demo/api/src/products/generated/product-variant.resolver.ts +++ b/demo/api/src/products/generated/product-variant.resolver.ts @@ -20,12 +20,14 @@ import { ProductVariant } from "../entities/product-variant.entity"; import { PaginatedProductVariants } from "./dto/paginated-product-variants"; import { ProductVariantInput, ProductVariantUpdateInput } from "./dto/product-variant.input"; import { ProductVariantsArgs } from "./dto/product-variants.args"; +import { ProductVariantsService } from "./product-variants.service"; @Resolver(() => ProductVariant) @RequiredPermission("products", { skipScopeCheck: true }) export class ProductVariantResolver { constructor( private readonly entityManager: EntityManager, + private readonly productVariantsService: ProductVariantsService, @InjectRepository(ProductVariant) private readonly repository: EntityRepository, @InjectRepository(Product) private readonly productRepository: EntityRepository, private readonly blocksTransformer: BlocksTransformerService, @@ -75,10 +77,18 @@ export class ProductVariantResolver { @Args("product", { type: () => ID }) product: string, @Args("input", { type: () => ProductVariantInput }) input: ProductVariantInput, ): Promise { + const lastPosition = await this.productVariantsService.getLastPosition({ product }); + let position = input.position; + if (position !== undefined && position < lastPosition + 1) { + await this.productVariantsService.incrementPositions({ product }, position); + } else { + position = lastPosition + 1; + } + const { image: imageInput, ...assignInput } = input; const productVariant = this.repository.create({ ...assignInput, - + position, product: Reference.create(await this.productRepository.findOneOrFail(product)), image: imageInput.transformToBlockData(), @@ -97,6 +107,18 @@ export class ProductVariantResolver { ): Promise { const productVariant = await this.repository.findOneOrFail(id); + if (input.position !== undefined) { + const lastPosition = await this.productVariantsService.getLastPosition({ product: productVariant.product.id }); + if (input.position > lastPosition + 1) { + input.position = lastPosition + 1; + } + if (productVariant.position < input.position) { + await this.productVariantsService.decrementPositions({ product: productVariant.product.id }, productVariant.position, input.position); + } else if (productVariant.position > input.position) { + await this.productVariantsService.incrementPositions({ product: productVariant.product.id }, input.position, productVariant.position); + } + } + const { image: imageInput, ...assignInput } = input; productVariant.assign({ ...assignInput, @@ -116,6 +138,7 @@ export class ProductVariantResolver { async deleteProductVariant(@Args("id", { type: () => ID }) id: string): Promise { const productVariant = await this.repository.findOneOrFail(id); this.entityManager.remove(productVariant); + await this.productVariantsService.decrementPositions({ product: productVariant.product.id }, productVariant.position); await this.entityManager.flush(); return true; } diff --git a/demo/api/src/products/generated/product-variants.service.ts b/demo/api/src/products/generated/product-variants.service.ts new file mode 100644 index 0000000000..254d6ed24a --- /dev/null +++ b/demo/api/src/products/generated/product-variants.service.ts @@ -0,0 +1,52 @@ +// 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 { FilterQuery } from "@mikro-orm/core"; +import { InjectRepository } from "@mikro-orm/nestjs"; +import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; +import { Injectable } from "@nestjs/common"; + +import { ProductVariant } from "../entities/product-variant.entity"; + +@Injectable() +export class ProductVariantsService { + constructor( + private readonly entityManager: EntityManager, + @InjectRepository(ProductVariant) private readonly repository: EntityRepository, + ) {} + + async incrementPositions(group: { product: string }, lowestPosition: number, highestPosition?: number) { + // Increment positions between newPosition (inclusive) and oldPosition (exclusive) + await this.repository.nativeUpdate( + { + $and: [ + { position: { $gte: lowestPosition, ...(highestPosition ? { $lt: highestPosition } : {}) } }, + this.getPositionGroupCondition(group), + ], + }, + { position: this.entityManager.raw("position + 1") }, + ); + } + + async decrementPositions(group: { product: string }, lowestPosition: number, highestPosition?: number) { + // Decrement positions between oldPosition (exclusive) and newPosition (inclusive) + await this.repository.nativeUpdate( + { + $and: [ + { position: { $gt: lowestPosition, ...(highestPosition ? { $lte: highestPosition } : {}) } }, + this.getPositionGroupCondition(group), + ], + }, + { position: this.entityManager.raw("position - 1") }, + ); + } + + async getLastPosition(group: { product: string }) { + return this.repository.count(this.getPositionGroupCondition(group)); + } + + getPositionGroupCondition(data: { product: string }): FilterQuery { + return { + product: { $eq: data.product }, + }; + } +} diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 467a75e25a..3397001ad9 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -1170,7 +1170,15 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr const lastPosition = await this.${instanceNamePlural}Service.getLastPosition(${ positionGroupProps.length ? `{ ${positionGroupProps - .map((prop) => (prop.name === "scope" ? `scope` : `${prop.name}: input.${prop.name}`)) + .map((prop) => + prop.name === "scope" + ? `scope` + : dedicatedResolverArgProps.find( + (dedicatedResolverArgProp) => dedicatedResolverArgProp.name === prop.name, + ) !== undefined + ? prop.name + : `${prop.name}: input.${prop.name}`, + ) .join(",")} }` : `` }); @@ -1179,7 +1187,15 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr await this.${instanceNamePlural}Service.incrementPositions(${ positionGroupProps.length ? `{ ${positionGroupProps - .map((prop) => (prop.name === "scope" ? `scope` : `${prop.name}: input.${prop.name}`)) + .map((prop) => + prop.name === "scope" + ? `scope` + : dedicatedResolverArgProps.find( + (dedicatedResolverArgProp) => dedicatedResolverArgProp.name === prop.name, + ) !== undefined + ? prop.name + : `${prop.name}: input.${prop.name}`, + ) .join(",")} }, ` : `` }position); From f592388fd9c59721a3b66d8a885282e31d138aaa Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 11 Nov 2024 11:36:29 +0100 Subject: [PATCH 3/7] Throw error if used field is not supported reference-type --- .../api/cms-api/src/generator/generate-crud.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 3397001ad9..f8d5201fbd 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -464,12 +464,15 @@ function generateService({ generatorOptions, metadata }: { generatorOptions: Cru const positionGroupType = positionGroupProps.length ? `{ ${positionGroupProps - .map( - (prop) => - `${prop.name}${prop.nullable ? `?` : ``}: ${ - [ReferenceType.EMBEDDED, ReferenceType.SCALAR].includes(prop.reference) ? prop.type : "string" - }`, - ) + .map((prop) => { + const notSupportedReferenceTypes = [ReferenceType.ONE_TO_MANY, ReferenceType.MANY_TO_MANY]; + if (notSupportedReferenceTypes.includes(prop.reference)) { + throw new Error(`Not supported reference-type for position-group. ${prop.name}`); + } + return `${prop.name}${prop.nullable ? `?` : ``}: ${ + [ReferenceType.EMBEDDED, ReferenceType.SCALAR].includes(prop.reference) ? prop.type : "string" + }`; + }) .join(",")} }` : false; From 43c29db664cb37b0d0ecd0035584a0d5e303c629 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 11 Nov 2024 13:30:25 +0100 Subject: [PATCH 4/7] Change check to match other if's --- packages/api/cms-api/src/generator/generate-crud.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index f8d5201fbd..8a0e98e700 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -470,7 +470,7 @@ function generateService({ generatorOptions, metadata }: { generatorOptions: Cru throw new Error(`Not supported reference-type for position-group. ${prop.name}`); } return `${prop.name}${prop.nullable ? `?` : ``}: ${ - [ReferenceType.EMBEDDED, ReferenceType.SCALAR].includes(prop.reference) ? prop.type : "string" + [ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) ? "string" : prop.type }`; }) .join(",")} }` From e5f2c198b7919b031870d8a00edfd70557beeba1 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Mon, 11 Nov 2024 13:53:28 +0100 Subject: [PATCH 5/7] Add changeset --- .changeset/real-impalas-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/real-impalas-walk.md diff --git a/.changeset/real-impalas-walk.md b/.changeset/real-impalas-walk.md new file mode 100644 index 0000000000..8c2abb4216 --- /dev/null +++ b/.changeset/real-impalas-walk.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-api": patch +--- + +Fix generated typing for position code From d297c67e0db53f293cf3d54457a7f28451f37a8d Mon Sep 17 00:00:00 2001 From: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:18:52 +0100 Subject: [PATCH 6/7] Improve changeset --- .changeset/real-impalas-walk.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/real-impalas-walk.md b/.changeset/real-impalas-walk.md index 8c2abb4216..0152c9ffd5 100644 --- a/.changeset/real-impalas-walk.md +++ b/.changeset/real-impalas-walk.md @@ -2,4 +2,4 @@ "@comet/cms-api": patch --- -Fix generated typing for position code +API Generator: Fix generated types for position code From f44e75a2ccd47d932cee9a149c8a05c16be6ee65 Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Fri, 15 Nov 2024 10:02:57 +0100 Subject: [PATCH 7/7] Rename argument from data to group --- packages/api/cms-api/src/generator/generate-crud.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 8a0e98e700..919a2069e5 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -550,7 +550,7 @@ function generateService({ generatorOptions, metadata }: { generatorOptions: Cru ${ positionGroupProps.length - ? `getPositionGroupCondition(data: ${positionGroupType}): FilterQuery<${metadata.className}> { + ? `getPositionGroupCondition(group: ${positionGroupType}): FilterQuery<${metadata.className}> { return { ${positionGroupProps.map((field) => `${field.name}: { $eq: data.${field.name} }`).join(",")} };