custom-fields.e2e-spec.ts 28 KB

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