Jelajahi Sumber

feat(core): Add `deleteProductOption` mutation to Admin API

Relates to 1134
Michael Bromley 3 tahun lalu
induk
melakukan
d77de9bb4d

+ 9 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2410,6 +2410,8 @@ export type Mutation = {
   deletePaymentMethod: DeletionResponse;
   /** Delete a Product */
   deleteProduct: DeletionResponse;
+  /** Delete a ProductOption */
+  deleteProductOption: DeletionResponse;
   /** Delete a ProductVariant */
   deleteProductVariant: DeletionResponse;
   deletePromotion: DeletionResponse;
@@ -2792,6 +2794,11 @@ export type MutationDeleteProductArgs = {
 };
 
 
+export type MutationDeleteProductOptionArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationDeleteProductVariantArgs = {
   id: Scalars['ID'];
 };
@@ -3250,6 +3257,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
   customerLastName?: Maybe<StringOperators>;
+  transactionId?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -3426,6 +3434,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
   customerLastName?: Maybe<SortOrder>;
+  transactionId?: Maybe<SortOrder>;
   id?: Maybe<SortOrder>;
   createdAt?: Maybe<SortOrder>;
   updatedAt?: Maybe<SortOrder>;

+ 8 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -2381,6 +2381,8 @@ export type Mutation = {
     createProductOption: ProductOption;
     /** Create a new ProductOption within a ProductOptionGroup */
     updateProductOption: ProductOption;
+    /** Delete a ProductOption */
+    deleteProductOption: DeletionResponse;
     reindex: Job;
     runPendingSearchIndexUpdates: Success;
     /** Create a new Product */
@@ -2743,6 +2745,10 @@ export type MutationUpdateProductOptionArgs = {
     input: UpdateProductOptionInput;
 };
 
+export type MutationDeleteProductOptionArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationCreateProductArgs = {
     input: CreateProductInput;
 };
@@ -3028,6 +3034,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
     customerLastName?: Maybe<StringOperators>;
+    transactionId?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3197,6 +3204,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
     customerLastName?: Maybe<SortOrder>;
+    transactionId?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
     updatedAt?: Maybe<SortOrder>;

+ 9 - 0
packages/common/src/generated-types.ts

@@ -2427,6 +2427,8 @@ export type Mutation = {
   createProductOption: ProductOption;
   /** Create a new ProductOption within a ProductOptionGroup */
   updateProductOption: ProductOption;
+  /** Delete a ProductOption */
+  deleteProductOption: DeletionResponse;
   reindex: Job;
   runPendingSearchIndexUpdates: Success;
   /** Create a new Product */
@@ -2858,6 +2860,11 @@ export type MutationUpdateProductOptionArgs = {
 };
 
 
+export type MutationDeleteProductOptionArgs = {
+  id: Scalars['ID'];
+};
+
+
 export type MutationCreateProductArgs = {
   input: CreateProductInput;
 };
@@ -3186,6 +3193,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
   customerLastName?: Maybe<StringOperators>;
+  transactionId?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -3362,6 +3370,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
   customerLastName?: Maybe<SortOrder>;
+  transactionId?: Maybe<SortOrder>;
   id?: Maybe<SortOrder>;
   createdAt?: Maybe<SortOrder>;
   updatedAt?: Maybe<SortOrder>;

+ 43 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -2381,6 +2381,8 @@ export type Mutation = {
     createProductOption: ProductOption;
     /** Create a new ProductOption within a ProductOptionGroup */
     updateProductOption: ProductOption;
+    /** Delete a ProductOption */
+    deleteProductOption: DeletionResponse;
     reindex: Job;
     runPendingSearchIndexUpdates: Success;
     /** Create a new Product */
@@ -2743,6 +2745,10 @@ export type MutationUpdateProductOptionArgs = {
     input: UpdateProductOptionInput;
 };
 
+export type MutationDeleteProductOptionArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationCreateProductArgs = {
     input: CreateProductInput;
 };
@@ -3028,6 +3034,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
     customerLastName?: Maybe<StringOperators>;
+    transactionId?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3197,6 +3204,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
     customerLastName?: Maybe<SortOrder>;
+    transactionId?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
     updatedAt?: Maybe<SortOrder>;
@@ -6836,6 +6844,18 @@ export type AddManualPayment2Mutation = {
     addManualPaymentToOrder: OrderWithLinesFragment | Pick<ManualPaymentStateError, 'errorCode' | 'message'>;
 };
 
+export type GetProductOptionGroupQueryVariables = Exact<{
+    id: Scalars['ID'];
+}>;
+
+export type GetProductOptionGroupQuery = {
+    productOptionGroup?: Maybe<
+        Pick<ProductOptionGroup, 'id' | 'code' | 'name'> & {
+            options: Array<Pick<ProductOption, 'id' | 'code' | 'name'>>;
+        }
+    >;
+};
+
 export type UpdateProductOptionGroupMutationVariables = Exact<{
     input: UpdateProductOptionGroupInput;
 }>;
@@ -6860,6 +6880,14 @@ export type UpdateProductOptionMutation = {
     updateProductOption: Pick<ProductOption, 'id' | 'code' | 'name' | 'groupId'>;
 };
 
+export type DeleteProductOptionMutationVariables = Exact<{
+    id: Scalars['ID'];
+}>;
+
+export type DeleteProductOptionMutation = {
+    deleteProductOption: Pick<DeletionResponse, 'result' | 'message'>;
+};
+
 export type RemoveOptionGroupFromProductMutationVariables = Exact<{
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -9255,6 +9283,15 @@ export namespace AddManualPayment2 {
     >;
 }
 
+export namespace GetProductOptionGroup {
+    export type Variables = GetProductOptionGroupQueryVariables;
+    export type Query = GetProductOptionGroupQuery;
+    export type ProductOptionGroup = NonNullable<GetProductOptionGroupQuery['productOptionGroup']>;
+    export type Options = NonNullable<
+        NonNullable<NonNullable<GetProductOptionGroupQuery['productOptionGroup']>['options']>[number]
+    >;
+}
+
 export namespace UpdateProductOptionGroup {
     export type Variables = UpdateProductOptionGroupMutationVariables;
     export type Mutation = UpdateProductOptionGroupMutation;
@@ -9278,6 +9315,12 @@ export namespace UpdateProductOption {
     export type UpdateProductOption = NonNullable<UpdateProductOptionMutation['updateProductOption']>;
 }
 
+export namespace DeleteProductOption {
+    export type Variables = DeleteProductOptionMutationVariables;
+    export type Mutation = DeleteProductOptionMutation;
+    export type DeleteProductOption = NonNullable<DeleteProductOptionMutation['deleteProductOption']>;
+}
+
 export namespace RemoveOptionGroupFromProduct {
     export type Variables = RemoveOptionGroupFromProductMutationVariables;
     export type Mutation = RemoveOptionGroupFromProductMutation;

+ 164 - 1
packages/core/e2e/product-option.e2e-spec.ts

@@ -8,14 +8,37 @@ import { omit } from '../../common/lib/omit';
 
 import { PRODUCT_OPTION_GROUP_FRAGMENT } from './graphql/fragments';
 import {
+    AddOptionGroupToProduct,
+    AddOptionGroupToProductMutation,
+    AddOptionGroupToProductMutationVariables,
+    CreateProduct,
+    CreateProductMutation,
+    CreateProductMutationVariables,
     CreateProductOption,
     CreateProductOptionGroup,
+    CreateProductVariants,
+    CreateProductVariantsMutation,
+    CreateProductVariantsMutationVariables,
+    DeleteProductOptionMutation,
+    DeleteProductOptionMutationVariables,
+    DeleteProductVariantMutation,
+    DeleteProductVariantMutationVariables,
+    DeletionResult,
+    GetProductOptionGroupQuery,
+    GetProductOptionGroupQueryVariables,
     LanguageCode,
     ProductOptionGroupFragment,
+    ProductVariantFragment,
     UpdateProductOption,
     UpdateProductOptionGroup,
 } from './graphql/generated-e2e-admin-types';
-import { CREATE_PRODUCT_OPTION_GROUP } from './graphql/shared-definitions';
+import {
+    ADD_OPTION_GROUP_TO_PRODUCT,
+    CREATE_PRODUCT,
+    CREATE_PRODUCT_OPTION_GROUP,
+    CREATE_PRODUCT_VARIANTS,
+    DELETE_PRODUCT_VARIANT,
+} from './graphql/shared-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 // tslint:disable:no-non-null-assertion
@@ -150,8 +173,139 @@ describe('ProductOption resolver', () => {
 
         expect(updateProductOption.name).toBe('Middling');
     });
+
+    describe('deletion', () => {
+        let sizeOptionGroupWithOptions: NonNullable<GetProductOptionGroupQuery['productOptionGroup']>;
+        let variants: CreateProductVariantsMutation['createProductVariants'];
+
+        beforeAll(async () => {
+            // Create a new product with a variant in each size option
+            const { createProduct } = await adminClient.query<
+                CreateProductMutation,
+                CreateProductMutationVariables
+            >(CREATE_PRODUCT, {
+                input: {
+                    translations: [
+                        {
+                            languageCode: LanguageCode.en,
+                            name: 'T-shirt',
+                            slug: 't-shirt',
+                            description: 'A television set',
+                        },
+                    ],
+                },
+            });
+
+            const result = await adminClient.query<
+                AddOptionGroupToProductMutation,
+                AddOptionGroupToProductMutationVariables
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
+                optionGroupId: sizeGroup.id,
+                productId: createProduct.id,
+            });
+
+            const { productOptionGroup } = await adminClient.query<
+                GetProductOptionGroupQuery,
+                GetProductOptionGroupQueryVariables
+            >(GET_PRODUCT_OPTION_GROUP, {
+                id: sizeGroup.id,
+            });
+
+            const variantInput: CreateProductVariantsMutationVariables['input'] =
+                productOptionGroup!.options.map((option, i) => ({
+                    productId: createProduct.id,
+                    sku: `TS-${option.code}`,
+                    optionIds: [option.id],
+                    translations: [{ languageCode: LanguageCode.en, name: `T-shirt ${option.code}` }],
+                }));
+
+            const { createProductVariants } = await adminClient.query<
+                CreateProductVariantsMutation,
+                CreateProductVariantsMutationVariables
+            >(CREATE_PRODUCT_VARIANTS, {
+                input: variantInput,
+            });
+            variants = createProductVariants;
+            sizeOptionGroupWithOptions = productOptionGroup!;
+        });
+
+        it(
+            'attempting to delete a non-existent id throws',
+            assertThrowsWithMessage(
+                () =>
+                    adminClient.query<DeleteProductOptionMutation, DeleteProductOptionMutationVariables>(
+                        DELETE_PRODUCT_OPTION,
+                        {
+                            id: '999999',
+                        },
+                    ),
+                "No ProductOption with the id '999999' could be found",
+            ),
+        );
+
+        it('cannot delete ProductOption that is used by a ProductVariant', async () => {
+            const { deleteProductOption } = await adminClient.query<
+                DeleteProductOptionMutation,
+                DeleteProductOptionMutationVariables
+            >(DELETE_PRODUCT_OPTION, {
+                id: sizeOptionGroupWithOptions.options.find(o => o.code === 'medium')!.id,
+            });
+
+            expect(deleteProductOption.result).toBe(DeletionResult.NOT_DELETED);
+            expect(deleteProductOption.message).toBe(
+                'Cannot delete the option "medium" as it is being used by 1 ProductVariant',
+            );
+        });
+
+        it('can delete ProductOption after deleting associated ProductVariant', async () => {
+            const { deleteProductVariant } = await adminClient.query<
+                DeleteProductVariantMutation,
+                DeleteProductVariantMutationVariables
+            >(DELETE_PRODUCT_VARIANT, {
+                id: variants.find(v => v!.name.includes('medium'))!.id,
+            });
+
+            expect(deleteProductVariant.result).toBe(DeletionResult.DELETED);
+
+            const { deleteProductOption } = await adminClient.query<
+                DeleteProductOptionMutation,
+                DeleteProductOptionMutationVariables
+            >(DELETE_PRODUCT_OPTION, {
+                id: sizeOptionGroupWithOptions.options.find(o => o.code === 'medium')!.id,
+            });
+
+            expect(deleteProductOption.result).toBe(DeletionResult.DELETED);
+        });
+
+        it('deleted ProductOptions not included in query result', async () => {
+            const { productOptionGroup } = await adminClient.query<
+                GetProductOptionGroupQuery,
+                GetProductOptionGroupQueryVariables
+            >(GET_PRODUCT_OPTION_GROUP, {
+                id: sizeGroup.id,
+            });
+
+            expect(productOptionGroup?.options.length).toBe(2);
+            expect(productOptionGroup?.options.findIndex(o => o.code === 'medium')).toBe(-1);
+        });
+    });
 });
 
+const GET_PRODUCT_OPTION_GROUP = gql`
+    query GetProductOptionGroup($id: ID!) {
+        productOptionGroup(id: $id) {
+            id
+            code
+            name
+            options {
+                id
+                code
+                name
+            }
+        }
+    }
+`;
+
 const UPDATE_PRODUCT_OPTION_GROUP = gql`
     mutation UpdateProductOptionGroup($input: UpdateProductOptionGroupInput!) {
         updateProductOptionGroup(input: $input) {
@@ -187,3 +341,12 @@ const UPDATE_PRODUCT_OPTION = gql`
         }
     }
 `;
+
+const DELETE_PRODUCT_OPTION = gql`
+    mutation DeleteProductOption($id: ID!) {
+        deleteProductOption(id: $id) {
+            result
+            message
+        }
+    }
+`;

+ 13 - 0
packages/core/src/api/resolvers/admin/product-option.resolver.ts

@@ -1,7 +1,10 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    DeletionResponse,
+    DeletionResult,
     MutationCreateProductOptionArgs,
     MutationCreateProductOptionGroupArgs,
+    MutationDeleteProductOptionArgs,
     MutationUpdateProductOptionArgs,
     MutationUpdateProductOptionGroupArgs,
     Permission,
@@ -98,4 +101,14 @@ export class ProductOptionResolver {
         const { input } = args;
         return this.productOptionService.update(ctx, input);
     }
+
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.DeleteCatalog, Permission.DeleteProduct)
+    async deleteProductOption(
+        @Ctx() ctx: RequestContext,
+        @Args() { id }: MutationDeleteProductOptionArgs,
+    ): Promise<DeletionResponse> {
+        return this.productOptionService.softDelete(ctx, id);
+    }
 }

+ 6 - 3
packages/core/src/api/resolvers/entity/product-option-group-entity.resolver.ts

@@ -33,10 +33,13 @@ export class ProductOptionGroupEntityResolver {
         @Ctx() ctx: RequestContext,
         @Parent() optionGroup: Translated<ProductOptionGroup>,
     ): Promise<Array<Translated<ProductOption>>> {
+        let options: Array<Translated<ProductOption>>;
         if (optionGroup.options) {
-            return optionGroup.options;
+            options = optionGroup.options;
+        } else {
+            const group = await this.productOptionGroupService.findOne(ctx, optionGroup.id);
+            options = group?.options ?? [];
         }
-        const group = await this.productOptionGroupService.findOne(ctx, optionGroup.id);
-        return group ? group.options : [];
+        return options.filter(o => !o.deletedAt);
     }
 }

+ 2 - 0
packages/core/src/api/schema/admin-api/product-option-group.api.graphql

@@ -12,6 +12,8 @@ type Mutation {
     createProductOption(input: CreateProductOptionInput!): ProductOption!
     "Create a new ProductOption within a ProductOptionGroup"
     updateProductOption(input: UpdateProductOptionInput!): ProductOption!
+    "Delete a ProductOption"
+    deleteProductOption(id: ID!): DeletionResponse!
 }
 
 input ProductOptionGroupTranslationInput {

+ 1 - 1
packages/core/src/event-bus/events/product-option-event.ts

@@ -27,7 +27,7 @@ export class ProductOptionEvent extends VendureEntityEvent<ProductOption, Produc
     constructor(
         ctx: RequestContext,
         entity: ProductOption,
-        type: 'created' | 'updated',
+        type: 'created' | 'updated' | 'deleted',
         input?: ProductOptionInputTypes,
     ) {
         super(entity, type, ctx, input);

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -116,6 +116,7 @@
     "facet-value-force-deleted": "The selected FacetValue was removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}} and deleted",
     "facet-value-used": "The selected FacetValue is assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "payment-method-used-in-channels": "The selected PaymentMethod is assigned to the following Channels: { channelCodes }. Set \"force: true\" to delete from all Channels.",
+    "product-option-used": "Cannot delete the option \"{code}\" as it is being used by {count, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "zone-used-in-channels": "The selected Zone cannot be deleted as it used as a default in the following Channels: { channelCodes }",
     "zone-used-in-tax-rates": "The selected Zone cannot be deleted as it is used in the following TaxRates: { taxRateNames }"
   }

+ 31 - 0
packages/core/src/service/services/product-option.service.ts

@@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
 import {
     CreateGroupOptionInput,
     CreateProductOptionInput,
+    DeletionResponse,
+    DeletionResult,
     UpdateProductOptionInput,
 } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
@@ -10,6 +12,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { TransactionalConnection } from '../../connection/transactional-connection';
+import { ProductVariant } from '../../entity/index';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
@@ -89,4 +92,32 @@ export class ProductOptionService {
         this.eventBus.publish(new ProductOptionEvent(ctx, option, 'updated', input));
         return assertFound(this.findOne(ctx, option.id));
     }
+
+    async softDelete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
+        const productOption = await this.connection.getEntityOrThrow(ctx, ProductOption, id);
+        const consumingVariants = await this.connection
+            .getRepository(ctx, ProductVariant)
+            .createQueryBuilder('variant')
+            .leftJoin('variant.options', 'option')
+            .where('variant.deletedAt IS NULL')
+            .andWhere('option.id = :id', { id })
+            .getMany();
+        if (consumingVariants.length) {
+            const message = ctx.translate('message.product-option-used', {
+                code: productOption.code,
+                count: consumingVariants.length,
+            });
+            return {
+                result: DeletionResult.NOT_DELETED,
+                message,
+            };
+        } else {
+            productOption.deletedAt = new Date();
+            await this.connection.getRepository(ctx, ProductOption).save(productOption, { reload: false });
+            this.eventBus.publish(new ProductOptionEvent(ctx, productOption, 'deleted', id));
+            return {
+                result: DeletionResult.DELETED,
+            };
+        }
+    }
 }

+ 8 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -2381,6 +2381,8 @@ export type Mutation = {
     createProductOption: ProductOption;
     /** Create a new ProductOption within a ProductOptionGroup */
     updateProductOption: ProductOption;
+    /** Delete a ProductOption */
+    deleteProductOption: DeletionResponse;
     reindex: Job;
     runPendingSearchIndexUpdates: Success;
     /** Create a new Product */
@@ -2743,6 +2745,10 @@ export type MutationUpdateProductOptionArgs = {
     input: UpdateProductOptionInput;
 };
 
+export type MutationDeleteProductOptionArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationCreateProductArgs = {
     input: CreateProductInput;
 };
@@ -3028,6 +3034,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
     customerLastName?: Maybe<StringOperators>;
+    transactionId?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3197,6 +3204,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
     customerLastName?: Maybe<SortOrder>;
+    transactionId?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
     updatedAt?: Maybe<SortOrder>;

+ 8 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -2381,6 +2381,8 @@ export type Mutation = {
     createProductOption: ProductOption;
     /** Create a new ProductOption within a ProductOptionGroup */
     updateProductOption: ProductOption;
+    /** Delete a ProductOption */
+    deleteProductOption: DeletionResponse;
     reindex: Job;
     runPendingSearchIndexUpdates: Success;
     /** Create a new Product */
@@ -2743,6 +2745,10 @@ export type MutationUpdateProductOptionArgs = {
     input: UpdateProductOptionInput;
 };
 
+export type MutationDeleteProductOptionArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationCreateProductArgs = {
     input: CreateProductInput;
 };
@@ -3028,6 +3034,7 @@ export type OrderAddress = {
 
 export type OrderFilterParameter = {
     customerLastName?: Maybe<StringOperators>;
+    transactionId?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -3197,6 +3204,7 @@ export type OrderProcessState = {
 
 export type OrderSortParameter = {
     customerLastName?: Maybe<SortOrder>;
+    transactionId?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
     updatedAt?: Maybe<SortOrder>;

File diff ditekan karena terlalu besar
+ 0 - 0
schema-admin.json


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini