Skip to content

Commit

Permalink
feat(core): Make Customers ChannelAware
Browse files Browse the repository at this point in the history
* feat(core): Make Customers ChannelAware

* test(core): Fix unit tests for ChannelAware Customers

* fix(core): Finalize ChannelAware Customers implementation

* fix(core): Implement review comments

Co-authored-by: hendrikdepauw <hendrik@advantitge.com>
  • Loading branch information
hendrik-advantitge and hendrik-advantitge authored Sep 14, 2020
1 parent 70a7665 commit 0f73473
Show file tree
Hide file tree
Showing 25 changed files with 780 additions and 110 deletions.
487 changes: 487 additions & 0 deletions packages/core/e2e/customer-channel.e2e-spec.ts

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions packages/core/e2e/customer.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ let sendEmailFn: jest.Mock;
class TestEmailPlugin implements OnModuleInit {
constructor(private eventBus: EventBus) {}
onModuleInit() {
this.eventBus.ofType(AccountRegistrationEvent).subscribe((event) => {
this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
sendEmailFn(event);
});
}
Expand Down Expand Up @@ -170,7 +170,7 @@ describe('Customer resolver', () => {
});

expect(result.customer!.addresses!.length).toBe(2);
firstCustomerAddressIds = result.customer!.addresses!.map((a) => a.id).sort();
firstCustomerAddressIds = result.customer!.addresses!.map(a => a.id).sort();
});

it('updateCustomerAddress updates the country', async () => {
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('Customer resolver', () => {
id: firstCustomer.id,
});
const otherAddress = result2.customer!.addresses!.filter(
(a) => a.id !== firstCustomerAddressIds[1],
a => a.id !== firstCustomerAddressIds[1],
)[0]!;
expect(otherAddress.defaultShippingAddress).toBe(false);
expect(otherAddress.defaultBillingAddress).toBe(false);
Expand All @@ -233,7 +233,7 @@ describe('Customer resolver', () => {
id: firstCustomer.id,
});
const otherAddress2 = result4.customer!.addresses!.filter(
(a) => a.id !== firstCustomerAddressIds[0],
a => a.id !== firstCustomerAddressIds[0],
)[0]!;
expect(otherAddress2.defaultShippingAddress).toBe(false);
expect(otherAddress2.defaultBillingAddress).toBe(false);
Expand Down Expand Up @@ -336,10 +336,10 @@ describe('Customer resolver', () => {
);
expect(customer!.addresses!.length).toBe(2);
const defaultAddress = customer!.addresses!.filter(
(a) => a.defaultBillingAddress && a.defaultShippingAddress,
a => a.defaultBillingAddress && a.defaultShippingAddress,
);
const otherAddress = customer!.addresses!.filter(
(a) => !a.defaultBillingAddress && !a.defaultShippingAddress,
a => !a.defaultBillingAddress && !a.defaultShippingAddress,
);
expect(defaultAddress.length).toBe(1);
expect(otherAddress.length).toBe(1);
Expand Down Expand Up @@ -448,7 +448,7 @@ describe('Customer resolver', () => {
GET_CUSTOMER_LIST,
);

expect(result.customers.items.map((c) => c.id).includes(thirdCustomer.id)).toBe(false);
expect(result.customers.items.map(c => c.id).includes(thirdCustomer.id)).toBe(false);
});

it(
Expand Down Expand Up @@ -593,7 +593,7 @@ const GET_CUSTOMER_WITH_USER = gql`
}
`;

const CREATE_ADDRESS = gql`
export const CREATE_ADDRESS = gql`
mutation CreateAddress($id: ID!, $input: CreateAddressInput!) {
createCustomerAddress(customerId: $id, input: $input) {
id
Expand All @@ -615,7 +615,7 @@ const CREATE_ADDRESS = gql`
}
`;

const UPDATE_ADDRESS = gql`
export const UPDATE_ADDRESS = gql`
mutation UpdateAddress($input: UpdateAddressInput!) {
updateCustomerAddress(input: $input) {
id
Expand Down Expand Up @@ -660,7 +660,7 @@ export const UPDATE_CUSTOMER = gql`
${CUSTOMER_FRAGMENT}
`;

const DELETE_CUSTOMER = gql`
export const DELETE_CUSTOMER = gql`
mutation DeleteCustomer($id: ID!) {
deleteCustomer(id: $id) {
result
Expand Down
45 changes: 44 additions & 1 deletion packages/core/src/api/middleware/auth-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { Request, Response } from 'express';
import { ForbiddenError } from '../../common/error/errors';
import { ConfigService } from '../../config/config.service';
import { CachedSession } from '../../config/session-cache/session-cache-strategy';
import { Customer } from '../../entity/customer/customer.entity';
import { ChannelService } from '../../service/services/channel.service';
import { CustomerService } from '../../service/services/customer.service';
import { SessionService } from '../../service/services/session.service';
import { extractSessionToken } from '../common/extract-session-token';
import { parseContext } from '../common/parse-context';
import { RequestContext } from '../common/request-context';
import { REQUEST_CONTEXT_KEY, RequestContextService } from '../common/request-context.service';
import { setSessionToken } from '../common/set-session-token';
import { PERMISSIONS_METADATA_KEY } from '../decorators/allow.decorator';
Expand All @@ -26,6 +30,8 @@ export class AuthGuard implements CanActivate {
private configService: ConfigService,
private requestContextService: RequestContextService,
private sessionService: SessionService,
private customerService: CustomerService,
private channelService: ChannelService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
Expand All @@ -35,7 +41,12 @@ export class AuthGuard implements CanActivate {
const isPublic = !!permissions && permissions.includes(Permission.Public);
const hasOwnerPermission = !!permissions && permissions.includes(Permission.Owner);
const session = await this.getSession(req, res, hasOwnerPermission);
const requestContext = await this.requestContextService.fromRequest(req, info, permissions, session);
let requestContext = await this.requestContextService.fromRequest(req, info, permissions, session);

const requestContextShouldBeReinitialized = await this.setActiveChannel(requestContext, session);
if (requestContextShouldBeReinitialized) {
requestContext = await this.requestContextService.fromRequest(req, info, permissions, session);
}
(req as any)[REQUEST_CONTEXT_KEY] = requestContext;

if (authDisabled || !permissions || isPublic) {
Expand All @@ -50,6 +61,38 @@ export class AuthGuard implements CanActivate {
}
}

private async setActiveChannel(
requestContext: RequestContext,
session?: CachedSession,
): Promise<boolean> {
if (!session) {
return false;
}
// In case the session does not have an activeChannelId or the activeChannelId
// does not correspond to the current channel, the activeChannelId on the session is set
const activeChannelShouldBeSet =
!session.activeChannelId || session.activeChannelId !== requestContext.channelId;
if (activeChannelShouldBeSet) {
await this.sessionService.setActiveChannel(session, requestContext.channel);
if (requestContext.activeUserId) {
const customer = await this.customerService.findOneByUserId(
requestContext,
requestContext.activeUserId,
false,
);
// To avoid assigning the customer to the active channel on every request,
// it is only done on the first request and whenever the channel changes
if (customer) {
await this.channelService.assignToChannels(Customer, customer.id, [
requestContext.channelId,
]);
}
}
return true;
}
return false;
}

private async getSession(
req: Request,
res: Response,
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/api/resolvers/admin/customer.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@ export class CustomerResolver {

@Query()
@Allow(Permission.ReadCustomer)
async customers(@Args() args: QueryCustomersArgs): Promise<PaginatedList<Customer>> {
return this.customerService.findAll(args.options || undefined);
async customers(
@Ctx() ctx: RequestContext,
@Args() args: QueryCustomersArgs,
): Promise<PaginatedList<Customer>> {
return this.customerService.findAll(ctx, args.options || undefined);
}

@Query()
@Allow(Permission.ReadCustomer)
async customer(@Args() args: QueryCustomerArgs): Promise<Customer | undefined> {
return this.customerService.findOne(args.id);
async customer(
@Ctx() ctx: RequestContext,
@Args() args: QueryCustomerArgs,
): Promise<Customer | undefined> {
return this.customerService.findOne(ctx, args.id);
}

@Mutation()
Expand Down Expand Up @@ -93,8 +99,11 @@ export class CustomerResolver {

@Mutation()
@Allow(Permission.DeleteCustomer)
async deleteCustomer(@Args() args: MutationDeleteCustomerArgs): Promise<DeletionResponse> {
return this.customerService.softDelete(args.id);
async deleteCustomer(
@Ctx() ctx: RequestContext,
@Args() args: MutationDeleteCustomerArgs,
): Promise<DeletionResponse> {
return this.customerService.softDelete(ctx, args.id);
}

@Mutation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class CustomerAdminEntityResolver {
if (customer.groups) {
return customer.groups;
}
return this.customerService.getCustomerGroups(customer.id);
return this.customerService.getCustomerGroups(ctx, customer.id);
}

@ResolveField()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export class CustomerGroupEntityResolver {
@Parent() customerGroup: CustomerGroup,
@Args() args: QueryCustomersArgs,
): Promise<PaginatedList<Customer>> {
return this.customerGroupService.getGroupCustomers(customerGroup.id, args.options || undefined);
return this.customerGroupService.getGroupCustomers(ctx, customerGroup.id, args.options || undefined);
}
}
6 changes: 3 additions & 3 deletions packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
) {
super(authService, userService, administratorService, configService);
this.nativeAuthStrategyIsConfigured = !!this.configService.authOptions.shopAuthenticationStrategy.find(
(strategy) => strategy.name === NATIVE_AUTH_STRATEGY_NAME,
strategy => strategy.name === NATIVE_AUTH_STRATEGY_NAME,
);
}

Expand Down Expand Up @@ -194,7 +194,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
this.requireNativeAuthStrategy();
const result = await super.updatePassword(ctx, args.currentPassword, args.newPassword);
if (result && ctx.activeUserId) {
const customer = await this.customerService.findOneByUserId(ctx.activeUserId);
const customer = await this.customerService.findOneByUserId(ctx, ctx.activeUserId);
if (customer) {
await this.historyService.createHistoryEntryForCustomer({
ctx,
Expand Down Expand Up @@ -234,7 +234,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
private requireNativeAuthStrategy() {
if (!this.nativeAuthStrategyIsConfigured) {
const authStrategyNames = this.configService.authOptions.shopAuthenticationStrategy
.map((s) => s.name)
.map(s => s.name)
.join(', ');
const errorMessage =
'This GraphQL operation requires that the NativeAuthenticationStrategy be configured for the Shop API.\n' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ShopCustomerResolver {
async activeCustomer(@Ctx() ctx: RequestContext): Promise<Customer | undefined> {
const userId = ctx.activeUserId;
if (userId) {
return this.customerService.findOneByUserId(userId);
return this.customerService.findOneByUserId(ctx, userId);
}
}

Expand Down Expand Up @@ -87,7 +87,7 @@ export class ShopCustomerResolver {
if (!userId) {
throw new ForbiddenError();
}
const customer = await this.customerService.findOneByUserId(userId);
const customer = await this.customerService.findOneByUserId(ctx, userId);
if (!customer) {
throw new InternalServerError(`error.no-customer-found-for-current-user`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export class ShopOrderResolver {
}
const sessionOrder = await this.getOrderFromContext(ctx);
if (sessionOrder) {
const customer = await this.customerService.createOrUpdate(args.input, true);
const customer = await this.customerService.createOrUpdate(ctx, args.input, true);
return this.orderService.addCustomerToOrder(ctx, sessionOrder.id, customer);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';

import { RequestContext } from '../../../api/common/request-context';
import { idsAreEqual } from '../../../common/utils';
import { OrderLine } from '../../../entity/order-line/order-line.entity';
import { Order } from '../../../entity/order/order.entity';
Expand All @@ -20,7 +21,7 @@ export const containsProducts = new PromotionCondition({
label: [{ languageCode: LanguageCode.en, value: 'Product variants' }],
},
},
async check(order: Order, args) {
async check(ctx: RequestContext, order: Order, args) {
const ids = args.productVariantIds;
let matches = 0;
for (const line of order.lines) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';

import { RequestContext } from '../../../api/common/request-context';
import { TtlCache } from '../../../common/ttl-cache';
import { idsAreEqual } from '../../../common/utils';
import { Order } from '../../../entity/order/order.entity';
Expand All @@ -26,15 +27,15 @@ export const customerGroup = new PromotionCondition({
const { CustomerService } = await import('../../../service/services/customer.service');
customerService = injector.get(CustomerService);
},
async check(order: Order, args) {
async check(ctx: RequestContext, order: Order, args) {
if (!order.customer) {
return false;
}
const customerId = order.customer.id;
let groupIds = cache.get(customerId);
if (!groupIds) {
const groups = await customerService.getCustomerGroups(customerId);
groupIds = groups.map((g) => g.id);
const groups = await customerService.getCustomerGroups(ctx, customerId);
groupIds = groups.map(g => g.id);
cache.set(customerId, groupIds);
}
return !!groupIds.find((id) => idsAreEqual(id, args.customerGroupId));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';

import { RequestContext } from '../../../api/common/request-context';
import { Order } from '../../../entity/order/order.entity';
import { PromotionCondition } from '../promotion-condition';
import { FacetValueChecker } from '../utils/facet-value-checker';
Expand All @@ -19,7 +20,7 @@ export const hasFacetValues = new PromotionCondition({
facetValueChecker = new FacetValueChecker(injector.getConnection());
},
// tslint:disable-next-line:no-shadowed-variable
async check(order: Order, args) {
async check(ctx: RequestContext, order: Order, args) {
let matches = 0;
for (const line of order.lines) {
if (await facetValueChecker.hasFacetValues(line, args.facets)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';

import { RequestContext } from '../../../api/common/request-context';
import { Order } from '../../../entity/order/order.entity';
import { PromotionCondition } from '../promotion-condition';

export const minimumOrderAmount = new PromotionCondition({
Expand All @@ -12,7 +14,7 @@ export const minimumOrderAmount = new PromotionCondition({
},
taxInclusive: { type: 'boolean' },
},
check(order, args) {
check(ctx: RequestContext, order: Order, args) {
if (args.taxInclusive) {
return order.subTotal >= args.amount;
} else {
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/config/promotion/promotion-condition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ConfigArg } from '@vendure/common/lib/generated-types';
import { ConfigArgType, ID } from '@vendure/common/lib/shared-types';

import { RequestContext } from '../../api/common/request-context';
import {
ConfigArgs,
ConfigArgValues,
Expand All @@ -18,6 +19,7 @@ import { Order } from '../../entity/order/order.entity';
* @docsPage promotion-condition
*/
export type CheckPromotionConditionFn<T extends ConfigArgs> = (
ctx: RequestContext,
order: Order,
args: ConfigArgValues<T>,
) => boolean | Promise<boolean>;
Expand Down Expand Up @@ -61,7 +63,7 @@ export class PromotionCondition<T extends ConfigArgs = ConfigArgs> extends Confi
this.priorityValue = config.priorityValue || 0;
}

async check(order: Order, args: ConfigArg[]): Promise<boolean> {
return this.checkFn(order, this.argsArrayToHash(args));
async check(ctx: RequestContext, order: Order, args: ConfigArg[]): Promise<boolean> {
return this.checkFn(ctx, order, this.argsArrayToHash(args));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type CachedSession = {
activeOrderId?: ID;
authenticationStrategy?: string;
user?: CachedSessionUser;
activeChannelId?: ID;
};

/**
Expand Down
Loading

0 comments on commit 0f73473

Please sign in to comment.