custom-fields.e2e-spec.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import { Asset, CustomFields, mergeConfig, TransactionalConnection } from '@vendure/core';
  3. import { createTestEnvironment } from '@vendure/testing';
  4. import { fail } from 'assert';
  5. import gql from 'graphql-tag';
  6. import path from 'path';
  7. import { initialData } from '../../../e2e-common/e2e-initial-data';
  8. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  9. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  10. import { fixPostgresTimezone } from './utils/fix-pg-timezone';
  11. fixPostgresTimezone();
  12. // tslint:disable:no-non-null-assertion
  13. const validateInjectorSpy = jest.fn();
  14. const customConfig = mergeConfig(testConfig(), {
  15. dbConnectionOptions: {
  16. timezone: 'Z',
  17. },
  18. customFields: {
  19. Product: [
  20. { name: 'nullable', type: 'string' },
  21. { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
  22. { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
  23. { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
  24. { name: 'intWithDefault', type: 'int', defaultValue: 5 },
  25. { name: 'floatWithDefault', type: 'float', defaultValue: 5.5678 },
  26. { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
  27. {
  28. name: 'dateTimeWithDefault',
  29. type: 'datetime',
  30. defaultValue: new Date('2019-04-30T12:59:16.4158386Z'),
  31. },
  32. { name: 'validateString', type: 'string', pattern: '^[0-9][a-z]+$' },
  33. { name: 'validateLocaleString', type: 'localeString', pattern: '^[0-9][a-z]+$' },
  34. { name: 'validateInt', type: 'int', min: 0, max: 10 },
  35. { name: 'validateFloat', type: 'float', min: 0.5, max: 10.5 },
  36. {
  37. name: 'validateDateTime',
  38. type: 'datetime',
  39. min: '2019-01-01T08:30',
  40. max: '2019-06-01T08:30',
  41. },
  42. {
  43. name: 'validateFn1',
  44. type: 'string',
  45. validate: value => {
  46. if (value !== 'valid') {
  47. return `The value ['${value}'] is not valid`;
  48. }
  49. },
  50. },
  51. {
  52. name: 'validateFn2',
  53. type: 'string',
  54. validate: value => {
  55. if (value !== 'valid') {
  56. return [
  57. {
  58. languageCode: LanguageCode.en,
  59. value: `The value ['${value}'] is not valid`,
  60. },
  61. ];
  62. }
  63. },
  64. },
  65. {
  66. name: 'validateFn3',
  67. type: 'string',
  68. validate: (value, injector) => {
  69. const connection = injector.get(TransactionalConnection);
  70. validateInjectorSpy(connection);
  71. },
  72. },
  73. {
  74. name: 'validateFn4',
  75. type: 'string',
  76. validate: async (value, injector) => {
  77. await new Promise(resolve => setTimeout(resolve, 1));
  78. return `async error`;
  79. },
  80. },
  81. {
  82. name: 'validateRelation',
  83. type: 'relation',
  84. entity: Asset,
  85. validate: async value => {
  86. await new Promise(resolve => setTimeout(resolve, 1));
  87. return `relation error`;
  88. },
  89. },
  90. {
  91. name: 'stringWithOptions',
  92. type: 'string',
  93. options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  94. },
  95. {
  96. name: 'nullableStringWithOptions',
  97. type: 'string',
  98. nullable: true,
  99. options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  100. },
  101. {
  102. name: 'nonPublic',
  103. type: 'string',
  104. defaultValue: 'hi!',
  105. public: false,
  106. },
  107. {
  108. name: 'public',
  109. type: 'string',
  110. defaultValue: 'ho!',
  111. public: true,
  112. },
  113. {
  114. name: 'longString',
  115. type: 'string',
  116. length: 10000,
  117. },
  118. {
  119. name: 'longLocaleString',
  120. type: 'localeString',
  121. length: 10000,
  122. },
  123. {
  124. name: 'readonlyString',
  125. type: 'string',
  126. readonly: true,
  127. },
  128. {
  129. name: 'internalString',
  130. type: 'string',
  131. internal: true,
  132. },
  133. {
  134. name: 'stringList',
  135. type: 'string',
  136. list: true,
  137. },
  138. {
  139. name: 'localeStringList',
  140. type: 'localeString',
  141. list: true,
  142. },
  143. {
  144. name: 'stringListWithDefault',
  145. type: 'string',
  146. list: true,
  147. defaultValue: ['cat'],
  148. },
  149. {
  150. name: 'intListWithValidation',
  151. type: 'int',
  152. list: true,
  153. validate: value => {
  154. if (!value.includes(42)) {
  155. return `Must include the number 42!`;
  156. }
  157. },
  158. },
  159. {
  160. name: 'uniqueString',
  161. type: 'string',
  162. unique: true,
  163. },
  164. ],
  165. Facet: [
  166. {
  167. name: 'translated',
  168. type: 'localeString',
  169. },
  170. ],
  171. Customer: [
  172. {
  173. name: 'score',
  174. type: 'int',
  175. readonly: true,
  176. },
  177. ],
  178. } as CustomFields,
  179. });
  180. describe('Custom fields', () => {
  181. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  182. beforeAll(async () => {
  183. await server.init({
  184. initialData,
  185. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  186. customerCount: 1,
  187. });
  188. await adminClient.asSuperAdmin();
  189. }, TEST_SETUP_TIMEOUT_MS);
  190. afterAll(async () => {
  191. await server.destroy();
  192. });
  193. it('globalSettings.serverConfig.customFieldConfig', async () => {
  194. const { globalSettings } = await adminClient.query(gql`
  195. query {
  196. globalSettings {
  197. serverConfig {
  198. customFieldConfig {
  199. Product {
  200. ... on CustomField {
  201. name
  202. type
  203. list
  204. }
  205. }
  206. }
  207. }
  208. }
  209. }
  210. `);
  211. expect(globalSettings.serverConfig.customFieldConfig).toEqual({
  212. Product: [
  213. { name: 'nullable', type: 'string', list: false },
  214. { name: 'notNullable', type: 'string', list: false },
  215. { name: 'stringWithDefault', type: 'string', list: false },
  216. { name: 'localeStringWithDefault', type: 'localeString', list: false },
  217. { name: 'intWithDefault', type: 'int', list: false },
  218. { name: 'floatWithDefault', type: 'float', list: false },
  219. { name: 'booleanWithDefault', type: 'boolean', list: false },
  220. { name: 'dateTimeWithDefault', type: 'datetime', list: false },
  221. { name: 'validateString', type: 'string', list: false },
  222. { name: 'validateLocaleString', type: 'localeString', list: false },
  223. { name: 'validateInt', type: 'int', list: false },
  224. { name: 'validateFloat', type: 'float', list: false },
  225. { name: 'validateDateTime', type: 'datetime', list: false },
  226. { name: 'validateFn1', type: 'string', list: false },
  227. { name: 'validateFn2', type: 'string', list: false },
  228. { name: 'validateFn3', type: 'string', list: false },
  229. { name: 'validateFn4', type: 'string', list: false },
  230. { name: 'validateRelation', type: 'relation', list: false },
  231. { name: 'stringWithOptions', type: 'string', list: false },
  232. { name: 'nullableStringWithOptions', type: 'string', list: false },
  233. { name: 'nonPublic', type: 'string', list: false },
  234. { name: 'public', type: 'string', list: false },
  235. { name: 'longString', type: 'string', list: false },
  236. { name: 'longLocaleString', type: 'localeString', list: false },
  237. { name: 'readonlyString', type: 'string', list: false },
  238. { name: 'stringList', type: 'string', list: true },
  239. { name: 'localeStringList', type: 'localeString', list: true },
  240. { name: 'stringListWithDefault', type: 'string', list: true },
  241. { name: 'intListWithValidation', type: 'int', list: true },
  242. { name: 'uniqueString', type: 'string', list: false },
  243. // The internal type should not be exposed at all
  244. // { name: 'internalString', type: 'string' },
  245. ],
  246. });
  247. });
  248. it('get nullable with no default', async () => {
  249. const { product } = await adminClient.query(gql`
  250. query {
  251. product(id: "T_1") {
  252. id
  253. name
  254. customFields {
  255. nullable
  256. }
  257. }
  258. }
  259. `);
  260. expect(product).toEqual({
  261. id: 'T_1',
  262. name: 'Laptop',
  263. customFields: {
  264. nullable: null,
  265. },
  266. });
  267. });
  268. it('get entity with localeString only', async () => {
  269. const { facet } = await adminClient.query(gql`
  270. query {
  271. facet(id: "T_1") {
  272. id
  273. name
  274. customFields {
  275. translated
  276. }
  277. }
  278. }
  279. `);
  280. expect(facet).toEqual({
  281. id: 'T_1',
  282. name: 'category',
  283. customFields: {
  284. translated: null,
  285. },
  286. });
  287. });
  288. it('get fields with default values', async () => {
  289. const { product } = await adminClient.query(gql`
  290. query {
  291. product(id: "T_1") {
  292. id
  293. name
  294. customFields {
  295. stringWithDefault
  296. localeStringWithDefault
  297. intWithDefault
  298. floatWithDefault
  299. booleanWithDefault
  300. dateTimeWithDefault
  301. stringListWithDefault
  302. }
  303. }
  304. }
  305. `);
  306. const customFields = {
  307. stringWithDefault: 'hello',
  308. localeStringWithDefault: 'hola',
  309. intWithDefault: 5,
  310. floatWithDefault: 5.5678,
  311. booleanWithDefault: true,
  312. dateTimeWithDefault: '2019-04-30T12:59:16.415Z',
  313. // MySQL does not support defaults on TEXT fields, which is what "simple-json" uses
  314. // internally. See https://stackoverflow.com/q/3466872/772859
  315. stringListWithDefault: customConfig.dbConnectionOptions.type === 'mysql' ? null : ['cat'],
  316. };
  317. expect(product).toEqual({
  318. id: 'T_1',
  319. name: 'Laptop',
  320. customFields,
  321. });
  322. });
  323. it(
  324. 'update non-nullable field',
  325. assertThrowsWithMessage(async () => {
  326. await adminClient.query(gql`
  327. mutation {
  328. updateProduct(input: { id: "T_1", customFields: { notNullable: null } }) {
  329. id
  330. }
  331. }
  332. `);
  333. }, "The custom field 'notNullable' value cannot be set to null"),
  334. );
  335. it(
  336. 'throws on attempt to update readonly field',
  337. assertThrowsWithMessage(async () => {
  338. await adminClient.query(gql`
  339. mutation {
  340. updateProduct(input: { id: "T_1", customFields: { readonlyString: "hello" } }) {
  341. id
  342. }
  343. }
  344. `);
  345. }, `Field "readonlyString" is not defined by type "UpdateProductCustomFieldsInput"`),
  346. );
  347. it(
  348. 'throws on attempt to update readonly field when no other custom fields defined',
  349. assertThrowsWithMessage(async () => {
  350. await adminClient.query(gql`
  351. mutation {
  352. updateCustomer(input: { id: "T_1", customFields: { score: 5 } }) {
  353. ... on Customer {
  354. id
  355. }
  356. }
  357. }
  358. `);
  359. }, `The custom field 'score' is readonly`),
  360. );
  361. it(
  362. 'throws on attempt to create readonly field',
  363. assertThrowsWithMessage(async () => {
  364. await adminClient.query(gql`
  365. mutation {
  366. createProduct(
  367. input: {
  368. translations: [{ languageCode: en, name: "test" }]
  369. customFields: { readonlyString: "hello" }
  370. }
  371. ) {
  372. id
  373. }
  374. }
  375. `);
  376. }, `Field "readonlyString" is not defined by type "CreateProductCustomFieldsInput"`),
  377. );
  378. it('string length allows long strings', async () => {
  379. const longString = Array.from({ length: 500 }, v => 'hello there!').join(' ');
  380. const result = await adminClient.query(
  381. gql`
  382. mutation ($stringValue: String!) {
  383. updateProduct(input: { id: "T_1", customFields: { longString: $stringValue } }) {
  384. id
  385. customFields {
  386. longString
  387. }
  388. }
  389. }
  390. `,
  391. { stringValue: longString },
  392. );
  393. expect(result.updateProduct.customFields.longString).toBe(longString);
  394. });
  395. it('string length allows long localeStrings', async () => {
  396. const longString = Array.from({ length: 500 }, v => 'hello there!').join(' ');
  397. const result = await adminClient.query(
  398. gql`
  399. mutation ($stringValue: String!) {
  400. updateProduct(
  401. input: {
  402. id: "T_1"
  403. translations: [
  404. { languageCode: en, customFields: { longLocaleString: $stringValue } }
  405. ]
  406. }
  407. ) {
  408. id
  409. customFields {
  410. longLocaleString
  411. }
  412. }
  413. }
  414. `,
  415. { stringValue: longString },
  416. );
  417. expect(result.updateProduct.customFields.longLocaleString).toBe(longString);
  418. });
  419. describe('validation', () => {
  420. it(
  421. 'invalid string',
  422. assertThrowsWithMessage(async () => {
  423. await adminClient.query(gql`
  424. mutation {
  425. updateProduct(input: { id: "T_1", customFields: { validateString: "hello" } }) {
  426. id
  427. }
  428. }
  429. `);
  430. }, `The custom field 'validateString' value ['hello'] does not match the pattern [^[0-9][a-z]+$]`),
  431. );
  432. it(
  433. 'invalid string option',
  434. assertThrowsWithMessage(async () => {
  435. await adminClient.query(gql`
  436. mutation {
  437. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "tiny" } }) {
  438. id
  439. }
  440. }
  441. `);
  442. }, `The custom field 'stringWithOptions' value ['tiny'] is invalid. Valid options are ['small', 'medium', 'large']`),
  443. );
  444. it('valid string option', async () => {
  445. const { updateProduct } = await adminClient.query(gql`
  446. mutation {
  447. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) {
  448. id
  449. customFields {
  450. stringWithOptions
  451. }
  452. }
  453. }
  454. `);
  455. expect(updateProduct.customFields.stringWithOptions).toBe('medium');
  456. });
  457. it('nullable string option with null', async () => {
  458. const { updateProduct } = await adminClient.query(gql`
  459. mutation {
  460. updateProduct(input: { id: "T_1", customFields: { nullableStringWithOptions: null } }) {
  461. id
  462. customFields {
  463. nullableStringWithOptions
  464. }
  465. }
  466. }
  467. `);
  468. expect(updateProduct.customFields.nullableStringWithOptions).toBeNull();
  469. });
  470. it(
  471. 'invalid localeString',
  472. assertThrowsWithMessage(async () => {
  473. await adminClient.query(gql`
  474. mutation {
  475. updateProduct(
  476. input: {
  477. id: "T_1"
  478. translations: [
  479. {
  480. id: "T_1"
  481. languageCode: en
  482. customFields: { validateLocaleString: "servus" }
  483. }
  484. ]
  485. }
  486. ) {
  487. id
  488. }
  489. }
  490. `);
  491. }, `The custom field 'validateLocaleString' value ['servus'] does not match the pattern [^[0-9][a-z]+$]`),
  492. );
  493. it(
  494. 'invalid int',
  495. assertThrowsWithMessage(async () => {
  496. await adminClient.query(gql`
  497. mutation {
  498. updateProduct(input: { id: "T_1", customFields: { validateInt: 12 } }) {
  499. id
  500. }
  501. }
  502. `);
  503. }, `The custom field 'validateInt' value [12] is greater than the maximum [10]`),
  504. );
  505. it(
  506. 'invalid float',
  507. assertThrowsWithMessage(async () => {
  508. await adminClient.query(gql`
  509. mutation {
  510. updateProduct(input: { id: "T_1", customFields: { validateFloat: 10.6 } }) {
  511. id
  512. }
  513. }
  514. `);
  515. }, `The custom field 'validateFloat' value [10.6] is greater than the maximum [10.5]`),
  516. );
  517. it(
  518. 'invalid datetime',
  519. assertThrowsWithMessage(async () => {
  520. await adminClient.query(gql`
  521. mutation {
  522. updateProduct(
  523. input: {
  524. id: "T_1"
  525. customFields: { validateDateTime: "2019-01-01T05:25:00.000Z" }
  526. }
  527. ) {
  528. id
  529. }
  530. }
  531. `);
  532. }, `The custom field 'validateDateTime' value [2019-01-01T05:25:00.000Z] is less than the minimum [2019-01-01T08:30]`),
  533. );
  534. it(
  535. 'invalid validate function with string',
  536. assertThrowsWithMessage(async () => {
  537. await adminClient.query(gql`
  538. mutation {
  539. updateProduct(input: { id: "T_1", customFields: { validateFn1: "invalid" } }) {
  540. id
  541. }
  542. }
  543. `);
  544. }, `The value ['invalid'] is not valid`),
  545. );
  546. it(
  547. 'invalid validate function with localized string',
  548. assertThrowsWithMessage(async () => {
  549. await adminClient.query(gql`
  550. mutation {
  551. updateProduct(input: { id: "T_1", customFields: { validateFn2: "invalid" } }) {
  552. id
  553. }
  554. }
  555. `);
  556. }, `The value ['invalid'] is not valid`),
  557. );
  558. it(
  559. 'invalid list field',
  560. assertThrowsWithMessage(async () => {
  561. await adminClient.query(gql`
  562. mutation {
  563. updateProduct(
  564. input: { id: "T_1", customFields: { intListWithValidation: [1, 2, 3] } }
  565. ) {
  566. id
  567. }
  568. }
  569. `);
  570. }, `Must include the number 42!`),
  571. );
  572. it('valid list field', async () => {
  573. const { updateProduct } = await adminClient.query(gql`
  574. mutation {
  575. updateProduct(input: { id: "T_1", customFields: { intListWithValidation: [1, 42, 3] } }) {
  576. id
  577. customFields {
  578. intListWithValidation
  579. }
  580. }
  581. }
  582. `);
  583. expect(updateProduct.customFields.intListWithValidation).toEqual([1, 42, 3]);
  584. });
  585. it('can inject providers into validation fn', async () => {
  586. const { updateProduct } = await adminClient.query(gql`
  587. mutation {
  588. updateProduct(input: { id: "T_1", customFields: { validateFn3: "some value" } }) {
  589. id
  590. customFields {
  591. validateFn3
  592. }
  593. }
  594. }
  595. `);
  596. expect(updateProduct.customFields.validateFn3).toBe('some value');
  597. expect(validateInjectorSpy).toHaveBeenCalledTimes(1);
  598. expect(validateInjectorSpy.mock.calls[0][0] instanceof TransactionalConnection).toBe(true);
  599. });
  600. it(
  601. 'supports async validation fn',
  602. assertThrowsWithMessage(async () => {
  603. await adminClient.query(gql`
  604. mutation {
  605. updateProduct(input: { id: "T_1", customFields: { validateFn4: "some value" } }) {
  606. id
  607. customFields {
  608. validateFn4
  609. }
  610. }
  611. }
  612. `);
  613. }, `async error`),
  614. );
  615. // https://github.com/vendure-ecommerce/vendure/issues/1000
  616. it(
  617. 'supports validation of relation types',
  618. assertThrowsWithMessage(async () => {
  619. await adminClient.query(gql`
  620. mutation {
  621. updateProduct(input: { id: "T_1", customFields: { validateRelationId: "T_1" } }) {
  622. id
  623. customFields {
  624. validateFn4
  625. }
  626. }
  627. }
  628. `);
  629. }, `relation error`),
  630. );
  631. // https://github.com/vendure-ecommerce/vendure/issues/1091
  632. it('handles well graphql internal fields', async () => {
  633. // throws "Cannot read property 'args' of undefined" if broken
  634. await adminClient.query(gql`
  635. mutation {
  636. __typename
  637. updateProduct(input: { id: "T_1", customFields: { nullable: "some value" } }) {
  638. __typename
  639. id
  640. customFields {
  641. __typename
  642. nullable
  643. }
  644. }
  645. }
  646. `);
  647. });
  648. });
  649. describe('public access', () => {
  650. it(
  651. 'non-public throws for Shop API',
  652. assertThrowsWithMessage(async () => {
  653. await shopClient.query(gql`
  654. query {
  655. product(id: "T_1") {
  656. id
  657. customFields {
  658. nonPublic
  659. }
  660. }
  661. }
  662. `);
  663. }, `Cannot query field "nonPublic" on type "ProductCustomFields"`),
  664. );
  665. it('publicly accessible via Shop API', async () => {
  666. const { product } = await shopClient.query(gql`
  667. query {
  668. product(id: "T_1") {
  669. id
  670. customFields {
  671. public
  672. }
  673. }
  674. }
  675. `);
  676. expect(product.customFields.public).toBe('ho!');
  677. });
  678. it(
  679. 'internal throws for Shop API',
  680. assertThrowsWithMessage(async () => {
  681. await shopClient.query(gql`
  682. query {
  683. product(id: "T_1") {
  684. id
  685. customFields {
  686. internalString
  687. }
  688. }
  689. }
  690. `);
  691. }, `Cannot query field "internalString" on type "ProductCustomFields"`),
  692. );
  693. it(
  694. 'internal throws for Admin API',
  695. assertThrowsWithMessage(async () => {
  696. await adminClient.query(gql`
  697. query {
  698. product(id: "T_1") {
  699. id
  700. customFields {
  701. internalString
  702. }
  703. }
  704. }
  705. `);
  706. }, `Cannot query field "internalString" on type "ProductCustomFields"`),
  707. );
  708. });
  709. describe('sort & filter', () => {
  710. it('can sort by custom fields', async () => {
  711. const { products } = await adminClient.query(gql`
  712. query {
  713. products(options: { sort: { nullable: ASC } }) {
  714. totalItems
  715. }
  716. }
  717. `);
  718. expect(products.totalItems).toBe(1);
  719. });
  720. // https://github.com/vendure-ecommerce/vendure/issues/1581
  721. it('can sort by localeString custom fields', async () => {
  722. const { products } = await adminClient.query(gql`
  723. query {
  724. products(options: { sort: { localeStringWithDefault: ASC } }) {
  725. totalItems
  726. }
  727. }
  728. `);
  729. expect(products.totalItems).toBe(1);
  730. });
  731. it('can filter by custom fields', async () => {
  732. const { products } = await adminClient.query(gql`
  733. query {
  734. products(options: { filter: { stringWithDefault: { contains: "hello" } } }) {
  735. totalItems
  736. }
  737. }
  738. `);
  739. expect(products.totalItems).toBe(1);
  740. });
  741. it('can filter by localeString custom fields', async () => {
  742. const { products } = await adminClient.query(gql`
  743. query {
  744. products(options: { filter: { localeStringWithDefault: { contains: "hola" } } }) {
  745. totalItems
  746. }
  747. }
  748. `);
  749. expect(products.totalItems).toBe(1);
  750. });
  751. it('can filter by custom list fields', async () => {
  752. const { products: result1 } = await adminClient.query(gql`
  753. query {
  754. products(options: { filter: { intListWithValidation: { inList: 42 } } }) {
  755. totalItems
  756. }
  757. }
  758. `);
  759. expect(result1.totalItems).toBe(1);
  760. const { products: result2 } = await adminClient.query(gql`
  761. query {
  762. products(options: { filter: { intListWithValidation: { inList: 43 } } }) {
  763. totalItems
  764. }
  765. }
  766. `);
  767. expect(result2.totalItems).toBe(0);
  768. });
  769. it(
  770. 'cannot sort by custom list fields',
  771. assertThrowsWithMessage(async () => {
  772. await adminClient.query(gql`
  773. query {
  774. products(options: { sort: { intListWithValidation: ASC } }) {
  775. totalItems
  776. }
  777. }
  778. `);
  779. }, `Field "intListWithValidation" is not defined by type "ProductSortParameter".`),
  780. );
  781. it(
  782. 'cannot filter by internal field in Admin API',
  783. assertThrowsWithMessage(async () => {
  784. await adminClient.query(gql`
  785. query {
  786. products(options: { filter: { internalString: { contains: "hello" } } }) {
  787. totalItems
  788. }
  789. }
  790. `);
  791. }, `Field "internalString" is not defined by type "ProductFilterParameter"`),
  792. );
  793. it(
  794. 'cannot filter by internal field in Shop API',
  795. assertThrowsWithMessage(async () => {
  796. await shopClient.query(gql`
  797. query {
  798. products(options: { filter: { internalString: { contains: "hello" } } }) {
  799. totalItems
  800. }
  801. }
  802. `);
  803. }, `Field "internalString" is not defined by type "ProductFilterParameter"`),
  804. );
  805. });
  806. describe('unique constraint', () => {
  807. it('setting unique value works', async () => {
  808. const result = await adminClient.query(
  809. gql`
  810. mutation {
  811. updateProduct(input: { id: "T_1", customFields: { uniqueString: "foo" } }) {
  812. id
  813. customFields {
  814. uniqueString
  815. }
  816. }
  817. }
  818. `,
  819. );
  820. expect(result.updateProduct.customFields.uniqueString).toBe('foo');
  821. });
  822. it('setting conflicting value fails', async () => {
  823. try {
  824. await adminClient.query(gql`
  825. mutation {
  826. createProduct(
  827. input: {
  828. translations: [
  829. { languageCode: en, name: "test 2", slug: "test-2", description: "" }
  830. ]
  831. customFields: { uniqueString: "foo" }
  832. }
  833. ) {
  834. id
  835. }
  836. }
  837. `);
  838. fail('Should have thrown');
  839. } catch (e: any) {
  840. let duplicateKeyErrMessage = 'unassigned';
  841. switch (customConfig.dbConnectionOptions.type) {
  842. case 'mariadb':
  843. case 'mysql':
  844. duplicateKeyErrMessage = `ER_DUP_ENTRY: Duplicate entry 'foo' for key`;
  845. break;
  846. case 'postgres':
  847. duplicateKeyErrMessage = `duplicate key value violates unique constraint`;
  848. break;
  849. case 'sqlite':
  850. case 'sqljs':
  851. duplicateKeyErrMessage = `UNIQUE constraint failed: product.customFieldsUniquestring`;
  852. break;
  853. }
  854. expect(e.message).toContain(duplicateKeyErrMessage);
  855. }
  856. });
  857. });
  858. });