Browse Source

feat(core): Enable Collection query by slug

Relates to #335
Michael Bromley 5 years ago
parent
commit
d5586bc516

+ 3 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2905,6 +2905,7 @@ export type Query = {
   assets: AssetList;
   channel?: Maybe<Channel>;
   channels: Array<Channel>;
+  /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
   collection?: Maybe<Collection>;
   collectionFilters: Array<ConfigurableOperationDefinition>;
   collections: CollectionList;
@@ -2982,7 +2983,8 @@ export type QueryChannelArgs = {
 
 
 export type QueryCollectionArgs = {
-  id: Scalars['ID'];
+  id?: Maybe<Scalars['ID']>;
+  slug?: Maybe<Scalars['String']>;
 };
 
 

+ 5 - 1
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -455,6 +455,7 @@ export type CreateCollectionTranslationInput = {
     name: Scalars['String'];
     slug: Scalars['String'];
     description: Scalars['String'];
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CreateCountryInput = {
@@ -2783,6 +2784,7 @@ export type Query = {
     channel?: Maybe<Channel>;
     activeChannel: Channel;
     collections: CollectionList;
+    /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperationDefinition>;
     countries: CountryList;
@@ -2853,7 +2855,8 @@ export type QueryCollectionsArgs = {
 };
 
 export type QueryCollectionArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type QueryCountriesArgs = {
@@ -3419,6 +3422,7 @@ export type UpdateCollectionTranslationInput = {
     name?: Maybe<Scalars['String']>;
     slug?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateCountryInput = {

+ 2 - 1
packages/common/src/generated-shop-types.ts

@@ -1928,7 +1928,8 @@ export type QueryCollectionsArgs = {
 };
 
 export type QueryCollectionArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type QueryOrderArgs = {

+ 5 - 1
packages/common/src/generated-types.ts

@@ -455,6 +455,7 @@ export type CreateCollectionTranslationInput = {
   name: Scalars['String'];
   slug: Scalars['String'];
   description: Scalars['String'];
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CreateCountryInput = {
@@ -2867,6 +2868,7 @@ export type Query = {
   channel?: Maybe<Channel>;
   activeChannel: Channel;
   collections: CollectionList;
+  /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
   collection?: Maybe<Collection>;
   collectionFilters: Array<ConfigurableOperationDefinition>;
   countries: CountryList;
@@ -2944,7 +2946,8 @@ export type QueryCollectionsArgs = {
 
 
 export type QueryCollectionArgs = {
-  id: Scalars['ID'];
+  id?: Maybe<Scalars['ID']>;
+  slug?: Maybe<Scalars['String']>;
 };
 
 
@@ -3539,6 +3542,7 @@ export type UpdateCollectionTranslationInput = {
   name?: Maybe<Scalars['String']>;
   slug?: Maybe<Scalars['String']>;
   description?: Maybe<Scalars['String']>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateCountryInput = {

+ 31 - 3
packages/core/e2e/collection.e2e-spec.ts

@@ -331,7 +331,7 @@ describe('Collection resolver', () => {
         });
     });
 
-    it('collection query', async () => {
+    it('collection by id', async () => {
         const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
             id: computersCollection.id,
         });
@@ -342,6 +342,34 @@ describe('Collection resolver', () => {
         expect(result.collection.id).toBe(computersCollection.id);
     });
 
+    it('collection by slug', async () => {
+        const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
+            slug: computersCollection.slug,
+        });
+        if (!result.collection) {
+            fail(`did not return the collection`);
+            return;
+        }
+        expect(result.collection.id).toBe(computersCollection.id);
+    });
+
+    it(
+        'throws if neither id nor slug provided',
+        assertThrowsWithMessage(async () => {
+            await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {});
+        }, 'Either the Collection id or slug must be provided'),
+    );
+
+    it(
+        'throws if id and slug do not refer to the same Product',
+        assertThrowsWithMessage(async () => {
+            await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
+                id: computersCollection.id,
+                slug: pearCollection.slug,
+            });
+        }, 'The provided id and slug refer to different Collections'),
+    );
+
     it('parent field', async () => {
         const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
             id: computersCollection.id,
@@ -1266,8 +1294,8 @@ describe('Collection resolver', () => {
 });
 
 export const GET_COLLECTION = gql`
-    query GetCollection($id: ID!) {
-        collection(id: $id) {
+    query GetCollection($id: ID, $slug: String) {
+        collection(id: $id, slug: $slug) {
             ...Collection
             productVariants {
                 items {

+ 14 - 8
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -455,6 +455,7 @@ export type CreateCollectionTranslationInput = {
     name: Scalars['String'];
     slug: Scalars['String'];
     description: Scalars['String'];
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CreateCountryInput = {
@@ -2783,6 +2784,7 @@ export type Query = {
     channel?: Maybe<Channel>;
     activeChannel: Channel;
     collections: CollectionList;
+    /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperationDefinition>;
     countries: CountryList;
@@ -2853,7 +2855,8 @@ export type QueryCollectionsArgs = {
 };
 
 export type QueryCollectionArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type QueryCountriesArgs = {
@@ -3419,6 +3422,7 @@ export type UpdateCollectionTranslationInput = {
     name?: Maybe<Scalars['String']>;
     slug?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateCountryInput = {
@@ -3710,7 +3714,8 @@ export type GetProductsWithVariantIdsQuery = { __typename?: 'Query' } & {
 };
 
 export type GetCollectionQueryVariables = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type GetCollectionQuery = { __typename?: 'Query' } & {
@@ -5457,16 +5462,17 @@ export type DisableProductMutation = { __typename?: 'Mutation' } & {
 };
 
 export type GetCollectionVariantsQueryVariables = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type GetCollectionVariantsQuery = { __typename?: 'Query' } & {
     collection?: Maybe<
-        { __typename?: 'Collection' } & {
-            productVariants: { __typename?: 'ProductVariantList' } & {
-                items: Array<{ __typename?: 'ProductVariant' } & Pick<ProductVariant, 'id' | 'name'>>;
-            };
-        }
+        { __typename?: 'Collection' } & Pick<Collection, 'id'> & {
+                productVariants: { __typename?: 'ProductVariantList' } & {
+                    items: Array<{ __typename?: 'ProductVariant' } & Pick<ProductVariant, 'id' | 'name'>>;
+                };
+            }
     >;
 };
 

+ 2 - 1
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1928,7 +1928,8 @@ export type QueryCollectionsArgs = {
 };
 
 export type QueryCollectionArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type QueryOrderArgs = {

+ 2 - 2
packages/core/e2e/product.e2e-spec.ts

@@ -6,7 +6,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import {
     AddOptionGroupToProduct,
@@ -249,7 +249,7 @@ describe('Product resolver', () => {
                     GET_PRODUCT_SIMPLE,
                     {},
                 );
-            }, 'Either the product id or slug must be provided'),
+            }, 'Either the Product id or slug must be provided'),
         );
 
         it(

+ 11 - 2
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -291,6 +291,14 @@ describe('Shop catalog', () => {
             ]);
         });
 
+        it('collection by slug', async () => {
+            const result = await shopClient.query<
+                GetCollectionVariants.Query,
+                GetCollectionVariants.Variables
+            >(GET_COLLECTION_VARIANTS, { slug: collection.slug });
+            expect(result.collection?.id).toBe(collection.id);
+        });
+
         it('omits variants from disabled products', async () => {
             await adminClient.query<DisableProduct.Mutation, DisableProduct.Variables>(DISABLE_PRODUCT, {
                 id: 'T_17',
@@ -393,8 +401,9 @@ const DISABLE_PRODUCT = gql`
 `;
 
 const GET_COLLECTION_VARIANTS = gql`
-    query GetCollectionVariants($id: ID!) {
-        collection(id: $id) {
+    query GetCollectionVariants($id: ID, $slug: String) {
+        collection(id: $id, slug: $slug) {
+            id
             productVariants {
                 items {
                     id

+ 24 - 15
packages/core/src/api/config/graphql-custom-fields.ts

@@ -130,23 +130,32 @@ export function addGraphQLCustomFields(
                 `;
         }
 
-        if (writeableLocaleStringFields && schema.getType(`${entityName}TranslationInput`)) {
-            if (writeableLocaleStringFields.length) {
-                customFieldTypeDefs += `
-                    input ${entityName}TranslationCustomFieldsInput {
-                        ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
-                    }
+        if (writeableLocaleStringFields) {
+            const translationInputs = [
+                `${entityName}TranslationInput`,
+                `Create${entityName}TranslationInput`,
+                `Update${entityName}TranslationInput`,
+            ];
+            for (const inputName of translationInputs) {
+                if (schema.getType(inputName)) {
+                    if (writeableLocaleStringFields.length) {
+                        customFieldTypeDefs += `
+                            input ${inputName}CustomFields {
+                                ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
+                            }
 
-                    extend input ${entityName}TranslationInput {
-                        customFields: ${entityName}TranslationCustomFieldsInput
-                    }
-                `;
-            } else {
-                customFieldTypeDefs += `
-                    extend input ${entityName}TranslationInput {
-                        customFields: JSON
+                            extend input ${inputName} {
+                                customFields: ${inputName}CustomFields
+                            }
+                        `;
+                    } else {
+                        customFieldTypeDefs += `
+                            extend input ${inputName} {
+                                customFields: JSON
+                            }
+                        `;
                     }
-                `;
+                }
             }
         }
     }

+ 14 - 1
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -12,6 +12,7 @@ import {
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
+import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
 import { Collection } from '../../../entity/collection/collection.entity';
 import { CollectionService } from '../../../service/services/collection.service';
@@ -56,7 +57,19 @@ export class CollectionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryCollectionArgs,
     ): Promise<Translated<Collection> | undefined> {
-        return this.collectionService.findOne(ctx, args.id).then(this.encodeFilters);
+        let collection: Translated<Collection> | undefined;
+        if (args.id) {
+            collection = await this.collectionService.findOne(ctx, args.id);
+            if (args.slug && collection && collection.slug !== args.slug) {
+                throw new UserInputError(`error.collection-id-slug-mismatch`);
+            }
+        } else if (args.slug) {
+            collection = await this.collectionService.findOneBySlug(ctx, args.slug);
+        } else {
+            throw new UserInputError(`error.collection-id-or-slug-must-be-provided`);
+        }
+
+        return this.encodeFilters(collection);
     }
 
     @Mutation()

+ 11 - 1
packages/core/src/api/resolvers/shop/shop-products.resolver.ts

@@ -88,7 +88,17 @@ export class ShopProductsResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryCollectionArgs,
     ): Promise<Translated<Collection> | undefined> {
-        const collection = await this.collectionService.findOne(ctx, args.id);
+        let collection: Translated<Collection> | undefined;
+        if (args.id) {
+            collection = await this.collectionService.findOne(ctx, args.id);
+            if (args.slug && collection && collection.slug !== args.slug) {
+                throw new UserInputError(`error.collection-id-slug-mismatch`);
+            }
+        } else if (args.slug) {
+            collection = await this.collectionService.findOneBySlug(ctx, args.slug);
+        } else {
+            throw new UserInputError(`error.collection-id-or-slug-must-be-provided`);
+        }
         if (collection && collection.isPrivate) {
             return;
         }

+ 2 - 1
packages/core/src/api/schema/admin-api/collection.api.graphql

@@ -1,6 +1,7 @@
 type Query {
     collections(options: CollectionListOptions): CollectionList!
-    collection(id: ID!): Collection
+    "Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result."
+    collection(id: ID, slug: String): Collection
     collectionFilters: [ConfigurableOperationDefinition!]!
 }
 

+ 1 - 1
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -4,7 +4,7 @@ type Query {
     activeOrder: Order
     availableCountries: [Country!]!
     collections(options: CollectionListOptions): CollectionList!
-    collection(id: ID!): Collection
+    collection(id: ID, slug: String): Collection
     eligibleShippingMethods: [ShippingMethodQuote!]!
     me: CurrentUser
     nextOrderStates: [String!]!

+ 6 - 6
packages/core/src/entity/product/product-translation.entity.ts

@@ -1,6 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
-import { Column, Entity, Index, ManyToOne, RelationId } from 'typeorm';
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { Column, Entity, Index, ManyToOne } from 'typeorm';
 
 import { Translation } from '../../common/types/locale-types';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
@@ -24,12 +24,12 @@ export class ProductTranslation extends VendureEntity implements Translation<Pro
 
     @Column('text') description: string;
 
-    @ManyToOne(type => Product, base => base.translations)
+    @ManyToOne(
+        type => Product,
+        base => base.translations,
+    )
     base: Product;
 
-    @RelationId((item: ProductTranslation) => item.base)
-    baseId: ID;
-
     @Column(type => CustomProductFieldsTranslation)
     customFields: CustomProductFieldsTranslation;
 }

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

@@ -18,6 +18,8 @@
     "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",
     "cannot-use-registered-email-address-for-guest-order":  "Cannot use a registered email address for a guest order. Please log in first",
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
+    "collection-id-or-slug-must-be-provided": "Either the Collection id or slug must be provided",
+    "collection-id-slug-mismatch": "The provided id and slug refer to different Collections",
     "country-code-not-valid":  "The countryCode \"{ countryCode }\" was not recognized",
     "coupon-code-expired":  "Coupon code \"{ couponCode }\" has expired",
     "coupon-code-limit-has-been-reached":  "Coupon code cannot be used more than {limit, plural, one {once} other {# times}} per customer",
@@ -60,7 +62,7 @@
     "password-reset-token-not-recognized": "Password reset token not recognized",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
     "products-cannot-be-removed-from-default-channel": "Products cannot be removed from the default Channel",
-    "product-id-or-slug-must-be-provided": "Either the product id or slug must be provided",
+    "product-id-or-slug-must-be-provided": "Either the Product id or slug must be provided",
     "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",
     "product-variant-options-combination-already-exists": "A ProductVariant already exists with the options: {optionNames}",

+ 15 - 0
packages/core/src/service/services/collection.service.ts

@@ -129,6 +129,21 @@ export class CollectionService implements OnModuleInit {
         return translateDeep(collection, ctx.languageCode, ['parent']);
     }
 
+    async findOneBySlug(ctx: RequestContext, slug: string): Promise<Translated<Collection> | undefined> {
+        const translation = await this.connection.getRepository(CollectionTranslation).findOne({
+            relations: ['base'],
+            where: {
+                languageCode: ctx.languageCode,
+                slug,
+            },
+        });
+
+        if (!translation) {
+            return;
+        }
+        return this.findOne(ctx, translation.base.id);
+    }
+
     getAvailableFilters(ctx: RequestContext): ConfigurableOperationDefinition[] {
         return this.configService.catalogOptions.collectionFilters.map(x =>
             configurableDefToOperation(ctx, x),

+ 2 - 1
packages/core/src/service/services/product.service.ts

@@ -112,6 +112,7 @@ export class ProductService {
 
     async findOneBySlug(ctx: RequestContext, slug: string): Promise<Translated<Product> | undefined> {
         const translation = await this.connection.getRepository(ProductTranslation).findOne({
+            relations: ['base'],
             where: {
                 languageCode: ctx.languageCode,
                 slug,
@@ -120,7 +121,7 @@ export class ProductService {
         if (!translation) {
             return;
         }
-        return this.findOne(ctx, translation.baseId);
+        return this.findOne(ctx, translation.base.id);
     }
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {

+ 5 - 1
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -455,6 +455,7 @@ export type CreateCollectionTranslationInput = {
     name: Scalars['String'];
     slug: Scalars['String'];
     description: Scalars['String'];
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CreateCountryInput = {
@@ -2783,6 +2784,7 @@ export type Query = {
     channel?: Maybe<Channel>;
     activeChannel: Channel;
     collections: CollectionList;
+    /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperationDefinition>;
     countries: CountryList;
@@ -2853,7 +2855,8 @@ export type QueryCollectionsArgs = {
 };
 
 export type QueryCollectionArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
 };
 
 export type QueryCountriesArgs = {
@@ -3419,6 +3422,7 @@ export type UpdateCollectionTranslationInput = {
     name?: Maybe<Scalars['String']>;
     slug?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateCountryInput = {

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff