diff --git a/packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts b/packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts
index 3631a48ca6..5aa9a913c6 100644
--- a/packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts
+++ b/packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts
@@ -65,4 +65,8 @@ export class CouponCodeSelectorComponent implements OnInit {
this.control = new UntypedFormControl(this.couponCodes ?? []);
}
}
+
+ remove(code: string) {
+ this.removeCouponCode.emit(code);
+ }
}
diff --git a/packages/core/e2e/custom-fields.e2e-spec.ts b/packages/core/e2e/custom-fields.e2e-spec.ts
index a39d7c7394..d436b8d557 100644
--- a/packages/core/e2e/custom-fields.e2e-spec.ts
+++ b/packages/core/e2e/custom-fields.e2e-spec.ts
@@ -183,6 +183,10 @@ const customConfig = mergeConfig(testConfig(), {
readonly: true,
},
],
+ Collection: [
+ { name: 'secretKey1', type: 'string', defaultValue: '', public: false, internal: true },
+ { name: 'secretKey2', type: 'string', defaultValue: '', public: false, internal: false },
+ ],
OrderLine: [{ name: 'validateInt', type: 'int', min: 0, max: 10 }],
} as CustomFields,
});
@@ -942,6 +946,20 @@ describe('Custom fields', () => {
`);
}, 'Cannot query field "internalString" on type "ProductCustomFields"'),
);
+
+ // https://github.com/vendure-ecommerce/vendure/issues/3049
+ it('does not leak private fields via JSON type', async () => {
+ const { collection } = await shopClient.query(gql`
+ query {
+ collection(id: "T_1") {
+ id
+ customFields
+ }
+ }
+ `);
+
+ expect(collection.customFields).toBe(null);
+ });
});
describe('sort & filter', () => {
@@ -1087,18 +1105,16 @@ describe('Custom fields', () => {
describe('unique constraint', () => {
it('setting unique value works', async () => {
- const result = await adminClient.query(
- gql`
- mutation {
- updateProduct(input: { id: "T_1", customFields: { uniqueString: "foo" } }) {
- id
- customFields {
- uniqueString
- }
+ const result = await adminClient.query(gql`
+ mutation {
+ updateProduct(input: { id: "T_1", customFields: { uniqueString: "foo" } }) {
+ id
+ customFields {
+ uniqueString
}
}
- `,
- );
+ }
+ `);
expect(result.updateProduct.customFields.uniqueString).toBe('foo');
});
diff --git a/packages/core/e2e/draft-order.e2e-spec.ts b/packages/core/e2e/draft-order.e2e-spec.ts
index e9759d7ed7..cf5ce03989 100644
--- a/packages/core/e2e/draft-order.e2e-spec.ts
+++ b/packages/core/e2e/draft-order.e2e-spec.ts
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { LanguageCode } from '@vendure/common/lib/generated-types';
import {
- DefaultLogger,
DefaultOrderPlacedStrategy,
mergeConfig,
Order,
@@ -15,7 +14,7 @@ import path from 'path';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
import { singleStageRefundablePaymentMethod } from './fixtures/test-payment-methods';
import { ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
@@ -24,14 +23,12 @@ import {
AddManualPaymentDocument,
AdminTransitionDocument,
CanceledOrderFragment,
- GetOrderDocument,
GetOrderPlacedAtDocument,
OrderWithLinesFragment,
} from './graphql/generated-e2e-admin-types';
import {
GetActiveCustomerOrdersQuery,
TestOrderFragmentFragment,
- TransitionToStateDocument,
UpdatedOrderFragment,
} from './graphql/generated-e2e-shop-types';
import { CREATE_PROMOTION, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
@@ -53,7 +50,6 @@ class TestOrderPlacedStrategy extends DefaultOrderPlacedStrategy {
describe('Draft Orders resolver', () => {
const { server, adminClient, shopClient } = createTestEnvironment(
mergeConfig(testConfig(), {
- logger: new DefaultLogger(),
paymentOptions: {
paymentMethodHandlers: [singleStageRefundablePaymentMethod],
},
@@ -125,9 +121,8 @@ describe('Draft Orders resolver', () => {
});
it('create draft order', async () => {
- const { createDraftOrder } = await adminClient.query
(
- CREATE_DRAFT_ORDER,
- );
+ const { createDraftOrder } =
+ await adminClient.query(CREATE_DRAFT_ORDER);
expect(createDraftOrder.state).toBe('Draft');
expect(createDraftOrder.active).toBe(false);
@@ -213,9 +208,8 @@ describe('Draft Orders resolver', () => {
it('custom does not see draft orders in history', async () => {
await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
- const { activeCustomer } = await shopClient.query(
- GET_ACTIVE_CUSTOMER_ORDERS,
- );
+ const { activeCustomer } =
+ await shopClient.query(GET_ACTIVE_CUSTOMER_ORDERS);
expect(activeCustomer?.orders.totalItems).toBe(0);
expect(activeCustomer?.orders.items.length).toBe(0);
diff --git a/packages/core/src/api/config/configure-graphql-module.ts b/packages/core/src/api/config/configure-graphql-module.ts
index bfe261c36d..2959b72637 100644
--- a/packages/core/src/api/config/configure-graphql-module.ts
+++ b/packages/core/src/api/config/configure-graphql-module.ts
@@ -7,6 +7,7 @@ import path from 'path';
import { ConfigModule } from '../../config/config.module';
import { ConfigService } from '../../config/config.service';
+import { AutoIncrementIdStrategy, EntityIdStrategy, UuidIdStrategy } from '../../config/index';
import { I18nModule } from '../../i18n/i18n.module';
import { I18nService } from '../../i18n/i18n.service';
import { getPluginAPIExtensions } from '../../plugin/plugin-metadata';
@@ -96,6 +97,24 @@ async function createGraphQLOptions(
options.apiType,
builtSchema,
);
+
+ const apolloServerPlugins = [
+ new TranslateErrorsPlugin(i18nService),
+ new AssetInterceptorPlugin(configService),
+ ...configService.apiOptions.apolloServerPlugins,
+ ];
+ // We only need to add the IdCodecPlugin if the user has configured
+ // a non-default EntityIdStrategy. This is a performance optimization
+ // that prevents unnecessary traversal of each response when no
+ // actual encoding/decoding is taking place.
+ if (
+ !isUsingDefaultEntityIdStrategy(
+ configService.entityOptions.entityIdStrategy ?? configService.entityIdStrategy,
+ )
+ ) {
+ apolloServerPlugins.unshift(new IdCodecPlugin(idCodecService));
+ }
+
return {
path: '/' + options.apiPath,
typeDefs: printSchema(builtSchema),
@@ -112,12 +131,7 @@ async function createGraphQLOptions(
context: (req: any) => req,
// This is handled by the Express cors plugin
cors: false,
- plugins: [
- new IdCodecPlugin(idCodecService),
- new TranslateErrorsPlugin(i18nService),
- new AssetInterceptorPlugin(configService),
- ...configService.apiOptions.apolloServerPlugins,
- ],
+ plugins: apolloServerPlugins,
validationRules: options.validationRules,
introspection: configService.apiOptions.introspection ?? true,
} as ApolloDriverConfig;
@@ -165,3 +179,10 @@ async function createGraphQLOptions(
return schema;
}
}
+
+function isUsingDefaultEntityIdStrategy(entityIdStrategy: EntityIdStrategy): boolean {
+ return (
+ entityIdStrategy.constructor === AutoIncrementIdStrategy ||
+ entityIdStrategy.constructor === UuidIdStrategy
+ );
+}
diff --git a/packages/core/src/api/config/generate-resolvers.ts b/packages/core/src/api/config/generate-resolvers.ts
index 2d72f8af57..0cb89df116 100644
--- a/packages/core/src/api/config/generate-resolvers.ts
+++ b/packages/core/src/api/config/generate-resolvers.ts
@@ -255,6 +255,16 @@ function generateCustomFieldRelationResolvers(
} as any;
}
}
+ const allCustomFieldsAreNonPublic =
+ customFields.length && customFields.every(f => f.public === false || f.internal === true);
+ if (allCustomFieldsAreNonPublic) {
+ // When an entity has only non-public custom fields, the GraphQL type used for the
+ // customFields field is `JSON`. This type will simply return the full object, which
+ // will cause a leak of private data unless we force a `null` return value in the case
+ // that there are no public fields.
+ // See https://github.com/vendure-ecommerce/vendure/issues/3049
+ shopResolvers[entityName] = { customFields: () => null };
+ }
}
return { adminResolvers, shopResolvers };
}
diff --git a/packages/core/src/api/resolvers/admin/order.resolver.ts b/packages/core/src/api/resolvers/admin/order.resolver.ts
index d677652336..0d337708f1 100644
--- a/packages/core/src/api/resolvers/admin/order.resolver.ts
+++ b/packages/core/src/api/resolvers/admin/order.resolver.ts
@@ -46,7 +46,10 @@ import { Transaction } from '../../decorators/transaction.decorator';
@Resolver()
export class OrderResolver {
- constructor(private orderService: OrderService, private connection: TransactionalConnection) {}
+ constructor(
+ private orderService: OrderService,
+ private connection: TransactionalConnection,
+ ) {}
@Query()
@Allow(Permission.ReadOrder)
@@ -63,7 +66,8 @@ export class OrderResolver {
async order(
@Ctx() ctx: RequestContext,
@Args() args: QueryOrderArgs,
- @Relations(Order) relations: RelationPaths,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
): Promise {
return this.orderService.findOne(ctx, args.id, relations);
}
diff --git a/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts b/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts
index 9d7383d66c..165d694bec 100644
--- a/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts
+++ b/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts
@@ -116,7 +116,7 @@ export class CollectionEntityResolver {
@Ctx() ctx: RequestContext,
@Parent() collection: Collection,
): Promise {
- if (collection.featuredAsset) {
+ if (collection.featuredAsset !== undefined) {
return collection.featuredAsset;
}
return this.assetService.getFeaturedAsset(ctx, collection);
diff --git a/packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts b/packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts
index bd57dd4339..e8cc63a348 100644
--- a/packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts
+++ b/packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts
@@ -31,7 +31,7 @@ export class OrderLineEntityResolver {
@Ctx() ctx: RequestContext,
@Parent() orderLine: OrderLine,
): Promise {
- if (orderLine.featuredAsset) {
+ if (orderLine.featuredAsset !== undefined) {
return orderLine.featuredAsset;
} else {
return this.assetService.getFeaturedAsset(ctx, orderLine);
diff --git a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts
index 9086c3be31..f89b988293 100644
--- a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts
+++ b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts
@@ -129,7 +129,7 @@ export class ProductEntityResolver {
@ResolveField()
async featuredAsset(@Ctx() ctx: RequestContext, @Parent() product: Product): Promise {
- if (product.featuredAsset) {
+ if (product.featuredAsset !== undefined) {
return product.featuredAsset;
}
return this.assetService.getFeaturedAsset(ctx, product);
diff --git a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts
index addcf02b32..ceea767662 100644
--- a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts
+++ b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts
@@ -14,9 +14,9 @@ import { Asset, Channel, FacetValue, Product, ProductOption, StockLevel, TaxRate
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
import { StockMovement } from '../../../entity/stock-movement/stock-movement.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
-import { StockLevelService } from '../../../service/services/stock-level.service';
import { AssetService } from '../../../service/services/asset.service';
import { ProductVariantService } from '../../../service/services/product-variant.service';
+import { StockLevelService } from '../../../service/services/stock-level.service';
import { StockMovementService } from '../../../service/services/stock-movement.service';
import { ApiType } from '../../common/get-api-type';
import { RequestContext } from '../../common/request-context';
@@ -103,7 +103,7 @@ export class ProductVariantEntityResolver {
@Ctx() ctx: RequestContext,
@Parent() productVariant: ProductVariant,
): Promise {
- if (productVariant.featuredAsset) {
+ if (productVariant.featuredAsset !== undefined) {
return productVariant.featuredAsset;
}
return this.assetService.getFeaturedAsset(ctx, productVariant);
diff --git a/packages/core/src/api/resolvers/shop/shop-order.resolver.ts b/packages/core/src/api/resolvers/shop/shop-order.resolver.ts
index d418034587..98758ce23f 100644
--- a/packages/core/src/api/resolvers/shop/shop-order.resolver.ts
+++ b/packages/core/src/api/resolvers/shop/shop-order.resolver.ts
@@ -76,7 +76,8 @@ export class ShopOrderResolver {
async order(
@Ctx() ctx: RequestContext,
@Args() args: QueryOrderArgs,
- @Relations(Order) relations: RelationPaths,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
): Promise {
const requiredRelations: RelationPaths = ['customer', 'customer.user'];
const order = await this.orderService.findOne(
@@ -98,7 +99,8 @@ export class ShopOrderResolver {
@Allow(Permission.Owner)
async activeOrder(
@Ctx() ctx: RequestContext,
- @Relations(Order) relations: RelationPaths,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
@Args() args: ActiveOrderArgs,
): Promise {
if (ctx.authorizedAsOwnerOnly) {
@@ -107,7 +109,7 @@ export class ShopOrderResolver {
args[ACTIVE_ORDER_INPUT_FIELD_NAME],
);
if (sessionOrder) {
- return this.orderService.findOne(ctx, sessionOrder.id);
+ return this.orderService.findOne(ctx, sessionOrder.id, relations);
} else {
return;
}
@@ -119,7 +121,8 @@ export class ShopOrderResolver {
async orderByCode(
@Ctx() ctx: RequestContext,
@Args() args: QueryOrderByCodeArgs,
- @Relations(Order) relations: RelationPaths,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
): Promise {
if (ctx.authorizedAsOwnerOnly) {
const requiredRelations: RelationPaths = ['customer', 'customer.user'];
@@ -294,6 +297,8 @@ export class ShopOrderResolver {
async addItemToOrder(
@Ctx() ctx: RequestContext,
@Args() args: MutationAddItemToOrderArgs & ActiveOrderArgs,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
): Promise> {
const order = await this.activeOrderService.getActiveOrder(
ctx,
@@ -306,6 +311,7 @@ export class ShopOrderResolver {
args.productVariantId,
args.quantity,
(args as any).customFields,
+ relations,
);
}
@@ -315,6 +321,8 @@ export class ShopOrderResolver {
async adjustOrderLine(
@Ctx() ctx: RequestContext,
@Args() args: MutationAdjustOrderLineArgs & ActiveOrderArgs,
+ @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+ relations: RelationPaths,
): Promise> {
if (args.quantity === 0) {
return this.removeOrderLine(ctx, { orderLineId: args.orderLineId });
@@ -330,6 +338,7 @@ export class ShopOrderResolver {
args.orderLineId,
args.quantity,
(args as any).customFields,
+ relations,
);
}
diff --git a/packages/core/src/config/config.module.ts b/packages/core/src/config/config.module.ts
index 8c7cb88feb..26b9662268 100644
--- a/packages/core/src/config/config.module.ts
+++ b/packages/core/src/config/config.module.ts
@@ -106,10 +106,11 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
} = this.configService.shippingOptions;
const { customPaymentProcess, process: paymentProcess } = this.configService.paymentOptions;
const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
- const { entityIdStrategy } = this.configService.entityOptions;
+ const { entityIdStrategy: entityIdStrategyCurrent } = this.configService.entityOptions;
const { healthChecks, errorHandlers } = this.configService.systemOptions;
const { assetImportStrategy } = this.configService.importExportOptions;
const { refundProcess: refundProcess } = this.configService.paymentOptions;
+ const entityIdStrategy = entityIdStrategyCurrent ?? entityIdStrategyDeprecated;
return [
...adminAuthenticationStrategy,
...shopAuthenticationStrategy,
@@ -127,8 +128,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
checkoutMergeStrategy,
orderCodeStrategy,
orderByCodeAccessStrategy,
- entityIdStrategyDeprecated,
- ...[entityIdStrategy].filter(notNullOrUndefined),
+ entityIdStrategy,
productVariantPriceCalculationStrategy,
productVariantPriceUpdateStrategy,
orderItemPriceCalculationStrategy,
diff --git a/packages/core/src/config/default-config.ts b/packages/core/src/config/default-config.ts
index e7b5f5b6dc..dcc0372c54 100644
--- a/packages/core/src/config/default-config.ts
+++ b/packages/core/src/config/default-config.ts
@@ -84,6 +84,7 @@ export const defaultConfig: RuntimeVendureConfig = {
introspection: true,
apolloServerPlugins: [],
},
+ entityIdStrategy: new AutoIncrementIdStrategy(),
authOptions: {
disableAuth: false,
tokenMethod: 'cookie',
@@ -118,7 +119,6 @@ export const defaultConfig: RuntimeVendureConfig = {
stockDisplayStrategy: new DefaultStockDisplayStrategy(),
stockLocationStrategy: new DefaultStockLocationStrategy(),
},
- entityIdStrategy: new AutoIncrementIdStrategy(),
assetOptions: {
assetNamingStrategy: new DefaultAssetNamingStrategy(),
assetStorageStrategy: new NoAssetStorageStrategy(),
@@ -131,6 +131,7 @@ export const defaultConfig: RuntimeVendureConfig = {
type: 'mysql',
},
entityOptions: {
+ entityIdStrategy: new AutoIncrementIdStrategy(),
moneyStrategy: new DefaultMoneyStrategy(),
entityDuplicators: defaultEntityDuplicators,
channelCacheTtl: 30000,
diff --git a/packages/core/src/config/order/order-seller-strategy.ts b/packages/core/src/config/order/order-seller-strategy.ts
index 371e79926a..59080493c4 100644
--- a/packages/core/src/config/order/order-seller-strategy.ts
+++ b/packages/core/src/config/order/order-seller-strategy.ts
@@ -46,7 +46,7 @@ export interface OrderSellerStrategy extends InjectableStrategy {
* This method is called whenever a new OrderLine is added to the Order via the `addItemToOrder` mutation or the
* underlying `addItemToOrder()` method of the {@link OrderService}.
*
- * It should return the ID of the Channel to which this OrderLine will be assigned, which will be used to set the
+ * It should return the Channel to which this OrderLine will be assigned, which will be used to set the
* {@link OrderLine} `sellerChannel` property.
*/
setOrderLineSellerChannel?(
diff --git a/packages/core/src/entity/order-line/order-line.entity.ts b/packages/core/src/entity/order-line/order-line.entity.ts
index a9c44fc229..4f747ca67d 100644
--- a/packages/core/src/entity/order-line/order-line.entity.ts
+++ b/packages/core/src/entity/order-line/order-line.entity.ts
@@ -76,6 +76,9 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
@ManyToOne(type => TaxCategory)
taxCategory: TaxCategory;
+ @EntityId({ nullable: true })
+ taxCategoryId: ID;
+
@Index()
@ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
featuredAsset: Asset;
diff --git a/packages/core/src/entity/product-variant/product-variant.entity.ts b/packages/core/src/entity/product-variant/product-variant.entity.ts
index 91226f07ff..2cc90ca184 100644
--- a/packages/core/src/entity/product-variant/product-variant.entity.ts
+++ b/packages/core/src/entity/product-variant/product-variant.entity.ts
@@ -107,6 +107,9 @@ export class ProductVariant
@ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
featuredAsset: Asset;
+ @EntityId({ nullable: true })
+ featuredAssetId: ID;
+
@OneToMany(type => ProductVariantAsset, productVariantAsset => productVariantAsset.productVariant, {
onDelete: 'SET NULL',
})
@@ -116,6 +119,9 @@ export class ProductVariant
@ManyToOne(type => TaxCategory, taxCategory => taxCategory.productVariants)
taxCategory: TaxCategory;
+ @EntityId({ nullable: true })
+ taxCategoryId: ID;
+
@OneToMany(type => ProductVariantPrice, price => price.variant, { eager: true })
productVariantPrices: ProductVariantPrice[];
diff --git a/packages/core/src/entity/product/product.entity.ts b/packages/core/src/entity/product/product.entity.ts
index 82968bcdf9..88a6e7c21a 100644
--- a/packages/core/src/entity/product/product.entity.ts
+++ b/packages/core/src/entity/product/product.entity.ts
@@ -1,4 +1,4 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
import { Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
@@ -8,6 +8,7 @@ import { Asset } from '../asset/asset.entity';
import { VendureEntity } from '../base/base.entity';
import { Channel } from '../channel/channel.entity';
import { CustomProductFields } from '../custom-entity-fields';
+import { EntityId } from '../entity-id.decorator';
import { FacetValue } from '../facet-value/facet-value.entity';
import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
import { ProductVariant } from '../product-variant/product-variant.entity';
@@ -47,6 +48,9 @@ export class Product
@ManyToOne(type => Asset, asset => asset.featuredInProducts, { onDelete: 'SET NULL' })
featuredAsset: Asset;
+ @EntityId({ nullable: true })
+ featuredAssetId: ID;
+
@OneToMany(type => ProductAsset, productAsset => productAsset.product)
assets: ProductAsset[];
diff --git a/packages/core/src/entity/tax-rate/tax-rate.entity.ts b/packages/core/src/entity/tax-rate/tax-rate.entity.ts
index eeb2ea433a..630ef74de3 100644
--- a/packages/core/src/entity/tax-rate/tax-rate.entity.ts
+++ b/packages/core/src/entity/tax-rate/tax-rate.entity.ts
@@ -1,5 +1,5 @@
import { TaxLine } from '@vendure/common/lib/generated-types';
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
import { Column, Entity, Index, ManyToOne } from 'typeorm';
import { grossPriceOf, netPriceOf, taxComponentOf, taxPayableOn } from '../../common/tax-utils';
@@ -8,6 +8,7 @@ import { HasCustomFields } from '../../config/custom-field/custom-field-types';
import { VendureEntity } from '../base/base.entity';
import { CustomTaxRateFields } from '../custom-entity-fields';
import { CustomerGroup } from '../customer-group/customer-group.entity';
+import { EntityId } from '../entity-id.decorator';
import { TaxCategory } from '../tax-category/tax-category.entity';
import { DecimalTransformer } from '../value-transformers';
import { Zone } from '../zone/zone.entity';
@@ -38,10 +39,16 @@ export class TaxRate extends VendureEntity implements HasCustomFields {
@ManyToOne(type => TaxCategory, taxCategory => taxCategory.taxRates)
category: TaxCategory;
+ @EntityId({ nullable: true })
+ categoryId: ID;
+
@Index()
@ManyToOne(type => Zone, zone => zone.taxRates)
zone: Zone;
+ @EntityId({ nullable: true })
+ zoneId: ID;
+
@Index()
@ManyToOne(type => CustomerGroup, customerGroup => customerGroup.taxRates, { nullable: true })
customerGroup?: CustomerGroup;
@@ -84,7 +91,13 @@ export class TaxRate extends VendureEntity implements HasCustomFields {
};
}
- test(zone: Zone, taxCategory: TaxCategory): boolean {
- return idsAreEqual(taxCategory.id, this.category.id) && idsAreEqual(zone.id, this.zone.id);
+ test(zone: Zone | ID, taxCategory: TaxCategory | ID): boolean {
+ const taxCategoryId = this.isId(taxCategory) ? taxCategory : taxCategory.id;
+ const zoneId = this.isId(zone) ? zone : zone.id;
+ return idsAreEqual(taxCategoryId, this.categoryId) && idsAreEqual(zoneId, this.zoneId);
+ }
+
+ private isId(entityOrId: T | ID): entityOrId is ID {
+ return typeof entityOrId === 'string' || typeof entityOrId === 'number';
}
}
diff --git a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts b/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
index a31635c3bf..b9ee716bb0 100644
--- a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
+++ b/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
@@ -405,7 +405,9 @@ export class IndexerController {
await this.removeSyntheticVariants(ctx, variants);
const productMap = new Map();
+ const originalChannel = ctx.channel;
for (const variant of variants) {
+ ctx.setChannel(originalChannel);
let product = productMap.get(variant.productId);
if (!product) {
product = await this.getProductInChannelQueryBuilder(ctx, variant.productId, ctx.channel);
@@ -496,6 +498,7 @@ export class IndexerController {
}
}
}
+ ctx.setChannel(originalChannel);
await this.queue.push(() =>
this.connection.getRepository(ctx, SearchIndexItem).save(items, { chunk: 2500 }),
diff --git a/packages/core/src/service/helpers/active-order/active-order.service.ts b/packages/core/src/service/helpers/active-order/active-order.service.ts
index 2945f6d23a..8b5bf6fc9d 100644
--- a/packages/core/src/service/helpers/active-order/active-order.service.ts
+++ b/packages/core/src/service/helpers/active-order/active-order.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { RequestContext } from '../../../api/common/request-context';
import { InternalServerError, UserInputError } from '../../../common/error/errors';
+import { idsAreEqual } from '../../../common/utils';
import { ConfigService } from '../../../config/config.service';
import { TransactionalConnection } from '../../../connection/transactional-connection';
import { Order } from '../../../entity/order/order.entity';
@@ -90,7 +91,7 @@ export class ActiveOrderService {
input: { [strategyName: string]: Record | undefined } | undefined,
createIfNotExists = false,
): Promise {
- let order: any;
+ let order: Order | undefined;
if (!order) {
const { activeOrderStrategy } = this.configService.orderOptions;
const strategyArray = Array.isArray(activeOrderStrategy)
@@ -119,7 +120,11 @@ export class ActiveOrderService {
}
if (order && ctx.session) {
- await this.sessionService.setActiveOrder(ctx, ctx.session, order);
+ const orderAlreadyAssignedToSession =
+ ctx.session.activeOrderId && idsAreEqual(ctx.session.activeOrderId, order.id);
+ if (!orderAlreadyAssignedToSession) {
+ await this.sessionService.setActiveOrder(ctx, ctx.session, order);
+ }
}
}
return order || undefined;
diff --git a/packages/core/src/service/helpers/order-calculator/order-calculator.ts b/packages/core/src/service/helpers/order-calculator/order-calculator.ts
index 64825dbce8..d9eb069bf7 100644
--- a/packages/core/src/service/helpers/order-calculator/order-calculator.ts
+++ b/packages/core/src/service/helpers/order-calculator/order-calculator.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { filterAsync } from '@vendure/common/lib/filter-async';
import { AdjustmentType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
import { RequestContext } from '../../../api/common/request-context';
import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
@@ -8,7 +9,7 @@ import { CacheKey } from '../../../common/constants';
import { InternalServerError } from '../../../common/error/errors';
import { idsAreEqual } from '../../../common/utils';
import { ConfigService } from '../../../config/config.service';
-import { OrderLine, TaxCategory, TaxRate } from '../../../entity';
+import { OrderLine, TaxRate } from '../../../entity';
import { Order } from '../../../entity/order/order.entity';
import { Promotion } from '../../../entity/promotion/promotion.entity';
import { Zone } from '../../../entity/zone/zone.entity';
@@ -76,7 +77,6 @@ export class OrderCalculator {
ctx,
order,
updatedOrderLine,
- activeTaxZone,
this.createTaxRateGetter(ctx, activeTaxZone),
);
}
@@ -113,7 +113,7 @@ export class OrderCalculator {
private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
const getTaxRate = this.createTaxRateGetter(ctx, activeZone);
for (const line of order.lines) {
- await this.applyTaxesToOrderLine(ctx, order, line, activeZone, getTaxRate);
+ await this.applyTaxesToOrderLine(ctx, order, line, getTaxRate);
}
this.calculateOrderTotals(order);
}
@@ -126,10 +126,9 @@ export class OrderCalculator {
ctx: RequestContext,
order: Order,
line: OrderLine,
- activeZone: Zone,
- getTaxRate: (taxCategory: TaxCategory) => Promise,
+ getTaxRate: (taxCategoryId: ID) => Promise,
) {
- const applicableTaxRate = await getTaxRate(line.taxCategory);
+ const applicableTaxRate = await getTaxRate(line.taxCategoryId);
const { taxLineCalculationStrategy } = this.configService.taxOptions;
line.taxLines = await taxLineCalculationStrategy.calculate({
ctx,
@@ -147,16 +146,16 @@ export class OrderCalculator {
private createTaxRateGetter(
ctx: RequestContext,
activeZone: Zone,
- ): (taxCategory: TaxCategory) => Promise {
- const taxRateCache = new Map();
+ ): (taxCategoryId: ID) => Promise {
+ const taxRateCache = new Map();
- return async (taxCategory: TaxCategory): Promise => {
- const cached = taxRateCache.get(taxCategory);
+ return async (taxCategoryId: ID): Promise => {
+ const cached = taxRateCache.get(taxCategoryId);
if (cached) {
return cached;
}
- const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategory);
- taxRateCache.set(taxCategory, rate);
+ const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategoryId);
+ taxRateCache.set(taxCategoryId, rate);
return rate;
};
}
diff --git a/packages/core/src/service/helpers/order-modifier/order-modifier.ts b/packages/core/src/service/helpers/order-modifier/order-modifier.ts
index 934d2388c1..a5153e1319 100644
--- a/packages/core/src/service/helpers/order-modifier/order-modifier.ts
+++ b/packages/core/src/service/helpers/order-modifier/order-modifier.ts
@@ -10,6 +10,7 @@ import {
} from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';
import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
+import { IsNull } from 'typeorm';
import { RequestContext } from '../../../api/common/request-context';
import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
@@ -140,7 +141,7 @@ export class OrderModifier {
): Promise {
for (const line of order.lines) {
const match =
- idsAreEqual(line.productVariant.id, productVariantId) &&
+ idsAreEqual(line.productVariantId, productVariantId) &&
(await this.customFieldsAreEqual(ctx, line, customFields, line.customFields));
if (match) {
return line;
@@ -164,12 +165,13 @@ export class OrderModifier {
return existingOrderLine;
}
- const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
+ const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId, order);
+ const featuredAssetId = productVariant.featuredAssetId ?? productVariant.featuredAssetId;
const orderLine = await this.connection.getRepository(ctx, OrderLine).save(
new OrderLine({
productVariant,
taxCategory: productVariant.taxCategory,
- featuredAsset: productVariant.featuredAsset ?? productVariant.product.featuredAsset,
+ featuredAsset: featuredAssetId ? { id: featuredAssetId } : undefined,
listPrice: productVariant.listPrice,
listPriceIncludesTax: productVariant.listPriceIncludesTax,
adjustments: [],
@@ -189,26 +191,15 @@ export class OrderModifier {
.set(orderLine.sellerChannel);
}
await this.customFieldRelationService.updateRelations(ctx, OrderLine, { customFields }, orderLine);
- const lineWithRelations = await this.connection.getEntityOrThrow(ctx, OrderLine, orderLine.id, {
- relations: [
- 'taxCategory',
- 'productVariant',
- 'productVariant.productVariantPrices',
- 'productVariant.taxCategory',
- ],
- });
- lineWithRelations.productVariant = this.translator.translate(
- await this.productVariantService.applyChannelPriceAndTax(
- lineWithRelations.productVariant,
- ctx,
- order,
- ),
- ctx,
- );
- order.lines.push(lineWithRelations);
- await this.connection.getRepository(ctx, Order).save(order, { reload: false });
- await this.eventBus.publish(new OrderLineEvent(ctx, order, lineWithRelations, 'created'));
- return lineWithRelations;
+ order.lines.push(orderLine);
+ await this.connection
+ .getRepository(ctx, Order)
+ .createQueryBuilder()
+ .relation('lines')
+ .of(order)
+ .add(orderLine);
+ await this.eventBus.publish(new OrderLineEvent(ctx, order, orderLine, 'created'));
+ return orderLine;
}
/**
@@ -896,11 +887,24 @@ export class OrderModifier {
private async getProductVariantOrThrow(
ctx: RequestContext,
productVariantId: ID,
+ order: Order,
): Promise {
- const productVariant = await this.productVariantService.findOne(ctx, productVariantId);
- if (!productVariant) {
+ const variant = await this.connection.findOneInChannel(
+ ctx,
+ ProductVariant,
+ productVariantId,
+ ctx.channelId,
+ {
+ relations: ['product', 'productVariantPrices', 'taxCategory'],
+ loadEagerRelations: false,
+ where: { deletedAt: IsNull() },
+ },
+ );
+
+ if (variant) {
+ return await this.productVariantService.applyChannelPriceAndTax(variant, ctx, order);
+ } else {
throw new EntityNotFoundError('ProductVariant', productVariantId);
}
- return productVariant;
}
}
diff --git a/packages/core/src/service/helpers/order-splitter/order-splitter.ts b/packages/core/src/service/helpers/order-splitter/order-splitter.ts
index 9dc510bb46..d550e63723 100644
--- a/packages/core/src/service/helpers/order-splitter/order-splitter.ts
+++ b/packages/core/src/service/helpers/order-splitter/order-splitter.ts
@@ -90,7 +90,9 @@ export class OrderSplitter {
...pick(line, [
'quantity',
'productVariant',
+ 'productVariantId',
'taxCategory',
+ 'taxCategoryId',
'featuredAsset',
'shippingLine',
'shippingLineId',
diff --git a/packages/core/src/service/services/asset.service.ts b/packages/core/src/service/services/asset.service.ts
index 234e504f21..b50b43f3a9 100644
--- a/packages/core/src/service/services/asset.service.ts
+++ b/packages/core/src/service/services/asset.service.ts
@@ -168,6 +168,7 @@ export class AssetService {
ctx.channelId,
{
relations: ['featuredAsset'],
+ loadEagerRelations: false,
},
);
} else {
@@ -178,6 +179,7 @@ export class AssetService {
relations: {
featuredAsset: true,
},
+ loadEagerRelations: false,
// TODO: satisfies
} as FindOneOptions)
.then(result => result ?? undefined);
diff --git a/packages/core/src/service/services/order-testing.service.ts b/packages/core/src/service/services/order-testing.service.ts
index 56603b46c2..b40973da94 100644
--- a/packages/core/src/service/services/order-testing.service.ts
+++ b/packages/core/src/service/services/order-testing.service.ts
@@ -130,6 +130,7 @@ export class OrderTestingService {
taxLines: [],
quantity: line.quantity,
taxCategory: productVariant.taxCategory,
+ taxCategoryId: productVariant.taxCategoryId,
});
mockOrder.lines.push(orderLine);
diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts
index d3afe58ad8..180411c361 100644
--- a/packages/core/src/service/services/order.service.ts
+++ b/packages/core/src/service/services/order.service.ts
@@ -53,7 +53,6 @@ import {
CancelPaymentError,
EmptyOrderLineSelectionError,
FulfillmentStateTransitionError,
- RefundStateTransitionError,
InsufficientStockOnHandError,
ItemsAlreadyFulfilledError,
ManualPaymentStateError,
@@ -61,6 +60,7 @@ import {
NothingToRefundError,
PaymentOrderMismatchError,
RefundOrderStateError,
+ RefundStateTransitionError,
SettlePaymentError,
} from '../../common/error/generated-graphql-admin-errors';
import {
@@ -489,7 +489,7 @@ export class OrderService {
* @since 2.2.0
*/
async updateOrderCustomer(ctx: RequestContext, { customerId, orderId, note }: SetOrderCustomerInput) {
- const order = await this.getOrderOrThrow(ctx, orderId);
+ const order = await this.getOrderOrThrow(ctx, orderId, ['channels', 'customer']);
const currentCustomer = order.customer;
if (currentCustomer?.id === customerId) {
// No change in customer, so just return the order as-is
@@ -539,6 +539,7 @@ export class OrderService {
productVariantId: ID,
quantity: number,
customFields?: { [key: string]: any },
+ relations?: RelationPaths,
): Promise> {
const order = await this.getOrderOrThrow(ctx, orderId);
const existingOrderLine = await this.orderModifier.getExistingOrderLine(
@@ -561,6 +562,7 @@ export class OrderService {
enabled: true,
deletedAt: IsNull(),
},
+ loadEagerRelations: false,
});
if (variant.product.enabled === false) {
throw new EntityNotFoundError('ProductVariant', productVariantId);
@@ -596,7 +598,7 @@ export class OrderService {
await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
}
const quantityWasAdjustedDown = correctedQuantity < quantity;
- const updatedOrder = await this.applyPriceAdjustments(ctx, order, [orderLine]);
+ const updatedOrder = await this.applyPriceAdjustments(ctx, order, [orderLine], relations);
if (quantityWasAdjustedDown) {
return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
} else {
@@ -614,6 +616,7 @@ export class OrderService {
orderLineId: ID,
quantity: number,
customFields?: { [key: string]: any },
+ relations?: RelationPaths,
): Promise> {
const order = await this.getOrderOrThrow(ctx, orderId);
const orderLine = this.getOrderLineOrThrow(order, orderLineId);
@@ -660,7 +663,7 @@ export class OrderService {
await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
}
const quantityWasAdjustedDown = correctedQuantity < quantity;
- const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines);
+ const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines, relations);
if (quantityWasAdjustedDown) {
return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
} else {
@@ -1663,8 +1666,23 @@ export class OrderService {
return order;
}
- private async getOrderOrThrow(ctx: RequestContext, orderId: ID): Promise {
- const order = await this.findOne(ctx, orderId);
+ private async getOrderOrThrow(
+ ctx: RequestContext,
+ orderId: ID,
+ relations?: RelationPaths,
+ ): Promise {
+ const order = await this.findOne(
+ ctx,
+ orderId,
+ relations ?? [
+ 'lines',
+ 'lines.productVariant',
+ 'lines.productVariant.productVariantPrices',
+ 'shippingLines',
+ 'surcharges',
+ 'customer',
+ ],
+ );
if (!order) {
throw new EntityNotFoundError('Order', orderId);
}
@@ -1730,6 +1748,7 @@ export class OrderService {
ctx: RequestContext,
order: Order,
updatedOrderLines?: OrderLine[],
+ relations?: RelationPaths,
): Promise {
const promotions = await this.promotionService.getActivePromotionsInChannel(ctx);
const activePromotionsPre = await this.promotionService.getActivePromotionsOnOrder(ctx, order.id);
@@ -1776,22 +1795,77 @@ export class OrderService {
}
}
+ // Get the shipping line IDs before doing the order calculation
+ // step, which can in some cases change the applied shipping lines.
+ const shippingLineIdsPre = order.shippingLines.map(l => l.id);
+
const updatedOrder = await this.orderCalculator.applyPriceAdjustments(
ctx,
order,
promotions,
updatedOrderLines ?? [],
);
+
+ const shippingLineIdsPost = updatedOrder.shippingLines.map(l => l.id);
+ await this.applyChangesToShippingLines(ctx, updatedOrder, shippingLineIdsPre, shippingLineIdsPost);
+
+ // Explicitly omit the shippingAddress and billingAddress properties to avoid
+ // a race condition where changing one or the other in parallel can
+ // overwrite the other's changes. The other omissions prevent the save
+ // function from doing more work than necessary.
await this.connection
.getRepository(ctx, Order)
- // Explicitly omit the shippingAddress and billingAddress properties to avoid
- // a race condition where changing one or the other in parallel can
- // overwrite the other's changes.
- .save(omit(updatedOrder, ['shippingAddress', 'billingAddress']), { reload: false });
+ .save(
+ omit(updatedOrder, [
+ 'shippingAddress',
+ 'billingAddress',
+ 'lines',
+ 'shippingLines',
+ 'aggregateOrder',
+ 'sellerOrders',
+ 'customer',
+ 'modifications',
+ ]),
+ {
+ reload: false,
+ },
+ );
await this.connection.getRepository(ctx, OrderLine).save(updatedOrder.lines, { reload: false });
await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre);
- return assertFound(this.findOne(ctx, order.id));
+ return assertFound(this.findOne(ctx, order.id, relations));
+ }
+
+ /**
+ * Applies changes to the shipping lines of an order, adding or removing the relations
+ * in the database.
+ */
+ private async applyChangesToShippingLines(
+ ctx: RequestContext,
+ order: Order,
+ shippingLineIdsPre: ID[],
+ shippingLineIdsPost: ID[],
+ ) {
+ const removedShippingLineIds = shippingLineIdsPre.filter(id => !shippingLineIdsPost.includes(id));
+ const newlyAddedShippingLineIds = shippingLineIdsPost.filter(id => !shippingLineIdsPre.includes(id));
+
+ for (const idToRemove of removedShippingLineIds) {
+ await this.connection
+ .getRepository(ctx, Order)
+ .createQueryBuilder()
+ .relation('shippingLines')
+ .of(order)
+ .remove(idToRemove);
+ }
+
+ for (const idToAdd of newlyAddedShippingLineIds) {
+ await this.connection
+ .getRepository(ctx, Order)
+ .createQueryBuilder()
+ .relation('shippingLines')
+ .of(order)
+ .add(idToAdd);
+ }
}
}
diff --git a/packages/core/src/service/services/tax-rate.service.ts b/packages/core/src/service/services/tax-rate.service.ts
index c0c2979d1a..cad588b3aa 100644
--- a/packages/core/src/service/services/tax-rate.service.ts
+++ b/packages/core/src/service/services/tax-rate.service.ts
@@ -164,7 +164,11 @@ export class TaxRateService {
* Returns the applicable TaxRate based on the specified Zone and TaxCategory. Used when calculating Order
* prices.
*/
- async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise {
+ async getApplicableTaxRate(
+ ctx: RequestContext,
+ zone: Zone | ID,
+ taxCategory: TaxCategory | ID,
+ ): Promise {
const rate = (await this.getActiveTaxRates(ctx)).find(r => r.test(zone, taxCategory));
return rate || this.defaultTaxRate;
}
diff --git a/packages/core/src/testing/order-test-utils.ts b/packages/core/src/testing/order-test-utils.ts
index 3d3c38a5f5..6471c0ead5 100644
--- a/packages/core/src/testing/order-test-utils.ts
+++ b/packages/core/src/testing/order-test-utils.ts
@@ -76,7 +76,9 @@ export const taxRateDefaultStandard = new TaxRate({
value: 20,
enabled: true,
zone: zoneDefault,
+ zoneId: zoneDefault.id,
category: taxCategoryStandard,
+ categoryId: taxCategoryStandard.id,
});
export const taxRateDefaultReduced = new TaxRate({
id: 'taxRateDefaultReduced',
@@ -84,7 +86,9 @@ export const taxRateDefaultReduced = new TaxRate({
value: 10,
enabled: true,
zone: zoneDefault,
+ zoneId: zoneDefault.id,
category: taxCategoryReduced,
+ categoryId: taxCategoryReduced.id,
});
export const taxRateDefaultZero = new TaxRate({
id: 'taxRateDefaultZero',
@@ -92,7 +96,9 @@ export const taxRateDefaultZero = new TaxRate({
value: 0,
enabled: true,
zone: zoneDefault,
+ zoneId: zoneDefault.id,
category: taxCategoryZero,
+ categoryId: taxCategoryZero.id,
});
export const taxRateOtherStandard = new TaxRate({
id: 'taxRateOtherStandard',
@@ -100,7 +106,9 @@ export const taxRateOtherStandard = new TaxRate({
value: 15,
enabled: true,
zone: zoneOther,
+ zoneId: zoneOther.id,
category: taxCategoryStandard,
+ categoryId: taxCategoryStandard.id,
});
export const taxRateOtherReduced = new TaxRate({
id: 'taxRateOtherReduced',
@@ -108,7 +116,9 @@ export const taxRateOtherReduced = new TaxRate({
value: 5,
enabled: true,
zone: zoneOther,
+ zoneId: zoneOther.id,
category: taxCategoryReduced,
+ categoryId: taxCategoryReduced.id,
});
export class MockTaxRateService {
@@ -124,14 +134,18 @@ export class MockTaxRateService {
/* noop */
}
- async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise {
+ async getApplicableTaxRate(
+ ctx: RequestContext,
+ zone: Zone | ID,
+ taxCategory: TaxCategory | ID,
+ ): Promise {
const rate = this.activeTaxRates.find(r => r.test(zone, taxCategory));
return rate || taxRateDefaultStandard;
}
}
export function createOrder(
- orderConfig: Partial> & {
+ orderConfig: Partial> & {
ctx: RequestContext;
lines: Array<{
listPrice: number;
@@ -145,6 +159,7 @@ export function createOrder(
({ listPrice, taxCategory, quantity }) =>
new OrderLine({
taxCategory,
+ taxCategoryId: taxCategory.id,
quantity,
orderPlacedQuantity: 0,
listPrice,
diff --git a/packages/dev-server/index.ts b/packages/dev-server/index.ts
index 845fa81e90..93a9aae1e7 100644
--- a/packages/dev-server/index.ts
+++ b/packages/dev-server/index.ts
@@ -8,7 +8,7 @@ import { devConfig } from './dev-config';
bootstrap(devConfig)
.then(app => {
if (process.env.RUN_JOB_QUEUE === '1') {
- app.get(JobQueueService).start();
+ return app.get(JobQueueService).start();
}
})
.catch(err => {
diff --git a/packages/dev-server/load-testing/init-load-test.ts b/packages/dev-server/load-testing/init-load-test.ts
index 48f96b3fce..90946daabe 100644
--- a/packages/dev-server/load-testing/init-load-test.ts
+++ b/packages/dev-server/load-testing/init-load-test.ts
@@ -3,7 +3,7 @@
import { bootstrap, JobQueueService, Logger } from '@vendure/core';
import { populate } from '@vendure/core/cli/populate';
import { clearAllTables, populateCustomers, SimpleGraphQLClient } from '@vendure/testing';
-import stringify from 'csv-stringify';
+import { stringify } from 'csv-stringify';
import fs from 'fs';
import path from 'path';
@@ -100,6 +100,9 @@ async function isDatabasePopulated(databaseName: string): Promise {
await client.connect();
try {
const res = await client.query('SELECT COUNT(id) as prodCount FROM product');
+ if (res.rows[0]?.prodcount < 1) {
+ return false;
+ }
return true;
} catch (e: any) {
if (e.message === 'relation "product" does not exist') {
diff --git a/packages/dev-server/load-testing/run-load-test.ts b/packages/dev-server/load-testing/run-load-test.ts
index 4b062970a6..f928974022 100644
--- a/packages/dev-server/load-testing/run-load-test.ts
+++ b/packages/dev-server/load-testing/run-load-test.ts
@@ -2,7 +2,7 @@
import { INestApplication } from '@nestjs/common';
import { bootstrap, JobQueueService } from '@vendure/core';
import { spawn } from 'child_process';
-import stringify from 'csv-stringify';
+import { stringify } from 'csv-stringify';
import fs from 'fs';
import path from 'path';
diff --git a/packages/elasticsearch-plugin/src/indexing/indexer.controller.ts b/packages/elasticsearch-plugin/src/indexing/indexer.controller.ts
index 328a3bdcdc..7eb65c62c4 100644
--- a/packages/elasticsearch-plugin/src/indexing/indexer.controller.ts
+++ b/packages/elasticsearch-plugin/src/indexing/indexer.controller.ts
@@ -511,7 +511,6 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
if (!product) {
return;
}
-
let updatedProductVariants: ProductVariant[] = [];
try {
updatedProductVariants = await this.connection.rawConnection.getRepository(ProductVariant).find({
@@ -541,6 +540,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
languageVariants.push(...variant.translations.map(t => t.languageCode));
const uniqueLanguageVariants = unique(languageVariants);
+ const originalChannel = ctx.channel;
for (const channel of product.channels) {
ctx.setChannel(channel);
const variantsInChannel = updatedProductVariants.filter(v =>
@@ -623,6 +623,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
}
}
}
+ ctx.setChannel(originalChannel);
// Because we can have a huge amount of variant for 1 product, we also chunk update operations
await this.executeBulkOperationsByChunks(
@@ -944,7 +945,13 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
const productCustomMappings = Object.entries(this.options.customProductMappings);
for (const [name, def] of productCustomMappings) {
- item[`product-${name}`] = await def.valueFn(v.product, variants, languageCode, this.injector, ctx);
+ item[`product-${name}`] = await def.valueFn(
+ v.product,
+ variants,
+ languageCode,
+ this.injector,
+ ctx,
+ );
}
return item;
} catch (err: any) {