瀏覽代碼

feat(server): Expose collections field on Product type

Michael Bromley 6 年之前
父節點
當前提交
2c93fa9c74

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


+ 27 - 7
schema.json

@@ -6256,13 +6256,9 @@
             "description": null,
             "args": [],
             "type": {
-              "kind": "NON_NULL",
-              "name": null,
-              "ofType": {
-                "kind": "OBJECT",
-                "name": "Collection",
-                "ofType": null
-              }
+              "kind": "OBJECT",
+              "name": "Collection",
+              "ofType": null
             },
             "isDeprecated": false,
             "deprecationReason": null
@@ -12955,6 +12951,30 @@
             "isDeprecated": false,
             "deprecationReason": null
           },
+          {
+            "name": "collections",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "Collection",
+                    "ofType": null
+                  }
+                }
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
           {
             "name": "customFields",
             "description": null,

+ 1 - 1
server/dev-config.ts

@@ -24,7 +24,7 @@ export const devConfig: VendureConfig = {
     adminApiPath: ADMIN_API_PATH,
     shopApiPath: SHOP_API_PATH,
     dbConnectionOptions: {
-        synchronize: true,
+        synchronize: false,
         logging: false,
 
         type: 'mysql',

+ 30 - 4
server/e2e/collection.e2e-spec.ts

@@ -101,7 +101,7 @@ describe('Collection resolver', () => {
 
             electronicsCollection = result.createCollection;
             expect(electronicsCollection).toMatchSnapshot();
-            expect(electronicsCollection.parent.name).toBe(ROOT_COLLECTION_NAME);
+            expect(electronicsCollection.parent!.name).toBe(ROOT_COLLECTION_NAME);
         });
 
         it('creates a nested category', async () => {
@@ -127,7 +127,7 @@ describe('Collection resolver', () => {
                 },
             );
             computersCollection = result.createCollection;
-            expect(computersCollection.parent.name).toBe(electronicsCollection.name);
+            expect(computersCollection.parent!.name).toBe(electronicsCollection.name);
         });
 
         it('creates a 2nd level nested category', async () => {
@@ -153,7 +153,7 @@ describe('Collection resolver', () => {
                 },
             );
             pearCollection = result.createCollection;
-            expect(pearCollection.parent.name).toBe(computersCollection.name);
+            expect(pearCollection.parent!.name).toBe(computersCollection.name);
         });
     });
 
@@ -224,7 +224,7 @@ describe('Collection resolver', () => {
                 },
             );
 
-            expect(result.moveCollection.parent.id).toBe(electronicsCollection.id);
+            expect(result.moveCollection.parent!.id).toBe(electronicsCollection.id);
 
             const positions = await getChildrenOf(electronicsCollection.id);
             expect(positions.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
@@ -508,6 +508,17 @@ describe('Collection resolver', () => {
         });
     });
 
+    describe('Product collections property', () => {
+        it('returns all collections to which the Product belongs', async () => {
+            const result = await client.query(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
+            expect(result.products.items[0].collections).toEqual([
+                { id: 'T_3', name: 'Electronics' },
+                { id: 'T_5', name: 'Pear' },
+                { id: 'T_7', name: 'Photo Pear' },
+            ]);
+        });
+    });
+
     function getFacetValueId(code: string): string {
         const match = facetValues.find(fv => fv.code === code);
         if (!match) {
@@ -585,3 +596,18 @@ const GET_COLLECTION_BREADCRUMBS = gql`
         }
     }
 `;
+
+const GET_COLLECTIONS_FOR_PRODUCTS = gql`
+    query GetCollectionsForProducts($term: String!) {
+        products(options: { filter: { name: { contains: $term } } }) {
+            items {
+                id
+                name
+                collections {
+                    id
+                    name
+                }
+            }
+        }
+    }
+`;

+ 8 - 0
server/src/api/resolvers/entity/collection-entity.resolver.ts

@@ -32,4 +32,12 @@ export class CollectionEntityResolver {
     ): Promise<CollectionBreadcrumb[]> {
         return this.collectionService.getBreadcrumbs(ctx, collection) as any;
     }
+
+    @ResolveProperty()
+    async parent(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<Collection> {
+        if (collection.parent) {
+            return collection.parent;
+        }
+        return this.collectionService.getParent(ctx, collection.id) as any;
+    }
 }

+ 14 - 1
server/src/api/resolvers/entity/product-entity.resolver.ts

@@ -1,15 +1,20 @@
 import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 
 import { Translated } from '../../../common/types/locale-types';
+import { Collection } from '../../../entity/collection/collection.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { Product } from '../../../entity/product/product.entity';
+import { CollectionService } from '../../../service/services/collection.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { RequestContext } from '../../common/request-context';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Product')
 export class ProductEntityResolver {
-    constructor(private productVariantService: ProductVariantService) {}
+    constructor(
+        private productVariantService: ProductVariantService,
+        private collectionService: CollectionService,
+    ) {}
 
     @ResolveProperty()
     async variants(
@@ -18,4 +23,12 @@ export class ProductEntityResolver {
     ): Promise<Array<Translated<ProductVariant>>> {
         return this.productVariantService.getVariantsByProductId(ctx, product.id);
     }
+
+    @ResolveProperty()
+    async collections(
+        @Ctx() ctx: RequestContext,
+        @Parent() product: Product,
+    ): Promise<Array<Translated<Collection>>> {
+        return this.collectionService.getCollectionsByProductId(ctx, product.id);
+    }
 }

+ 1 - 1
server/src/api/schema/type/collection.type.graphql

@@ -9,7 +9,7 @@ type Collection implements Node {
     description: String!
     featuredAsset: Asset
     assets: [Asset!]!
-    parent: Collection!
+    parent: Collection
     children: [Collection!]
     filters: [ConfigurableOperation!]!
     translations: [CollectionTranslation!]!

+ 1 - 0
server/src/api/schema/type/product.type.graphql

@@ -12,6 +12,7 @@ type Product implements Node {
     optionGroups: [ProductOptionGroup!]!
     facetValues: [FacetValue!]!
     translations: [ProductTranslation!]!
+    collections: [Collection!]!
 }
 
 type ProductTranslation {

+ 36 - 0
server/src/service/services/collection.service.ts

@@ -105,6 +105,25 @@ export class CollectionService implements OnModuleInit {
         return this.availableFilters.map(configurableDefToOperation);
     }
 
+    async getParent(ctx: RequestContext, collectionId: ID): Promise<Collection | undefined> {
+        const parent = await this.connection
+            .getRepository(Collection)
+            .createQueryBuilder('collection')
+            .leftJoinAndSelect('collection.translations', 'translation')
+            .where(
+                qb =>
+                    `collection.id = ${qb
+                        .subQuery()
+                        .select('child.parentId')
+                        .from(Collection, 'child')
+                        .where('child.id = :id', { id: collectionId })
+                        .getQuery()}`,
+            )
+            .getOne();
+
+        return parent && translateDeep(parent, ctx.languageCode);
+    }
+
     async getBreadcrumbs(
         ctx: RequestContext,
         collection: Collection,
@@ -121,6 +140,23 @@ export class CollectionService implements OnModuleInit {
         ];
     }
 
+    async getCollectionsByProductId(
+        ctx: RequestContext,
+        productId: ID,
+    ): Promise<Array<Translated<Collection>>> {
+        const result = await this.connection
+            .getRepository(Collection)
+            .createQueryBuilder('collection')
+            .leftJoinAndSelect('collection.translations', 'translation')
+            .leftJoin('collection.productVariants', 'variant')
+            .where('variant.product = :productId', { productId })
+            .groupBy('collection.id')
+            .orderBy('collection.id', 'ASC')
+            .getMany();
+
+        return result.map(collection => translateDeep(collection, ctx.languageCode));
+    }
+
     /**
      * Returns the descendants of a Collection as a flat array.
      */

+ 2 - 0
server/src/service/services/product.service.ts

@@ -26,6 +26,7 @@ import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ChannelService } from './channel.service';
+import { CollectionService } from './collection.service';
 import { FacetValueService } from './facet-value.service';
 import { ProductVariantService } from './product-variant.service';
 import { TaxRateService } from './tax-rate.service';
@@ -48,6 +49,7 @@ export class ProductService {
         private productVariantService: ProductVariantService,
         private facetValueService: FacetValueService,
         private taxRateService: TaxRateService,
+        private collectionService: CollectionService,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
         private eventBus: EventBus,

+ 4 - 2
shared/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-20T14:52:47+01:00
+// Generated in 2019-03-21T09:29:54+01:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -1388,7 +1388,7 @@ export interface Collection extends Node {
 
     assets: Asset[];
 
-    parent: Collection;
+    parent?: Maybe<Collection>;
 
     children?: Maybe<Collection[]>;
 
@@ -1470,6 +1470,8 @@ export interface Product extends Node {
 
     translations: ProductTranslation[];
 
+    collections: Collection[];
+
     customFields?: Maybe<Json>;
 }
 

+ 6 - 4
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-20T14:52:48+01:00
+// Generated in 2019-03-21T09:29:56+01:00
 export type Maybe<T> = T | null;
 
 
@@ -2067,7 +2067,7 @@ export namespace GetCollectionList {
     
     featuredAsset: Maybe<FeaturedAsset>;
     
-    parent: Parent;
+    parent: Maybe<Parent>;
   } 
 
   export type FeaturedAsset = Asset.Fragment
@@ -3615,7 +3615,7 @@ export namespace Collection {
     
     translations: Translations[];
     
-    parent: Parent;
+    parent: Maybe<Parent>;
     
     children: Maybe<Children[]>;
   }
@@ -4814,7 +4814,7 @@ export interface Collection extends Node {
   
   assets: Asset[];
   
-  parent: Collection;
+  parent?: Maybe<Collection>;
   
   children?: Maybe<Collection[]>;
   
@@ -5514,6 +5514,8 @@ export interface Product extends Node {
   
   translations: ProductTranslation[];
   
+  collections: Collection[];
+  
   customFields?: Maybe<Json>;
 }
 

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