Explorar el Código

Merge branch 'minor' into major

Michael Bromley hace 3 años
padre
commit
610e2e0f46

+ 11 - 0
CHANGELOG.md

@@ -1,3 +1,14 @@
+## <small>1.7.1 (2022-08-29)</small>
+
+
+#### Fixes
+
+* **core** Fix DefaultSearchPlugin pagination/sort with non-default lang ([5f7bea4](https://github.com/vendure-ecommerce/vendure/commit/5f7bea4)), closes [#1752](https://github.com/vendure-ecommerce/vendure/issues/1752) [#1746](https://github.com/vendure-ecommerce/vendure/issues/1746)
+* **core** Fix delete order method when called with id (#1751) ([fc57b0d](https://github.com/vendure-ecommerce/vendure/commit/fc57b0d)), closes [#1751](https://github.com/vendure-ecommerce/vendure/issues/1751)
+* **core** Fix regression with custom field relations & product by slug ([e90b99a](https://github.com/vendure-ecommerce/vendure/commit/e90b99a)), closes [#1723](https://github.com/vendure-ecommerce/vendure/issues/1723)
+* **core** Password change checks pw validity (#1745) ([4b6ac3b](https://github.com/vendure-ecommerce/vendure/commit/4b6ac3b)), closes [#1745](https://github.com/vendure-ecommerce/vendure/issues/1745)
+* **core** Work-around for nested custom field relations issue ([651710a](https://github.com/vendure-ecommerce/vendure/commit/651710a)), closes [#1664](https://github.com/vendure-ecommerce/vendure/issues/1664)
+
 ## 1.7.0 (2022-08-26)
 
 

+ 58 - 2
packages/core/e2e/custom-field-relations.e2e-spec.ts

@@ -29,7 +29,7 @@ import path from 'path';
 import { Entity, JoinColumn, OneToOne } from 'typeorm';
 
 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 { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { AddItemToOrderMutation } from './graphql/generated-e2e-shop-types';
@@ -79,6 +79,7 @@ class Vendor extends VendureEntity {
     constructor() {
         super();
     }
+
     @OneToOne(type => Product, { eager: true })
     @JoinColumn()
     featuredProduct: Product;
@@ -112,6 +113,16 @@ customFieldConfig.Product?.push(
         public: true,
     },
 );
+customFieldConfig.User?.push({
+    name: 'cfVendor',
+    type: 'relation',
+    entity: Vendor,
+    graphQLType: 'Vendor',
+    list: false,
+    eager: true,
+    internal: false,
+    public: true,
+});
 
 const testResolverSpy = jest.fn();
 
@@ -138,6 +149,7 @@ class TestResolver1636 {
                 getAssetTest(id: ID!): Boolean!
             }
             type Vendor {
+                id: ID
                 featuredProduct: Product
             }
         `,
@@ -146,6 +158,7 @@ class TestResolver1636 {
     adminApiExtensions: {
         schema: gql`
             type Vendor {
+                id: ID
                 featuredProduct: Product
             }
         `,
@@ -800,7 +813,7 @@ describe('Custom field relations', () => {
             });
 
             // https://github.com/vendure-ecommerce/vendure/issues/1664
-            it('successfully gets product with eager-loading custom field relation', async () => {
+            it('successfully gets product by id with eager-loading custom field relation', async () => {
                 const { product } = await shopClient.query(gql`
                     query {
                         product(id: "T_1") {
@@ -818,6 +831,49 @@ describe('Custom field relations', () => {
 
                 expect(product).toBeDefined();
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/1664
+            it('successfully gets product by id with nested eager-loading custom field relation', async () => {
+                const { customer } = await adminClient.query(gql`
+                    query {
+                        customer(id: "T_1") {
+                            id
+                            firstName
+                            lastName
+                            emailAddress
+                            phoneNumber
+                            user {
+                                customFields {
+                                    cfVendor {
+                                        id
+                                    }
+                                }
+                            }
+                        }
+                    }
+                `);
+
+                expect(customer).toBeDefined();
+            });
+
+            it('successfully gets product by slug with eager-loading custom field relation', async () => {
+                const { product } = await shopClient.query(gql`
+                    query {
+                        product(slug: "laptop") {
+                            id
+                            customFields {
+                                cfVendor {
+                                    featuredProduct {
+                                        id
+                                    }
+                                }
+                            }
+                        }
+                    }
+                `);
+
+                expect(product).toBeDefined();
+            });
         });
 
         describe('ProductOptionGroup, ProductOption entity', () => {

+ 23 - 0
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -1658,6 +1658,29 @@ describe('Default search plugin', () => {
                 expect(search1.items.map(i => i.productName)).toContain('Laptop');
                 expect(search1.items.map(i => i.productVariantName)).toContain('laptop variant fr');
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/1752
+            // https://github.com/vendure-ecommerce/vendure/issues/1746
+            it('sort by name with non-default languageCode', async () => {
+                const result = await adminClient.query<
+                    SearchProductsShopQuery,
+                    SearchProductShopQueryVariables
+                >(
+                    SEARCH_PRODUCTS,
+                    {
+                        input: {
+                            take: 2,
+                            sort: {
+                                name: SortOrder.ASC,
+                            },
+                        },
+                    },
+                    {
+                        languageCode: LanguageCode.de,
+                    },
+                );
+                expect(result.search.items.length).toEqual(2);
+            });
         });
     });
 });

+ 5 - 2
packages/core/src/api/resolvers/base/base-auth.resolver.ts

@@ -1,4 +1,7 @@
-import { AuthenticationResult as ShopAuthenticationResult } from '@vendure/common/lib/generated-shop-types';
+import {
+    AuthenticationResult as ShopAuthenticationResult,
+    PasswordValidationError,
+} from '@vendure/common/lib/generated-shop-types';
 import {
     AuthenticationResult as AdminAuthenticationResult,
     CurrentUser,
@@ -138,7 +141,7 @@ export class BaseAuthResolver {
         ctx: RequestContext,
         currentPassword: string,
         newPassword: string,
-    ): Promise<boolean | InvalidCredentialsError> {
+    ): Promise<boolean | InvalidCredentialsError | PasswordValidationError> {
         const { activeUserId } = ctx;
         if (!activeUserId) {
             throw new ForbiddenError();

+ 63 - 0
packages/core/src/connection/remove-custom-fields-with-eager-relations.ts

@@ -0,0 +1,63 @@
+import { SelectQueryBuilder } from 'typeorm';
+
+import { Logger } from '../config/index';
+
+/**
+ * This is a work-around for this issue: https://github.com/vendure-ecommerce/vendure/issues/1664
+ *
+ * Explanation:
+ * When calling `FindOptionsUtils.joinEagerRelations()`, there appears to be a bug in TypeORM whereby
+ * it will throw the following error *if* the `options.relations` array contains any customField relations
+ * where the related entity itself has eagerly-loaded relations.
+ *
+ * For example, let's say we define a custom field on the Product entity like this:
+ * ```
+ * Product: [{
+ *   name: 'featuredFacet',
+ *   type: 'relation',
+ *   entity: Facet,
+ * }],
+ * ```
+ * and then we pass into `TransactionalConnection.findOneInChannel()` an options array of:
+ *
+ * ```
+ * { relations: ['customFields.featuredFacet'] }
+ * ```
+ * it will throw an error because the `Facet` entity itself has eager relations (namely the `translations` property).
+ * This will cause TypeORM to throw the error:
+ * ```
+ * TypeORMError: "entity__customFields" alias was not found. Maybe you forgot to join it?
+ * ```
+ *
+ * So this method introspects the QueryBuilder metadata and checks for any custom field relations which
+ * themselves have eager relations. If found, it removes those items from the `options.relations` array.
+ *
+ * TODO: Ideally create a minimal reproduction case and report in the TypeORM repo for an upstream fix.
+ */
+
+export function removeCustomFieldsWithEagerRelations(
+    qb: SelectQueryBuilder<any>,
+    relations: string[] = [],
+): string[] {
+    let resultingRelations = relations;
+    const mainAlias = qb.expressionMap.mainAlias;
+    const customFieldsMetadata = mainAlias?.metadata.embeddeds.find(
+        metadata => metadata.propertyName === 'customFields',
+    );
+    if (customFieldsMetadata) {
+        const customFieldRelationsWithEagerRelations = customFieldsMetadata.relations.filter(
+            relation => !!relation.inverseEntityMetadata.ownRelations.find(or => or.isEager === true),
+        );
+        for (const relation of customFieldRelationsWithEagerRelations) {
+            const propertyName = relation.propertyName;
+            const relationsToRemove = relations.filter(r => r.startsWith(`customFields.${propertyName}`));
+            if (relationsToRemove.length) {
+                Logger.debug(
+                    `TransactionalConnection.findOneInChannel cannot automatically join relation [${mainAlias?.metadata.name}.customFields.${propertyName}]`,
+                );
+                resultingRelations = relations.filter(r => !r.startsWith(`customFields.${propertyName}`));
+            }
+        }
+    }
+    return resultingRelations;
+}

+ 26 - 63
packages/core/src/connection/transactional-connection.ts

@@ -19,6 +19,7 @@ import { ChannelAware, SoftDeletable } from '../common/types/common-types';
 import { Logger } from '../config/index';
 import { VendureEntity } from '../entity/base/base.entity';
 
+import { removeCustomFieldsWithEagerRelations } from './remove-custom-fields-with-eager-relations';
 import { TransactionWrapper } from './transaction-wrapper';
 import { GetEntityOrThrowOptions } from './types';
 
@@ -263,10 +264,31 @@ export class TransactionalConnection {
         channelId: ID,
         options: FindOneOptions = {},
     ) {
-        const qb = this.getRepository(ctx, entity).createQueryBuilder('entity');
-        options.relations = this.removeCustomFieldsWithEagerRelations(qb, options.relations);
-        FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, options);
-        if (options.loadEagerRelations !== false) {
+        let qb = this.getRepository(ctx, entity).createQueryBuilder('entity');
+        options.relations = removeCustomFieldsWithEagerRelations(qb, options.relations);
+        let skipEagerRelations = false;
+        try {
+            FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, options);
+        } catch (e: any) {
+            // https://github.com/vendure-ecommerce/vendure/issues/1664
+            // This is a failsafe to catch edge cases related to the TypeORM
+            // bug described in the doc block of `removeCustomFieldsWithEagerRelations`.
+            // In this case, a nested custom field relation has an eager-loaded relation,
+            // and is throwing an error. In this case we throw our hands up and say
+            // "sod it!", refuse to load _any_ relations at all, and rely on the
+            // GraphQL entity resolvers to take care of them.
+            Logger.debug(
+                `TransactionalConnection.findOneInChannel ran into issues joining nested custom field relations. Running the query without joining any relations instead.`,
+            );
+            qb = this.getRepository(ctx, entity).createQueryBuilder('entity');
+            FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
+                ...options,
+                relations: [],
+                loadEagerRelations: false,
+            });
+            skipEagerRelations = true;
+        }
+        if (options.loadEagerRelations !== false && !skipEagerRelations) {
             // tslint:disable-next-line:no-non-null-assertion
             FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         }
@@ -277,65 +299,6 @@ export class TransactionalConnection {
             .getOne();
     }
 
-    /**
-     * This is a work-around for this issue: https://github.com/vendure-ecommerce/vendure/issues/1664
-     *
-     * Explanation:
-     * When calling `FindOptionsUtils.joinEagerRelations()`, there appears to be a bug in TypeORM whereby
-     * it will throw the following error *if* the `options.relations` array contains any customField relations
-     * where the related entity itself has eagerly-loaded relations.
-     *
-     * For example, let's say we define a custom field on the Product entity like this:
-     * ```
-     * Product: [{
-     *   name: 'featuredFacet',
-     *   type: 'relation',
-     *   entity: Facet,
-     * }],
-     * ```
-     * and then we pass into `TransactionalConnection.findOneInChannel()` an options array of:
-     *
-     * ```
-     * { relations: ['customFields.featuredFacet'] }
-     * ```
-     * it will throw an error because the `Facet` entity itself has eager relations (namely the `translations` property).
-     * This will cause TypeORM to throw the error:
-     * ```
-     * TypeORMError: "entity__customFields" alias was not found. Maybe you forgot to join it?
-     * ```
-     *
-     * So this method introspects the QueryBuilder metadata and checks for any custom field relations which
-     * themselves have eager relations. If found, it removes those items from the `options.relations` array.
-     *
-     * TODO: Ideally create a minimal reproduction case and report in the TypeORM repo for an upstream fix.
-     */
-    private removeCustomFieldsWithEagerRelations(
-        qb: SelectQueryBuilder<any>,
-        relations: string[] = [],
-    ): string[] {
-        let resultingRelations = relations;
-        const mainAlias = qb.expressionMap.mainAlias;
-        const customFieldsMetadata = mainAlias?.metadata.embeddeds.find(
-            metadata => metadata.propertyName === 'customFields',
-        );
-        if (customFieldsMetadata) {
-            const customFieldRelationsWithEagerRelations = customFieldsMetadata.relations.filter(
-                relation => !!relation.inverseEntityMetadata.ownRelations.find(or => or.isEager === true),
-            );
-            for (const relation of customFieldRelationsWithEagerRelations) {
-                const propertyName = relation.propertyName;
-                const relationsToRemove = relations.filter(r => r.startsWith(`customFields.${propertyName}`));
-                if (relationsToRemove.length) {
-                    Logger.debug(
-                        `TransactionalConnection.findOneInChannel cannot automatically join relation [${mainAlias?.metadata.name}.customFields.${propertyName}]`,
-                    );
-                    resultingRelations = relations.filter(r => !r.startsWith(`customFields.${propertyName}`));
-                }
-            }
-        }
-        return resultingRelations;
-    }
-
     /**
      * @description
      * Like the TypeOrm `Repository.findByIds()` method, but limits the results to

+ 4 - 4
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -98,10 +98,10 @@ export class MysqlSearchStrategy implements SearchStrategy {
         this.applyTermAndFilters(ctx, qb, input);
         if (sort) {
             if (sort.name) {
-                qb.addOrderBy(input.groupByProduct ? 'MIN(si.productName)' : 'productName', sort.name);
+                qb.addOrderBy(input.groupByProduct ? 'MIN(si.productName)' : 'si.productName', sort.name);
             }
             if (sort.price) {
-                qb.addOrderBy(input.groupByProduct ? 'MIN(si.price)' : 'price', sort.price);
+                qb.addOrderBy(input.groupByProduct ? 'MIN(si.price)' : 'si.price', sort.price);
             }
         } else {
             if (input.term && input.term.length > this.minTermLength) {
@@ -113,8 +113,8 @@ export class MysqlSearchStrategy implements SearchStrategy {
         }
 
         return qb
-            .take(take)
-            .skip(skip)
+            .limit(take)
+            .offset(skip)
             .getRawMany()
             .then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
     }

+ 2 - 2
packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -116,8 +116,8 @@ export class PostgresSearchStrategy implements SearchStrategy {
         }
 
         return qb
-            .take(take)
-            .skip(skip)
+            .limit(take)
+            .offset(skip)
             .getRawMany()
             .then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
     }

+ 3 - 6
packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -112,13 +112,10 @@ export class SqliteSearchStrategy implements SearchStrategy {
         }
 
         return await qb
-            .take(take)
-            .skip(skip)
+            .limit(take)
+            .offset(skip)
             .getRawMany()
-            .then(res => {
-                // console.warn(qb.getQueryAndParameters(), "take", take, "skip", skip, "length", res.length)
-                return res.map(r => mapToSearchResult(r, ctx.channel.currencyCode));
-            });
+            .then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
     }
 
     async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {

+ 1 - 1
packages/core/src/service/services/order.service.ts

@@ -1558,7 +1558,7 @@ export class OrderService {
                 ? orderOrId
                 : await this.connection
                       .getRepository(ctx, Order)
-                      .findOneOrFail(orderOrId, { relations: ['lines'] });
+                      .findOneOrFail(orderOrId, { relations: ['lines', 'shippingLines'] });
         // If there is a Session referencing the Order to be deleted, we must first remove that
         // reference in order to avoid a foreign key error. See https://github.com/vendure-ecommerce/vendure/issues/1454
         const sessions = await this.connection

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

@@ -20,6 +20,7 @@ import { ProductOptionInUseError } from '../../common/error/generated-graphql-ad
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
+import { removeCustomFieldsWithEagerRelations } from '../../connection/remove-custom-fields-with-eager-relations';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Channel } from '../../entity/channel/channel.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
@@ -173,8 +174,12 @@ export class ProductService {
         relations?: RelationPaths<Product>,
     ): Promise<Translated<Product> | undefined> {
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
+        const effectiveRelations = removeCustomFieldsWithEagerRelations(
+            qb,
+            relations ? [...new Set(this.relations.concat(relations))] : this.relations,
+        );
         FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
-            relations: relations ? [...new Set(this.relations.concat(relations))] : this.relations,
+            relations: effectiveRelations,
         });
         // tslint:disable-next-line:no-non-null-assertion
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);

+ 6 - 1
packages/core/src/service/services/user.service.ts

@@ -367,7 +367,7 @@ export class UserService {
         userId: ID,
         currentPassword: string,
         newPassword: string,
-    ): Promise<boolean | InvalidCredentialsError> {
+    ): Promise<boolean | InvalidCredentialsError | PasswordValidationError> {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
@@ -378,6 +378,11 @@ export class UserService {
         if (!user) {
             throw new EntityNotFoundError('User', userId);
         }
+        const password = newPassword;
+        const passwordValidationResult = await this.validatePassword(ctx, password);
+        if (passwordValidationResult !== true) {
+            return passwordValidationResult;
+        }
         const nativeAuthMethod = user.getNativeAuthenticationMethod();
         const matches = await this.passwordCipher.check(currentPassword, nativeAuthMethod.passwordHash);
         if (!matches) {

+ 22 - 0
packages/dev-server/test-plugins/issue-1664.ts

@@ -1,4 +1,5 @@
 import {
+    Facet,
     LanguageCode,
     LocaleString,
     PluginCommonModule,
@@ -75,6 +76,27 @@ const schema = gql`
                 component: 'cp-product-vendor-selector',
             },
         });
+        config.customFields.User.push({
+            name: 'vendor',
+            label: [{ languageCode: LanguageCode.en_AU, value: 'Vendor' }],
+            type: 'relation',
+            entity: Vendor,
+            eager: true,
+            nullable: false,
+            defaultValue: null,
+            ui: {
+                component: 'cp-product-vendor-selector',
+            },
+        });
+        config.customFields.User.push({
+            name: 'facet',
+            label: [{ languageCode: LanguageCode.en_AU, value: 'Facet' }],
+            type: 'relation',
+            entity: Facet,
+            eager: true,
+            nullable: false,
+            defaultValue: null,
+        });
 
         config.customFields.Product.push({
             name: 'shopifyId',