custom-field-relations.e2e-spec.ts 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278
  1. import {
  2. assertFound,
  3. Asset,
  4. Collection,
  5. Country,
  6. CustomFields,
  7. DefaultLogger,
  8. defaultShippingCalculator,
  9. defaultShippingEligibilityChecker,
  10. Facet,
  11. FacetValue,
  12. LogLevel,
  13. manualFulfillmentHandler,
  14. mergeConfig,
  15. PluginCommonModule,
  16. Product,
  17. ProductOption,
  18. ProductOptionGroup,
  19. ProductVariant,
  20. RequestContext,
  21. ShippingMethod,
  22. TransactionalConnection,
  23. VendureEntity,
  24. VendurePlugin,
  25. } from '@vendure/core';
  26. import { createTestEnvironment } from '@vendure/testing';
  27. import gql from 'graphql-tag';
  28. import path from 'path';
  29. import { Repository } from 'typeorm';
  30. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  31. import { initialData } from '../../../e2e-common/e2e-initial-data';
  32. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  33. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  34. import { TestPlugin1636_1664 } from './fixtures/test-plugins/issue-1636-1664/issue-1636-1664-plugin';
  35. import { PluginIssue2453 } from './fixtures/test-plugins/issue-2453/plugin-issue2453';
  36. import { TestCustomEntity, WithCustomEntity } from './fixtures/test-plugins/with-custom-entity';
  37. import { AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-types';
  38. import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
  39. import { sortById } from './utils/test-order-utils';
  40. // From https://github.com/microsoft/TypeScript/issues/13298#issuecomment-654906323
  41. // to ensure that we _always_ test all entities which support custom fields
  42. type ValueOf<T> = T[keyof T];
  43. type NonEmptyArray<T> = [T, ...T[]];
  44. type MustInclude<T, U extends T[]> = [T] extends [ValueOf<U>] ? U : never;
  45. const enumerate =
  46. <T>() =>
  47. <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) =>
  48. elements;
  49. const entitiesWithCustomFields = enumerate<keyof CustomFields>()(
  50. 'Address',
  51. 'Administrator',
  52. 'Asset',
  53. 'Channel',
  54. 'Collection',
  55. 'Customer',
  56. 'CustomerGroup',
  57. 'Facet',
  58. 'FacetValue',
  59. 'Fulfillment',
  60. 'GlobalSettings',
  61. 'Order',
  62. 'OrderLine',
  63. 'PaymentMethod',
  64. 'Product',
  65. 'ProductOption',
  66. 'ProductOptionGroup',
  67. 'ProductVariant',
  68. 'Promotion',
  69. 'Region',
  70. 'Seller',
  71. 'ShippingMethod',
  72. 'TaxCategory',
  73. 'TaxRate',
  74. 'User',
  75. 'Zone',
  76. );
  77. const customFieldConfig: CustomFields = {};
  78. for (const entity of entitiesWithCustomFields) {
  79. customFieldConfig[entity] = [
  80. { name: 'single', type: 'relation', entity: Asset, graphQLType: 'Asset', list: false },
  81. { name: 'multi', type: 'relation', entity: Asset, graphQLType: 'Asset', list: true },
  82. ];
  83. }
  84. customFieldConfig.Product?.push(
  85. { name: 'cfCollection', type: 'relation', entity: Collection, list: false },
  86. { name: 'cfCountry', type: 'relation', entity: Country, list: false },
  87. { name: 'cfFacetValue', type: 'relation', entity: FacetValue, list: false },
  88. { name: 'cfFacet', type: 'relation', entity: Facet, list: false },
  89. { name: 'cfProductOptionGroup', type: 'relation', entity: ProductOptionGroup, list: false },
  90. { name: 'cfProductOption', type: 'relation', entity: ProductOption, list: false },
  91. { name: 'cfProductVariant', type: 'relation', entity: ProductVariant, list: false },
  92. { name: 'cfProduct', type: 'relation', entity: Product, list: false },
  93. { name: 'cfShippingMethod', type: 'relation', entity: ShippingMethod, list: false },
  94. { name: 'cfInternalAsset', type: 'relation', entity: Asset, list: false, internal: true },
  95. );
  96. customFieldConfig.ProductVariant?.push({
  97. name: 'cfRelatedProducts',
  98. type: 'relation',
  99. entity: Product,
  100. list: true,
  101. internal: false,
  102. public: true,
  103. });
  104. const customConfig = mergeConfig(testConfig(), {
  105. paymentOptions: {
  106. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  107. },
  108. // logger: new DefaultLogger({ level: LogLevel.Debug }),
  109. dbConnectionOptions: {
  110. timezone: 'Z',
  111. },
  112. customFields: customFieldConfig,
  113. plugins: [TestPlugin1636_1664, WithCustomEntity, PluginIssue2453],
  114. });
  115. describe('Custom field relations', () => {
  116. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  117. beforeAll(async () => {
  118. await server.init({
  119. initialData,
  120. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  121. customerCount: 3,
  122. });
  123. await adminClient.asSuperAdmin();
  124. }, TEST_SETUP_TIMEOUT_MS);
  125. afterAll(async () => {
  126. await server.destroy();
  127. });
  128. it('customFieldConfig query returns entity and scalar fields', async () => {
  129. const { globalSettings } = await adminClient.query(gql`
  130. query {
  131. globalSettings {
  132. serverConfig {
  133. customFieldConfig {
  134. Customer {
  135. ... on RelationCustomFieldConfig {
  136. name
  137. entity
  138. scalarFields
  139. }
  140. }
  141. }
  142. }
  143. }
  144. }
  145. `);
  146. const single = globalSettings.serverConfig.customFieldConfig.Customer[0];
  147. expect(single.entity).toBe('Asset');
  148. expect(single.scalarFields).toEqual([
  149. 'id',
  150. 'createdAt',
  151. 'updatedAt',
  152. 'name',
  153. 'type',
  154. 'fileSize',
  155. 'mimeType',
  156. 'width',
  157. 'height',
  158. 'source',
  159. 'preview',
  160. ]);
  161. });
  162. describe('special data resolution', () => {
  163. let productId: string;
  164. const productCustomFieldRelationsSelection = `
  165. id
  166. customFields {
  167. cfCollection {
  168. languageCode
  169. name
  170. }
  171. cfCountry {
  172. languageCode
  173. name
  174. }
  175. cfFacetValue {
  176. languageCode
  177. name
  178. }
  179. cfFacet {
  180. languageCode
  181. name
  182. }
  183. cfProductOptionGroup {
  184. languageCode
  185. name
  186. }
  187. cfProductOption {
  188. languageCode
  189. name
  190. }
  191. cfProductVariant {
  192. languageCode
  193. name
  194. }
  195. cfProduct {
  196. languageCode
  197. name
  198. }
  199. cfShippingMethod {
  200. languageCode
  201. name
  202. }
  203. }`;
  204. function assertTranslatableCustomFieldValues(product: { customFields: any }) {
  205. expect(product.customFields.cfCollection).toEqual({
  206. languageCode: 'en',
  207. name: '__root_collection__',
  208. });
  209. expect(product.customFields.cfCountry).toEqual({ languageCode: 'en', name: 'Australia' });
  210. expect(product.customFields.cfFacetValue).toEqual({
  211. languageCode: 'en',
  212. name: 'electronics',
  213. });
  214. expect(product.customFields.cfFacet).toEqual({ languageCode: 'en', name: 'category' });
  215. expect(product.customFields.cfProductOptionGroup).toEqual({
  216. languageCode: 'en',
  217. name: 'screen size',
  218. });
  219. expect(product.customFields.cfProductOption).toEqual({
  220. languageCode: 'en',
  221. name: '13 inch',
  222. });
  223. expect(product.customFields.cfProductVariant).toEqual({
  224. languageCode: 'en',
  225. name: 'Laptop 13 inch 8GB',
  226. });
  227. expect(product.customFields.cfProduct).toEqual({ languageCode: 'en', name: 'Laptop' });
  228. expect(product.customFields.cfShippingMethod).toEqual({
  229. languageCode: 'en',
  230. name: 'Standard Shipping',
  231. });
  232. }
  233. it('translatable entities get translated', async () => {
  234. const { createProduct } = await adminClient.query(gql`
  235. mutation {
  236. createProduct(
  237. input: {
  238. translations: [
  239. {
  240. languageCode: en
  241. name: "Test product"
  242. description: ""
  243. slug: "test-product"
  244. }
  245. ]
  246. customFields: {
  247. cfCollectionId: "T_1"
  248. cfCountryId: "T_1"
  249. cfFacetValueId: "T_1"
  250. cfFacetId: "T_1"
  251. cfProductOptionGroupId: "T_1"
  252. cfProductOptionId: "T_1"
  253. cfProductVariantId: "T_1"
  254. cfProductId: "T_1"
  255. cfShippingMethodId: "T_1"
  256. }
  257. }
  258. ) { ${productCustomFieldRelationsSelection} }
  259. }
  260. `);
  261. productId = createProduct.id;
  262. assertTranslatableCustomFieldValues(createProduct);
  263. });
  264. it('translatable entities get translated on findOneInChannel', async () => {
  265. const { product } = await adminClient.query(gql`
  266. query {
  267. product(id: "${productId}") { ${productCustomFieldRelationsSelection} }
  268. }
  269. `);
  270. assertTranslatableCustomFieldValues(product);
  271. });
  272. // https://github.com/vendure-ecommerce/vendure/issues/2453
  273. it('translatable eager-loaded relation works (issue 2453)', async () => {
  274. const { collections } = await adminClient.query(gql`
  275. query {
  276. collections(options: { sort: { name: DESC } }) {
  277. totalItems
  278. items {
  279. id
  280. name
  281. customFields {
  282. campaign {
  283. name
  284. languageCode
  285. }
  286. }
  287. }
  288. }
  289. }
  290. `);
  291. expect(collections.totalItems).toBe(3);
  292. expect(collections.items.find((c: any) => c.id === 'T_3')).toEqual({
  293. customFields: {
  294. campaign: {
  295. languageCode: 'en',
  296. name: 'Clearance Up to 70% Off frames',
  297. },
  298. },
  299. id: 'T_3',
  300. name: 'children collection',
  301. });
  302. });
  303. it('ProductVariant prices get resolved', async () => {
  304. const { product } = await adminClient.query(gql`
  305. query {
  306. product(id: "${productId}") {
  307. id
  308. customFields {
  309. cfProductVariant {
  310. price
  311. currencyCode
  312. priceWithTax
  313. }
  314. }
  315. }
  316. }`);
  317. expect(product.customFields.cfProductVariant).toEqual({
  318. price: 129900,
  319. currencyCode: 'USD',
  320. priceWithTax: 155880,
  321. });
  322. });
  323. });
  324. describe('entity-specific implementation', () => {
  325. function assertCustomFieldIds(customFields: any, single: string, multi: string[]) {
  326. expect(customFields.single).toEqual({ id: single });
  327. expect(customFields.multi.sort(sortById)).toEqual(multi.map(id => ({ id })));
  328. }
  329. const customFieldsSelection = `
  330. customFields {
  331. single {
  332. id
  333. }
  334. multi {
  335. id
  336. }
  337. }`;
  338. describe('Address entity', () => {
  339. it('admin createCustomerAddress', async () => {
  340. const { createCustomerAddress } = await adminClient.query(gql`
  341. mutation {
  342. createCustomerAddress(
  343. customerId: "T_1"
  344. input: {
  345. countryCode: "GB"
  346. streetLine1: "Test Street"
  347. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  348. }
  349. ) {
  350. id
  351. ${customFieldsSelection}
  352. }
  353. }
  354. `);
  355. assertCustomFieldIds(createCustomerAddress.customFields, 'T_1', ['T_1', 'T_2']);
  356. });
  357. it('shop createCustomerAddress', async () => {
  358. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  359. const { createCustomerAddress } = await shopClient.query(gql`
  360. mutation {
  361. createCustomerAddress(
  362. input: {
  363. countryCode: "GB"
  364. streetLine1: "Test Street"
  365. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  366. }
  367. ) {
  368. id
  369. ${customFieldsSelection}
  370. }
  371. }
  372. `);
  373. assertCustomFieldIds(createCustomerAddress.customFields, 'T_1', ['T_1', 'T_2']);
  374. });
  375. it('admin updateCustomerAddress', async () => {
  376. const { updateCustomerAddress } = await adminClient.query(gql`
  377. mutation {
  378. updateCustomerAddress(
  379. input: { id: "T_1", customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] } }
  380. ) {
  381. id
  382. ${customFieldsSelection}
  383. }
  384. }
  385. `);
  386. assertCustomFieldIds(updateCustomerAddress.customFields, 'T_2', ['T_3', 'T_4']);
  387. });
  388. it('shop updateCustomerAddress', async () => {
  389. const { updateCustomerAddress } = await shopClient.query(gql`
  390. mutation {
  391. updateCustomerAddress(
  392. input: { id: "T_1", customFields: { singleId: "T_3", multiIds: ["T_4", "T_2"] } }
  393. ) {
  394. id
  395. ${customFieldsSelection}
  396. }
  397. }
  398. `);
  399. assertCustomFieldIds(updateCustomerAddress.customFields, 'T_3', ['T_2', 'T_4']);
  400. });
  401. });
  402. describe('Collection entity', () => {
  403. let collectionId: string;
  404. it('admin createCollection', async () => {
  405. const { createCollection } = await adminClient.query(gql`
  406. mutation {
  407. createCollection(
  408. input: {
  409. translations: [
  410. { languageCode: en, name: "Test", description: "test", slug: "test" }
  411. ]
  412. filters: []
  413. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  414. }
  415. ) {
  416. id
  417. ${customFieldsSelection}
  418. }
  419. }
  420. `);
  421. assertCustomFieldIds(createCollection.customFields, 'T_1', ['T_1', 'T_2']);
  422. collectionId = createCollection.id;
  423. });
  424. it('admin updateCollection', async () => {
  425. const { updateCollection } = await adminClient.query(gql`
  426. mutation {
  427. updateCollection(
  428. input: {
  429. id: "${collectionId}"
  430. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  431. }
  432. ) {
  433. id
  434. ${customFieldsSelection}
  435. }
  436. }
  437. `);
  438. assertCustomFieldIds(updateCollection.customFields, 'T_2', ['T_3', 'T_4']);
  439. });
  440. });
  441. describe('Customer entity', () => {
  442. let customerId: string;
  443. it('admin createCustomer', async () => {
  444. const { createCustomer } = await adminClient.query(gql`
  445. mutation {
  446. createCustomer(
  447. input: {
  448. emailAddress: "test@test.com"
  449. firstName: "Test"
  450. lastName: "Person"
  451. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  452. }
  453. ) {
  454. ... on Customer {
  455. id
  456. ${customFieldsSelection}
  457. }
  458. }
  459. }
  460. `);
  461. assertCustomFieldIds(createCustomer.customFields, 'T_1', ['T_1', 'T_2']);
  462. customerId = createCustomer.id;
  463. });
  464. it('admin updateCustomer', async () => {
  465. const { updateCustomer } = await adminClient.query(gql`
  466. mutation {
  467. updateCustomer(
  468. input: {
  469. id: "${customerId}"
  470. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  471. }
  472. ) {
  473. ...on Customer {
  474. id
  475. ${customFieldsSelection}
  476. }
  477. }
  478. }
  479. `);
  480. assertCustomFieldIds(updateCustomer.customFields, 'T_2', ['T_3', 'T_4']);
  481. });
  482. it('shop updateCustomer', async () => {
  483. const { updateCustomer } = await shopClient.query(gql`
  484. mutation {
  485. updateCustomer(input: { customFields: { singleId: "T_4", multiIds: ["T_2", "T_4"] } }) {
  486. id
  487. ${customFieldsSelection}
  488. }
  489. }
  490. `);
  491. assertCustomFieldIds(updateCustomer.customFields, 'T_4', ['T_2', 'T_4']);
  492. });
  493. });
  494. describe('Facet entity', () => {
  495. let facetId: string;
  496. it('admin createFacet', async () => {
  497. const { createFacet } = await adminClient.query(gql`
  498. mutation {
  499. createFacet(
  500. input: {
  501. code: "test"
  502. isPrivate: false
  503. translations: [{ languageCode: en, name: "test" }]
  504. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  505. }
  506. ) {
  507. id
  508. ${customFieldsSelection}
  509. }
  510. }
  511. `);
  512. assertCustomFieldIds(createFacet.customFields, 'T_1', ['T_1', 'T_2']);
  513. facetId = createFacet.id;
  514. });
  515. it('admin updateFacet', async () => {
  516. const { updateFacet } = await adminClient.query(gql`
  517. mutation {
  518. updateFacet(
  519. input: {
  520. id: "${facetId}"
  521. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  522. }
  523. ) {
  524. id
  525. ${customFieldsSelection}
  526. }
  527. }
  528. `);
  529. assertCustomFieldIds(updateFacet.customFields, 'T_2', ['T_3', 'T_4']);
  530. });
  531. });
  532. describe('FacetValue entity', () => {
  533. let facetValueId: string;
  534. it('admin createFacetValues', async () => {
  535. const { createFacetValues } = await adminClient.query(gql`
  536. mutation {
  537. createFacetValues(
  538. input: {
  539. code: "test"
  540. facetId: "T_1"
  541. translations: [{ languageCode: en, name: "test" }]
  542. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  543. }
  544. ) {
  545. id
  546. ${customFieldsSelection}
  547. }
  548. }
  549. `);
  550. assertCustomFieldIds(createFacetValues[0].customFields, 'T_1', ['T_1', 'T_2']);
  551. facetValueId = createFacetValues[0].id;
  552. });
  553. it('admin updateFacetValues', async () => {
  554. const { updateFacetValues } = await adminClient.query(gql`
  555. mutation {
  556. updateFacetValues(
  557. input: {
  558. id: "${facetValueId}"
  559. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  560. }
  561. ) {
  562. id
  563. ${customFieldsSelection}
  564. }
  565. }
  566. `);
  567. assertCustomFieldIds(updateFacetValues[0].customFields, 'T_2', ['T_3', 'T_4']);
  568. });
  569. });
  570. // describe('Fulfillment entity', () => {
  571. // // Currently no GraphQL API to set customFields on fulfillments
  572. // });
  573. describe('GlobalSettings entity', () => {
  574. it('admin updateGlobalSettings', async () => {
  575. const { updateGlobalSettings } = await adminClient.query(gql`
  576. mutation {
  577. updateGlobalSettings(
  578. input: {
  579. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  580. }
  581. ) {
  582. ... on GlobalSettings {
  583. id
  584. ${customFieldsSelection}
  585. }
  586. }
  587. }
  588. `);
  589. assertCustomFieldIds(updateGlobalSettings.customFields, 'T_2', ['T_3', 'T_4']);
  590. });
  591. });
  592. describe('Order entity', () => {
  593. let orderId: string;
  594. beforeAll(async () => {
  595. const { addItemToOrder } = await shopClient.query<any, AddItemToOrderMutationVariables>(
  596. ADD_ITEM_TO_ORDER,
  597. {
  598. productVariantId: 'T_1',
  599. quantity: 1,
  600. },
  601. );
  602. orderId = addItemToOrder.id;
  603. });
  604. it('shop setOrderCustomFields', async () => {
  605. const { setOrderCustomFields } = await shopClient.query(gql`
  606. mutation {
  607. setOrderCustomFields(
  608. input: {
  609. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  610. }
  611. ) {
  612. ... on Order {
  613. id
  614. ${customFieldsSelection}
  615. }
  616. }
  617. }
  618. `);
  619. assertCustomFieldIds(setOrderCustomFields.customFields, 'T_2', ['T_3', 'T_4']);
  620. });
  621. it('admin setOrderCustomFields', async () => {
  622. const { setOrderCustomFields } = await adminClient.query(gql`
  623. mutation {
  624. setOrderCustomFields(
  625. input: {
  626. id: "${orderId}"
  627. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  628. }
  629. ) {
  630. ... on Order {
  631. id
  632. ${customFieldsSelection}
  633. }
  634. }
  635. }
  636. `);
  637. assertCustomFieldIds(setOrderCustomFields.customFields, 'T_1', ['T_1', 'T_2']);
  638. });
  639. // https://github.com/vendure-ecommerce/vendure/issues/1664#issuecomment-1320872627
  640. it('admin order query with eager-loaded custom field relation', async () => {
  641. const { order } = await adminClient.query(gql`
  642. query {
  643. order(id: 1) {
  644. id
  645. customFields {
  646. productOwner {
  647. id
  648. }
  649. }
  650. }
  651. }
  652. `);
  653. // we're just making sure it does not throw here.
  654. expect(order).toEqual({
  655. customFields: {
  656. productOwner: null,
  657. },
  658. id: 'T_1',
  659. });
  660. });
  661. });
  662. describe('OrderLine entity', () => {
  663. it('shop addItemToOrder', async () => {
  664. const { addItemToOrder } = await shopClient.query(
  665. gql`mutation {
  666. addItemToOrder(productVariantId: "T_1", quantity: 1, customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }) {
  667. ... on Order {
  668. id
  669. ${customFieldsSelection}
  670. }
  671. }
  672. }`,
  673. );
  674. assertCustomFieldIds(addItemToOrder.customFields, 'T_1', ['T_1', 'T_2']);
  675. });
  676. });
  677. describe('Product, ProductVariant entity', () => {
  678. let productId: string;
  679. it('admin createProduct', async () => {
  680. const { createProduct } = await adminClient.query(gql`
  681. mutation {
  682. createProduct(
  683. input: {
  684. translations: [{ languageCode: en, name: "test" slug: "test" description: "test" }]
  685. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  686. }
  687. ) {
  688. id
  689. ${customFieldsSelection}
  690. }
  691. }
  692. `);
  693. assertCustomFieldIds(createProduct.customFields, 'T_1', ['T_1', 'T_2']);
  694. productId = createProduct.id;
  695. });
  696. it('admin updateProduct', async () => {
  697. const { updateProduct } = await adminClient.query(gql`
  698. mutation {
  699. updateProduct(
  700. input: {
  701. id: "${productId}"
  702. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  703. }
  704. ) {
  705. id
  706. ${customFieldsSelection}
  707. }
  708. }
  709. `);
  710. assertCustomFieldIds(updateProduct.customFields, 'T_2', ['T_3', 'T_4']);
  711. });
  712. let productVariantId: string;
  713. it('admin createProductVariant', async () => {
  714. const { createProductVariants } = await adminClient.query(gql`
  715. mutation {
  716. createProductVariants(
  717. input: [{
  718. sku: "TEST01"
  719. productId: "${productId}"
  720. translations: [{ languageCode: en, name: "test" }]
  721. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  722. }]
  723. ) {
  724. id
  725. ${customFieldsSelection}
  726. }
  727. }
  728. `);
  729. assertCustomFieldIds(createProductVariants[0].customFields, 'T_1', ['T_1', 'T_2']);
  730. productVariantId = createProductVariants[0].id;
  731. });
  732. it('admin updateProductVariant', async () => {
  733. const { updateProductVariants } = await adminClient.query(gql`
  734. mutation {
  735. updateProductVariants(
  736. input: [{
  737. id: "${productVariantId}"
  738. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  739. }]
  740. ) {
  741. id
  742. ${customFieldsSelection}
  743. }
  744. }
  745. `);
  746. assertCustomFieldIds(updateProductVariants[0].customFields, 'T_2', ['T_3', 'T_4']);
  747. });
  748. describe('issue 1664', () => {
  749. // https://github.com/vendure-ecommerce/vendure/issues/1664
  750. it('successfully gets product by id with eager-loading custom field relation', async () => {
  751. const { product } = await shopClient.query(gql`
  752. query {
  753. product(id: "T_1") {
  754. id
  755. customFields {
  756. cfVendor {
  757. featuredProduct {
  758. id
  759. }
  760. }
  761. }
  762. }
  763. }
  764. `);
  765. expect(product).toBeDefined();
  766. });
  767. // https://github.com/vendure-ecommerce/vendure/issues/1664
  768. it('successfully gets product by id with nested eager-loading custom field relation', async () => {
  769. const { customer } = await adminClient.query(gql`
  770. query {
  771. customer(id: "T_1") {
  772. id
  773. firstName
  774. lastName
  775. emailAddress
  776. phoneNumber
  777. user {
  778. customFields {
  779. cfVendor {
  780. id
  781. }
  782. }
  783. }
  784. }
  785. }
  786. `);
  787. expect(customer).toBeDefined();
  788. });
  789. // https://github.com/vendure-ecommerce/vendure/issues/1664
  790. it('successfully gets product.variants with nested custom field relation', async () => {
  791. await adminClient.query(gql`
  792. mutation {
  793. updateProductVariants(
  794. input: [{ id: "T_1", customFields: { cfRelatedProductsIds: ["T_2"] } }]
  795. ) {
  796. id
  797. }
  798. }
  799. `);
  800. const { product } = await adminClient.query(gql`
  801. query {
  802. product(id: "T_1") {
  803. variants {
  804. id
  805. customFields {
  806. cfRelatedProducts {
  807. featuredAsset {
  808. id
  809. }
  810. }
  811. }
  812. }
  813. }
  814. }
  815. `);
  816. expect(product).toBeDefined();
  817. expect(product.variants[0].customFields.cfRelatedProducts).toEqual([
  818. {
  819. featuredAsset: { id: 'T_2' },
  820. },
  821. ]);
  822. });
  823. it('successfully gets product by slug with eager-loading custom field relation', async () => {
  824. const { product } = await shopClient.query(gql`
  825. query {
  826. product(slug: "laptop") {
  827. id
  828. customFields {
  829. cfVendor {
  830. featuredProduct {
  831. id
  832. }
  833. }
  834. }
  835. }
  836. }
  837. `);
  838. expect(product).toBeDefined();
  839. });
  840. it('does not error on custom field relation with eager custom field relation', async () => {
  841. const { product } = await adminClient.query(gql`
  842. query {
  843. product(slug: "laptop") {
  844. name
  845. customFields {
  846. owner {
  847. id
  848. code
  849. customFields {
  850. profile {
  851. id
  852. name
  853. }
  854. }
  855. }
  856. }
  857. }
  858. }
  859. `);
  860. expect(product).toBeDefined();
  861. });
  862. });
  863. });
  864. describe('ProductOptionGroup, ProductOption entity', () => {
  865. let productOptionGroupId: string;
  866. it('admin createProductOptionGroup', async () => {
  867. const { createProductOptionGroup } = await adminClient.query(gql`
  868. mutation {
  869. createProductOptionGroup(
  870. input: {
  871. code: "test"
  872. options: []
  873. translations: [{ languageCode: en, name: "test" }]
  874. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  875. }
  876. ) {
  877. id
  878. ${customFieldsSelection}
  879. }
  880. }
  881. `);
  882. assertCustomFieldIds(createProductOptionGroup.customFields, 'T_1', ['T_1', 'T_2']);
  883. productOptionGroupId = createProductOptionGroup.id;
  884. });
  885. it('admin updateProductOptionGroup', async () => {
  886. const { updateProductOptionGroup } = await adminClient.query(gql`
  887. mutation {
  888. updateProductOptionGroup(
  889. input: {
  890. id: "${productOptionGroupId}"
  891. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  892. }
  893. ) {
  894. id
  895. ${customFieldsSelection}
  896. }
  897. }
  898. `);
  899. assertCustomFieldIds(updateProductOptionGroup.customFields, 'T_2', ['T_3', 'T_4']);
  900. });
  901. let productOptionId: string;
  902. it('admin createProductOption', async () => {
  903. const { createProductOption } = await adminClient.query(gql`
  904. mutation {
  905. createProductOption(
  906. input: {
  907. productOptionGroupId: "${productOptionGroupId}"
  908. code: "test-option"
  909. translations: [{ languageCode: en, name: "test-option" }]
  910. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  911. }
  912. ) {
  913. id
  914. ${customFieldsSelection}
  915. }
  916. }
  917. `);
  918. assertCustomFieldIds(createProductOption.customFields, 'T_1', ['T_1', 'T_2']);
  919. productOptionId = createProductOption.id;
  920. });
  921. it('admin updateProductOption', async () => {
  922. const { updateProductOption } = await adminClient.query(gql`
  923. mutation {
  924. updateProductOption(
  925. input: {
  926. id: "${productOptionId}"
  927. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  928. }
  929. ) {
  930. id
  931. ${customFieldsSelection}
  932. }
  933. }
  934. `);
  935. assertCustomFieldIds(updateProductOption.customFields, 'T_2', ['T_3', 'T_4']);
  936. });
  937. });
  938. // describe('User entity', () => {
  939. // // Currently no GraphQL API to set User custom fields
  940. // });
  941. describe('ShippingMethod entity', () => {
  942. let shippingMethodId: string;
  943. it('admin createShippingMethod', async () => {
  944. const { createShippingMethod } = await adminClient.query(gql`
  945. mutation {
  946. createShippingMethod(
  947. input: {
  948. code: "test"
  949. calculator: {
  950. code: "${defaultShippingCalculator.code}"
  951. arguments: [
  952. { name: "rate" value: "10"},
  953. { name: "includesTax" value: "true"},
  954. { name: "taxRate" value: "10"},
  955. ]
  956. }
  957. checker: {
  958. code: "${defaultShippingEligibilityChecker.code}"
  959. arguments: [
  960. { name: "orderMinimum" value: "0"},
  961. ]
  962. }
  963. fulfillmentHandler: "${manualFulfillmentHandler.code}"
  964. translations: [{ languageCode: en, name: "test" description: "test" }]
  965. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] }
  966. }
  967. ) {
  968. id
  969. ${customFieldsSelection}
  970. }
  971. }
  972. `);
  973. assertCustomFieldIds(createShippingMethod.customFields, 'T_1', ['T_1', 'T_2']);
  974. shippingMethodId = createShippingMethod.id;
  975. });
  976. it('admin updateShippingMethod', async () => {
  977. const { updateShippingMethod } = await adminClient.query(gql`
  978. mutation {
  979. updateShippingMethod(
  980. input: {
  981. id: "${shippingMethodId}"
  982. translations: []
  983. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  984. }
  985. ) {
  986. id
  987. ${customFieldsSelection}
  988. }
  989. }
  990. `);
  991. assertCustomFieldIds(updateShippingMethod.customFields, 'T_2', ['T_3', 'T_4']);
  992. });
  993. it('shop eligibleShippingMethods (ShippingMethodQuote)', async () => {
  994. const { eligibleShippingMethods } = await shopClient.query(gql`
  995. query {
  996. eligibleShippingMethods {
  997. id
  998. name
  999. code
  1000. description
  1001. ${customFieldsSelection}
  1002. }
  1003. }
  1004. `);
  1005. const testShippingMethodQuote = eligibleShippingMethods.find(
  1006. (quote: any) => quote.code === 'test',
  1007. );
  1008. assertCustomFieldIds(testShippingMethodQuote.customFields, 'T_2', ['T_3', 'T_4']);
  1009. });
  1010. });
  1011. describe('PaymentMethod entity', () => {
  1012. let paymentMethodId: string;
  1013. it('admin createShippingMethod', async () => {
  1014. const { createPaymentMethod } = await adminClient.query(gql`
  1015. mutation {
  1016. createPaymentMethod(
  1017. input: {
  1018. code: "test"
  1019. enabled: true
  1020. handler: {
  1021. code: "${testSuccessfulPaymentMethod.code}"
  1022. arguments: []
  1023. }
  1024. customFields: { singleId: "T_1", multiIds: ["T_1", "T_2"] },
  1025. translations: [{ languageCode: en, name: "test" }]
  1026. }
  1027. ) {
  1028. id
  1029. ${customFieldsSelection}
  1030. }
  1031. }
  1032. `);
  1033. assertCustomFieldIds(createPaymentMethod.customFields, 'T_1', ['T_1', 'T_2']);
  1034. paymentMethodId = createPaymentMethod.id;
  1035. });
  1036. it('admin updatePaymentMethod', async () => {
  1037. const { updatePaymentMethod } = await adminClient.query(gql`
  1038. mutation {
  1039. updatePaymentMethod(
  1040. input: {
  1041. id: "${paymentMethodId}"
  1042. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] },
  1043. }
  1044. ) {
  1045. id
  1046. ${customFieldsSelection}
  1047. }
  1048. }
  1049. `);
  1050. assertCustomFieldIds(updatePaymentMethod.customFields, 'T_2', ['T_3', 'T_4']);
  1051. });
  1052. it('shop eligiblePaymentMethods (PaymentMethodQuote)', async () => {
  1053. const { eligiblePaymentMethods } = await shopClient.query(gql`
  1054. query {
  1055. eligiblePaymentMethods {
  1056. id
  1057. name
  1058. description
  1059. ${customFieldsSelection}
  1060. }
  1061. }
  1062. `);
  1063. assertCustomFieldIds(eligiblePaymentMethods[0].customFields, 'T_2', ['T_3', 'T_4']);
  1064. });
  1065. });
  1066. describe('Asset entity', () => {
  1067. it('set custom field relations on Asset', async () => {
  1068. const { updateAsset } = await adminClient.query(gql`
  1069. mutation {
  1070. updateAsset(
  1071. input: {
  1072. id: "T_1",
  1073. customFields: { singleId: "T_2", multiIds: ["T_3", "T_4"] }
  1074. }
  1075. ) {
  1076. id
  1077. ${customFieldsSelection}
  1078. }
  1079. }
  1080. `);
  1081. assertCustomFieldIds(updateAsset.customFields, 'T_2', ['T_3', 'T_4']);
  1082. });
  1083. it('findOne on Asset', async () => {
  1084. const { asset } = await adminClient.query(gql`
  1085. query {
  1086. asset(id: "T_1") {
  1087. id
  1088. ${customFieldsSelection}
  1089. }
  1090. }
  1091. `);
  1092. expect(asset.customFields.single.id).toBe('T_2');
  1093. expect(asset.customFields.multi.length).toEqual(2);
  1094. });
  1095. // https://github.com/vendure-ecommerce/vendure/issues/1636
  1096. it('calling TransactionalConnection.findOneInChannel() returns custom field relations', async () => {
  1097. TestPlugin1636_1664.testResolverSpy.mockReset();
  1098. await shopClient.query(gql`
  1099. query {
  1100. getAssetTest(id: "T_1")
  1101. }
  1102. `);
  1103. const args = TestPlugin1636_1664.testResolverSpy.mock.calls[0];
  1104. expect(args[0].customFields.single.id).toEqual(2);
  1105. expect(args[0].customFields.multi.length).toEqual(2);
  1106. });
  1107. });
  1108. });
  1109. it('null values', async () => {
  1110. const { updateCustomerAddress } = await adminClient.query(gql`
  1111. mutation {
  1112. updateCustomerAddress(
  1113. input: { id: "T_1", customFields: { singleId: null, multiIds: ["T_1", "null"] } }
  1114. ) {
  1115. id
  1116. customFields {
  1117. single {
  1118. id
  1119. }
  1120. multi {
  1121. id
  1122. }
  1123. }
  1124. }
  1125. }
  1126. `);
  1127. expect(updateCustomerAddress.customFields.single).toEqual(null);
  1128. expect(updateCustomerAddress.customFields.multi).toEqual([{ id: 'T_1' }]);
  1129. });
  1130. describe('bi-direction relations', () => {
  1131. let customEntityRepository: Repository<TestCustomEntity>;
  1132. let customEntity: TestCustomEntity;
  1133. let collectionIdInternal: number;
  1134. beforeAll(async () => {
  1135. customEntityRepository = server.app
  1136. .get(TransactionalConnection)
  1137. .getRepository(RequestContext.empty(), TestCustomEntity);
  1138. const customEntityId = (await customEntityRepository.save({})).id;
  1139. const { createCollection } = await adminClient.query(gql`
  1140. mutation {
  1141. createCollection(
  1142. input: {
  1143. translations: [
  1144. { languageCode: en, name: "Test", description: "test", slug: "test" }
  1145. ]
  1146. filters: []
  1147. customFields: { customEntityListIds: [${customEntityId}] customEntityId: ${customEntityId} }
  1148. }
  1149. ) {
  1150. id
  1151. }
  1152. }
  1153. `);
  1154. collectionIdInternal = parseInt(createCollection.id.replace('T_', ''), 10);
  1155. customEntity = await assertFound(
  1156. customEntityRepository.findOne({
  1157. where: { id: customEntityId },
  1158. relations: {
  1159. customEntityInverse: true,
  1160. customEntityListInverse: true,
  1161. },
  1162. }),
  1163. );
  1164. });
  1165. it('can create inverse relation for list=false', () => {
  1166. expect(customEntity.customEntityInverse).toEqual([
  1167. expect.objectContaining({ id: collectionIdInternal }),
  1168. ]);
  1169. });
  1170. it('can create inverse relation for list=true', () => {
  1171. expect(customEntity.customEntityListInverse).toEqual([
  1172. expect.objectContaining({ id: collectionIdInternal }),
  1173. ]);
  1174. });
  1175. });
  1176. });