Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crud-generator position group for ref-fields #2613

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-impalas-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/cms-api": patch
---

API Generator: Fix generated types for position code
10 changes: 8 additions & 2 deletions demo/api/src/products/entities/product-variant.entity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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";

@ObjectType()
@Entity()
@RootBlockEntity()
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "products" })
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "products", position: { groupByFields: ["product"] } })
export class ProductVariant extends BaseEntity<ProductVariant, "id"> {
[OptionalProps]?: "createdAt" | "updatedAt";

Expand All @@ -25,6 +26,11 @@ export class ProductVariant extends BaseEntity<ProductVariant, "id"> {
@RootBlock(DamImageBlock)
image: BlockDataInterface;

@Property({ columnType: "integer" })
@Field(() => Int)
@Min(1)
position: number;

@ManyToOne(() => Product, { ref: true })
@CrudField({
resolveField: true, // default is true
Expand Down
10 changes: 8 additions & 2 deletions demo/api/src/products/generated/dto/product-variant.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IsEnum } from "class-validator";

export enum ProductVariantSortField {
name = "name",
position = "position",
product = "product",
createdAt = "createdAt",
updatedAt = "updatedAt",
Expand Down
25 changes: 24 additions & 1 deletion demo/api/src/products/generated/product-variant.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProductVariant>,
@InjectRepository(Product) private readonly productRepository: EntityRepository<Product>,
private readonly blocksTransformer: BlocksTransformerService,
Expand Down Expand Up @@ -75,10 +77,18 @@ export class ProductVariantResolver {
@Args("product", { type: () => ID }) product: string,
@Args("input", { type: () => ProductVariantInput }) input: ProductVariantInput,
): Promise<ProductVariant> {
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(),
Expand All @@ -97,6 +107,18 @@ export class ProductVariantResolver {
): Promise<ProductVariant> {
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,
Expand All @@ -116,6 +138,7 @@ export class ProductVariantResolver {
async deleteProductVariant(@Args("id", { type: () => ID }) id: string): Promise<boolean> {
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;
}
Expand Down
52 changes: 52 additions & 0 deletions demo/api/src/products/generated/product-variants.service.ts
Original file line number Diff line number Diff line change
@@ -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<ProductVariant>,
) {}

async incrementPositions(group: { product: string }, lowestPosition: number, highestPosition?: number) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better if the product entity was passed here, and not the ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be much more complicated. in this case grouping is done by references to other entities. they can be configured differently. most of the time it will probably be something like this:

@ManyToOne(() => MyEntity, { ref: true })
myReferencedEntity?: Ref<MyEntity>;

but it could also be a eager reference. They require different code. id is always present.

// 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<ProductVariant> {
Ben-Ho marked this conversation as resolved.
Show resolved Hide resolved
return {
product: { $eq: data.product },
};
}
}
82 changes: 73 additions & 9 deletions packages/api/cms-api/src/generator/generate-crud.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -462,7 +462,19 @@ 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) => {
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.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference) ? "string" : prop.type
}`;
})
.join(",")} }`
: false;

const serviceOut = `import { FilterQuery } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
Expand Down Expand Up @@ -538,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(",")}
};
Expand Down Expand Up @@ -1161,7 +1173,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(",")} }`
: ``
});
Expand All @@ -1170,7 +1190,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);
Expand Down Expand Up @@ -1207,7 +1235,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) {
Expand All @@ -1216,13 +1253,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(",")} },`
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved
: ``
}input.position, ${instanceNameSingular}.position);
}
Expand Down Expand Up @@ -1251,7 +1306,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);`
: ""
Expand Down
Loading