|
|
@@ -0,0 +1,487 @@
|
|
|
+/* tslint:disable:no-non-null-assertion */
|
|
|
+import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
|
|
|
+import gql from 'graphql-tag';
|
|
|
+import path from 'path';
|
|
|
+
|
|
|
+import { initialData } from '../../../e2e-common/e2e-initial-data';
|
|
|
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
|
|
|
+
|
|
|
+import { CUSTOMER_FRAGMENT } from './graphql/fragments';
|
|
|
+import {
|
|
|
+ AddCustomersToGroup,
|
|
|
+ CreateAddress,
|
|
|
+ CreateChannel,
|
|
|
+ CreateCustomer,
|
|
|
+ CreateCustomerGroup,
|
|
|
+ CurrencyCode,
|
|
|
+ DeleteCustomer,
|
|
|
+ DeleteCustomerAddress,
|
|
|
+ GetCustomerGroup,
|
|
|
+ GetCustomerList,
|
|
|
+ LanguageCode,
|
|
|
+ Me,
|
|
|
+ RemoveCustomersFromGroup,
|
|
|
+ UpdateAddress,
|
|
|
+ UpdateCustomer,
|
|
|
+} from './graphql/generated-e2e-admin-types';
|
|
|
+import { Register } from './graphql/generated-e2e-shop-types';
|
|
|
+import {
|
|
|
+ CREATE_CHANNEL,
|
|
|
+ CREATE_CUSTOMER_GROUP,
|
|
|
+ CUSTOMER_GROUP_FRAGMENT,
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ ME,
|
|
|
+ REMOVE_CUSTOMERS_FROM_GROUP,
|
|
|
+} from './graphql/shared-definitions';
|
|
|
+import { DELETE_ADDRESS, REGISTER_ACCOUNT } from './graphql/shop-definitions';
|
|
|
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
|
|
|
+
|
|
|
+describe('ChannelAware Customers', () => {
|
|
|
+ const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
|
|
|
+ const SECOND_CHANNEL_TOKEN = 'second_channel_token';
|
|
|
+ let firstCustomer: GetCustomerList.Items;
|
|
|
+ let secondCustomer: GetCustomerList.Items;
|
|
|
+ let thirdCustomer: GetCustomerList.Items;
|
|
|
+ const numberOfCustomers = 3;
|
|
|
+ let customerGroupId: string;
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ await server.init({
|
|
|
+ initialData,
|
|
|
+ productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
|
|
|
+ customerCount: numberOfCustomers,
|
|
|
+ });
|
|
|
+ await adminClient.asSuperAdmin();
|
|
|
+
|
|
|
+ const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ {
|
|
|
+ options: { take: numberOfCustomers },
|
|
|
+ },
|
|
|
+ );
|
|
|
+ firstCustomer = customers.items[0];
|
|
|
+ secondCustomer = customers.items[1];
|
|
|
+ thirdCustomer = customers.items[2];
|
|
|
+
|
|
|
+ await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(CREATE_CHANNEL, {
|
|
|
+ input: {
|
|
|
+ code: 'second-channel',
|
|
|
+ token: SECOND_CHANNEL_TOKEN,
|
|
|
+ defaultLanguageCode: LanguageCode.en,
|
|
|
+ currencyCode: CurrencyCode.GBP,
|
|
|
+ pricesIncludeTax: true,
|
|
|
+ defaultShippingZoneId: 'T_1',
|
|
|
+ defaultTaxZoneId: 'T_1',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const { createCustomerGroup } = await adminClient.query<
|
|
|
+ CreateCustomerGroup.Mutation,
|
|
|
+ CreateCustomerGroup.Variables
|
|
|
+ >(CREATE_CUSTOMER_GROUP, {
|
|
|
+ input: {
|
|
|
+ name: 'TestGroup',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ customerGroupId = createCustomerGroup.id;
|
|
|
+ }, TEST_SETUP_TIMEOUT_MS);
|
|
|
+
|
|
|
+ afterAll(async () => {
|
|
|
+ await server.destroy();
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Address manipulation', () => {
|
|
|
+ it(
|
|
|
+ 'throws when updating address from customer from other channel',
|
|
|
+ assertThrowsWithMessage(async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<UpdateAddress.Mutation, UpdateAddress.Variables>(UPDATE_ADDRESS, {
|
|
|
+ input: {
|
|
|
+ id: 'T_1',
|
|
|
+ streetLine1: 'Dummy street',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, `No Address with the id '1' could be found`),
|
|
|
+ );
|
|
|
+
|
|
|
+ it(
|
|
|
+ 'throws when creating address for customer from other channel',
|
|
|
+ assertThrowsWithMessage(async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<CreateAddress.Mutation, CreateAddress.Variables>(CREATE_ADDRESS, {
|
|
|
+ id: firstCustomer.id,
|
|
|
+ input: {
|
|
|
+ streetLine1: 'Dummy street',
|
|
|
+ countryCode: 'BE',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, `No Customer with the id '1' could be found`),
|
|
|
+ );
|
|
|
+
|
|
|
+ it(
|
|
|
+ 'throws when deleting address from customer from other channel',
|
|
|
+ assertThrowsWithMessage(async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<DeleteCustomerAddress.Mutation, DeleteCustomerAddress.Variables>(
|
|
|
+ DELETE_ADDRESS,
|
|
|
+ {
|
|
|
+ id: 'T_1',
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }, `No Address with the id '1' could be found`),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Customer manipulation', () => {
|
|
|
+ it(
|
|
|
+ 'throws when deleting customer from other channel',
|
|
|
+ assertThrowsWithMessage(async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<DeleteCustomer.Mutation, DeleteCustomer.Variables>(DELETE_CUSTOMER, {
|
|
|
+ id: firstCustomer.id,
|
|
|
+ });
|
|
|
+ }, `No Customer with the id '1' could be found`),
|
|
|
+ );
|
|
|
+
|
|
|
+ it(
|
|
|
+ 'throws when updating customer from other channel',
|
|
|
+ assertThrowsWithMessage(async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<UpdateCustomer.Mutation, UpdateCustomer.Variables>(UPDATE_CUSTOMER, {
|
|
|
+ input: {
|
|
|
+ id: firstCustomer.id,
|
|
|
+ firstName: 'John',
|
|
|
+ lastName: 'Doe',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, `No Customer with the id '1' could be found`),
|
|
|
+ );
|
|
|
+
|
|
|
+ it('creates customers on current and default channel', async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<CreateCustomer.Mutation, CreateCustomer.Variables>(CREATE_CUSTOMER, {
|
|
|
+ input: {
|
|
|
+ firstName: 'John',
|
|
|
+ lastName: 'Doe',
|
|
|
+ emailAddress: 'john.doe@test.com',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ const customersSecondChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ const customersDefaultChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+
|
|
|
+ expect(customersSecondChannel.customers.totalItems).toBe(1);
|
|
|
+ expect(customersDefaultChannel.customers.totalItems).toBe(numberOfCustomers + 1);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('only shows customers from current channel', async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ );
|
|
|
+ expect(customers.totalItems).toBe(1);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('shows all customers on default channel', async () => {
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ );
|
|
|
+ expect(customers.totalItems).toBe(numberOfCustomers + 1);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('brings customer to current channel when creating with existing emailAddress', async () => {
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ let customersDefaultChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ let customersSecondChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+ expect(customersDefaultChannel.customers.items.map(customer => customer.emailAddress)).toContain(
|
|
|
+ firstCustomer.emailAddress,
|
|
|
+ );
|
|
|
+ expect(
|
|
|
+ customersSecondChannel.customers.items.map(customer => customer.emailAddress),
|
|
|
+ ).not.toContain(firstCustomer.emailAddress);
|
|
|
+
|
|
|
+ await adminClient.query<CreateCustomer.Mutation, CreateCustomer.Variables>(CREATE_CUSTOMER, {
|
|
|
+ input: {
|
|
|
+ firstName: firstCustomer.firstName + '_new',
|
|
|
+ lastName: firstCustomer.lastName + '_new',
|
|
|
+ emailAddress: firstCustomer.emailAddress,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ customersSecondChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ customersDefaultChannel = await adminClient.query<
|
|
|
+ GetCustomerList.Query,
|
|
|
+ GetCustomerList.Variables
|
|
|
+ >(GET_CUSTOMER_LIST);
|
|
|
+ const firstCustomerOnNewChannel = customersSecondChannel.customers.items.find(
|
|
|
+ customer => customer.emailAddress === firstCustomer.emailAddress,
|
|
|
+ );
|
|
|
+ const firstCustomerOnDefaultChannel = customersDefaultChannel.customers.items.find(
|
|
|
+ customer => customer.emailAddress === firstCustomer.emailAddress,
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(firstCustomerOnNewChannel).not.toBeNull();
|
|
|
+ expect(firstCustomerOnNewChannel?.emailAddress).toBe(firstCustomer.emailAddress);
|
|
|
+ expect(firstCustomerOnNewChannel?.firstName).toBe(firstCustomer.firstName + '_new');
|
|
|
+ expect(firstCustomerOnNewChannel?.lastName).toBe(firstCustomer.lastName + '_new');
|
|
|
+
|
|
|
+ expect(firstCustomerOnDefaultChannel).not.toBeNull();
|
|
|
+ expect(firstCustomerOnDefaultChannel?.emailAddress).toBe(firstCustomer.emailAddress);
|
|
|
+ expect(firstCustomerOnDefaultChannel?.firstName).toBe(firstCustomer.firstName + '_new');
|
|
|
+ expect(firstCustomerOnDefaultChannel?.lastName).toBe(firstCustomer.lastName + '_new');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Shop API', () => {
|
|
|
+ it('assigns authenticated customers to the channels they visit', async () => {
|
|
|
+ shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await shopClient.asUserWithCredentials(secondCustomer.emailAddress, 'test');
|
|
|
+ await shopClient.query<Me.Query>(ME);
|
|
|
+
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ );
|
|
|
+ expect(customers.totalItems).toBe(3);
|
|
|
+ expect(customers.items.map(customer => customer.emailAddress)).toContain(
|
|
|
+ secondCustomer.emailAddress,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it('assigns newly registered customers to channel', async () => {
|
|
|
+ shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await shopClient.asAnonymousUser();
|
|
|
+ await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
|
|
|
+ input: {
|
|
|
+ emailAddress: 'john.doe.2@test.com',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
|
|
|
+ GET_CUSTOMER_LIST,
|
|
|
+ );
|
|
|
+ expect(customers.totalItems).toBe(4);
|
|
|
+ expect(customers.items.map(customer => customer.emailAddress)).toContain('john.doe.2@test.com');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('Customergroup manipulation', () => {
|
|
|
+ it('does not add a customer from another channel to customerGroup', async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<AddCustomersToGroup.Mutation, AddCustomersToGroup.Variables>(
|
|
|
+ ADD_CUSTOMERS_TO_GROUP,
|
|
|
+ {
|
|
|
+ groupId: customerGroupId,
|
|
|
+ customerIds: [thirdCustomer.id],
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ const { customerGroup } = await adminClient.query<
|
|
|
+ GetCustomerGroup.Query,
|
|
|
+ GetCustomerGroup.Variables
|
|
|
+ >(GET_CUSTOMER_GROUP, {
|
|
|
+ id: customerGroupId,
|
|
|
+ });
|
|
|
+ expect(customerGroup!.customers.totalItems).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('only shows customers from current channel in customerGroup', async () => {
|
|
|
+ adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<AddCustomersToGroup.Mutation, AddCustomersToGroup.Variables>(
|
|
|
+ ADD_CUSTOMERS_TO_GROUP,
|
|
|
+ {
|
|
|
+ groupId: customerGroupId,
|
|
|
+ customerIds: [secondCustomer.id, thirdCustomer.id],
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ const { customerGroup } = await adminClient.query<
|
|
|
+ GetCustomerGroup.Query,
|
|
|
+ GetCustomerGroup.Variables
|
|
|
+ >(GET_CUSTOMER_GROUP, {
|
|
|
+ id: customerGroupId,
|
|
|
+ });
|
|
|
+ expect(customerGroup!.customers.totalItems).toBe(1);
|
|
|
+ expect(customerGroup!.customers.items.map(customer => customer.id)).toContain(secondCustomer.id);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('throws when deleting customer from other channel from customerGroup', async () => {
|
|
|
+ adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
|
|
|
+ await adminClient.query<RemoveCustomersFromGroup.Mutation, RemoveCustomersFromGroup.Variables>(
|
|
|
+ REMOVE_CUSTOMERS_FROM_GROUP,
|
|
|
+ {
|
|
|
+ groupId: customerGroupId,
|
|
|
+ customerIds: [thirdCustomer.id],
|
|
|
+ },
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+export const CREATE_ADDRESS = gql`
|
|
|
+ mutation CreateAddress($id: ID!, $input: CreateAddressInput!) {
|
|
|
+ createCustomerAddress(customerId: $id, input: $input) {
|
|
|
+ id
|
|
|
+ fullName
|
|
|
+ company
|
|
|
+ streetLine1
|
|
|
+ streetLine2
|
|
|
+ city
|
|
|
+ province
|
|
|
+ postalCode
|
|
|
+ country {
|
|
|
+ code
|
|
|
+ name
|
|
|
+ }
|
|
|
+ phoneNumber
|
|
|
+ defaultShippingAddress
|
|
|
+ defaultBillingAddress
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const UPDATE_ADDRESS = gql`
|
|
|
+ mutation UpdateAddress($input: UpdateAddressInput!) {
|
|
|
+ updateCustomerAddress(input: $input) {
|
|
|
+ id
|
|
|
+ defaultShippingAddress
|
|
|
+ defaultBillingAddress
|
|
|
+ country {
|
|
|
+ code
|
|
|
+ name
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const CREATE_CUSTOMER = gql`
|
|
|
+ mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
|
|
|
+ createCustomer(input: $input, password: $password) {
|
|
|
+ ...Customer
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ${CUSTOMER_FRAGMENT}
|
|
|
+`;
|
|
|
+
|
|
|
+export const UPDATE_CUSTOMER = gql`
|
|
|
+ mutation UpdateCustomer($input: UpdateCustomerInput!) {
|
|
|
+ updateCustomer(input: $input) {
|
|
|
+ ...Customer
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ${CUSTOMER_FRAGMENT}
|
|
|
+`;
|
|
|
+
|
|
|
+export const DELETE_CUSTOMER = gql`
|
|
|
+ mutation DeleteCustomer($id: ID!) {
|
|
|
+ deleteCustomer(id: $id) {
|
|
|
+ result
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const UPDATE_CUSTOMER_NOTE = gql`
|
|
|
+ mutation UpdateCustomerNote($input: UpdateCustomerNoteInput!) {
|
|
|
+ updateCustomerNote(input: $input) {
|
|
|
+ id
|
|
|
+ data
|
|
|
+ isPublic
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const DELETE_CUSTOMER_NOTE = gql`
|
|
|
+ mutation DeleteCustomerNote($id: ID!) {
|
|
|
+ deleteCustomerNote(id: $id) {
|
|
|
+ result
|
|
|
+ message
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const UPDATE_CUSTOMER_GROUP = gql`
|
|
|
+ mutation UpdateCustomerGroup($input: UpdateCustomerGroupInput!) {
|
|
|
+ updateCustomerGroup(input: $input) {
|
|
|
+ ...CustomerGroup
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ${CUSTOMER_GROUP_FRAGMENT}
|
|
|
+`;
|
|
|
+
|
|
|
+export const DELETE_CUSTOMER_GROUP = gql`
|
|
|
+ mutation DeleteCustomerGroup($id: ID!) {
|
|
|
+ deleteCustomerGroup(id: $id) {
|
|
|
+ result
|
|
|
+ message
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const GET_CUSTOMER_GROUPS = gql`
|
|
|
+ query GetCustomerGroups($options: CustomerGroupListOptions) {
|
|
|
+ customerGroups(options: $options) {
|
|
|
+ items {
|
|
|
+ id
|
|
|
+ name
|
|
|
+ }
|
|
|
+ totalItems
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const GET_CUSTOMER_GROUP = gql`
|
|
|
+ query GetCustomerGroup($id: ID!, $options: CustomerListOptions) {
|
|
|
+ customerGroup(id: $id) {
|
|
|
+ id
|
|
|
+ name
|
|
|
+ customers(options: $options) {
|
|
|
+ items {
|
|
|
+ id
|
|
|
+ }
|
|
|
+ totalItems
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+export const ADD_CUSTOMERS_TO_GROUP = gql`
|
|
|
+ mutation AddCustomersToGroup($groupId: ID!, $customerIds: [ID!]!) {
|
|
|
+ addCustomersToGroup(customerGroupId: $groupId, customerIds: $customerIds) {
|
|
|
+ ...CustomerGroup
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ${CUSTOMER_GROUP_FRAGMENT}
|
|
|
+`;
|
|
|
+
|
|
|
+export const GET_CUSTOMER_WITH_GROUPS = gql`
|
|
|
+ query GetCustomerWithGroups($id: ID!) {
|
|
|
+ customer(id: $id) {
|
|
|
+ id
|
|
|
+ groups {
|
|
|
+ id
|
|
|
+ name
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|